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