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.customization.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