1 package com.android.systemui.qs;
2 
3 import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.LEFT;
4 import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.RIGHT;
5 
6 import android.content.Context;
7 import android.content.res.ColorStateList;
8 import android.content.res.Configuration;
9 import android.content.res.Resources;
10 import android.content.res.TypedArray;
11 import android.graphics.drawable.Animatable2;
12 import android.graphics.drawable.AnimatedVectorDrawable;
13 import android.graphics.drawable.Drawable;
14 import android.util.AttributeSet;
15 import android.util.Log;
16 import android.view.KeyEvent;
17 import android.view.View;
18 import android.view.ViewGroup;
19 import android.widget.ImageView;
20 
21 import androidx.annotation.IntDef;
22 import androidx.annotation.NonNull;
23 
24 import com.android.settingslib.Utils;
25 import com.android.systemui.res.R;
26 
27 import java.util.ArrayList;
28 
29 /**
30  * Page indicator for using with pageable layouts
31  *
32  * Supports {@code android.R.attr.tint}. If missing, it will use the current accent color.
33  */
34 public class PageIndicator extends ViewGroup {
35 
36     private static final String TAG = "PageIndicator";
37     private static final boolean DEBUG = false;
38 
39     private static final long ANIMATION_DURATION = 250;
40 
41     private static final float MINOR_ALPHA = .42f;
42 
43     private final ArrayList<Integer> mQueuedPositions = new ArrayList<>();
44 
45     private int mPageIndicatorWidth;
46     private int mPageIndicatorHeight;
47     private int mPageDotWidth;
48     private @NonNull ColorStateList mTint;
49 
50     private int mPosition = -1;
51     private boolean mAnimating;
52     private PageScrollActionListener mPageScrollActionListener;
53 
54     private final Animatable2.AnimationCallback mAnimationCallback =
55             new Animatable2.AnimationCallback() {
56 
57                 @Override
58                 public void onAnimationEnd(Drawable drawable) {
59                     super.onAnimationEnd(drawable);
60                     if (DEBUG) Log.d(TAG, "onAnimationEnd - queued: " + mQueuedPositions.size());
61                     if (drawable instanceof AnimatedVectorDrawable) {
62                         ((AnimatedVectorDrawable) drawable).unregisterAnimationCallback(
63                                 mAnimationCallback);
64                     }
65                     mAnimating = false;
66                     if (mQueuedPositions.size() != 0) {
67                         setPosition(mQueuedPositions.remove(0));
68                     }
69                 }
70             };
71 
PageIndicator(Context context, AttributeSet attrs)72     public PageIndicator(Context context, AttributeSet attrs) {
73         super(context, attrs);
74 
75         TypedArray array = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.tint});
76         if (array.hasValue(0)) {
77             mTint = array.getColorStateList(0);
78         } else {
79             mTint = Utils.getColorAccent(context);
80         }
81         array.recycle();
82 
83         Resources res = context.getResources();
84         mPageIndicatorWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_width);
85         mPageIndicatorHeight = res.getDimensionPixelSize(R.dimen.qs_page_indicator_height);
86         mPageDotWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_dot_width);
87         LeftRightArrowPressedListener arrowListener =
88                 LeftRightArrowPressedListener.createAndRegisterListenerForView(this);
89         arrowListener.setArrowKeyPressedListener(keyCode -> {
90             if (mPageScrollActionListener != null) {
91                 int swipeDirection = keyCode == KeyEvent.KEYCODE_DPAD_LEFT ? LEFT : RIGHT;
92                 mPageScrollActionListener.onScrollActionTriggered(swipeDirection);
93             }
94         });
95     }
96 
97     @Override
onConfigurationChanged(Configuration newConfig)98     protected void onConfigurationChanged(Configuration newConfig) {
99         super.onConfigurationChanged(newConfig);
100         updateResources();
101     }
102 
updateResources()103     private void updateResources() {
104         Resources res = getResources();
105         boolean changed = false;
106         int pageIndicatorWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_width);
107         if (pageIndicatorWidth != mPageIndicatorWidth) {
108             mPageIndicatorWidth = pageIndicatorWidth;
109             changed = true;
110         }
111         int pageIndicatorHeight = res.getDimensionPixelSize(R.dimen.qs_page_indicator_height);
112         if (pageIndicatorHeight != mPageIndicatorHeight) {
113             mPageIndicatorHeight = pageIndicatorHeight;
114             changed = true;
115         }
116         int pageIndicatorDotWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_dot_width);
117         if (pageIndicatorDotWidth != mPageDotWidth) {
118             mPageDotWidth = pageIndicatorDotWidth;
119             changed = true;
120         }
121         if (changed) {
122             invalidate();
123         }
124     }
125 
setNumPages(int numPages)126     public void setNumPages(int numPages) {
127         setVisibility(numPages > 1 ? View.VISIBLE : View.GONE);
128         int childCount = getChildCount();
129         // We're checking if the width needs to be updated as it's possible that the number of pages
130         // was changed while the page indicator was not visible, automatically skipping onMeasure.
131         if (numPages == childCount && calculateWidth(childCount) == getMeasuredWidth()) {
132             return;
133         }
134         if (mAnimating) {
135             Log.w(TAG, "setNumPages during animation");
136         }
137         while (numPages < getChildCount()) {
138             removeViewAt(getChildCount() - 1);
139         }
140         while (numPages > getChildCount()) {
141             ImageView v = new ImageView(mContext);
142             v.setImageResource(R.drawable.minor_a_b);
143             v.setImageTintList(mTint);
144             addView(v, new LayoutParams(mPageIndicatorWidth, mPageIndicatorHeight));
145         }
146         // Refresh state.
147         setIndex(mPosition >> 1);
148         requestLayout();
149     }
150 
151     /**
152      * @return the current tint list for this view.
153      */
154     @NonNull
getTintList()155     public ColorStateList getTintList() {
156         return mTint;
157     }
158 
159     /**
160      * Set the color for this view.
161      * <br>
162      * Calling this will change the color of the current view and any new dots that are added to it.
163      * @param color the new color
164      */
setTintList(@onNull ColorStateList color)165     public void setTintList(@NonNull ColorStateList color) {
166         if (color.equals(mTint)) {
167             return;
168         }
169         mTint = color;
170         final int N = getChildCount();
171         for (int i = 0; i < N; i++) {
172             View v = getChildAt(i);
173             if (v instanceof ImageView) {
174                 ((ImageView) v).setImageTintList(mTint);
175             }
176         }
177     }
178 
setLocation(float location)179     public void setLocation(float location) {
180         int index = (int) location;
181         setContentDescription(getContext().getString(R.string.accessibility_quick_settings_page,
182                 (index + 1), getChildCount()));
183         int position = index << 1 | ((location != index) ? 1 : 0);
184         if (DEBUG) Log.d(TAG, "setLocation " + location + " " + index + " " + position);
185 
186         int lastPosition = mPosition;
187         if (mQueuedPositions.size() != 0) {
188             lastPosition = mQueuedPositions.get(mQueuedPositions.size() - 1);
189         }
190         if (position == lastPosition) return;
191         if (mAnimating) {
192             if (DEBUG) Log.d(TAG, "Queueing transition to " + Integer.toHexString(position));
193             mQueuedPositions.add(position);
194             return;
195         }
196 
197         setPosition(position);
198     }
199 
setPosition(int position)200     private void setPosition(int position) {
201         if (isVisibleToUser() && Math.abs(mPosition - position) == 1) {
202             animate(mPosition, position);
203         } else {
204             if (DEBUG) Log.d(TAG, "Skipping animation " + isVisibleToUser() + " " + mPosition
205                     + " " + position);
206             setIndex(position >> 1);
207         }
208         mPosition = position;
209     }
210 
setIndex(int index)211     private void setIndex(int index) {
212         final int N = getChildCount();
213         for (int i = 0; i < N; i++) {
214             ImageView v = (ImageView) getChildAt(i);
215             // Clear out any animation positioning.
216             v.setTranslationX(0);
217             v.setImageResource(R.drawable.major_a_b);
218             v.setAlpha(getAlpha(i == index));
219         }
220     }
221 
animate(int from, int to)222     private void animate(int from, int to) {
223         if (DEBUG) Log.d(TAG, "Animating from " + Integer.toHexString(from) + " to "
224                 + Integer.toHexString(to));
225         int fromIndex = from >> 1;
226         int toIndex = to >> 1;
227 
228         // Set the position of everything, then we will manually control the two views involved
229         // in the animation.
230         setIndex(fromIndex);
231 
232         boolean fromTransition = (from & 1) != 0;
233         boolean isAState = fromTransition ? from > to : from < to;
234         int firstIndex = Math.min(fromIndex, toIndex);
235         int secondIndex = Math.max(fromIndex, toIndex);
236         if (secondIndex == firstIndex) {
237             secondIndex++;
238         }
239         ImageView first = (ImageView) getChildAt(firstIndex);
240         ImageView second = (ImageView) getChildAt(secondIndex);
241         if (first == null || second == null) {
242             // may happen during reInflation or other weird cases
243             return;
244         }
245         // Lay the two views on top of each other.
246         second.setTranslationX(first.getX() - second.getX());
247 
248         playAnimation(first, getTransition(fromTransition, isAState, false));
249         first.setAlpha(getAlpha(false));
250 
251         playAnimation(second, getTransition(fromTransition, isAState, true));
252         second.setAlpha(getAlpha(true));
253 
254         mAnimating = true;
255     }
256 
257     private float getAlpha(boolean isMajor) {
258         return isMajor ? 1 : MINOR_ALPHA;
259     }
260 
261     private void playAnimation(ImageView imageView, int res) {
262         final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) getContext().getDrawable(res);
263         imageView.setImageDrawable(avd);
264         avd.forceAnimationOnUI();
265         avd.registerAnimationCallback(mAnimationCallback);
266         avd.start();
267     }
268 
269     private int getTransition(boolean fromB, boolean isMajorAState, boolean isMajor) {
270         if (isMajor) {
271             if (fromB) {
272                 if (isMajorAState) {
273                     return R.drawable.major_b_a_animation;
274                 } else {
275                     return R.drawable.major_b_c_animation;
276                 }
277             } else {
278                 if (isMajorAState) {
279                     return R.drawable.major_a_b_animation;
280                 } else {
281                     return R.drawable.major_c_b_animation;
282                 }
283             }
284         } else {
285             if (fromB) {
286                 if (isMajorAState) {
287                     return R.drawable.minor_b_c_animation;
288                 } else {
289                     return R.drawable.minor_b_a_animation;
290                 }
291             } else {
292                 if (isMajorAState) {
293                     return R.drawable.minor_c_b_animation;
294                 } else {
295                     return R.drawable.minor_a_b_animation;
296                 }
297             }
298         }
299     }
300 
301     private int calculateWidth(int numPages) {
302         return (mPageIndicatorWidth - mPageDotWidth) * (numPages - 1) + mPageDotWidth;
303     }
304 
305     @Override
306     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
307         final int N = getChildCount();
308         if (N == 0) {
309             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
310             return;
311         }
312         final int widthChildSpec = MeasureSpec.makeMeasureSpec(mPageIndicatorWidth,
313                 MeasureSpec.EXACTLY);
314         final int heightChildSpec = MeasureSpec.makeMeasureSpec(mPageIndicatorHeight,
315                 MeasureSpec.EXACTLY);
316         for (int i = 0; i < N; i++) {
317             getChildAt(i).measure(widthChildSpec, heightChildSpec);
318         }
319         int width = calculateWidth(N);
320         setMeasuredDimension(width, mPageIndicatorHeight);
321     }
322 
323     @Override
324     protected void onLayout(boolean changed, int l, int t, int r, int b) {
325         final int N = getChildCount();
326         if (N == 0) {
327             return;
328         }
329         for (int i = 0; i < N; i++) {
330             int left = (mPageIndicatorWidth - mPageDotWidth) * i;
331             getChildAt(i).layout(left, 0, mPageIndicatorWidth + left, mPageIndicatorHeight);
332         }
333     }
334 
335     void setPageScrollActionListener(PageScrollActionListener listener) {
336         mPageScrollActionListener = listener;
337     }
338 
339     interface PageScrollActionListener {
340 
341         @IntDef({LEFT, RIGHT})
342         @interface Direction { }
343 
344         int LEFT = 0;
345         int RIGHT = 1;
346 
347         void onScrollActionTriggered(@Direction int swipeDirection);
348     }
349 }
350