Commit 33c5fe60 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Commit Bot

[Autofill Assistant] Add animations when setting carousel chips.

This CL adds animations when carousels chips are changed. When setting
the new list of chips, we compute the minimum set of operations (insert,
delete, substitute) required to transform the old list into the new one
(also known as Levenshtein Distance) and we apply those operations.

Animations themselves are automatically added by the RecyclerView
containing the chips.

Videos:
 - Before: http://go/aa-carousel-before
 - After: http://go/aa-carousel-after

Change-Id: Iaaf78e3a8280fead319dea338bd2a0eb5042aff6
Reviewed-on: https://chromium-review.googlesource.com/c/1491618
Commit-Queue: Jordan Demeulenaere <jdemeulenaere@chromium.org>
Reviewed-by: default avatarStephane Zermatten <szermatt@chromium.org>
Cr-Commit-Position: refs/heads/master@{#636769}
parent 573ba3d1
......@@ -238,8 +238,14 @@ class AutofillAssistantUiController implements AssistantCoordinator.Delegate {
}
private void setChips(AssistantCarouselModel model, List<AssistantChip> chips) {
model.getChipsModel().set(chips);
model.set(ALIGNMENT, computeAlignment(chips));
// We apply the minimum set of operations on the current chips to transform it in the target
// list of chips. When testing for chip equivalence, we only compare their type and text but
// all substitutions will still be applied so we are sure we display the given {@code chips}
// with their associated callbacks.
EditDistance.transform(model.getChipsModel(), chips,
(a, b) -> a.getType() == b.getType() && a.getText().equals(b.getText()));
}
@CalledByNative
......
......@@ -9,6 +9,7 @@ import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.util.TypedValue;
import android.view.View;
......@@ -36,10 +37,6 @@ public class AssistantCarouselCoordinator {
mView = new RecyclerView(context);
mView.setLayoutManager(mLayoutManager);
mView.addItemDecoration(new SpaceItemDecoration(context));
mView.getItemAnimator().setAddDuration(0);
mView.getItemAnimator().setChangeDuration(0);
mView.getItemAnimator().setMoveDuration(0);
mView.getItemAnimator().setRemoveDuration(0);
mView.setAdapter(new RecyclerViewAdapter<>(
new SimpleRecyclerViewMcp<>(model.getChipsModel(), AssistantChip::getType,
AssistantChipViewHolder::bind),
......@@ -125,7 +122,9 @@ public class AssistantCarouselCoordinator {
@Override
public void getItemOffsets(
Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (mCentered && parent.getAdapter().getItemCount() == 1) {
// We use RecyclerView.State#getItemCount() as it returns the correct value when the
// carousel is being animated.
if (mCentered && state.getItemCount() == 1) {
// We have one view and want it horizontally centered. By this time the parent
// measured width is correct (as it matches its parent), but we need to explicitly
// measure how big the chip view wants to be.
......@@ -133,7 +132,7 @@ public class AssistantCarouselCoordinator {
view.measure(
View.MeasureSpec.makeMeasureSpec(availableWidth, View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec(
parent.getMeasuredHeight(), View.MeasureSpec.AT_MOST));
parent.getMeasuredHeight(), View.MeasureSpec.UNSPECIFIED));
int margin = (availableWidth - view.getMeasuredWidth()) / 2;
outRect.left = margin;
......@@ -142,6 +141,18 @@ public class AssistantCarouselCoordinator {
}
int position = parent.getChildAdapterPosition(view);
// If old position != NO_POSITION, it means the carousel is being animated and we should
// use that position in our logic.
ViewHolder viewHolder = parent.getChildViewHolder(view);
if (viewHolder != null && viewHolder.getOldPosition() != RecyclerView.NO_POSITION) {
position = viewHolder.getOldPosition();
}
if (position == RecyclerView.NO_POSITION) {
return;
}
int left;
int right;
if (position == 0) {
......@@ -150,7 +161,7 @@ public class AssistantCarouselCoordinator {
left = mInnerSpacePx;
}
if (position == parent.getAdapter().getItemCount() - 1) {
if (position == state.getItemCount() - 1) {
right = mOuterSpacePx;
} else {
right = mInnerSpacePx;
......
......@@ -149,6 +149,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantMetrics.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantPreferencesUtil.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantUiController.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/EditDistance.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/FeedbackContext.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantCarouselCoordinator.java",
"java/src/org/chromium/chrome/browser/autofill_assistant/carousel/AssistantCarouselModel.java",
......@@ -1941,6 +1942,7 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetViewTest.java",
"javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetModernViewTest.java",
"javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantUiTest.java",
"javatests/src/org/chromium/chrome/browser/autofill_assistant/EditDistanceTest.java",
"javatests/src/org/chromium/chrome/browser/banners/AppBannerManagerTest.java",
"javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkBridgeTest.java",
"javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkModelTest.java",
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.autofill_assistant;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.verify;
import android.support.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.chromium.ui.modelutil.ListModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Unit test suite for {@link EditDistance}.
*/
@RunWith(JUnit4.class)
public class EditDistanceTest {
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Test
@SmallTest
public void testEmptySource() {
ListModel<Integer> spiedListModel = createSpiedListModel();
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).add(0, 3);
inOrder.verify(spiedListModel).add(0, 2);
inOrder.verify(spiedListModel).add(0, 1);
}
@Test
@SmallTest
public void testEmptyTarget() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Collections.emptyList());
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).removeAt(2);
inOrder.verify(spiedListModel).removeAt(1);
inOrder.verify(spiedListModel).removeAt(0);
}
@Test
@SmallTest
public void testEmptySourceAndEmptyTarget() {
testTransformation(createSpiedListModel(), Collections.emptyList());
}
@Test
@SmallTest
public void testEqualSourceAndTarget() {
testTransformation(createSpiedListModel(1, 2, 3), Arrays.asList(1, 2, 3));
}
@Test
@SmallTest
public void testInsertBeginning() {
ListModel<Integer> spiedListModel = createSpiedListModel(2, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).add(0, 1);
}
@Test
@SmallTest
public void testInsertMiddle() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).add(1, 2);
}
@Test
@SmallTest
public void testInsertEnd() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).add(2, 3);
}
@Test
@SmallTest
public void testDeleteBeginning() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(2, 3));
verify(spiedListModel).removeAt(0);
}
@Test
@SmallTest
public void testDeleteMiddle() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(1, 3));
verify(spiedListModel).removeAt(1);
}
@Test
@SmallTest
public void testDeleteEnd() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2));
verify(spiedListModel).removeAt(2);
}
@Test
@SmallTest
public void testSubstitutionBeginning() {
ListModel<Integer> spiedListModel = createSpiedListModel(0, 2, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).update(0, 1);
}
@Test
@SmallTest
public void testSubstitutionMiddle() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 0, 3);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).update(1, 2);
}
@Test
@SmallTest
public void testSubstitutionEnd() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 0);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3));
verify(spiedListModel).update(2, 3);
}
@Test
@SmallTest
public void testMultipleInsert() {
ListModel<Integer> spiedListModel = createSpiedListModel(3, 6);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).add(2, 8);
inOrder.verify(spiedListModel).add(2, 7);
inOrder.verify(spiedListModel).add(1, 5);
inOrder.verify(spiedListModel).add(1, 4);
inOrder.verify(spiedListModel).add(0, 2);
inOrder.verify(spiedListModel).add(0, 1);
}
@Test
@SmallTest
public void testMultipleDelete() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3, 4, 5, 6, 7, 8);
testTransformation(spiedListModel, Arrays.asList(3, 6));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).removeAt(7);
inOrder.verify(spiedListModel).removeAt(6);
inOrder.verify(spiedListModel).removeAt(4);
inOrder.verify(spiedListModel).removeAt(3);
inOrder.verify(spiedListModel).removeAt(1);
inOrder.verify(spiedListModel).removeAt(0);
}
@Test
@SmallTest
public void testMultipleSubstitutions() {
ListModel<Integer> spiedListModel = createSpiedListModel(0, 0, 3, 0, 0, 6, 0, 0);
testTransformation(spiedListModel, Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8));
verify(spiedListModel).update(0, 1);
verify(spiedListModel).update(1, 2);
verify(spiedListModel).update(3, 4);
verify(spiedListModel).update(4, 5);
verify(spiedListModel).update(6, 7);
verify(spiedListModel).update(7, 8);
}
@Test
@SmallTest
public void testPopAndAppend() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(2, 3, 4));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).add(3, 4);
inOrder.verify(spiedListModel).removeAt(0);
}
@Test
@SmallTest
public void testAppendAndPop() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3);
testTransformation(spiedListModel, Arrays.asList(0, 1, 2));
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).removeAt(2);
inOrder.verify(spiedListModel).add(0, 0);
}
@Test
@SmallTest
public void testMixedOperations() {
ListModel<Integer> spiedListModel = createSpiedListModel(1, 2, 3, 4, 5, 6);
testTransformation(spiedListModel, Arrays.asList(2, 8, 4, 5, 7, 6));
// 0-cost substitutions.
verify(spiedListModel).update(1, 2);
verify(spiedListModel).update(3, 4);
verify(spiedListModel).update(4, 5);
verify(spiedListModel).update(5, 6);
verify(spiedListModel).update(2, 8);
InOrder inOrder = inOrder(spiedListModel);
inOrder.verify(spiedListModel).add(5, 7);
inOrder.verify(spiedListModel).removeAt(0);
}
private void testTransformation(ListModel<Integer> source, List<Integer> target) {
EditDistance.transform(source, target, Integer::equals);
List<Integer> sourceList = new ArrayList<>();
for (Integer value : source) {
sourceList.add(value);
}
Assert.assertEquals(target, sourceList);
}
private static ListModel<Integer> createSpiedListModel(Integer... values) {
ListModel<Integer> model = new ListModel<>();
model.set(values);
return Mockito.spy(model);
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment