mobile/android/base/widget/GeckoSwipeRefreshLayout.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial