1 package com.android.launcher3.pageindicators; 2 3 import android.animation.Animator; 4 import android.animation.AnimatorListenerAdapter; 5 import android.animation.ObjectAnimator; 6 import android.animation.ValueAnimator; 7 import android.content.Context; 8 import android.content.res.Resources; 9 import android.graphics.Canvas; 10 import android.graphics.Color; 11 import android.graphics.Paint; 12 import android.graphics.Rect; 13 import android.os.Handler; 14 import android.os.Looper; 15 import android.util.AttributeSet; 16 import android.util.Property; 17 import android.view.View; 18 import android.view.ViewConfiguration; 19 20 import com.android.launcher3.Insettable; 21 import com.android.launcher3.Launcher; 22 import com.android.launcher3.R; 23 import com.android.launcher3.Utilities; 24 import com.android.launcher3.util.Themes; 25 26 /** 27 * A PageIndicator that briefly shows a fraction of a line when moving between pages 28 * 29 * The fraction is 1 / number of pages and the position is based on the progress of the page scroll. 30 */ 31 public class WorkspacePageIndicator extends View implements Insettable, PageIndicator { 32 33 private static final int LINE_ANIMATE_DURATION = ViewConfiguration.getScrollBarFadeDuration(); 34 private static final int LINE_FADE_DELAY = ViewConfiguration.getScrollDefaultDelay(); 35 public static final int WHITE_ALPHA = (int) (0.70f * 255); 36 public static final int BLACK_ALPHA = (int) (0.65f * 255); 37 38 private static final int LINE_ALPHA_ANIMATOR_INDEX = 0; 39 private static final int NUM_PAGES_ANIMATOR_INDEX = 1; 40 private static final int TOTAL_SCROLL_ANIMATOR_INDEX = 2; 41 private static final int ANIMATOR_COUNT = 3; 42 43 private ValueAnimator[] mAnimators = new ValueAnimator[ANIMATOR_COUNT]; 44 45 private final Handler mDelayedLineFadeHandler = new Handler(Looper.getMainLooper()); 46 private final Launcher mLauncher; 47 48 private boolean mShouldAutoHide = true; 49 50 // The alpha of the line when it is showing. 51 private int mActiveAlpha = 0; 52 // The alpha that the line is being animated to or already at (either 0 or mActiveAlpha). 53 private int mToAlpha; 54 // A float value representing the number of pages, to allow for an animation when it changes. 55 private float mNumPagesFloat; 56 private int mCurrentScroll; 57 private int mTotalScroll; 58 private Paint mLinePaint; 59 private final int mLineHeight; 60 61 private static final Property<WorkspacePageIndicator, Integer> PAINT_ALPHA 62 = new Property<WorkspacePageIndicator, Integer>(Integer.class, "paint_alpha") { 63 @Override 64 public Integer get(WorkspacePageIndicator obj) { 65 return obj.mLinePaint.getAlpha(); 66 } 67 68 @Override 69 public void set(WorkspacePageIndicator obj, Integer alpha) { 70 obj.mLinePaint.setAlpha(alpha); 71 obj.invalidate(); 72 } 73 }; 74 75 private static final Property<WorkspacePageIndicator, Float> NUM_PAGES 76 = new Property<WorkspacePageIndicator, Float>(Float.class, "num_pages") { 77 @Override 78 public Float get(WorkspacePageIndicator obj) { 79 return obj.mNumPagesFloat; 80 } 81 82 @Override 83 public void set(WorkspacePageIndicator obj, Float numPages) { 84 obj.mNumPagesFloat = numPages; 85 obj.invalidate(); 86 } 87 }; 88 89 private static final Property<WorkspacePageIndicator, Integer> TOTAL_SCROLL 90 = new Property<WorkspacePageIndicator, Integer>(Integer.class, "total_scroll") { 91 @Override 92 public Integer get(WorkspacePageIndicator obj) { 93 return obj.mTotalScroll; 94 } 95 96 @Override 97 public void set(WorkspacePageIndicator obj, Integer totalScroll) { 98 obj.mTotalScroll = totalScroll; 99 obj.invalidate(); 100 } 101 }; 102 103 private Runnable mHideLineRunnable = () -> animateLineToAlpha(0); 104 WorkspacePageIndicator(Context context)105 public WorkspacePageIndicator(Context context) { 106 this(context, null); 107 } 108 WorkspacePageIndicator(Context context, AttributeSet attrs)109 public WorkspacePageIndicator(Context context, AttributeSet attrs) { 110 this(context, attrs, 0); 111 } 112 WorkspacePageIndicator(Context context, AttributeSet attrs, int defStyle)113 public WorkspacePageIndicator(Context context, AttributeSet attrs, int defStyle) { 114 super(context, attrs, defStyle); 115 116 Resources res = context.getResources(); 117 mLinePaint = new Paint(); 118 mLinePaint.setAlpha(0); 119 120 mLauncher = Launcher.getLauncher(context); 121 mLineHeight = res.getDimensionPixelSize(R.dimen.workspace_page_indicator_line_height); 122 123 boolean darkText = Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText); 124 mActiveAlpha = darkText ? BLACK_ALPHA : WHITE_ALPHA; 125 mLinePaint.setColor(darkText ? Color.BLACK : Color.WHITE); 126 } 127 128 @Override onDraw(Canvas canvas)129 protected void onDraw(Canvas canvas) { 130 if (mTotalScroll == 0 || mNumPagesFloat == 0) { 131 return; 132 } 133 134 // Compute and draw line rect. 135 float progress = Utilities.boundToRange(((float) mCurrentScroll) / mTotalScroll, 0f, 1f); 136 int availableWidth = getWidth(); 137 int lineWidth = (int) (availableWidth / mNumPagesFloat); 138 int lineLeft = (int) (progress * (availableWidth - lineWidth)); 139 int lineRight = lineLeft + lineWidth; 140 141 canvas.drawRoundRect(lineLeft, getHeight() / 2 - mLineHeight / 2, lineRight, 142 getHeight() / 2 + mLineHeight / 2, mLineHeight, mLineHeight, mLinePaint); 143 } 144 145 @Override setScroll(int currentScroll, int totalScroll)146 public void setScroll(int currentScroll, int totalScroll) { 147 if (getAlpha() == 0) { 148 return; 149 } 150 animateLineToAlpha(mActiveAlpha); 151 152 mCurrentScroll = currentScroll; 153 if (mTotalScroll == 0) { 154 mTotalScroll = totalScroll; 155 } else if (mTotalScroll != totalScroll) { 156 animateToTotalScroll(totalScroll); 157 } else { 158 invalidate(); 159 } 160 161 if (mShouldAutoHide) { 162 hideAfterDelay(); 163 } 164 } 165 hideAfterDelay()166 private void hideAfterDelay() { 167 mDelayedLineFadeHandler.removeCallbacksAndMessages(null); 168 mDelayedLineFadeHandler.postDelayed(mHideLineRunnable, LINE_FADE_DELAY); 169 } 170 171 @Override setActiveMarker(int activePage)172 public void setActiveMarker(int activePage) { } 173 174 @Override setMarkersCount(int numMarkers)175 public void setMarkersCount(int numMarkers) { 176 if (Float.compare(numMarkers, mNumPagesFloat) != 0) { 177 setupAndRunAnimation(ObjectAnimator.ofFloat(this, NUM_PAGES, numMarkers), 178 NUM_PAGES_ANIMATOR_INDEX); 179 } else { 180 if (mAnimators[NUM_PAGES_ANIMATOR_INDEX] != null) { 181 mAnimators[NUM_PAGES_ANIMATOR_INDEX].cancel(); 182 mAnimators[NUM_PAGES_ANIMATOR_INDEX] = null; 183 } 184 } 185 } 186 187 @Override setShouldAutoHide(boolean shouldAutoHide)188 public void setShouldAutoHide(boolean shouldAutoHide) { 189 mShouldAutoHide = shouldAutoHide; 190 if (shouldAutoHide && mLinePaint.getAlpha() > 0) { 191 hideAfterDelay(); 192 } else if (!shouldAutoHide) { 193 mDelayedLineFadeHandler.removeCallbacksAndMessages(null); 194 } 195 } 196 animateLineToAlpha(int alpha)197 private void animateLineToAlpha(int alpha) { 198 if (alpha == mToAlpha) { 199 // Ignore the new animation if it is going to the same alpha as the current animation. 200 return; 201 } 202 mToAlpha = alpha; 203 setupAndRunAnimation(ObjectAnimator.ofInt(this, PAINT_ALPHA, alpha), 204 LINE_ALPHA_ANIMATOR_INDEX); 205 } 206 animateToTotalScroll(int totalScroll)207 private void animateToTotalScroll(int totalScroll) { 208 setupAndRunAnimation(ObjectAnimator.ofInt(this, TOTAL_SCROLL, totalScroll), 209 TOTAL_SCROLL_ANIMATOR_INDEX); 210 } 211 212 /** 213 * Starts the given animator and stores it in the provided index in {@link #mAnimators} until 214 * the animation ends. 215 * 216 * If an animator is already at the index (i.e. it is already playing), it is canceled and 217 * replaced with the new animator. 218 */ setupAndRunAnimation(ValueAnimator animator, final int animatorIndex)219 private void setupAndRunAnimation(ValueAnimator animator, final int animatorIndex) { 220 if (mAnimators[animatorIndex] != null) { 221 mAnimators[animatorIndex].cancel(); 222 } 223 mAnimators[animatorIndex] = animator; 224 mAnimators[animatorIndex].addListener(new AnimatorListenerAdapter() { 225 @Override 226 public void onAnimationEnd(Animator animation) { 227 mAnimators[animatorIndex] = null; 228 } 229 }); 230 mAnimators[animatorIndex].setDuration(LINE_ANIMATE_DURATION); 231 mAnimators[animatorIndex].start(); 232 } 233 234 /** 235 * Pauses all currently running animations. 236 */ 237 @Override pauseAnimations()238 public void pauseAnimations() { 239 for (int i = 0; i < ANIMATOR_COUNT; i++) { 240 if (mAnimators[i] != null) { 241 mAnimators[i].pause(); 242 } 243 } 244 } 245 246 /** 247 * Force-ends all currently running or paused animations. 248 */ 249 @Override skipAnimationsToEnd()250 public void skipAnimationsToEnd() { 251 for (int i = 0; i < ANIMATOR_COUNT; i++) { 252 if (mAnimators[i] != null) { 253 mAnimators[i].end(); 254 } 255 } 256 } 257 258 /** 259 * We need to override setInsets to prevent InsettableFrameLayout from applying different 260 * margins on the page indicator. 261 */ 262 @Override setInsets(Rect insets)263 public void setInsets(Rect insets) { 264 } 265 } 266