• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.widget;
18 
19 import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.animation.ArgbEvaluator;
24 import android.animation.ValueAnimator;
25 import android.content.Context;
26 import android.os.Bundle;
27 import android.text.TextUtils;
28 import android.util.Log;
29 import android.util.TypedValue;
30 import android.view.View;
31 
32 import androidx.annotation.VisibleForTesting;
33 import androidx.preference.PreferenceGroup;
34 import androidx.preference.PreferenceGroupAdapter;
35 import androidx.preference.PreferenceScreen;
36 import androidx.preference.PreferenceViewHolder;
37 import androidx.recyclerview.widget.RecyclerView;
38 
39 import com.android.settings.R;
40 import com.android.settings.SettingsPreferenceFragment;
41 
42 import com.google.android.material.appbar.AppBarLayout;
43 
44 public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter {
45 
46     private static final String TAG = "HighlightableAdapter";
47     @VisibleForTesting
48     static final long DELAY_COLLAPSE_DURATION_MILLIS = 300L;
49     @VisibleForTesting
50     static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L;
51     private static final long HIGHLIGHT_DURATION = 15000L;
52     private static final long HIGHLIGHT_FADE_OUT_DURATION = 500L;
53     private static final long HIGHLIGHT_FADE_IN_DURATION = 200L;
54 
55     @VisibleForTesting
56     final int mHighlightColor;
57     @VisibleForTesting
58     boolean mFadeInAnimated;
59 
60     private final int mNormalBackgroundRes;
61     private final String mHighlightKey;
62     private boolean mHighlightRequested;
63     private int mHighlightPosition = RecyclerView.NO_POSITION;
64 
65 
66     /**
67      * Tries to override initial expanded child count.
68      * <p/>
69      * Initial expanded child count will be ignored if:
70      * 1. fragment contains request to highlight a particular row.
71      * 2. count value is invalid.
72      */
adjustInitialExpandedChildCount(SettingsPreferenceFragment host)73     public static void adjustInitialExpandedChildCount(SettingsPreferenceFragment host) {
74         if (host == null) {
75             return;
76         }
77         final PreferenceScreen screen = host.getPreferenceScreen();
78         if (screen == null) {
79             return;
80         }
81         final Bundle arguments = host.getArguments();
82         if (arguments != null) {
83             final String highlightKey = arguments.getString(EXTRA_FRAGMENT_ARG_KEY);
84             if (!TextUtils.isEmpty(highlightKey)) {
85                 // Has highlight row - expand everything
86                 screen.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
87                 return;
88             }
89         }
90 
91         final int initialCount = host.getInitialExpandedChildCount();
92         if (initialCount <= 0) {
93             return;
94         }
95         screen.setInitialExpandedChildrenCount(initialCount);
96     }
97 
HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, String key, boolean highlightRequested)98     public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, String key,
99             boolean highlightRequested) {
100         super(preferenceGroup);
101         mHighlightKey = key;
102         mHighlightRequested = highlightRequested;
103         final Context context = preferenceGroup.getContext();
104         final TypedValue outValue = new TypedValue();
105         context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
106                 outValue, true /* resolveRefs */);
107         mNormalBackgroundRes = outValue.resourceId;
108         mHighlightColor = context.getColor(R.color.preference_highligh_color);
109     }
110 
111     @Override
onBindViewHolder(PreferenceViewHolder holder, int position)112     public void onBindViewHolder(PreferenceViewHolder holder, int position) {
113         super.onBindViewHolder(holder, position);
114         updateBackground(holder, position);
115     }
116 
117     @VisibleForTesting
updateBackground(PreferenceViewHolder holder, int position)118     void updateBackground(PreferenceViewHolder holder, int position) {
119         View v = holder.itemView;
120         if (position == mHighlightPosition
121                 && (mHighlightKey != null
122                 && TextUtils.equals(mHighlightKey, getItem(position).getKey()))) {
123             // This position should be highlighted. If it's highlighted before - skip animation.
124             addHighlightBackground(holder, !mFadeInAnimated);
125         } else if (Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
126             // View with highlight is reused for a view that should not have highlight
127             removeHighlightBackground(holder, false /* animate */);
128         }
129     }
130 
131     /**
132      * A function can highlight a specific setting in recycler view.
133      * note: Before highlighting a setting, screen collapses tool bar with an animation.
134      */
requestHighlight(View root, RecyclerView recyclerView, AppBarLayout appBarLayout)135     public void requestHighlight(View root, RecyclerView recyclerView, AppBarLayout appBarLayout) {
136         if (mHighlightRequested || recyclerView == null || TextUtils.isEmpty(mHighlightKey)) {
137             return;
138         }
139         final int position = getPreferenceAdapterPosition(mHighlightKey);
140         if (position < 0) {
141             return;
142         }
143 
144         // Collapse app bar after 300 milliseconds.
145         if (appBarLayout != null) {
146             root.postDelayed(() -> {
147                 appBarLayout.setExpanded(false, true);
148             }, DELAY_COLLAPSE_DURATION_MILLIS);
149         }
150 
151         // Scroll to correct position after 600 milliseconds.
152         root.postDelayed(() -> {
153             mHighlightRequested = true;
154             // Remove the animator to avoid a RecyclerView crash.
155             recyclerView.setItemAnimator(null);
156             recyclerView.smoothScrollToPosition(position);
157             mHighlightPosition = position;
158         }, DELAY_HIGHLIGHT_DURATION_MILLIS);
159 
160         // Highlight preference after 900 milliseconds.
161         root.postDelayed(() -> {
162             notifyItemChanged(position);
163         }, DELAY_COLLAPSE_DURATION_MILLIS + DELAY_HIGHLIGHT_DURATION_MILLIS);
164     }
165 
isHighlightRequested()166     public boolean isHighlightRequested() {
167         return mHighlightRequested;
168     }
169 
170     @VisibleForTesting
requestRemoveHighlightDelayed(PreferenceViewHolder holder)171     void requestRemoveHighlightDelayed(PreferenceViewHolder holder) {
172         final View v = holder.itemView;
173         v.postDelayed(() -> {
174             mHighlightPosition = RecyclerView.NO_POSITION;
175             removeHighlightBackground(holder, true /* animate */);
176         }, HIGHLIGHT_DURATION);
177     }
178 
addHighlightBackground(PreferenceViewHolder holder, boolean animate)179     private void addHighlightBackground(PreferenceViewHolder holder, boolean animate) {
180         final View v = holder.itemView;
181         v.setTag(R.id.preference_highlighted, true);
182         if (!animate) {
183             v.setBackgroundColor(mHighlightColor);
184             Log.d(TAG, "AddHighlight: Not animation requested - setting highlight background");
185             requestRemoveHighlightDelayed(holder);
186             return;
187         }
188         mFadeInAnimated = true;
189         final int colorFrom = mNormalBackgroundRes;
190         final int colorTo = mHighlightColor;
191         final ValueAnimator fadeInLoop = ValueAnimator.ofObject(
192                 new ArgbEvaluator(), colorFrom, colorTo);
193         fadeInLoop.setDuration(HIGHLIGHT_FADE_IN_DURATION);
194         fadeInLoop.addUpdateListener(
195                 animator -> v.setBackgroundColor((int) animator.getAnimatedValue()));
196         fadeInLoop.setRepeatMode(ValueAnimator.REVERSE);
197         fadeInLoop.setRepeatCount(4);
198         fadeInLoop.start();
199         Log.d(TAG, "AddHighlight: starting fade in animation");
200         holder.setIsRecyclable(false);
201         requestRemoveHighlightDelayed(holder);
202     }
203 
removeHighlightBackground(PreferenceViewHolder holder, boolean animate)204     private void removeHighlightBackground(PreferenceViewHolder holder, boolean animate) {
205         final View v = holder.itemView;
206         if (!animate) {
207             v.setTag(R.id.preference_highlighted, false);
208             v.setBackgroundResource(mNormalBackgroundRes);
209             Log.d(TAG, "RemoveHighlight: No animation requested - setting normal background");
210             return;
211         }
212 
213         if (!Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
214             // Not highlighted, no-op
215             Log.d(TAG, "RemoveHighlight: Not highlighted - skipping");
216             return;
217         }
218         int colorFrom = mHighlightColor;
219         int colorTo = mNormalBackgroundRes;
220 
221         v.setTag(R.id.preference_highlighted, false);
222         final ValueAnimator colorAnimation = ValueAnimator.ofObject(
223                 new ArgbEvaluator(), colorFrom, colorTo);
224         colorAnimation.setDuration(HIGHLIGHT_FADE_OUT_DURATION);
225         colorAnimation.addUpdateListener(
226                 animator -> v.setBackgroundColor((int) animator.getAnimatedValue()));
227         colorAnimation.addListener(new AnimatorListenerAdapter() {
228             @Override
229             public void onAnimationEnd(Animator animation) {
230                 // Animation complete - the background is now white. Change to mNormalBackgroundRes
231                 // so it is white and has ripple on touch.
232                 v.setBackgroundResource(mNormalBackgroundRes);
233                 holder.setIsRecyclable(true);
234             }
235         });
236         colorAnimation.start();
237         Log.d(TAG, "Starting fade out animation");
238     }
239 }
240