This cl fixes two kinds of issues. 1) Highlight two different preferences while clicking a search result. 2) Preference only is highlighted one time. For the first problem, we don't allow to reuse view holder in the highlight scenario, and then allow it later. The root cause of second problem is scrolling to target preference needs x milliseconds. Thus, we should wait a few milliseconds, and then start to highlight preference. Test: Test some search results, and see the correct behavior Fix: 186060148 Fix: 186010165 Fix: 187886982 Change-Id: I7df3e34efe39ee386fe9ce91d7d6c52cf390e2e7
240 lines
9.3 KiB
Java
240 lines
9.3 KiB
Java
/*
|
|
* Copyright (C) 2018 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.
|
|
*/
|
|
|
|
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.os.Bundle;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
import android.util.TypedValue;
|
|
import android.view.View;
|
|
|
|
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;
|
|
|
|
import com.android.settings.R;
|
|
import com.android.settings.SettingsPreferenceFragment;
|
|
|
|
import com.google.android.material.appbar.AppBarLayout;
|
|
|
|
public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter {
|
|
|
|
private static final String TAG = "HighlightableAdapter";
|
|
@VisibleForTesting
|
|
static final long DELAY_COLLAPSE_DURATION_MILLIS = 300L;
|
|
@VisibleForTesting
|
|
static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L;
|
|
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 mNormalBackgroundRes;
|
|
private final 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.
|
|
*/
|
|
public static void adjustInitialExpandedChildCount(SettingsPreferenceFragment host) {
|
|
if (host == null) {
|
|
return;
|
|
}
|
|
final PreferenceScreen screen = host.getPreferenceScreen();
|
|
if (screen == null) {
|
|
return;
|
|
}
|
|
final Bundle arguments = host.getArguments();
|
|
if (arguments != null) {
|
|
final String highlightKey = arguments.getString(EXTRA_FRAGMENT_ARG_KEY);
|
|
if (!TextUtils.isEmpty(highlightKey)) {
|
|
// Has highlight row - expand everything
|
|
screen.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
|
|
return;
|
|
}
|
|
}
|
|
|
|
final int initialCount = host.getInitialExpandedChildCount();
|
|
if (initialCount <= 0) {
|
|
return;
|
|
}
|
|
screen.setInitialExpandedChildrenCount(initialCount);
|
|
}
|
|
|
|
public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, String key,
|
|
boolean highlightRequested) {
|
|
super(preferenceGroup);
|
|
mHighlightKey = key;
|
|
mHighlightRequested = highlightRequested;
|
|
final Context context = preferenceGroup.getContext();
|
|
final TypedValue outValue = new TypedValue();
|
|
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
|
|
outValue, true /* resolveRefs */);
|
|
mNormalBackgroundRes = outValue.resourceId;
|
|
mHighlightColor = context.getColor(R.color.preference_highligh_color);
|
|
}
|
|
|
|
@Override
|
|
public void onBindViewHolder(PreferenceViewHolder holder, int position) {
|
|
super.onBindViewHolder(holder, position);
|
|
updateBackground(holder, position);
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void updateBackground(PreferenceViewHolder holder, int position) {
|
|
View v = holder.itemView;
|
|
if (position == mHighlightPosition
|
|
&& (mHighlightKey != null
|
|
&& TextUtils.equals(mHighlightKey, getItem(position).getKey()))) {
|
|
// This position should be highlighted. If it's highlighted before - skip animation.
|
|
addHighlightBackground(holder, !mFadeInAnimated);
|
|
} 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 */);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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)) {
|
|
return;
|
|
}
|
|
final int position = getPreferenceAdapterPosition(mHighlightKey);
|
|
if (position < 0) {
|
|
return;
|
|
}
|
|
|
|
// Collapse app bar after 300 milliseconds.
|
|
if (appBarLayout != null) {
|
|
root.postDelayed(() -> {
|
|
appBarLayout.setExpanded(false, true);
|
|
}, DELAY_COLLAPSE_DURATION_MILLIS);
|
|
}
|
|
|
|
// Scroll to correct position after 600 milliseconds.
|
|
root.postDelayed(() -> {
|
|
mHighlightRequested = true;
|
|
// Remove the animator to avoid a RecyclerView crash.
|
|
recyclerView.setItemAnimator(null);
|
|
recyclerView.smoothScrollToPosition(position);
|
|
mHighlightPosition = position;
|
|
}, DELAY_HIGHLIGHT_DURATION_MILLIS);
|
|
|
|
// Highlight preference after 900 milliseconds.
|
|
root.postDelayed(() -> {
|
|
notifyItemChanged(position);
|
|
}, DELAY_COLLAPSE_DURATION_MILLIS + DELAY_HIGHLIGHT_DURATION_MILLIS);
|
|
}
|
|
|
|
public boolean isHighlightRequested() {
|
|
return mHighlightRequested;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void requestRemoveHighlightDelayed(PreferenceViewHolder holder) {
|
|
final View v = holder.itemView;
|
|
v.postDelayed(() -> {
|
|
mHighlightPosition = RecyclerView.NO_POSITION;
|
|
removeHighlightBackground(holder, true /* animate */);
|
|
}, HIGHLIGHT_DURATION);
|
|
}
|
|
|
|
private void addHighlightBackground(PreferenceViewHolder holder, boolean animate) {
|
|
final View v = holder.itemView;
|
|
v.setTag(R.id.preference_highlighted, true);
|
|
if (!animate) {
|
|
v.setBackgroundColor(mHighlightColor);
|
|
Log.d(TAG, "AddHighlight: Not animation requested - setting highlight background");
|
|
requestRemoveHighlightDelayed(holder);
|
|
return;
|
|
}
|
|
mFadeInAnimated = true;
|
|
final int colorFrom = mNormalBackgroundRes;
|
|
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");
|
|
holder.setIsRecyclable(false);
|
|
requestRemoveHighlightDelayed(holder);
|
|
}
|
|
|
|
private void removeHighlightBackground(PreferenceViewHolder holder, boolean animate) {
|
|
final View v = holder.itemView;
|
|
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 = mNormalBackgroundRes;
|
|
|
|
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);
|
|
holder.setIsRecyclable(true);
|
|
}
|
|
});
|
|
colorAnimation.start();
|
|
Log.d(TAG, "Starting fade out animation");
|
|
}
|
|
}
|