From 1fc4af8e5c8fcc6b3fd2447528d8258fee8daf62 Mon Sep 17 00:00:00 2001 From: David Liu Date: Sat, 26 Oct 2024 06:37:40 +0000 Subject: [PATCH 1/2] Fix expressive design preference group Apply it to Settings DashboardFragment to make all settings support this. Bug: 376137920 Test: atest HighlightablePreferenceGroupAdapterTest Flag: EXEMPT resource only update Change-Id: I9220c13444244a082441aefc1962c2a394638001 --- .../settings/widget/HighlightablePreferenceGroupAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java b/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java index 4eb30d29355..8a749dc01d9 100644 --- a/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java +++ b/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java @@ -33,7 +33,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceGroup; -import androidx.preference.PreferenceGroupAdapter; import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; import androidx.recyclerview.widget.RecyclerView; @@ -42,10 +41,11 @@ 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.google.android.material.appbar.AppBarLayout; -public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter { +public class HighlightablePreferenceGroupAdapter extends SettingsPreferenceGroupAdapter { private static final String TAG = "HighlightableAdapter"; @VisibleForTesting From 7e625884e377e7ec5a71ab751d814d4c3e9e597e Mon Sep 17 00:00:00 2001 From: David Liu Date: Tue, 29 Oct 2024 00:30:23 +0000 Subject: [PATCH 2/2] [Expressive design] preference group - fix preference highlight Support preference highlight feature Bug: 376137920 Test: atest HighlightablePreferenceGroupAdapterTest Flag: EXEMPT resource only update Change-Id: I017f292778c984c0a8a207b837144fb9e344e9a8 --- .../preference_background_highlighted.xml | 24 +++ .../HighlightablePreferenceGroupAdapter.java | 184 ++++++++++-------- ...ghlightablePreferenceGroupAdapterTest.java | 11 +- 3 files changed, 132 insertions(+), 87 deletions(-) create mode 100644 res/drawable/preference_background_highlighted.xml diff --git a/res/drawable/preference_background_highlighted.xml b/res/drawable/preference_background_highlighted.xml new file mode 100644 index 00000000000..1d98ced4cce --- /dev/null +++ b/res/drawable/preference_background_highlighted.xml @@ -0,0 +1,24 @@ + + + + + + + + \ 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 8a749dc01d9..68b44695264 100644 --- a/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java +++ b/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java @@ -29,9 +29,11 @@ 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.PreferenceScreen; import androidx.preference.PreferenceViewHolder; @@ -42,40 +44,34 @@ 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 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. - *

- * Initial expanded child count will be ignored if: - * 1. fragment contains request to highlight a particular row. - * 2. count value is invalid. + * + *

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) { @@ -102,7 +98,8 @@ public class HighlightablePreferenceGroupAdapter extends SettingsPreferenceGroup screen.setInitialExpandedChildrenCount(initialCount); } - public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, + public HighlightablePreferenceGroupAdapter( + @NonNull PreferenceGroup preferenceGroup, @Nullable String key, boolean highlightRequested) { super(preferenceGroup); @@ -110,14 +107,12 @@ public class HighlightablePreferenceGroupAdapter extends SettingsPreferenceGroup 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); } @@ -125,22 +120,23 @@ public class HighlightablePreferenceGroupAdapter extends SettingsPreferenceGroup @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)) { @@ -155,21 +151,24 @@ public class HighlightablePreferenceGroupAdapter extends SettingsPreferenceGroup 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(() -> { - if (ensureHighlightPosition()) { - recyclerView.smoothScrollToPosition(mHighlightPosition); - highlightAndFocusTargetItem(recyclerView, mHighlightPosition); - } - }, AccessibilityUtil.isTouchExploreEnabled(mContext) - ? DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y : DELAY_HIGHLIGHT_DURATION_MILLIS); + root.postDelayed( + () -> { + if (ensureHighlightPosition()) { + recyclerView.smoothScrollToPosition(mHighlightPosition); + highlightAndFocusTargetItem(recyclerView, mHighlightPosition); + } + }, + AccessibilityUtil.isTouchExploreEnabled(mContext) + ? DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y + : DELAY_HIGHLIGHT_DURATION_MILLIS); } private void highlightAndFocusTargetItem(RecyclerView recyclerView, int highlightPosition) { @@ -178,20 +177,23 @@ public class HighlightablePreferenceGroupAdapter extends SettingsPreferenceGroup 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() { - @Override - public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { - if (newState == RecyclerView.SCROLL_STATE_IDLE) { - notifyItemChanged(mHighlightPosition); - ViewHolder target = recyclerView - .findViewHolderForAdapterPosition(highlightPosition); - if (target != null) { - target.itemView.requestFocus(); + recyclerView.addOnScrollListener( + new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged( + @NonNull RecyclerView recyclerView, int newState) { + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + notifyItemChanged(mHighlightPosition); + ViewHolder target = + recyclerView.findViewHolderForAdapterPosition( + highlightPosition); + if (target != null) { + target.itemView.requestFocus(); + } + recyclerView.removeOnScrollListener(this); + } } - recyclerView.removeOnScrollListener(this); - } - } - }); + }); } } @@ -218,44 +220,54 @@ public class HighlightablePreferenceGroupAdapter extends SettingsPreferenceGroup } @VisibleForTesting - void requestRemoveHighlightDelayed(PreferenceViewHolder holder) { + void requestRemoveHighlightDelayed(PreferenceViewHolder holder, int position) { final View v = holder.itemView; - v.postDelayed(() -> { - mHighlightPosition = RecyclerView.NO_POSITION; - removeHighlightBackground(holder, true /* animate */); - }, HIGHLIGHT_DURATION); + v.postDelayed( + () -> { + mHighlightPosition = RecyclerView.NO_POSITION; + 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; } @@ -265,25 +277,33 @@ public class HighlightablePreferenceGroupAdapter extends SettingsPreferenceGroup 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() { - @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); - holder.setIsRecyclable(true); - } - }); + animator -> v.setBackgroundResource((int) animator.getAnimatedValue())); + colorAnimation.addListener( + new AnimatorListenerAdapter() { + @Override + 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; + } + } } diff --git a/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java b/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java index 03684ad88a5..b5dfc315895 100644 --- a/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java +++ b/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java @@ -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 @@ -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 @@ -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); } }