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