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 package com.android.wallpaper.widget;
17 
18 import android.content.Context;
19 import android.content.res.ColorStateList;
20 import android.content.res.TypedArray;
21 import android.graphics.drawable.Animatable2;
22 import android.graphics.drawable.AnimatedVectorDrawable;
23 import android.graphics.drawable.Drawable;
24 import android.util.AttributeSet;
25 import android.util.Log;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.widget.ImageView;
29 
30 import com.android.wallpaper.R;
31 
32 import java.lang.reflect.InvocationTargetException;
33 import java.lang.reflect.Method;
34 import java.util.ArrayList;
35 
36 /**
37  * Page indicator widget, based on QS's page indicator:
38  *
39  * Based on QS PageIndicator
40  * Path: frameworks/base/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
41  */
42 public class PageIndicator extends ViewGroup {
43 
44     private static final String TAG = "PageIndicator";
45     private static final boolean DEBUG = false;
46 
47     // The size of a single dot in relation to the whole animation.
48     private static final float SINGLE_SCALE = .4f;
49 
50     static final float MINOR_ALPHA = .42f;
51 
52     private final ArrayList<Integer> mQueuedPositions = new ArrayList<>();
53 
54     private final int mPageIndicatorWidth;
55     private final int mPageIndicatorHeight;
56     private final int mPageDotWidth;
57 
58     private int mPosition = -1;
59     private boolean mAnimating;
60 
61     private static Method sMethodForceAnimationOnUI = null;
62     private final Animatable2.AnimationCallback mAnimationCallback =
63             new Animatable2.AnimationCallback() {
64 
65                 @Override
66                 public void onAnimationEnd(Drawable drawable) {
67                     super.onAnimationEnd(drawable);
68                     if (drawable instanceof AnimatedVectorDrawable) {
69                         ((AnimatedVectorDrawable) drawable).unregisterAnimationCallback(
70                                 mAnimationCallback);
71                     }
72                     if (DEBUG) {
73                         Log.d(TAG, "onAnimationEnd - queued: " + mQueuedPositions.size());
74                     }
75                     mAnimating = false;
76                     if (mQueuedPositions.size() != 0) {
77                         setPosition(mQueuedPositions.remove(0));
78                     }
79                 }
80             };
81 
82 
PageIndicator(Context context, AttributeSet attrs)83     public PageIndicator(Context context, AttributeSet attrs) {
84         super(context, attrs);
85         mPageIndicatorWidth =
86                 (int) context.getResources().getDimension(R.dimen.preview_indicator_width);
87         mPageIndicatorHeight =
88                 (int) context.getResources().getDimension(R.dimen.preview_indicator_height);
89         mPageDotWidth = (int) (mPageIndicatorWidth * SINGLE_SCALE);
90     }
91 
setNumPages(int numPages)92     public void setNumPages(int numPages) {
93         setVisibility(numPages > 1 ? View.VISIBLE : View.INVISIBLE);
94         if (mAnimating) {
95             Log.w(TAG, "setNumPages during animation");
96         }
97         while (numPages < getChildCount()) {
98             removeViewAt(getChildCount() - 1);
99         }
100         TypedArray array = getContext().obtainStyledAttributes(
101                 new int[]{android.R.attr.colorControlActivated});
102         int color = array.getColor(0, 0);
103         array.recycle();
104         while (numPages > getChildCount()) {
105             ImageView v = new ImageView(getContext());
106             v.setImageResource(R.drawable.minor_a_b);
107             v.setImageTintList(ColorStateList.valueOf(color));
108             addView(v, new LayoutParams(mPageIndicatorWidth, mPageIndicatorHeight));
109         }
110         // Refresh state.
111         setIndex(mPosition >> 1);
112     }
113 
setLocation(float location)114     public void setLocation(float location) {
115         int index = (int) location;
116         setContentDescription(getContext().getString(R.string.accessibility_preview_pager,
117                 (index + 1), getChildCount()));
118         int position = index << 1 | ((location != index) ? 1 : 0);
119         if (DEBUG) {
120             Log.d(TAG, "setLocation " + location + " " + index + " " + position);
121         }
122 
123         int lastPosition = mPosition;
124         if (mQueuedPositions.size() != 0) {
125             lastPosition = mQueuedPositions.get(mQueuedPositions.size() - 1);
126         }
127         if (DEBUG) {
128             Log.d(TAG, position + " " + lastPosition);
129         }
130         if (position == lastPosition) return;
131         if (mAnimating) {
132             if (DEBUG) {
133                 Log.d(TAG, "Queueing transition to " + Integer.toHexString(position));
134             }
135             mQueuedPositions.add(position);
136             return;
137         }
138 
139         setPosition(position);
140     }
141 
setPosition(int position)142     private void setPosition(int position) {
143         if (mPosition >= 0 && Math.abs(mPosition - position) == 1) {
144             animate(mPosition, position);
145         } else {
146             if (DEBUG) {
147                 Log.d(TAG, "Skipping animation " + mPosition
148                         + " " + position);
149             }
150             setIndex(position >> 1);
151         }
152         mPosition = position;
153     }
154 
setIndex(int index)155     private void setIndex(int index) {
156         final int N = getChildCount();
157         for (int i = 0; i < N; i++) {
158             ImageView v = (ImageView) getChildAt(i);
159             // Clear out any animation positioning.
160             v.setTranslationX(0);
161             v.setImageResource(R.drawable.major_a_b);
162             v.setAlpha(getAlpha(i == index));
163         }
164     }
165 
animate(int from, int to)166     private void animate(int from, int to) {
167         if (DEBUG) {
168             Log.d(TAG, "Animating from " + Integer.toHexString(from) + " to "
169                     + Integer.toHexString(to));
170         }
171         int fromIndex = from >> 1;
172         int toIndex = to >> 1;
173 
174         // Set the position of everything, then we will manually control the two views involved
175         // in the animation.
176         setIndex(fromIndex);
177 
178         boolean fromTransition = (from & 1) != 0;
179         boolean isAState = fromTransition ? from > to : from < to;
180         int firstIndex = Math.min(fromIndex, toIndex);
181         int secondIndex = Math.max(fromIndex, toIndex);
182         if (secondIndex == firstIndex) {
183             secondIndex++;
184         }
185         ImageView first = (ImageView) getChildAt(firstIndex);
186         ImageView second = (ImageView) getChildAt(secondIndex);
187         if (first == null || second == null) {
188             // may happen during reInflation or other weird cases
189             return;
190         }
191         // Lay the two views on top of each other.
192         second.setTranslationX(first.getX() - second.getX());
193 
194         playAnimation(first, getTransition(fromTransition, isAState, false));
195         first.setAlpha(getAlpha(false));
196 
197         playAnimation(second, getTransition(fromTransition, isAState, true));
198         second.setAlpha(getAlpha(true));
199 
200         mAnimating = true;
201     }
202 
203     private float getAlpha(boolean isMajor) {
204         return isMajor ? 1 : MINOR_ALPHA;
205     }
206 
207     private void playAnimation(ImageView imageView, int res) {
208         Drawable drawable = getContext().getDrawable(res);
209         if (!(drawable instanceof AnimatedVectorDrawable)) {
210             return;
211         }
212         final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) drawable;
213         imageView.setImageDrawable(avd);
214         try {
215             forceAnimationOnUI(avd);
216         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
217             Log.e(TAG, "Catch an exception in playAnimation", e);
218         }
219         avd.registerAnimationCallback(mAnimationCallback);
220         avd.start();
221     }
222 
223     private void forceAnimationOnUI(AnimatedVectorDrawable avd)
224             throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
225         if (sMethodForceAnimationOnUI == null) {
226             sMethodForceAnimationOnUI = AnimatedVectorDrawable.class.getMethod(
227                     "forceAnimationOnUI");
228         }
229         if (sMethodForceAnimationOnUI != null) {
230             sMethodForceAnimationOnUI.invoke(avd);
231         }
232     }
233 
234     private int getTransition(boolean fromB, boolean isMajorAState, boolean isMajor) {
235         if (isMajor) {
236             if (fromB) {
237                 if (isMajorAState) {
238                     return R.drawable.major_b_a_animation;
239                 } else {
240                     return R.drawable.major_b_c_animation;
241                 }
242             } else {
243                 if (isMajorAState) {
244                     return R.drawable.major_a_b_animation;
245                 } else {
246                     return R.drawable.major_c_b_animation;
247                 }
248             }
249         } else {
250             if (fromB) {
251                 if (isMajorAState) {
252                     return R.drawable.minor_b_c_animation;
253                 } else {
254                     return R.drawable.minor_b_a_animation;
255                 }
256             } else {
257                 if (isMajorAState) {
258                     return R.drawable.minor_c_b_animation;
259                 } else {
260                     return R.drawable.minor_a_b_animation;
261                 }
262             }
263         }
264     }
265 
266     @Override
267     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
268         final int N = getChildCount();
269         if (N == 0) {
270             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
271             return;
272         }
273         final int widthChildSpec = MeasureSpec.makeMeasureSpec(mPageIndicatorWidth,
274                 MeasureSpec.EXACTLY);
275         final int heightChildSpec = MeasureSpec.makeMeasureSpec(mPageIndicatorHeight,
276                 MeasureSpec.EXACTLY);
277         for (int i = 0; i < N; i++) {
278             getChildAt(i).measure(widthChildSpec, heightChildSpec);
279         }
280         int width = (mPageIndicatorWidth - mPageDotWidth) * (N - 1) + mPageDotWidth;
281         setMeasuredDimension(width, mPageIndicatorHeight);
282     }
283 
284     @Override
285     protected void onLayout(boolean changed, int l, int t, int r, int b) {
286         final int N = getChildCount();
287         if (N == 0) {
288             return;
289         }
290         for (int i = 0; i < N; i++) {
291             int left = (mPageIndicatorWidth - mPageDotWidth) * i;
292             getChildAt(i).layout(left, 0, mPageIndicatorWidth + left, mPageIndicatorHeight);
293         }
294     }
295 }
296