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