Loading res/drawable/preference_background_highlighted.xml 0 → 100644 +24 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2014 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:colorControlHighlight"> <item> <shape android:shape="rectangle" android:tint="@color/preference_highlight_color"> </shape> </item> </ripple> No newline at end of file src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java +104 −84 Original line number Diff line number Diff line Loading @@ -29,11 +29,12 @@ import android.util.Log; import android.util.TypedValue; import android.view.View; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceGroupAdapter; import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; import androidx.recyclerview.widget.RecyclerView; Loading @@ -42,40 +43,35 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.accessibility.AccessibilityUtil; import com.android.settingslib.widget.SettingsPreferenceGroupAdapter; import com.android.settingslib.widget.SettingsThemeHelper; import com.google.android.material.appbar.AppBarLayout; public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter { public class HighlightablePreferenceGroupAdapter extends SettingsPreferenceGroupAdapter { private static final String TAG = "HighlightableAdapter"; @VisibleForTesting static final long DELAY_COLLAPSE_DURATION_MILLIS = 300L; @VisibleForTesting static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L; @VisibleForTesting static final long DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y = 300L; @VisibleForTesting static final long DELAY_COLLAPSE_DURATION_MILLIS = 300L; @VisibleForTesting static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L; @VisibleForTesting static final long DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y = 300L; 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; @VisibleForTesting @DrawableRes final int mHighlightBackgroundRes; @VisibleForTesting boolean mFadeInAnimated; private final Context mContext; private final int mNormalBackgroundRes; private final @DrawableRes int mNormalBackgroundRes; private final @Nullable String mHighlightKey; private boolean mHighlightRequested; private int mHighlightPosition = RecyclerView.NO_POSITION; /** * Tries to override initial expanded child count. * <p/> * Initial expanded child count will be ignored if: * 1. fragment contains request to highlight a particular row. * 2. count value is invalid. * * <p>Initial expanded child count will be ignored if: 1. fragment contains request to highlight * a particular row. 2. count value is invalid. */ public static void adjustInitialExpandedChildCount(SettingsPreferenceFragment host) { if (host == null) { Loading @@ -102,7 +98,8 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter screen.setInitialExpandedChildrenCount(initialCount); } public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, public HighlightablePreferenceGroupAdapter( @NonNull PreferenceGroup preferenceGroup, @Nullable String key, boolean highlightRequested) { super(preferenceGroup); Loading @@ -110,14 +107,12 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter mHighlightRequested = highlightRequested; mContext = preferenceGroup.getContext(); final TypedValue outValue = new TypedValue(); mContext.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true /* resolveRefs */); mNormalBackgroundRes = outValue.resourceId; mHighlightColor = mContext.getColor(R.color.preference_highlight_color); mNormalBackgroundRes = R.drawable.preference_background; mHighlightBackgroundRes = R.drawable.preference_background_highlighted; } @Override public void onBindViewHolder(PreferenceViewHolder holder, int position) { public void onBindViewHolder(@NonNull PreferenceViewHolder holder, int position) { super.onBindViewHolder(holder, position); updateBackground(holder, position); } Loading @@ -125,22 +120,23 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter @VisibleForTesting void updateBackground(PreferenceViewHolder holder, int position) { View v = holder.itemView; if (position == mHighlightPosition && (mHighlightKey != null && TextUtils.equals(mHighlightKey, getItem(position).getKey())) Preference preference = getItem(position); if (preference != null && position == mHighlightPosition && (mHighlightKey != null && TextUtils.equals(mHighlightKey, preference.getKey())) && v.isShown()) { // This position should be highlighted. If it's highlighted before - skip animation. v.requestAccessibilityFocus(); addHighlightBackground(holder, !mFadeInAnimated); addHighlightBackground(holder, !mFadeInAnimated, position); } else if (Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) { // View with highlight is reused for a view that should not have highlight removeHighlightBackground(holder, false /* animate */); removeHighlightBackground(holder, false /* animate */, position); } } /** * A function can highlight a specific setting in recycler view. * note: Before highlighting a setting, screen collapses tool bar with an animation. * A function can highlight a specific setting in recycler view. note: Before highlighting a * setting, screen collapses tool bar with an animation. */ public void requestHighlight(View root, RecyclerView recyclerView, AppBarLayout appBarLayout) { if (mHighlightRequested || recyclerView == null || TextUtils.isEmpty(mHighlightKey)) { Loading @@ -155,21 +151,24 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter mHighlightRequested = true; // Collapse app bar after 300 milliseconds. if (appBarLayout != null) { root.postDelayed(() -> { appBarLayout.setExpanded(false, true); }, DELAY_COLLAPSE_DURATION_MILLIS); root.postDelayed( () -> appBarLayout.setExpanded(false, true), DELAY_COLLAPSE_DURATION_MILLIS); } // Remove the animator as early as possible to avoid a RecyclerView crash. recyclerView.setItemAnimator(null); // Scroll to correct position after a short delay. root.postDelayed(() -> { root.postDelayed( () -> { if (ensureHighlightPosition()) { recyclerView.smoothScrollToPosition(mHighlightPosition); highlightAndFocusTargetItem(recyclerView, mHighlightPosition); } }, AccessibilityUtil.isTouchExploreEnabled(mContext) ? DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y : DELAY_HIGHLIGHT_DURATION_MILLIS); }, AccessibilityUtil.isTouchExploreEnabled(mContext) ? DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y : DELAY_HIGHLIGHT_DURATION_MILLIS); } private void highlightAndFocusTargetItem(RecyclerView recyclerView, int highlightPosition) { Loading @@ -178,13 +177,16 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter notifyItemChanged(mHighlightPosition); target.itemView.requestFocus(); } else { // otherwise we're about to scroll to that view (but we might not be scrolling yet) recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { recyclerView.addOnScrollListener( new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { public void onScrollStateChanged( @NonNull RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { notifyItemChanged(mHighlightPosition); ViewHolder target = recyclerView .findViewHolderForAdapterPosition(highlightPosition); ViewHolder target = recyclerView.findViewHolderForAdapterPosition( highlightPosition); if (target != null) { target.itemView.requestFocus(); } Loading Loading @@ -218,44 +220,54 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter } @VisibleForTesting void requestRemoveHighlightDelayed(PreferenceViewHolder holder) { void requestRemoveHighlightDelayed(PreferenceViewHolder holder, int position) { final View v = holder.itemView; v.postDelayed(() -> { v.postDelayed( () -> { mHighlightPosition = RecyclerView.NO_POSITION; removeHighlightBackground(holder, true /* animate */); }, HIGHLIGHT_DURATION); removeHighlightBackground(holder, true /* animate */, position); }, HIGHLIGHT_DURATION); } private void addHighlightBackground(PreferenceViewHolder holder, boolean animate) { private void addHighlightBackground( PreferenceViewHolder holder, boolean animate, int position) { final View v = holder.itemView; v.setTag(R.id.preference_highlighted, true); final int backgroundFrom = getBackgroundRes(position, false); final int backgroundTo = getBackgroundRes(position, true); if (!animate) { v.setBackgroundColor(mHighlightColor); v.setBackgroundResource(backgroundTo); Log.d(TAG, "AddHighlight: Not animation requested - setting highlight background"); requestRemoveHighlightDelayed(holder); requestRemoveHighlightDelayed(holder, position); return; } mFadeInAnimated = true; final int colorFrom = mNormalBackgroundRes; final int colorTo = mHighlightColor; final ValueAnimator fadeInLoop = ValueAnimator.ofObject( new ArgbEvaluator(), colorFrom, colorTo); // TODO(b/377561018): Fix fade-in animation final ValueAnimator fadeInLoop = ValueAnimator.ofObject(new ArgbEvaluator(), backgroundFrom, backgroundTo); fadeInLoop.setDuration(HIGHLIGHT_FADE_IN_DURATION); fadeInLoop.addUpdateListener( animator -> v.setBackgroundColor((int) animator.getAnimatedValue())); animator -> v.setBackgroundResource((int) animator.getAnimatedValue())); fadeInLoop.setRepeatMode(ValueAnimator.REVERSE); fadeInLoop.setRepeatCount(4); fadeInLoop.start(); Log.d(TAG, "AddHighlight: starting fade in animation"); holder.setIsRecyclable(false); requestRemoveHighlightDelayed(holder); requestRemoveHighlightDelayed(holder, position); } private void removeHighlightBackground(PreferenceViewHolder holder, boolean animate) { private void removeHighlightBackground( PreferenceViewHolder holder, boolean animate, int position) { final View v = holder.itemView; int backgroundFrom = getBackgroundRes(position, true); int backgroundTo = getBackgroundRes(position, false); if (!animate) { v.setTag(R.id.preference_highlighted, false); v.setBackgroundResource(mNormalBackgroundRes); v.setBackgroundResource(backgroundTo); Log.d(TAG, "RemoveHighlight: No animation requested - setting normal background"); return; } Loading @@ -265,25 +277,33 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter Log.d(TAG, "RemoveHighlight: Not highlighted - skipping"); return; } int colorFrom = mHighlightColor; int colorTo = mNormalBackgroundRes; v.setTag(R.id.preference_highlighted, false); final ValueAnimator colorAnimation = ValueAnimator.ofObject( new ArgbEvaluator(), colorFrom, colorTo); // TODO(b/377561018): Fix fade-out animation final ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), backgroundFrom, backgroundTo); colorAnimation.setDuration(HIGHLIGHT_FADE_OUT_DURATION); colorAnimation.addUpdateListener( animator -> v.setBackgroundColor((int) animator.getAnimatedValue())); colorAnimation.addListener(new AnimatorListenerAdapter() { animator -> v.setBackgroundResource((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); public void onAnimationEnd(@NonNull Animator animation) { // Animation complete - the background needs to be the target background. v.setBackgroundResource(backgroundTo); holder.setIsRecyclable(true); } }); colorAnimation.start(); Log.d(TAG, "Starting fade out animation"); } private @DrawableRes int getBackgroundRes(int position, boolean isHighlighted) { if (SettingsThemeHelper.isExpressiveTheme(mContext)) { Log.d(TAG, "[Expressive Theme] get rounded background, highlight = " + isHighlighted); return getRoundCornerDrawableRes(position, false, isHighlighted); } else { return (isHighlighted) ? mHighlightBackgroundRes : mNormalBackgroundRes; } } } tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java +6 −5 Original line number Diff line number Diff line Loading @@ -231,7 +231,7 @@ public class HighlightablePreferenceGroupAdapterTest { 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); verify(mAdapter).requestRemoveHighlightDelayed(mViewHolder, 10); } @Test Loading @@ -256,14 +256,14 @@ public class HighlightablePreferenceGroupAdapterTest { // through animation. assertThat(mAdapter.mFadeInAnimated).isTrue(); // remove highlight should be requested. verify(mAdapter).requestRemoveHighlightDelayed(mViewHolder); verify(mAdapter).requestRemoveHighlightDelayed(mViewHolder, 10); 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); verify(mViewHolder.itemView).setBackgroundResource(mAdapter.mHighlightBackgroundRes); // remove highlight should be requested. verify(mAdapter, times(2)).requestRemoveHighlightDelayed(mViewHolder); verify(mAdapter, times(2)).requestRemoveHighlightDelayed(mViewHolder, 10); } @Test Loading @@ -273,7 +273,8 @@ public class HighlightablePreferenceGroupAdapterTest { mAdapter.updateBackground(mViewHolder, 0); assertThat(mViewHolder.itemView.getBackground()).isNotInstanceOf(ColorDrawable.class); assertThat(mViewHolder.itemView.getBackground()) .isNotEqualTo(mAdapter.mHighlightBackgroundRes); assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isEqualTo(false); } } Loading
res/drawable/preference_background_highlighted.xml 0 → 100644 +24 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2014 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:colorControlHighlight"> <item> <shape android:shape="rectangle" android:tint="@color/preference_highlight_color"> </shape> </item> </ripple> No newline at end of file
src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java +104 −84 Original line number Diff line number Diff line Loading @@ -29,11 +29,12 @@ import android.util.Log; import android.util.TypedValue; import android.view.View; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceGroupAdapter; import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; import androidx.recyclerview.widget.RecyclerView; Loading @@ -42,40 +43,35 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.accessibility.AccessibilityUtil; import com.android.settingslib.widget.SettingsPreferenceGroupAdapter; import com.android.settingslib.widget.SettingsThemeHelper; import com.google.android.material.appbar.AppBarLayout; public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter { public class HighlightablePreferenceGroupAdapter extends SettingsPreferenceGroupAdapter { private static final String TAG = "HighlightableAdapter"; @VisibleForTesting static final long DELAY_COLLAPSE_DURATION_MILLIS = 300L; @VisibleForTesting static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L; @VisibleForTesting static final long DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y = 300L; @VisibleForTesting static final long DELAY_COLLAPSE_DURATION_MILLIS = 300L; @VisibleForTesting static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L; @VisibleForTesting static final long DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y = 300L; 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; @VisibleForTesting @DrawableRes final int mHighlightBackgroundRes; @VisibleForTesting boolean mFadeInAnimated; private final Context mContext; private final int mNormalBackgroundRes; private final @DrawableRes int mNormalBackgroundRes; private final @Nullable String mHighlightKey; private boolean mHighlightRequested; private int mHighlightPosition = RecyclerView.NO_POSITION; /** * Tries to override initial expanded child count. * <p/> * Initial expanded child count will be ignored if: * 1. fragment contains request to highlight a particular row. * 2. count value is invalid. * * <p>Initial expanded child count will be ignored if: 1. fragment contains request to highlight * a particular row. 2. count value is invalid. */ public static void adjustInitialExpandedChildCount(SettingsPreferenceFragment host) { if (host == null) { Loading @@ -102,7 +98,8 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter screen.setInitialExpandedChildrenCount(initialCount); } public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, public HighlightablePreferenceGroupAdapter( @NonNull PreferenceGroup preferenceGroup, @Nullable String key, boolean highlightRequested) { super(preferenceGroup); Loading @@ -110,14 +107,12 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter mHighlightRequested = highlightRequested; mContext = preferenceGroup.getContext(); final TypedValue outValue = new TypedValue(); mContext.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true /* resolveRefs */); mNormalBackgroundRes = outValue.resourceId; mHighlightColor = mContext.getColor(R.color.preference_highlight_color); mNormalBackgroundRes = R.drawable.preference_background; mHighlightBackgroundRes = R.drawable.preference_background_highlighted; } @Override public void onBindViewHolder(PreferenceViewHolder holder, int position) { public void onBindViewHolder(@NonNull PreferenceViewHolder holder, int position) { super.onBindViewHolder(holder, position); updateBackground(holder, position); } Loading @@ -125,22 +120,23 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter @VisibleForTesting void updateBackground(PreferenceViewHolder holder, int position) { View v = holder.itemView; if (position == mHighlightPosition && (mHighlightKey != null && TextUtils.equals(mHighlightKey, getItem(position).getKey())) Preference preference = getItem(position); if (preference != null && position == mHighlightPosition && (mHighlightKey != null && TextUtils.equals(mHighlightKey, preference.getKey())) && v.isShown()) { // This position should be highlighted. If it's highlighted before - skip animation. v.requestAccessibilityFocus(); addHighlightBackground(holder, !mFadeInAnimated); addHighlightBackground(holder, !mFadeInAnimated, position); } else if (Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) { // View with highlight is reused for a view that should not have highlight removeHighlightBackground(holder, false /* animate */); removeHighlightBackground(holder, false /* animate */, position); } } /** * A function can highlight a specific setting in recycler view. * note: Before highlighting a setting, screen collapses tool bar with an animation. * A function can highlight a specific setting in recycler view. note: Before highlighting a * setting, screen collapses tool bar with an animation. */ public void requestHighlight(View root, RecyclerView recyclerView, AppBarLayout appBarLayout) { if (mHighlightRequested || recyclerView == null || TextUtils.isEmpty(mHighlightKey)) { Loading @@ -155,21 +151,24 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter mHighlightRequested = true; // Collapse app bar after 300 milliseconds. if (appBarLayout != null) { root.postDelayed(() -> { appBarLayout.setExpanded(false, true); }, DELAY_COLLAPSE_DURATION_MILLIS); root.postDelayed( () -> appBarLayout.setExpanded(false, true), DELAY_COLLAPSE_DURATION_MILLIS); } // Remove the animator as early as possible to avoid a RecyclerView crash. recyclerView.setItemAnimator(null); // Scroll to correct position after a short delay. root.postDelayed(() -> { root.postDelayed( () -> { if (ensureHighlightPosition()) { recyclerView.smoothScrollToPosition(mHighlightPosition); highlightAndFocusTargetItem(recyclerView, mHighlightPosition); } }, AccessibilityUtil.isTouchExploreEnabled(mContext) ? DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y : DELAY_HIGHLIGHT_DURATION_MILLIS); }, AccessibilityUtil.isTouchExploreEnabled(mContext) ? DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y : DELAY_HIGHLIGHT_DURATION_MILLIS); } private void highlightAndFocusTargetItem(RecyclerView recyclerView, int highlightPosition) { Loading @@ -178,13 +177,16 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter notifyItemChanged(mHighlightPosition); target.itemView.requestFocus(); } else { // otherwise we're about to scroll to that view (but we might not be scrolling yet) recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { recyclerView.addOnScrollListener( new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { public void onScrollStateChanged( @NonNull RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { notifyItemChanged(mHighlightPosition); ViewHolder target = recyclerView .findViewHolderForAdapterPosition(highlightPosition); ViewHolder target = recyclerView.findViewHolderForAdapterPosition( highlightPosition); if (target != null) { target.itemView.requestFocus(); } Loading Loading @@ -218,44 +220,54 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter } @VisibleForTesting void requestRemoveHighlightDelayed(PreferenceViewHolder holder) { void requestRemoveHighlightDelayed(PreferenceViewHolder holder, int position) { final View v = holder.itemView; v.postDelayed(() -> { v.postDelayed( () -> { mHighlightPosition = RecyclerView.NO_POSITION; removeHighlightBackground(holder, true /* animate */); }, HIGHLIGHT_DURATION); removeHighlightBackground(holder, true /* animate */, position); }, HIGHLIGHT_DURATION); } private void addHighlightBackground(PreferenceViewHolder holder, boolean animate) { private void addHighlightBackground( PreferenceViewHolder holder, boolean animate, int position) { final View v = holder.itemView; v.setTag(R.id.preference_highlighted, true); final int backgroundFrom = getBackgroundRes(position, false); final int backgroundTo = getBackgroundRes(position, true); if (!animate) { v.setBackgroundColor(mHighlightColor); v.setBackgroundResource(backgroundTo); Log.d(TAG, "AddHighlight: Not animation requested - setting highlight background"); requestRemoveHighlightDelayed(holder); requestRemoveHighlightDelayed(holder, position); return; } mFadeInAnimated = true; final int colorFrom = mNormalBackgroundRes; final int colorTo = mHighlightColor; final ValueAnimator fadeInLoop = ValueAnimator.ofObject( new ArgbEvaluator(), colorFrom, colorTo); // TODO(b/377561018): Fix fade-in animation final ValueAnimator fadeInLoop = ValueAnimator.ofObject(new ArgbEvaluator(), backgroundFrom, backgroundTo); fadeInLoop.setDuration(HIGHLIGHT_FADE_IN_DURATION); fadeInLoop.addUpdateListener( animator -> v.setBackgroundColor((int) animator.getAnimatedValue())); animator -> v.setBackgroundResource((int) animator.getAnimatedValue())); fadeInLoop.setRepeatMode(ValueAnimator.REVERSE); fadeInLoop.setRepeatCount(4); fadeInLoop.start(); Log.d(TAG, "AddHighlight: starting fade in animation"); holder.setIsRecyclable(false); requestRemoveHighlightDelayed(holder); requestRemoveHighlightDelayed(holder, position); } private void removeHighlightBackground(PreferenceViewHolder holder, boolean animate) { private void removeHighlightBackground( PreferenceViewHolder holder, boolean animate, int position) { final View v = holder.itemView; int backgroundFrom = getBackgroundRes(position, true); int backgroundTo = getBackgroundRes(position, false); if (!animate) { v.setTag(R.id.preference_highlighted, false); v.setBackgroundResource(mNormalBackgroundRes); v.setBackgroundResource(backgroundTo); Log.d(TAG, "RemoveHighlight: No animation requested - setting normal background"); return; } Loading @@ -265,25 +277,33 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter Log.d(TAG, "RemoveHighlight: Not highlighted - skipping"); return; } int colorFrom = mHighlightColor; int colorTo = mNormalBackgroundRes; v.setTag(R.id.preference_highlighted, false); final ValueAnimator colorAnimation = ValueAnimator.ofObject( new ArgbEvaluator(), colorFrom, colorTo); // TODO(b/377561018): Fix fade-out animation final ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), backgroundFrom, backgroundTo); colorAnimation.setDuration(HIGHLIGHT_FADE_OUT_DURATION); colorAnimation.addUpdateListener( animator -> v.setBackgroundColor((int) animator.getAnimatedValue())); colorAnimation.addListener(new AnimatorListenerAdapter() { animator -> v.setBackgroundResource((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); public void onAnimationEnd(@NonNull Animator animation) { // Animation complete - the background needs to be the target background. v.setBackgroundResource(backgroundTo); holder.setIsRecyclable(true); } }); colorAnimation.start(); Log.d(TAG, "Starting fade out animation"); } private @DrawableRes int getBackgroundRes(int position, boolean isHighlighted) { if (SettingsThemeHelper.isExpressiveTheme(mContext)) { Log.d(TAG, "[Expressive Theme] get rounded background, highlight = " + isHighlighted); return getRoundCornerDrawableRes(position, false, isHighlighted); } else { return (isHighlighted) ? mHighlightBackgroundRes : mNormalBackgroundRes; } } }
tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java +6 −5 Original line number Diff line number Diff line Loading @@ -231,7 +231,7 @@ public class HighlightablePreferenceGroupAdapterTest { 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); verify(mAdapter).requestRemoveHighlightDelayed(mViewHolder, 10); } @Test Loading @@ -256,14 +256,14 @@ public class HighlightablePreferenceGroupAdapterTest { // through animation. assertThat(mAdapter.mFadeInAnimated).isTrue(); // remove highlight should be requested. verify(mAdapter).requestRemoveHighlightDelayed(mViewHolder); verify(mAdapter).requestRemoveHighlightDelayed(mViewHolder, 10); 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); verify(mViewHolder.itemView).setBackgroundResource(mAdapter.mHighlightBackgroundRes); // remove highlight should be requested. verify(mAdapter, times(2)).requestRemoveHighlightDelayed(mViewHolder); verify(mAdapter, times(2)).requestRemoveHighlightDelayed(mViewHolder, 10); } @Test Loading @@ -273,7 +273,8 @@ public class HighlightablePreferenceGroupAdapterTest { mAdapter.updateBackground(mViewHolder, 0); assertThat(mViewHolder.itemView.getBackground()).isNotInstanceOf(ColorDrawable.class); assertThat(mViewHolder.itemView.getBackground()) .isNotEqualTo(mAdapter.mHighlightBackgroundRes); assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isEqualTo(false); } }