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