From 730ea972d2045e43aa998bae7a4b87cb8a5c0dd9 Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Fri, 23 Feb 2018 10:36:04 -0800 Subject: [PATCH] Search highlight polish - Blink when starting highlight - Extend highlight to 15 seconds - Fade out when stopping highlight Bug: 73313161 Test: visual Change-Id: Ie6c3d640566f2eecc501d4c4f96df512171ff4b6 --- res/color/preference_highligh_color.xml | 2 +- .../HighlightablePreferenceGroupAdapter.java | 95 ++++++++++++++++--- ...ghlightablePreferenceGroupAdapterTest.java | 32 ++++++- 3 files changed, 112 insertions(+), 17 deletions(-) diff --git a/res/color/preference_highligh_color.xml b/res/color/preference_highligh_color.xml index 0a8f7706dc1..f8d54d470e7 100644 --- a/res/color/preference_highligh_color.xml +++ b/res/color/preference_highligh_color.xml @@ -16,5 +16,5 @@ --> - + \ No newline at end of file diff --git a/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java b/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java index cad11b70044..d2deda18508 100644 --- a/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java +++ b/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java @@ -18,7 +18,12 @@ package com.android.settings.widget; import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; import android.content.Context; +import android.graphics.Color; import android.os.Bundle; import android.support.annotation.VisibleForTesting; import android.support.v7.preference.PreferenceGroup; @@ -27,6 +32,7 @@ import android.support.v7.preference.PreferenceScreen; import android.support.v7.preference.PreferenceViewHolder; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; +import android.util.Log; import android.util.TypedValue; import android.view.View; @@ -35,14 +41,20 @@ import com.android.settings.SettingsPreferenceFragment; public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter { + private static final String TAG = "HighlightableAdapter"; @VisibleForTesting static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L; - private static final long HIGHLIGHT_DURATION = 5000L; + private static final long HIGHLIGHT_DURATION = 15000L; + private static final long HIGHLIGHT_FADE_OUT_DURATION = 500L; + private static final long HIGHLIGHT_FADE_IN_DURATION = 200L; + + @VisibleForTesting + final int mHighlightColor; + @VisibleForTesting + boolean mFadeInAnimated; - private final int mHighlightColor; private final int mNormalBackgroundRes; private final String mHighlightKey; - private boolean mHighlightRequested; private int mHighlightPosition = RecyclerView.NO_POSITION; @@ -102,14 +114,11 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter void updateBackground(PreferenceViewHolder holder, int position) { View v = holder.itemView; if (position == mHighlightPosition) { - v.setBackgroundColor(mHighlightColor); - v.setTag(R.id.preference_highlighted, true); - v.postDelayed(() -> { - mHighlightPosition = RecyclerView.NO_POSITION; - removeHighlightBackground(v); - }, HIGHLIGHT_DURATION); + // This position should be highlighted. If it's highlighted before - skip animation. + addHighlightBackground(v, !mFadeInAnimated); } else if (Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) { - removeHighlightBackground(v); + // View with highlight is reused for a view that should not have highlight + removeHighlightBackground(v, false /* animate */); } } @@ -123,7 +132,7 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter return; } mHighlightRequested = true; - recyclerView.getLayoutManager().scrollToPosition(position); + recyclerView.smoothScrollToPosition(position); mHighlightPosition = position; notifyItemChanged(position); }, DELAY_HIGHLIGHT_DURATION_MILLIS); @@ -133,8 +142,68 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter return mHighlightRequested; } - private void removeHighlightBackground(View v) { - v.setBackgroundResource(mNormalBackgroundRes); + @VisibleForTesting + void requestRemoveHighlightDelayed(View v) { + v.postDelayed(() -> { + mHighlightPosition = RecyclerView.NO_POSITION; + removeHighlightBackground(v, true /* animate */); + }, HIGHLIGHT_DURATION); + } + + private void addHighlightBackground(View v, boolean animate) { + v.setTag(R.id.preference_highlighted, true); + if (!animate) { + v.setBackgroundColor(mHighlightColor); + Log.d(TAG, "AddHighlight: Not animation requested - setting highlight background"); + requestRemoveHighlightDelayed(v); + return; + } + mFadeInAnimated = true; + final int colorFrom = Color.WHITE; + final int colorTo = mHighlightColor; + final ValueAnimator fadeInLoop = ValueAnimator.ofObject( + new ArgbEvaluator(), colorFrom, colorTo); + fadeInLoop.setDuration(HIGHLIGHT_FADE_IN_DURATION); + fadeInLoop.addUpdateListener( + animator -> v.setBackgroundColor((int) animator.getAnimatedValue())); + fadeInLoop.setRepeatMode(ValueAnimator.REVERSE); + fadeInLoop.setRepeatCount(4); + fadeInLoop.start(); + Log.d(TAG, "AddHighlight: starting fade in animation"); + requestRemoveHighlightDelayed(v); + } + + private void removeHighlightBackground(View v, boolean animate) { + if (!animate) { + v.setTag(R.id.preference_highlighted, false); + v.setBackgroundResource(mNormalBackgroundRes); + Log.d(TAG, "RemoveHighlight: No animation requested - setting normal background"); + return; + } + + if (!Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) { + // Not highlighted, no-op + Log.d(TAG, "RemoveHighlight: Not highlighted - skipping"); + return; + } + int colorFrom = mHighlightColor; + int colorTo = Color.WHITE; + v.setTag(R.id.preference_highlighted, false); + final ValueAnimator colorAnimation = ValueAnimator.ofObject( + new ArgbEvaluator(), colorFrom, colorTo); + colorAnimation.setDuration(HIGHLIGHT_FADE_OUT_DURATION); + colorAnimation.addUpdateListener( + animator -> v.setBackgroundColor((int) animator.getAnimatedValue())); + colorAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Animation complete - the background is now white. Change to mNormalBackgroundRes + // so it is white and has ripple on touch. + v.setBackgroundResource(mNormalBackgroundRes); + } + }); + colorAnimation.start(); + Log.d(TAG, "Starting fade out animation"); } } diff --git a/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java b/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java index fd5800fa3b7..8869c3078d2 100644 --- a/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java +++ b/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java @@ -21,6 +21,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -71,8 +73,8 @@ public class HighlightablePreferenceGroupAdapterTest { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; when(mPreferenceCatetory.getContext()).thenReturn(mContext); - mAdapter = new HighlightablePreferenceGroupAdapter(mPreferenceCatetory, TEST_KEY, - false /* highlighted*/); + mAdapter = spy(new HighlightablePreferenceGroupAdapter(mPreferenceCatetory, TEST_KEY, + false /* highlighted*/)); mViewHolder = PreferenceViewHolder.createInstanceForTests( View.inflate(mContext, R.layout.app_preference_item, null)); } @@ -163,12 +165,36 @@ public class HighlightablePreferenceGroupAdapterTest { } @Test - public void updateBackground_highlight_shouldChangeBackgroundAndSetHighlightedTag() { + public void updateBackground_highlight_shouldAnimateBackgroundAndSetHighlightedTag() { ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10); + assertThat(mAdapter.mFadeInAnimated).isFalse(); mAdapter.updateBackground(mViewHolder, 10); + + assertThat(mAdapter.mFadeInAnimated).isTrue(); assertThat(mViewHolder.itemView.getBackground()).isInstanceOf(ColorDrawable.class); assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isEqualTo(true); + verify(mAdapter).requestRemoveHighlightDelayed(mViewHolder.itemView); + } + + @Test + public void updateBackgroundTwice_highlight_shouldAnimateOnce() { + ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10); + ReflectionHelpers.setField(mViewHolder, "itemView", spy(mViewHolder.itemView)); + assertThat(mAdapter.mFadeInAnimated).isFalse(); + mAdapter.updateBackground(mViewHolder, 10); + // mFadeInAnimated change from false to true - indicating background change is scheduled + // through animation. + assertThat(mAdapter.mFadeInAnimated).isTrue(); + // remove highlight should be requested. + verify(mAdapter).requestRemoveHighlightDelayed(mViewHolder.itemView); + + ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10); + mAdapter.updateBackground(mViewHolder, 10); + // only sets background color once - if it's animation this would be called many times + verify(mViewHolder.itemView).setBackgroundColor(mAdapter.mHighlightColor); + // remove highlight should be requested. + verify(mAdapter, times(2)).requestRemoveHighlightDelayed(mViewHolder.itemView); } @Test