Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 730ea972 authored by Fan Zhang's avatar Fan Zhang
Browse files

Search highlight polish

- Blink when starting highlight
- Extend highlight to 15 seconds
- Fade out when stopping highlight

Bug: 73313161
Test: visual
Change-Id: Ie6c3d640566f2eecc501d4c4f96df512171ff4b6
parent 8e6d5ad2
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -16,5 +16,5 @@
  -->

<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>
 No newline at end of file
+82 −13
Original line number Diff line number Diff line
@@ -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) {
    @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");
    }
}
+29 −3
Original line number Diff line number Diff line
@@ -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