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 @@
+
+
+
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 PreferenceGroupAdapter 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 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); } @@ -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)) { @@ -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(() -> { - 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 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() { - @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 PreferenceGroupAdapter } @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 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() { - @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); } }