Search highlight polish
- Blink when starting highlight - Extend highlight to 15 seconds - Fade out when stopping highlight Bug: 73313161 Test: visual Change-Id: Ie6c3d640566f2eecc501d4c4f96df512171ff4b6
This commit is contained in:
@@ -16,5 +16,5 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:alpha="0.1" android:color="?android:attr/colorAccent" />
|
<item android:alpha="0.26" android:color="?android:attr/colorAccent" />
|
||||||
</selector>
|
</selector>
|
@@ -18,7 +18,12 @@ package com.android.settings.widget;
|
|||||||
|
|
||||||
import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY;
|
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.content.Context;
|
||||||
|
import android.graphics.Color;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.VisibleForTesting;
|
import android.support.annotation.VisibleForTesting;
|
||||||
import android.support.v7.preference.PreferenceGroup;
|
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.preference.PreferenceViewHolder;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -35,14 +41,20 @@ import com.android.settings.SettingsPreferenceFragment;
|
|||||||
|
|
||||||
public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter {
|
public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter {
|
||||||
|
|
||||||
|
private static final String TAG = "HighlightableAdapter";
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L;
|
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 int mNormalBackgroundRes;
|
||||||
private final String mHighlightKey;
|
private final String mHighlightKey;
|
||||||
|
|
||||||
private boolean mHighlightRequested;
|
private boolean mHighlightRequested;
|
||||||
private int mHighlightPosition = RecyclerView.NO_POSITION;
|
private int mHighlightPosition = RecyclerView.NO_POSITION;
|
||||||
|
|
||||||
@@ -102,14 +114,11 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter
|
|||||||
void updateBackground(PreferenceViewHolder holder, int position) {
|
void updateBackground(PreferenceViewHolder holder, int position) {
|
||||||
View v = holder.itemView;
|
View v = holder.itemView;
|
||||||
if (position == mHighlightPosition) {
|
if (position == mHighlightPosition) {
|
||||||
v.setBackgroundColor(mHighlightColor);
|
// This position should be highlighted. If it's highlighted before - skip animation.
|
||||||
v.setTag(R.id.preference_highlighted, true);
|
addHighlightBackground(v, !mFadeInAnimated);
|
||||||
v.postDelayed(() -> {
|
|
||||||
mHighlightPosition = RecyclerView.NO_POSITION;
|
|
||||||
removeHighlightBackground(v);
|
|
||||||
}, HIGHLIGHT_DURATION);
|
|
||||||
} else if (Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
mHighlightRequested = true;
|
mHighlightRequested = true;
|
||||||
recyclerView.getLayoutManager().scrollToPosition(position);
|
recyclerView.smoothScrollToPosition(position);
|
||||||
mHighlightPosition = position;
|
mHighlightPosition = position;
|
||||||
notifyItemChanged(position);
|
notifyItemChanged(position);
|
||||||
}, DELAY_HIGHLIGHT_DURATION_MILLIS);
|
}, DELAY_HIGHLIGHT_DURATION_MILLIS);
|
||||||
@@ -133,8 +142,68 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter
|
|||||||
return mHighlightRequested;
|
return mHighlightRequested;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeHighlightBackground(View v) {
|
@VisibleForTesting
|
||||||
v.setBackgroundResource(mNormalBackgroundRes);
|
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);
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,8 @@ import static com.google.common.truth.Truth.assertThat;
|
|||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.mock;
|
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.verify;
|
||||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -71,8 +73,8 @@ public class HighlightablePreferenceGroupAdapterTest {
|
|||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
mContext = RuntimeEnvironment.application;
|
mContext = RuntimeEnvironment.application;
|
||||||
when(mPreferenceCatetory.getContext()).thenReturn(mContext);
|
when(mPreferenceCatetory.getContext()).thenReturn(mContext);
|
||||||
mAdapter = new HighlightablePreferenceGroupAdapter(mPreferenceCatetory, TEST_KEY,
|
mAdapter = spy(new HighlightablePreferenceGroupAdapter(mPreferenceCatetory, TEST_KEY,
|
||||||
false /* highlighted*/);
|
false /* highlighted*/));
|
||||||
mViewHolder = PreferenceViewHolder.createInstanceForTests(
|
mViewHolder = PreferenceViewHolder.createInstanceForTests(
|
||||||
View.inflate(mContext, R.layout.app_preference_item, null));
|
View.inflate(mContext, R.layout.app_preference_item, null));
|
||||||
}
|
}
|
||||||
@@ -163,12 +165,36 @@ public class HighlightablePreferenceGroupAdapterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void updateBackground_highlight_shouldChangeBackgroundAndSetHighlightedTag() {
|
public void updateBackground_highlight_shouldAnimateBackgroundAndSetHighlightedTag() {
|
||||||
ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
|
ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
|
||||||
|
assertThat(mAdapter.mFadeInAnimated).isFalse();
|
||||||
|
|
||||||
mAdapter.updateBackground(mViewHolder, 10);
|
mAdapter.updateBackground(mViewHolder, 10);
|
||||||
|
|
||||||
|
assertThat(mAdapter.mFadeInAnimated).isTrue();
|
||||||
assertThat(mViewHolder.itemView.getBackground()).isInstanceOf(ColorDrawable.class);
|
assertThat(mViewHolder.itemView.getBackground()).isInstanceOf(ColorDrawable.class);
|
||||||
assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isEqualTo(true);
|
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
|
@Test
|
||||||
|
Reference in New Issue
Block a user