mobile/android/base/widget/GeckoSwipeRefreshLayout.java

branch
TOR_BUG_9701
changeset 15
b8a032363ba2
equal deleted inserted replaced
-1:000000000000 0:0fb8615e8f7b
1 /*
2 * Copyright (C) 2013 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 org.mozilla.gecko.widget;
18
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.graphics.Canvas;
23 import android.graphics.Paint;
24 import android.graphics.Rect;
25 import android.graphics.RectF;
26 import android.support.v4.view.ViewCompat;
27 import android.util.AttributeSet;
28 import android.util.DisplayMetrics;
29 import android.view.MotionEvent;
30 import android.view.View;
31 import android.view.ViewConfiguration;
32 import android.view.ViewGroup;
33 import android.view.animation.AccelerateInterpolator;
34 import android.view.animation.Animation;
35 import android.view.animation.Animation.AnimationListener;
36 import android.view.animation.AnimationUtils;
37 import android.view.animation.DecelerateInterpolator;
38 import android.view.animation.Interpolator;
39 import android.view.animation.Transformation;
40 import android.widget.AbsListView;
41
42 /**
43 * GeckoSwipeRefreshLayout is mostly lifted from Android's support library (v4) with these
44 * modfications:
45 * - Removes elastic "rubber banding" effect when overscrolling the child view.
46 * - Changes the height of the progress bar to match the height of HomePager's page indicator.
47 * - Uses a rectangle rather than a circle for the SwipeProgressBar indicator.
48 *
49 * This class also embeds package-access dependent classes SwipeProgressBar and
50 * BakedBezierInterpolator.
51 *
52 * Original source: https://android.googlesource.com/platform/frameworks/support/+/
53 * android-support-lib-19.1.0/v4/java/android/support/v4/widget/SwipeRefreshLayout.java
54 */
55 public class GeckoSwipeRefreshLayout extends ViewGroup {
56 private static final long RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300;
57 private static final float ACCELERATE_INTERPOLATION_FACTOR = 1.5f;
58 private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;
59 // Reduce the height (from 4 to 3) of the progress bar to match HomePager's page indicator.
60 private static final float PROGRESS_BAR_HEIGHT = 3;
61 private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f;
62 private static final int REFRESH_TRIGGER_DISTANCE = 120;
63
64 private SwipeProgressBar mProgressBar; //the thing that shows progress is going
65 private View mTarget; //the content that gets pulled down
66 private int mOriginalOffsetTop;
67 private OnRefreshListener mListener;
68 private MotionEvent mDownEvent;
69 private int mFrom;
70 private boolean mRefreshing = false;
71 private int mTouchSlop;
72 private float mDistanceToTriggerSync = -1;
73 private float mPrevY;
74 private int mMediumAnimationDuration;
75 private float mFromPercentage = 0;
76 private float mCurrPercentage = 0;
77 private int mProgressBarHeight;
78 private int mCurrentTargetOffsetTop;
79 // Target is returning to its start offset because it was cancelled or a
80 // refresh was triggered.
81 private boolean mReturningToStart;
82 private final DecelerateInterpolator mDecelerateInterpolator;
83 private final AccelerateInterpolator mAccelerateInterpolator;
84 private static final int[] LAYOUT_ATTRS = new int[] {
85 android.R.attr.enabled
86 };
87
88 private final Animation mAnimateToStartPosition = new Animation() {
89 @Override
90 public void applyTransformation(float interpolatedTime, Transformation t) {
91 int targetTop = 0;
92 if (mFrom != mOriginalOffsetTop) {
93 targetTop = (mFrom + (int)((mOriginalOffsetTop - mFrom) * interpolatedTime));
94 }
95 int offset = targetTop - mTarget.getTop();
96 final int currentTop = mTarget.getTop();
97 if (offset + currentTop < 0) {
98 offset = 0 - currentTop;
99 }
100 setTargetOffsetTopAndBottom(offset);
101 }
102 };
103
104 private Animation mShrinkTrigger = new Animation() {
105 @Override
106 public void applyTransformation(float interpolatedTime, Transformation t) {
107 float percent = mFromPercentage + ((0 - mFromPercentage) * interpolatedTime);
108 mProgressBar.setTriggerPercentage(percent);
109 }
110 };
111
112 private final AnimationListener mReturnToStartPositionListener = new BaseAnimationListener() {
113 @Override
114 public void onAnimationEnd(Animation animation) {
115 // Once the target content has returned to its start position, reset
116 // the target offset to 0
117 mCurrentTargetOffsetTop = 0;
118 }
119 };
120
121 private final AnimationListener mShrinkAnimationListener = new BaseAnimationListener() {
122 @Override
123 public void onAnimationEnd(Animation animation) {
124 mCurrPercentage = 0;
125 }
126 };
127
128 private final Runnable mReturnToStartPosition = new Runnable() {
129
130 @Override
131 public void run() {
132 mReturningToStart = true;
133 animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(),
134 mReturnToStartPositionListener);
135 }
136
137 };
138
139 // Cancel the refresh gesture and animate everything back to its original state.
140 private final Runnable mCancel = new Runnable() {
141
142 @Override
143 public void run() {
144 mReturningToStart = true;
145 // Timeout fired since the user last moved their finger; animate the
146 // trigger to 0 and put the target back at its original position
147 if (mProgressBar != null) {
148 mFromPercentage = mCurrPercentage;
149 mShrinkTrigger.setDuration(mMediumAnimationDuration);
150 mShrinkTrigger.setAnimationListener(mShrinkAnimationListener);
151 mShrinkTrigger.reset();
152 mShrinkTrigger.setInterpolator(mDecelerateInterpolator);
153 startAnimation(mShrinkTrigger);
154 }
155 animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(),
156 mReturnToStartPositionListener);
157 }
158
159 };
160
161 /**
162 * Simple constructor to use when creating a GeckoSwipeRefreshLayout from code.
163 * @param context
164 */
165 public GeckoSwipeRefreshLayout(Context context) {
166 this(context, null);
167 }
168
169 /**
170 * Constructor that is called when inflating GeckoSwipeRefreshLayout from XML.
171 * @param context
172 * @param attrs
173 */
174 public GeckoSwipeRefreshLayout(Context context, AttributeSet attrs) {
175 super(context, attrs);
176
177 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
178
179 mMediumAnimationDuration = getResources().getInteger(
180 android.R.integer.config_mediumAnimTime);
181
182 setWillNotDraw(false);
183 mProgressBar = new SwipeProgressBar(this);
184 final DisplayMetrics metrics = getResources().getDisplayMetrics();
185 mProgressBarHeight = (int) (metrics.density * PROGRESS_BAR_HEIGHT);
186 mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
187 mAccelerateInterpolator = new AccelerateInterpolator(ACCELERATE_INTERPOLATION_FACTOR);
188
189 final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
190 setEnabled(a.getBoolean(0, true));
191 a.recycle();
192 }
193
194 @Override
195 public void onAttachedToWindow() {
196 super.onAttachedToWindow();
197 removeCallbacks(mCancel);
198 removeCallbacks(mReturnToStartPosition);
199 }
200
201 @Override
202 public void onDetachedFromWindow() {
203 super.onDetachedFromWindow();
204 removeCallbacks(mReturnToStartPosition);
205 removeCallbacks(mCancel);
206 }
207
208 private void animateOffsetToStartPosition(int from, AnimationListener listener) {
209 mFrom = from;
210 mAnimateToStartPosition.reset();
211 mAnimateToStartPosition.setDuration(mMediumAnimationDuration);
212 mAnimateToStartPosition.setAnimationListener(listener);
213 mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
214 mTarget.startAnimation(mAnimateToStartPosition);
215 }
216
217 /**
218 * Set the listener to be notified when a refresh is triggered via the swipe
219 * gesture.
220 */
221 public void setOnRefreshListener(OnRefreshListener listener) {
222 mListener = listener;
223 }
224
225 private void setTriggerPercentage(float percent) {
226 if (percent == 0f) {
227 // No-op. A null trigger means it's uninitialized, and setting it to zero-percent
228 // means we're trying to reset state, so there's nothing to reset in this case.
229 mCurrPercentage = 0;
230 return;
231 }
232 mCurrPercentage = percent;
233 mProgressBar.setTriggerPercentage(percent);
234 }
235
236 /**
237 * Notify the widget that refresh state has changed. Do not call this when
238 * refresh is triggered by a swipe gesture.
239 *
240 * @param refreshing Whether or not the view should show refresh progress.
241 */
242 public void setRefreshing(boolean refreshing) {
243 if (mRefreshing != refreshing) {
244 ensureTarget();
245 mCurrPercentage = 0;
246 mRefreshing = refreshing;
247 if (mRefreshing) {
248 mProgressBar.start();
249 } else {
250 mProgressBar.stop();
251 }
252 }
253 }
254
255 /**
256 * Set the four colors used in the progress animation. The first color will
257 * also be the color of the bar that grows in response to a user swipe
258 * gesture.
259 *
260 * @param colorRes1 Color resource.
261 * @param colorRes2 Color resource.
262 * @param colorRes3 Color resource.
263 * @param colorRes4 Color resource.
264 */
265 public void setColorScheme(int colorRes1, int colorRes2, int colorRes3, int colorRes4) {
266 ensureTarget();
267 final Resources res = getResources();
268 final int color1 = res.getColor(colorRes1);
269 final int color2 = res.getColor(colorRes2);
270 final int color3 = res.getColor(colorRes3);
271 final int color4 = res.getColor(colorRes4);
272 mProgressBar.setColorScheme(color1, color2, color3,color4);
273 }
274
275 /**
276 * @return Whether the SwipeRefreshWidget is actively showing refresh
277 * progress.
278 */
279 public boolean isRefreshing() {
280 return mRefreshing;
281 }
282
283 private void ensureTarget() {
284 // Don't bother getting the parent height if the parent hasn't been laid out yet.
285 if (mTarget == null) {
286 if (getChildCount() > 1 && !isInEditMode()) {
287 throw new IllegalStateException(
288 "GeckoSwipeRefreshLayout can host only one direct child");
289 }
290 mTarget = getChildAt(0);
291 mOriginalOffsetTop = mTarget.getTop() + getPaddingTop();
292 }
293 if (mDistanceToTriggerSync == -1) {
294 if (getParent() != null && ((View)getParent()).getHeight() > 0) {
295 final DisplayMetrics metrics = getResources().getDisplayMetrics();
296 mDistanceToTriggerSync = (int) Math.min(
297 ((View) getParent()) .getHeight() * MAX_SWIPE_DISTANCE_FACTOR,
298 REFRESH_TRIGGER_DISTANCE * metrics.density);
299 }
300 }
301 }
302
303 @Override
304 public void draw(Canvas canvas) {
305 super.draw(canvas);
306 mProgressBar.draw(canvas);
307 }
308
309 @Override
310 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
311 final int width = getMeasuredWidth();
312 final int height = getMeasuredHeight();
313 mProgressBar.setBounds(0, 0, width, mProgressBarHeight);
314 if (getChildCount() == 0) {
315 return;
316 }
317 final View child = getChildAt(0);
318 final int childLeft = getPaddingLeft();
319 final int childTop = mCurrentTargetOffsetTop + getPaddingTop();
320 final int childWidth = width - getPaddingLeft() - getPaddingRight();
321 final int childHeight = height - getPaddingTop() - getPaddingBottom();
322 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
323 }
324
325 @Override
326 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
327 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
328 if (getChildCount() > 1 && !isInEditMode()) {
329 throw new IllegalStateException("GeckoSwipeRefreshLayout can host only one direct child");
330 }
331 if (getChildCount() > 0) {
332 getChildAt(0).measure(
333 MeasureSpec.makeMeasureSpec(
334 getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
335 MeasureSpec.EXACTLY),
336 MeasureSpec.makeMeasureSpec(
337 getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
338 MeasureSpec.EXACTLY));
339 }
340 }
341
342 /**
343 * @return Whether it is possible for the child view of this layout to
344 * scroll up. Override this if the child view is a custom view.
345 */
346 public boolean canChildScrollUp() {
347 if (android.os.Build.VERSION.SDK_INT < 14) {
348 if (mTarget instanceof AbsListView) {
349 final AbsListView absListView = (AbsListView) mTarget;
350 return absListView.getChildCount() > 0
351 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
352 .getTop() < absListView.getPaddingTop());
353 } else {
354 return mTarget.getScrollY() > 0;
355 }
356 } else {
357 return ViewCompat.canScrollVertically(mTarget, -1);
358 }
359 }
360
361 @Override
362 public boolean onInterceptTouchEvent(MotionEvent ev) {
363 ensureTarget();
364 boolean handled = false;
365 if (mReturningToStart && ev.getAction() == MotionEvent.ACTION_DOWN) {
366 mReturningToStart = false;
367 }
368 if (isEnabled() && !mReturningToStart && !canChildScrollUp()) {
369 handled = onTouchEvent(ev);
370 }
371 return !handled ? super.onInterceptTouchEvent(ev) : handled;
372 }
373
374 @Override
375 public void requestDisallowInterceptTouchEvent(boolean b) {
376 // Nope.
377 }
378
379 @Override
380 public boolean onTouchEvent(MotionEvent event) {
381 final int action = event.getAction();
382 boolean handled = false;
383 switch (action) {
384 case MotionEvent.ACTION_DOWN:
385 mCurrPercentage = 0;
386 mDownEvent = MotionEvent.obtain(event);
387 mPrevY = mDownEvent.getY();
388 break;
389 case MotionEvent.ACTION_MOVE:
390 if (mDownEvent != null && !mReturningToStart) {
391 final float eventY = event.getY();
392 float yDiff = eventY - mDownEvent.getY();
393 if (yDiff > mTouchSlop) {
394 // User velocity passed min velocity; trigger a refresh
395 if (yDiff > mDistanceToTriggerSync) {
396 // User movement passed distance; trigger a refresh
397 startRefresh();
398 handled = true;
399 break;
400 } else {
401 // Just track the user's movement
402 setTriggerPercentage(
403 mAccelerateInterpolator.getInterpolation(
404 yDiff / mDistanceToTriggerSync));
405 float offsetTop = yDiff;
406 if (mPrevY > eventY) {
407 offsetTop = yDiff - mTouchSlop;
408 }
409 // Removed this call to disable "rubber-band" overscroll effect.
410 // updateContentOffsetTop((int) offsetTop);
411 if (mPrevY > eventY && (mTarget.getTop() < mTouchSlop)) {
412 // If the user puts the view back at the top, we
413 // don't need to. This shouldn't be considered
414 // cancelling the gesture as the user can restart from the top.
415 removeCallbacks(mCancel);
416 } else {
417 updatePositionTimeout();
418 }
419 mPrevY = event.getY();
420 handled = true;
421 }
422 }
423 }
424 break;
425 case MotionEvent.ACTION_UP:
426 case MotionEvent.ACTION_CANCEL:
427 if (mDownEvent != null) {
428 mDownEvent.recycle();
429 mDownEvent = null;
430 }
431 break;
432 }
433 return handled;
434 }
435
436 private void startRefresh() {
437 removeCallbacks(mCancel);
438 mReturnToStartPosition.run();
439 setRefreshing(true);
440 mListener.onRefresh();
441 }
442
443 private void updateContentOffsetTop(int targetTop) {
444 final int currentTop = mTarget.getTop();
445 if (targetTop > mDistanceToTriggerSync) {
446 targetTop = (int) mDistanceToTriggerSync;
447 } else if (targetTop < 0) {
448 targetTop = 0;
449 }
450 setTargetOffsetTopAndBottom(targetTop - currentTop);
451 }
452
453 private void setTargetOffsetTopAndBottom(int offset) {
454 mTarget.offsetTopAndBottom(offset);
455 mCurrentTargetOffsetTop = mTarget.getTop();
456 }
457
458 private void updatePositionTimeout() {
459 removeCallbacks(mCancel);
460 postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT);
461 }
462
463 /**
464 * Classes that wish to be notified when the swipe gesture correctly
465 * triggers a refresh should implement this interface.
466 */
467 public interface OnRefreshListener {
468 public void onRefresh();
469 }
470
471 /**
472 * Simple AnimationListener to avoid having to implement unneeded methods in
473 * AnimationListeners.
474 */
475 private class BaseAnimationListener implements AnimationListener {
476 @Override
477 public void onAnimationStart(Animation animation) {
478 }
479
480 @Override
481 public void onAnimationEnd(Animation animation) {
482 }
483
484 @Override
485 public void onAnimationRepeat(Animation animation) {
486 }
487 }
488
489 /**
490 * The only modification to this class is the shape of the refresh indicator to be a rectangle
491 * rather than a circle.
492 */
493 private static final class SwipeProgressBar {
494 // Default progress animation colors are grays.
495 private final static int COLOR1 = 0xB3000000;
496 private final static int COLOR2 = 0x80000000;
497 private final static int COLOR3 = 0x4d000000;
498 private final static int COLOR4 = 0x1a000000;
499
500 // The duration of the animation cycle.
501 private static final int ANIMATION_DURATION_MS = 2000;
502
503 // The duration of the animation to clear the bar.
504 private static final int FINISH_ANIMATION_DURATION_MS = 1000;
505
506 // Interpolator for varying the speed of the animation.
507 private static final Interpolator INTERPOLATOR = BakedBezierInterpolator.getInstance();
508
509 private final Paint mPaint = new Paint();
510 private final RectF mClipRect = new RectF();
511 private float mTriggerPercentage;
512 private long mStartTime;
513 private long mFinishTime;
514 private boolean mRunning;
515
516 // Colors used when rendering the animation,
517 private int mColor1;
518 private int mColor2;
519 private int mColor3;
520 private int mColor4;
521 private View mParent;
522
523 private Rect mBounds = new Rect();
524
525 public SwipeProgressBar(View parent) {
526 mParent = parent;
527 mColor1 = COLOR1;
528 mColor2 = COLOR2;
529 mColor3 = COLOR3;
530 mColor4 = COLOR4;
531 }
532
533 /**
534 * Set the four colors used in the progress animation. The first color will
535 * also be the color of the bar that grows in response to a user swipe
536 * gesture.
537 *
538 * @param color1 Integer representation of a color.
539 * @param color2 Integer representation of a color.
540 * @param color3 Integer representation of a color.
541 * @param color4 Integer representation of a color.
542 */
543 void setColorScheme(int color1, int color2, int color3, int color4) {
544 mColor1 = color1;
545 mColor2 = color2;
546 mColor3 = color3;
547 mColor4 = color4;
548 }
549
550 /**
551 * Update the progress the user has made toward triggering the swipe
552 * gesture. and use this value to update the percentage of the trigger that
553 * is shown.
554 */
555 void setTriggerPercentage(float triggerPercentage) {
556 mTriggerPercentage = triggerPercentage;
557 mStartTime = 0;
558 ViewCompat.postInvalidateOnAnimation(mParent);
559 }
560
561 /**
562 * Start showing the progress animation.
563 */
564 void start() {
565 if (!mRunning) {
566 mTriggerPercentage = 0;
567 mStartTime = AnimationUtils.currentAnimationTimeMillis();
568 mRunning = true;
569 mParent.postInvalidate();
570 }
571 }
572
573 /**
574 * Stop showing the progress animation.
575 */
576 void stop() {
577 if (mRunning) {
578 mTriggerPercentage = 0;
579 mFinishTime = AnimationUtils.currentAnimationTimeMillis();
580 mRunning = false;
581 mParent.postInvalidate();
582 }
583 }
584
585 /**
586 * @return Return whether the progress animation is currently running.
587 */
588 boolean isRunning() {
589 return mRunning || mFinishTime > 0;
590 }
591
592 void draw(Canvas canvas) {
593 final int width = mBounds.width();
594 final int height = mBounds.height();
595 final int cx = width / 2;
596 final int cy = height / 2;
597 boolean drawTriggerWhileFinishing = false;
598 int restoreCount = canvas.save();
599 canvas.clipRect(mBounds);
600
601 if (mRunning || (mFinishTime > 0)) {
602 long now = AnimationUtils.currentAnimationTimeMillis();
603 long elapsed = (now - mStartTime) % ANIMATION_DURATION_MS;
604 long iterations = (now - mStartTime) / ANIMATION_DURATION_MS;
605 float rawProgress = (elapsed / (ANIMATION_DURATION_MS / 100f));
606
607 // If we're not running anymore, that means we're running through
608 // the finish animation.
609 if (!mRunning) {
610 // If the finish animation is done, don't draw anything, and
611 // don't repost.
612 if ((now - mFinishTime) >= FINISH_ANIMATION_DURATION_MS) {
613 mFinishTime = 0;
614 return;
615 }
616
617 // Otherwise, use a 0 opacity alpha layer to clear the animation
618 // from the inside out. This layer will prevent the circles from
619 // drawing within its bounds.
620 long finishElapsed = (now - mFinishTime) % FINISH_ANIMATION_DURATION_MS;
621 float finishProgress = (finishElapsed / (FINISH_ANIMATION_DURATION_MS / 100f));
622 float pct = (finishProgress / 100f);
623 // Radius of the circle is half of the screen.
624 float clearRadius = width / 2 * INTERPOLATOR.getInterpolation(pct);
625 mClipRect.set(cx - clearRadius, 0, cx + clearRadius, height);
626 canvas.saveLayerAlpha(mClipRect, 0, 0);
627 // Only draw the trigger if there is a space in the center of
628 // this refreshing view that needs to be filled in by the
629 // trigger. If the progress view is just still animating, let it
630 // continue animating.
631 drawTriggerWhileFinishing = true;
632 }
633
634 // First fill in with the last color that would have finished drawing.
635 if (iterations == 0) {
636 canvas.drawColor(mColor1);
637 } else {
638 if (rawProgress >= 0 && rawProgress < 25) {
639 canvas.drawColor(mColor4);
640 } else if (rawProgress >= 25 && rawProgress < 50) {
641 canvas.drawColor(mColor1);
642 } else if (rawProgress >= 50 && rawProgress < 75) {
643 canvas.drawColor(mColor2);
644 } else {
645 canvas.drawColor(mColor3);
646 }
647 }
648
649 // Then draw up to 4 overlapping concentric circles of varying radii, based on how far
650 // along we are in the cycle.
651 // progress 0-50 draw mColor2
652 // progress 25-75 draw mColor3
653 // progress 50-100 draw mColor4
654 // progress 75 (wrap to 25) draw mColor1
655 if ((rawProgress >= 0 && rawProgress <= 25)) {
656 float pct = (((rawProgress + 25) * 2) / 100f);
657 drawCircle(canvas, cx, cy, mColor1, pct);
658 }
659 if (rawProgress >= 0 && rawProgress <= 50) {
660 float pct = ((rawProgress * 2) / 100f);
661 drawCircle(canvas, cx, cy, mColor2, pct);
662 }
663 if (rawProgress >= 25 && rawProgress <= 75) {
664 float pct = (((rawProgress - 25) * 2) / 100f);
665 drawCircle(canvas, cx, cy, mColor3, pct);
666 }
667 if (rawProgress >= 50 && rawProgress <= 100) {
668 float pct = (((rawProgress - 50) * 2) / 100f);
669 drawCircle(canvas, cx, cy, mColor4, pct);
670 }
671 if ((rawProgress >= 75 && rawProgress <= 100)) {
672 float pct = (((rawProgress - 75) * 2) / 100f);
673 drawCircle(canvas, cx, cy, mColor1, pct);
674 }
675 if (mTriggerPercentage > 0 && drawTriggerWhileFinishing) {
676 // There is some portion of trigger to draw. Restore the canvas,
677 // then draw the trigger. Otherwise, the trigger does not appear
678 // until after the bar has finished animating and appears to
679 // just jump in at a larger width than expected.
680 canvas.restoreToCount(restoreCount);
681 restoreCount = canvas.save();
682 canvas.clipRect(mBounds);
683 drawTrigger(canvas, cx, cy);
684 }
685 // Keep running until we finish out the last cycle.
686 ViewCompat.postInvalidateOnAnimation(mParent);
687 } else {
688 // Otherwise if we're in the middle of a trigger, draw that.
689 if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) {
690 drawTrigger(canvas, cx, cy);
691 }
692 }
693 canvas.restoreToCount(restoreCount);
694 }
695
696 private void drawTrigger(Canvas canvas, int cx, int cy) {
697 mPaint.setColor(mColor1);
698 // Use a rectangle to instead of a circle as per UX.
699 // canvas.drawCircle(cx, cy, cx * mTriggerPercentage, mPaint);
700 canvas.drawRect(cx - cx * mTriggerPercentage, 0, cx + cx * mTriggerPercentage,
701 mBounds.bottom, mPaint);
702 }
703
704 /**
705 * Draws a circle centered in the view.
706 *
707 * @param canvas the canvas to draw on
708 * @param cx the center x coordinate
709 * @param cy the center y coordinate
710 * @param color the color to draw
711 * @param pct the percentage of the view that the circle should cover
712 */
713 private void drawCircle(Canvas canvas, float cx, float cy, int color, float pct) {
714 mPaint.setColor(color);
715 canvas.save();
716 canvas.translate(cx, cy);
717 float radiusScale = INTERPOLATOR.getInterpolation(pct);
718 canvas.scale(radiusScale, radiusScale);
719 canvas.drawCircle(0, 0, cx, mPaint);
720 canvas.restore();
721 }
722
723 /**
724 * Set the drawing bounds of this SwipeProgressBar.
725 */
726 void setBounds(int left, int top, int right, int bottom) {
727 mBounds.left = left;
728 mBounds.top = top;
729 mBounds.right = right;
730 mBounds.bottom = bottom;
731 }
732 }
733
734 private static final class BakedBezierInterpolator implements Interpolator {
735 private static final BakedBezierInterpolator INSTANCE = new BakedBezierInterpolator();
736
737 public final static BakedBezierInterpolator getInstance() {
738 return INSTANCE;
739 }
740
741 /**
742 * Use getInstance instead of instantiating.
743 */
744 private BakedBezierInterpolator() {
745 super();
746 }
747
748 /**
749 * Lookup table values.
750 * Generated using a Bezier curve from (0,0) to (1,1) with control points:
751 * P0 (0,0)
752 * P1 (0.4, 0)
753 * P2 (0.2, 1.0)
754 * P3 (1.0, 1.0)
755 *
756 * Values sampled with x at regular intervals between 0 and 1.
757 */
758 private static final float[] VALUES = new float[] {
759 0.0f, 0.0002f, 0.0009f, 0.0019f, 0.0036f, 0.0059f, 0.0086f, 0.0119f, 0.0157f, 0.0209f,
760 0.0257f, 0.0321f, 0.0392f, 0.0469f, 0.0566f, 0.0656f, 0.0768f, 0.0887f, 0.1033f, 0.1186f,
761 0.1349f, 0.1519f, 0.1696f, 0.1928f, 0.2121f, 0.237f, 0.2627f, 0.2892f, 0.3109f, 0.3386f,
762 0.3667f, 0.3952f, 0.4241f, 0.4474f, 0.4766f, 0.5f, 0.5234f, 0.5468f, 0.5701f, 0.5933f,
763 0.6134f, 0.6333f, 0.6531f, 0.6698f, 0.6891f, 0.7054f, 0.7214f, 0.7346f, 0.7502f, 0.763f,
764 0.7756f, 0.7879f, 0.8f, 0.8107f, 0.8212f, 0.8326f, 0.8415f, 0.8503f, 0.8588f, 0.8672f,
765 0.8754f, 0.8833f, 0.8911f, 0.8977f, 0.9041f, 0.9113f, 0.9165f, 0.9232f, 0.9281f, 0.9328f,
766 0.9382f, 0.9434f, 0.9476f, 0.9518f, 0.9557f, 0.9596f, 0.9632f, 0.9662f, 0.9695f, 0.9722f,
767 0.9753f, 0.9777f, 0.9805f, 0.9826f, 0.9847f, 0.9866f, 0.9884f, 0.9901f, 0.9917f, 0.9931f,
768 0.9944f, 0.9955f, 0.9964f, 0.9973f, 0.9981f, 0.9986f, 0.9992f, 0.9995f, 0.9998f, 1.0f, 1.0f
769 };
770
771 private static final float STEP_SIZE = 1.0f / (VALUES.length - 1);
772
773 @Override
774 public float getInterpolation(float input) {
775 if (input >= 1.0f) {
776 return 1.0f;
777 }
778
779 if (input <= 0f) {
780 return 0f;
781 }
782
783 int position = Math.min(
784 (int)(input * (VALUES.length - 1)),
785 VALUES.length - 2);
786
787 float quantized = position * STEP_SIZE;
788 float difference = input - quantized;
789 float weight = difference / STEP_SIZE;
790
791 return VALUES[position] + weight * (VALUES[position + 1] - VALUES[position]);
792 }
793
794 }
795 }

mercurial