Merge changes from topic "fix-expressive-design-preference-group" into main

* changes:
  [Expressive design] preference group - fix preference highlight
  Fix expressive design preference group
This commit is contained in:
David Liu
2024-11-06 20:47:59 +00:00
committed by Android (Google) Code Review
3 changed files with 134 additions and 89 deletions

View File

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