1 /* 2 * Copyright (C) 2016 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 17 package com.android.launcher3.pageindicators; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.ValueAnimator; 24 import android.animation.ValueAnimator.AnimatorUpdateListener; 25 import android.content.Context; 26 import android.graphics.Canvas; 27 import android.graphics.Outline; 28 import android.graphics.Paint; 29 import android.graphics.Paint.Style; 30 import android.graphics.RectF; 31 import android.util.AttributeSet; 32 import android.util.Property; 33 import android.view.View; 34 import android.view.ViewOutlineProvider; 35 import android.view.animation.Interpolator; 36 import android.view.animation.OvershootInterpolator; 37 38 import com.android.launcher3.R; 39 import com.android.launcher3.Utilities; 40 41 /** 42 * {@link PageIndicator} which shows dots per page. The active page is shown with the current 43 * accent color. 44 */ 45 public class PageIndicatorDots extends PageIndicator { 46 47 private static final float SHIFT_PER_ANIMATION = 0.5f; 48 private static final float SHIFT_THRESHOLD = 0.1f; 49 private static final long ANIMATION_DURATION = 150; 50 51 private static final int ENTER_ANIMATION_START_DELAY = 300; 52 private static final int ENTER_ANIMATION_STAGGERED_DELAY = 150; 53 private static final int ENTER_ANIMATION_DURATION = 400; 54 55 // This value approximately overshoots to 1.5 times the original size. 56 private static final float ENTER_ANIMATION_OVERSHOOT_TENSION = 4.9f; 57 58 private static final RectF sTempRect = new RectF(); 59 60 private static final Property<PageIndicatorDots, Float> CURRENT_POSITION 61 = new Property<PageIndicatorDots, Float>(float.class, "current_position") { 62 @Override 63 public Float get(PageIndicatorDots obj) { 64 return obj.mCurrentPosition; 65 } 66 67 @Override 68 public void set(PageIndicatorDots obj, Float pos) { 69 obj.mCurrentPosition = pos; 70 obj.invalidate(); 71 obj.invalidateOutline(); 72 } 73 }; 74 75 /** 76 * Listener for keep running the animation until the final state is reached. 77 */ 78 private final AnimatorListenerAdapter mAnimCycleListener = new AnimatorListenerAdapter() { 79 80 @Override 81 public void onAnimationEnd(Animator animation) { 82 mAnimator = null; 83 animateToPostion(mFinalPosition); 84 } 85 }; 86 87 private final Paint mCirclePaint; 88 private final float mDotRadius; 89 private final int mActiveColor; 90 private final int mInActiveColor; 91 private final boolean mIsRtl; 92 93 private int mActivePage; 94 95 /** 96 * The current position of the active dot including the animation progress. 97 * For ex: 98 * 0.0 => Active dot is at position 0 99 * 0.33 => Active dot is at position 0 and is moving towards 1 100 * 0.50 => Active dot is at position [0, 1] 101 * 0.77 => Active dot has left position 0 and is collapsing towards position 1 102 * 1.0 => Active dot is at position 1 103 */ 104 private float mCurrentPosition; 105 private float mFinalPosition; 106 private ObjectAnimator mAnimator; 107 108 private float[] mEntryAnimationRadiusFactors; 109 110 public PageIndicatorDots(Context context) { 111 this(context, null); 112 } 113 114 public PageIndicatorDots(Context context, AttributeSet attrs) { 115 this(context, attrs, 0); 116 } 117 118 public PageIndicatorDots(Context context, AttributeSet attrs, int defStyleAttr) { 119 super(context, attrs, defStyleAttr); 120 121 mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 122 mCirclePaint.setStyle(Style.FILL); 123 mDotRadius = getResources().getDimension(R.dimen.page_indicator_dot_size) / 2; 124 setOutlineProvider(new MyOutlineProver()); 125 126 mActiveColor = Utilities.getColorAccent(context); 127 mInActiveColor = getResources().getColor(R.color.page_indicator_dot_color); 128 129 mIsRtl = Utilities.isRtl(getResources()); 130 } 131 132 @Override 133 public void setScroll(int currentScroll, int totalScroll) { 134 if (mNumPages > 1) { 135 if (mIsRtl) { 136 currentScroll = totalScroll - currentScroll; 137 } 138 int scrollPerPage = totalScroll / (mNumPages - 1); 139 int absScroll = mActivePage * scrollPerPage; 140 float scrollThreshold = SHIFT_THRESHOLD * scrollPerPage; 141 142 if ((absScroll - currentScroll) > scrollThreshold) { 143 // current scroll is before absolute scroll 144 animateToPostion(mActivePage - SHIFT_PER_ANIMATION); 145 } else if ((currentScroll - absScroll) > scrollThreshold) { 146 // current scroll is ahead of absolute scroll 147 animateToPostion(mActivePage + SHIFT_PER_ANIMATION); 148 } else { 149 animateToPostion(mActivePage); 150 } 151 } 152 } 153 154 private void animateToPostion(float position) { 155 mFinalPosition = position; 156 if (Math.abs(mCurrentPosition - mFinalPosition) < SHIFT_THRESHOLD) { 157 mCurrentPosition = mFinalPosition; 158 } 159 if (mAnimator == null && Float.compare(mCurrentPosition, mFinalPosition) != 0) { 160 float positionForThisAnim = mCurrentPosition > mFinalPosition ? 161 mCurrentPosition - SHIFT_PER_ANIMATION : mCurrentPosition + SHIFT_PER_ANIMATION; 162 mAnimator = ObjectAnimator.ofFloat(this, CURRENT_POSITION, positionForThisAnim); 163 mAnimator.addListener(mAnimCycleListener); 164 mAnimator.setDuration(ANIMATION_DURATION); 165 mAnimator.start(); 166 } 167 } 168 169 public void stopAllAnimations() { 170 if (mAnimator != null) { 171 mAnimator.removeAllListeners(); 172 mAnimator.cancel(); 173 mAnimator = null; 174 } 175 mFinalPosition = mActivePage; 176 CURRENT_POSITION.set(this, mFinalPosition); 177 } 178 179 /** 180 * Sets up up the page indicator to play the entry animation. 181 * {@link #playEntryAnimation()} must be called after this. 182 */ 183 public void prepareEntryAnimation() { 184 mEntryAnimationRadiusFactors = new float[mNumPages]; 185 invalidate(); 186 } 187 188 public void playEntryAnimation() { 189 int count = mEntryAnimationRadiusFactors.length; 190 if (count == 0) { 191 mEntryAnimationRadiusFactors = null; 192 invalidate(); 193 return; 194 } 195 196 Interpolator interpolator = new OvershootInterpolator(ENTER_ANIMATION_OVERSHOOT_TENSION); 197 AnimatorSet animSet = new AnimatorSet(); 198 for (int i = 0; i < count; i++) { 199 ValueAnimator anim = ValueAnimator.ofFloat(0, 1).setDuration(ENTER_ANIMATION_DURATION); 200 final int index = i; 201 anim.addUpdateListener(new AnimatorUpdateListener() { 202 @Override 203 public void onAnimationUpdate(ValueAnimator animation) { 204 mEntryAnimationRadiusFactors[index] = (Float) animation.getAnimatedValue(); 205 invalidate(); 206 } 207 }); 208 anim.setInterpolator(interpolator); 209 anim.setStartDelay(ENTER_ANIMATION_START_DELAY + ENTER_ANIMATION_STAGGERED_DELAY * i); 210 animSet.play(anim); 211 } 212 213 animSet.addListener(new AnimatorListenerAdapter() { 214 215 @Override 216 public void onAnimationEnd(Animator animation) { 217 mEntryAnimationRadiusFactors = null; 218 invalidateOutline(); 219 invalidate(); 220 } 221 }); 222 animSet.start(); 223 } 224 225 @Override 226 public void setActiveMarker(int activePage) { 227 if (mActivePage != activePage) { 228 mActivePage = activePage; 229 } 230 } 231 232 @Override 233 protected void onPageCountChanged() { 234 requestLayout(); 235 } 236 237 @Override 238 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 239 // Add extra spacing of mDotRadius on all sides so than entry animation could be run. 240 int width = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY ? 241 MeasureSpec.getSize(widthMeasureSpec) : (int) ((mNumPages * 3 + 2) * mDotRadius); 242 int height= MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY ? 243 MeasureSpec.getSize(heightMeasureSpec) : (int) (4 * mDotRadius); 244 setMeasuredDimension(width, height); 245 } 246 247 @Override 248 protected void onDraw(Canvas canvas) { 249 // Draw all page indicators; 250 float circleGap = 3 * mDotRadius; 251 float startX = (getWidth() - mNumPages * circleGap + mDotRadius) / 2; 252 253 float x = startX + mDotRadius; 254 float y = canvas.getHeight() / 2; 255 256 if (mEntryAnimationRadiusFactors != null) { 257 // During entry animation, only draw the circles 258 if (mIsRtl) { 259 x = getWidth() - x; 260 circleGap = -circleGap; 261 } 262 for (int i = 0; i < mEntryAnimationRadiusFactors.length; i++) { 263 mCirclePaint.setColor(i == mActivePage ? mActiveColor : mInActiveColor); 264 canvas.drawCircle(x, y, mDotRadius * mEntryAnimationRadiusFactors[i], mCirclePaint); 265 x += circleGap; 266 } 267 } else { 268 mCirclePaint.setColor(mInActiveColor); 269 for (int i = 0; i < mNumPages; i++) { 270 canvas.drawCircle(x, y, mDotRadius, mCirclePaint); 271 x += circleGap; 272 } 273 274 mCirclePaint.setColor(mActiveColor); 275 canvas.drawRoundRect(getActiveRect(), mDotRadius, mDotRadius, mCirclePaint); 276 } 277 } 278 279 private RectF getActiveRect() { 280 float startCircle = (int) mCurrentPosition; 281 float delta = mCurrentPosition - startCircle; 282 float diameter = 2 * mDotRadius; 283 float circleGap = 3 * mDotRadius; 284 float startX = (getWidth() - mNumPages * circleGap + mDotRadius) / 2; 285 286 sTempRect.top = getHeight() * 0.5f - mDotRadius; 287 sTempRect.bottom = getHeight() * 0.5f + mDotRadius; 288 sTempRect.left = startX + startCircle * circleGap; 289 sTempRect.right = sTempRect.left + diameter; 290 291 if (delta < SHIFT_PER_ANIMATION) { 292 // dot is capturing the right circle. 293 sTempRect.right += delta * circleGap * 2; 294 } else { 295 // Dot is leaving the left circle. 296 sTempRect.right += circleGap; 297 298 delta -= SHIFT_PER_ANIMATION; 299 sTempRect.left += delta * circleGap * 2; 300 } 301 302 if (mIsRtl) { 303 float rectWidth = sTempRect.width(); 304 sTempRect.right = getWidth() - sTempRect.left; 305 sTempRect.left = sTempRect.right - rectWidth; 306 } 307 return sTempRect; 308 } 309 310 private class MyOutlineProver extends ViewOutlineProvider { 311 312 @Override 313 public void getOutline(View view, Outline outline) { 314 if (mEntryAnimationRadiusFactors == null) { 315 RectF activeRect = getActiveRect(); 316 outline.setRoundRect( 317 (int) activeRect.left, 318 (int) activeRect.top, 319 (int) activeRect.right, 320 (int) activeRect.bottom, 321 mDotRadius 322 ); 323 } 324 } 325 } 326 } 327