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