mobile/android/base/widget/GeckoSwipeRefreshLayout.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     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  */
    17 package org.mozilla.gecko.widget;
    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;
    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;
    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     };
    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     };
   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     };
   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     };
   121     private final AnimationListener mShrinkAnimationListener = new BaseAnimationListener() {
   122         @Override
   123         public void onAnimationEnd(Animation animation) {
   124             mCurrPercentage = 0;
   125         }
   126     };
   128     private final Runnable mReturnToStartPosition = new Runnable() {
   130         @Override
   131         public void run() {
   132             mReturningToStart = true;
   133             animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(),
   134                     mReturnToStartPositionListener);
   135         }
   137     };
   139     // Cancel the refresh gesture and animate everything back to its original state.
   140     private final Runnable mCancel = new Runnable() {
   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         }
   159     };
   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     }
   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);
   177         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
   179         mMediumAnimationDuration = getResources().getInteger(
   180                 android.R.integer.config_mediumAnimTime);
   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);
   189         final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
   190         setEnabled(a.getBoolean(0, true));
   191         a.recycle();
   192     }
   194     @Override
   195     public void onAttachedToWindow() {
   196         super.onAttachedToWindow();
   197         removeCallbacks(mCancel);
   198         removeCallbacks(mReturnToStartPosition);
   199     }
   201     @Override
   202     public void onDetachedFromWindow() {
   203         super.onDetachedFromWindow();
   204         removeCallbacks(mReturnToStartPosition);
   205         removeCallbacks(mCancel);
   206     }
   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     }
   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     }
   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     }
   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     }
   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     }
   275     /**
   276      * @return Whether the SwipeRefreshWidget is actively showing refresh
   277      *         progress.
   278      */
   279     public boolean isRefreshing() {
   280         return mRefreshing;
   281     }
   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     }
   303     @Override
   304     public void draw(Canvas canvas) {
   305         super.draw(canvas);
   306         mProgressBar.draw(canvas);
   307     }
   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     }
   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     }
   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     }
   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     }
   374     @Override
   375     public void requestDisallowInterceptTouchEvent(boolean b) {
   376         // Nope.
   377     }
   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     }
   436     private void startRefresh() {
   437         removeCallbacks(mCancel);
   438         mReturnToStartPosition.run();
   439         setRefreshing(true);
   440         mListener.onRefresh();
   441     }
   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     }
   453     private void setTargetOffsetTopAndBottom(int offset) {
   454         mTarget.offsetTopAndBottom(offset);
   455         mCurrentTargetOffsetTop = mTarget.getTop();
   456     }
   458     private void updatePositionTimeout() {
   459         removeCallbacks(mCancel);
   460         postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT);
   461     }
   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     }
   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         }
   480         @Override
   481         public void onAnimationEnd(Animation animation) {
   482         }
   484         @Override
   485         public void onAnimationRepeat(Animation animation) {
   486         }
   487     }
   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;
   500         // The duration of the animation cycle.
   501         private static final int ANIMATION_DURATION_MS = 2000;
   503         // The duration of the animation to clear the bar.
   504         private static final int FINISH_ANIMATION_DURATION_MS = 1000;
   506         // Interpolator for varying the speed of the animation.
   507         private static final Interpolator INTERPOLATOR = BakedBezierInterpolator.getInstance();
   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;
   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;
   523         private Rect mBounds = new Rect();
   525         public SwipeProgressBar(View parent) {
   526             mParent = parent;
   527             mColor1 = COLOR1;
   528             mColor2 = COLOR2;
   529             mColor3 = COLOR3;
   530             mColor4 = COLOR4;
   531         }
   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         }
   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         }
   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         }
   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         }
   585         /**
   586          * @return Return whether the progress animation is currently running.
   587          */
   588         boolean isRunning() {
   589             return mRunning || mFinishTime > 0;
   590         }
   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);
   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));
   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                     }
   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                 }
   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                 }
   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         }
   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         }
   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         }
   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     }
   734     private static final class BakedBezierInterpolator implements Interpolator {
   735         private static final BakedBezierInterpolator INSTANCE = new BakedBezierInterpolator();
   737         public final static BakedBezierInterpolator getInstance() {
   738             return INSTANCE;
   739         }
   741         /**
   742          * Use getInstance instead of instantiating.
   743          */
   744         private BakedBezierInterpolator() {
   745             super();
   746         }
   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         };
   771         private static final float STEP_SIZE = 1.0f / (VALUES.length - 1);
   773         @Override
   774         public float getInterpolation(float input) {
   775             if (input >= 1.0f) {
   776                 return 1.0f;
   777             }
   779             if (input <= 0f) {
   780                 return 0f;
   781             }
   783             int position = Math.min(
   784                     (int)(input * (VALUES.length - 1)),
   785                     VALUES.length - 2);
   787             float quantized = position * STEP_SIZE;
   788             float difference = input - quantized;
   789             float weight = difference / STEP_SIZE;
   791             return VALUES[position] + weight * (VALUES[position + 1] - VALUES[position]);
   792         }
   794     }
   795 }

mercurial