• 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.graphics.Color;
27 import android.os.Bundle;
28 import android.text.TextUtils;
29 import android.util.Log;
30 import android.util.TypedValue;
31 import android.view.View;
32 
33 import androidx.annotation.VisibleForTesting;
34 import androidx.preference.PreferenceGroup;
35 import androidx.preference.PreferenceGroupAdapter;
36 import androidx.preference.PreferenceScreen;
37 import androidx.preference.PreferenceViewHolder;
38 import androidx.recyclerview.widget.RecyclerView;
39 
40 import com.android.settings.R;
41 import com.android.settings.SettingsPreferenceFragment;
42 
43 public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter {
44 
45     private static final String TAG = "HighlightableAdapter";
46     @VisibleForTesting
47     static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L;
48     private static final long HIGHLIGHT_DURATION = 15000L;
49     private static final long HIGHLIGHT_FADE_OUT_DURATION = 500L;
50     private static final long HIGHLIGHT_FADE_IN_DURATION = 200L;
51 
52     @VisibleForTesting
53     final int mHighlightColor;
54     @VisibleForTesting
55     boolean mFadeInAnimated;
56 
57     private final int mNormalBackgroundRes;
58     private final String mHighlightKey;
59     private boolean mHighlightRequested;
60     private int mHighlightPosition = RecyclerView.NO_POSITION;
61 
62 
63     /**
64      * Tries to override initial expanded child count.
65      * <p/>
66      * Initial expanded child count will be ignored if:
67      * 1. fragment contains request to highlight a particular row.
68      * 2. count value is invalid.
69      */
adjustInitialExpandedChildCount(SettingsPreferenceFragment host)70     public static void adjustInitialExpandedChildCount(SettingsPreferenceFragment host) {
71         if (host == null) {
72             return;
73         }
74         final PreferenceScreen screen = host.getPreferenceScreen();
75         if (screen == null) {
76             return;
77         }
78         final Bundle arguments = host.getArguments();
79         if (arguments != null) {
80             final String highlightKey = arguments.getString(EXTRA_FRAGMENT_ARG_KEY);
81             if (!TextUtils.isEmpty(highlightKey)) {
82                 // Has highlight row - expand everything
83                 screen.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
84                 return;
85             }
86         }
87 
88         final int initialCount = host.getInitialExpandedChildCount();
89         if (initialCount <= 0) {
90             return;
91         }
92         screen.setInitialExpandedChildrenCount(initialCount);
93     }
94 
HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, String key, boolean highlightRequested)95     public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, String key,
96             boolean highlightRequested) {
97         super(preferenceGroup);
98         mHighlightKey = key;
99         mHighlightRequested = highlightRequested;
100         final Context context = preferenceGroup.getContext();
101         final TypedValue outValue = new TypedValue();
102         context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
103                 outValue, true /* resolveRefs */);
104         mNormalBackgroundRes = outValue.resourceId;
105         mHighlightColor = context.getColor(R.color.preference_highligh_color);
106     }
107 
108     @Override
onBindViewHolder(PreferenceViewHolder holder, int position)109     public void onBindViewHolder(PreferenceViewHolder holder, int position) {
110         super.onBindViewHolder(holder, position);
111         updateBackground(holder, position);
112     }
113 
114     @VisibleForTesting
updateBackground(PreferenceViewHolder holder, int position)115     void updateBackground(PreferenceViewHolder holder, int position) {
116         View v = holder.itemView;
117         if (position == mHighlightPosition) {
118             // This position should be highlighted. If it's highlighted before - skip animation.
119             addHighlightBackground(v, !mFadeInAnimated);
120         } else if (Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
121             // View with highlight is reused for a view that should not have highlight
122             removeHighlightBackground(v, false /* animate */);
123         }
124     }
125 
requestHighlight(View root, RecyclerView recyclerView)126     public void requestHighlight(View root, RecyclerView recyclerView) {
127         if (mHighlightRequested || recyclerView == null || TextUtils.isEmpty(mHighlightKey)) {
128             return;
129         }
130         root.postDelayed(() -> {
131             final int position = getPreferenceAdapterPosition(mHighlightKey);
132             if (position < 0) {
133                 return;
134             }
135             mHighlightRequested = true;
136             recyclerView.smoothScrollToPosition(position);
137             mHighlightPosition = position;
138             notifyItemChanged(position);
139         }, DELAY_HIGHLIGHT_DURATION_MILLIS);
140     }
141 
isHighlightRequested()142     public boolean isHighlightRequested() {
143         return mHighlightRequested;
144     }
145 
146     @VisibleForTesting
requestRemoveHighlightDelayed(View v)147     void requestRemoveHighlightDelayed(View v) {
148         v.postDelayed(() -> {
149             mHighlightPosition = RecyclerView.NO_POSITION;
150             removeHighlightBackground(v, true /* animate */);
151         }, HIGHLIGHT_DURATION);
152     }
153 
addHighlightBackground(View v, boolean animate)154     private void addHighlightBackground(View v, boolean animate) {
155         v.setTag(R.id.preference_highlighted, true);
156         if (!animate) {
157             v.setBackgroundColor(mHighlightColor);
158             Log.d(TAG, "AddHighlight: Not animation requested - setting highlight background");
159             requestRemoveHighlightDelayed(v);
160             return;
161         }
162         mFadeInAnimated = true;
163         final int colorFrom = Color.WHITE;
164         final int colorTo = mHighlightColor;
165         final ValueAnimator fadeInLoop = ValueAnimator.ofObject(
166                 new ArgbEvaluator(), colorFrom, colorTo);
167         fadeInLoop.setDuration(HIGHLIGHT_FADE_IN_DURATION);
168         fadeInLoop.addUpdateListener(
169                 animator -> v.setBackgroundColor((int) animator.getAnimatedValue()));
170         fadeInLoop.setRepeatMode(ValueAnimator.REVERSE);
171         fadeInLoop.setRepeatCount(4);
172         fadeInLoop.start();
173         Log.d(TAG, "AddHighlight: starting fade in animation");
174         requestRemoveHighlightDelayed(v);
175     }
176 
removeHighlightBackground(View v, boolean animate)177     private void removeHighlightBackground(View v, boolean animate) {
178         if (!animate) {
179             v.setTag(R.id.preference_highlighted, false);
180             v.setBackgroundResource(mNormalBackgroundRes);
181             Log.d(TAG, "RemoveHighlight: No animation requested - setting normal background");
182             return;
183         }
184 
185         if (!Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
186             // Not highlighted, no-op
187             Log.d(TAG, "RemoveHighlight: Not highlighted - skipping");
188             return;
189         }
190         int colorFrom = mHighlightColor;
191         int colorTo = Color.WHITE;
192 
193         v.setTag(R.id.preference_highlighted, false);
194         final ValueAnimator colorAnimation = ValueAnimator.ofObject(
195                 new ArgbEvaluator(), colorFrom, colorTo);
196         colorAnimation.setDuration(HIGHLIGHT_FADE_OUT_DURATION);
197         colorAnimation.addUpdateListener(
198                 animator -> v.setBackgroundColor((int) animator.getAnimatedValue()));
199         colorAnimation.addListener(new AnimatorListenerAdapter() {
200             @Override
201             public void onAnimationEnd(Animator animation) {
202                 // Animation complete - the background is now white. Change to mNormalBackgroundRes
203                 // so it is white and has ripple on touch.
204                 v.setBackgroundResource(mNormalBackgroundRes);
205             }
206         });
207         colorAnimation.start();
208         Log.d(TAG, "Starting fade out animation");
209     }
210 }
211