SwipeRefreshLayout完美新增及完善上拉載入功能
關於Google推出的下拉重新整理控制元件SwipeRefreshLayout的相關使用方法,大家可以去參考http://blog.csdn.net/geeklei/article/details/38876981,本文也借鑑了其中的一些內容和“顏路的部落格”中《官方下拉重新整理SwipeRefreshLayout增加上拉載入更多》一文。
話不多說,直接先上改造效果圖(截圖時卡,湊合看吧):
下拉重新整理和上拉載入
簡單講下原始程式碼的原理:
下拉時,計算手指移動距離,如果超過一個系統預設的臨界值mTouchSlop,該事件就不下發到子控制元件進行處理,而是SwipeRefreshLayout自己處理。
變數mDistanceToTriggerSync指定了下拉重新整理的臨界值,如果下拉距離沒有大於該值,則計算下拉距離和mDistanceToTriggerSync的比值,並用該值作為進度百分比對進度條mProgressBar進行設定,同時移動子控制元件(ListView之類)的位置,螢幕上可以看到進度條顏色緩慢拉長的動畫,同時子控制元件向下移動。
如果下拉距離大於mDistanceToTriggerSync,則設定動畫把子控制元件位置復位,然後啟動下拉重新整理的色條迴圈動畫,並執行下拉重新整理的監聽事件。
關於進度條SwipeProgressBar的動畫顯示,Google的程式碼裡埋藏了一個坑人的陷阱。現象就是如果你在底部加了進度條,動畫效果異常,不會出現漸變的色條,只是生硬的轉換。上面參考的文章裡也碰到了這個問題。其實原因很簡單,看下圖:
把進度條SwipeProgressBar的高度設定大了後,可以看出其動畫效果是在進度條的中心向外部迴圈畫圓,每個迴圈中圓的顏色不同。重點是圓心的位置。
看SwipeProgressBar的如下程式碼,會發現在計算圓心高度cy的時候,取值是進度條高度的一半,這樣的話圓心會一直在上面,底部進度條自然動畫異常
- void draw(Canvas canvas) {
- finalint width = mBounds.width();
- finalint height = mBounds.height();
- finalint cx = width / 2;
- finalint cy = height / 2;
- boolean drawTriggerWhileFinishing = false;
- int restoreCount = canvas.save();
- canvas.clipRect(mBounds);
修改SwipeProgressBar的程式碼,使其圓心在所在進度條的中心:
- void draw(Canvas canvas) {
- finalint width = mBounds.width();
- finalint height = mBounds.height();
- finalint cx = width / 2;
- // final int cy = height / 2;
- finalint cy = mBounds.bottom - height / 2;
- boolean drawTriggerWhileFinishing = false;
- int restoreCount = canvas.save();
- canvas.clipRect(mBounds);
效果如圖:
明白了原始程式碼的原理,就好入手進行修改了,修改的程式碼會在後面貼出來,註釋很詳細,這裡就不具體分析了。對SDK<14的滑動部分暫時沒有進行處理,直接返回了false,待後續改進。(已改進)
下面看修改後的功能:
1.可設定是否開啟下拉重新整理功能,可設定是否開啟上拉載入功能,預設全部開啟。
2.可設定是否在資料不滿一屏的情況下開啟上拉載入功能,預設關閉。
3.可單獨設定上下進度條的顏色,也可同時設定一樣的顏色。
囉嗦了這麼多,上程式碼:
SwipeProgressBar:
- package com.dahuo.learn.swiperefreshandload.view;
- /*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- import android.graphics.Canvas;
- import android.graphics.Paint;
- import android.graphics.Rect;
- import android.graphics.RectF;
- import android.support.v4.view.ViewCompat;
- import android.view.View;
- import android.view.animation.AnimationUtils;
- import android.view.animation.Interpolator;
- /**
- * Custom progress bar that shows a cycle of colors as widening circles that
- * overdraw each other. When finished, the bar is cleared from the inside out as
- * the main cycle continues. Before running, this can also indicate how close
- * the user is to triggering something (e.g. how far they need to pull down to
- * trigger a refresh).
- */
- finalclass SwipeProgressBar {
- // Default progress animation colors are grays.
- privatefinalstaticint COLOR1 = 0xB3000000;
- privatefinalstaticint COLOR2 = 0x80000000;
- privatefinalstaticint COLOR3 = 0x4d000000;
- privatefinalstaticint COLOR4 = 0x1a000000;
- // The duration of the animation cycle.
- privatestaticfinalint ANIMATION_DURATION_MS = 2000;
- // The duration of the animation to clear the bar.
- privatestaticfinalint FINISH_ANIMATION_DURATION_MS = 1000;
- // Interpolator for varying the speed of the animation.
- privatestaticfinal Interpolator INTERPOLATOR = BakedBezierInterpolator.getInstance();
- privatefinal Paint mPaint = new Paint();
- privatefinal RectF mClipRect = new RectF();
- privatefloat mTriggerPercentage;
- privatelong mStartTime;
- privatelong mFinishTime;
- privateboolean mRunning;
- // Colors used when rendering the animation,
- privateint mColor1;
- privateint mColor2;
- privateint mColor3;
- privateint mColor4;
- private View mParent;
- private Rect mBounds = new Rect();
- public SwipeProgressBar(View parent) {
- mParent = parent;
- mColor1 = COLOR1;
- mColor2 = COLOR2;
- mColor3 = COLOR3;
- mColor4 = COLOR4;
- }
- /**
- * Set the four colors used in the progress animation. The first color will
- * also be the color of the bar that grows in response to a user swipe
- * gesture.
- *
- * @param color1 Integer representation of a color.
- * @param color2 Integer representation of a color.
- * @param color3 Integer representation of a color.
- * @param color4 Integer representation of a color.
- */
- void setColorScheme(int color1, int color2, int color3, int color4) {
- mColor1 = color1;
- mColor2 = color2;
- mColor3 = color3;
- mColor4 = color4;
- }
- /**
- * Update the progress the user has made toward triggering the swipe
- * gesture. and use this value to update the percentage of the trigger that
- * is shown.
- */
- void setTriggerPercentage(float triggerPercentage) {
- mTriggerPercentage = triggerPercentage;
- mStartTime = 0;
- ViewCompat.postInvalidateOnAnimation(mParent);
- }
- /**
- * Start showing the progress animation.
- */
- void start() {
- if (!mRunning) {
- mTriggerPercentage = 0;
- mStartTime = AnimationUtils.currentAnimationTimeMillis();
- mRunning = true;
- mParent.postInvalidate();
- }
- }
- /**
- * Stop showing the progress animation.
- */
- void stop() {
- if (mRunning) {
- mTriggerPercentage = 0;
- mFinishTime = AnimationUtils.currentAnimationTimeMillis();
- mRunning = false;
- mParent.postInvalidate();
- }
- }
- /**
- * @return Return whether the progress animation is currently running.
- */
- boolean isRunning() {
- return mRunning || mFinishTime > 0;
- }
- void draw(Canvas canvas) {
- finalint width = mBounds.width();
- finalint height = mBounds.height();
- finalint cx = width / 2;
- // final int cy = height / 2;
- finalint cy = mBounds.bottom - height / 2;
- boolean drawTriggerWhileFinishing = false;
- int restoreCount = canvas.save();
- canvas.clipRect(mBounds);
- if (mRunning || (mFinishTime > 0)) {
- long now = AnimationUtils.currentAnimationTimeMillis();
- long elapsed = (now - mStartTime) % ANIMATION_DURATION_MS;
- long iterations = (now - mStartTime) / ANIMATION_DURATION_MS;
- float rawProgress = (elapsed / (ANIMATION_DURATION_MS / 100f));
- // If we're not running anymore, that means we're running through
- // the finish animation.
- if (!mRunning) {
- // If the finish animation is done, don't draw anything, and
- // don't repost.
- if ((now - mFinishTime) >= FINISH_ANIMATION_DURATION_MS) {
- mFinishTime = 0;
- return;
- }
- // Otherwise, use a 0 opacity alpha layer to clear the animation
- // from the inside out. This layer will prevent the circles from
- // drawing within its bounds.
- long finishElapsed = (now - mFinishTime) % FINISH_ANIMATION_DURATION_MS;
- float finishProgress = (finishElapsed / (FINISH_ANIMATION_DURATION_MS / 100f));
- float pct = (finishProgress / 100f);
- // Radius of the circle is half of the screen.
- float clearRadius = width / 2 * INTERPOLATOR.getInterpolation(pct);
- mClipRect.set(cx - clearRadius, 0, cx + clearRadius, height);
- canvas.saveLayerAlpha(mClipRect, 0, 0);
- // Only draw the trigger if there is a space in the center of
- // this refreshing view that needs to be filled in by the
- // trigger. If the progress view is just still animating, let it
- // continue animating.
- drawTriggerWhileFinishing = true;
- }
- // First fill in with the last color that would have finished drawing.
- if (iterations == 0) {
- canvas.drawColor(mColor1);
- } else {
- if (rawProgress >= 0 && rawProgress < 25) {
- canvas.drawColor(mColor4);
- } elseif (rawProgress >= 25 && rawProgress < 50) {
- canvas.drawColor(mColor1);
- } elseif (rawProgress >= 50 && rawProgress < 75) {
- canvas.drawColor(mColor2);
- } else {
- canvas.drawColor(mColor3);
- }
- }
- // Then draw up to 4 overlapping concentric circles of varying radii, based on how far
- // along we are in the cycle.
- // progress 0-50 draw mColor2
- // progress 25-75 draw mColor3
- // progress 50-100 draw mColor4
- // progress 75 (wrap to 25) draw mColor1
- if ((rawProgress >= 0 && rawProgress <= 25)) {
- float pct = (((rawProgress + 25) * 2) / 100f);
- drawCircle(canvas, cx, cy, mColor1, pct);
- }
- if (rawProgress >= 0 && rawProgress <= 50) {
- float pct = ((rawProgress * 2) / 100f);
- drawCircle(canvas, cx, cy, mColor2, pct);
- }
- if (rawProgress >= 25 && rawProgress <= 75) {
- float pct = (((rawProgress - 25) * 2) / 100f);
- drawCircle(canvas, cx, cy, mColor3, pct);
- }
- if (rawProgress >= 50 && rawProgress <= 100) {
- float pct = (((rawProgress - 50) * 2) / 100f);
- drawCircle(canvas, cx, cy, mColor4, pct);
- }
- if ((rawProgress >= 75 && rawProgress <= 100)) {
- float pct = (((rawProgress - 75) * 2) / 100f);
- drawCircle(canvas, cx, cy, mColor1, pct);
- }
- if (mTriggerPercentage > 0 && drawTriggerWhileFinishing) {
- // There is some portion of trigger to draw. Restore the canvas,
- // then draw the trigger. Otherwise, the trigger does not appear
- // until after the bar has finished animating and appears to
- // just jump in at a larger width than expected.
- canvas.restoreToCount(restoreCount);
- restoreCount = canvas.save();
- canvas.clipRect(mBounds);
- drawTrigger(canvas, cx, cy);
- }
- // Keep running until we finish out the last cycle.
- ViewCompat.postInvalidateOnAnimation(mParent);
- } else {
- // Otherwise if we're in the middle of a trigger, draw that.
- if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) {
- drawTrigger(canvas, cx, cy);
- }
- }
- canvas.restoreToCount(restoreCount);
- }
- privatevoid drawTrigger(Canvas canvas, int cx, int cy) {
- mPaint.setColor(mColor1);
- canvas.drawCircle(cx, cy, cx * mTriggerPercentage, mPaint);
- }
- /**
- * Draws a circle centered in the view.
- *
- * @param canvas the canvas to draw on
- * @param cx the center x coordinate
- * @param cy the center y coordinate
- * @param color the color to draw
- * @param pct the percentage of the view that the circle should cover
- */
- privatevoid drawCircle(Canvas canvas, float cx, float cy, int color, float pct) {
- mPaint.setColor(color);
- canvas.save();
- canvas.translate(cx, cy);
- float radiusScale = INTERPOLATOR.getInterpolation(pct);
- canvas.scale(radiusScale, radiusScale);
- canvas.drawCircle(0, 0, cx, mPaint);
- canvas.restore();
- }
- /**
- * Set the drawing bounds of this SwipeProgressBar.
- */
- void setBounds(int left, int top, int right, int bottom) {
- mBounds.left = left;
- mBounds.top = top;
- mBounds.right = right;
- mBounds.bottom = bottom;
- }
- }
SwipeRefreshLayout:
- /*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.dahuo.learn.swiperefreshandload.view;
- import android.content.Context;
- import android.content.res.Resources;
- import android.content.res.TypedArray;
- import android.graphics.Canvas;
- import android.support.v4.view.MotionEventCompat;
- import android.support.v4.view.ViewCompat;
- import android.util.AttributeSet;
- import android.util.DisplayMetrics;
- import android.util.Log;
- import android.view.MotionEvent;
- import android.view.View;
- import android.view.ViewConfiguration;
- import android.view.ViewGroup;
- import android.view.animation.AccelerateInterpolator;
- import android.view.animation.Animation;
- import android.view.animation.Animation.AnimationListener;
- import android.view.animation.DecelerateInterpolator;
- import android.view.animation.Transformation;
- import android.widget.AbsListView;
- /**
- * The SwipeRefreshLayout should be used whenever the user can refresh the
- * contents of a view via a vertical swipe gesture. The activity that
- * instantiates this view should add an OnRefreshListener to be notified
- * whenever the swipe to refresh gesture is completed. The SwipeRefreshLayout
- * will notify the listener each and every time the gesture is completed again;
- * the listener is responsible for correctly determining when to actually
- * initiate a refresh of its content. If the listener determines there should
- * not be a refresh, it must call setRefreshing(false) to cancel any visual
- * indication of a refresh. If an activity wishes to show just the progress
- * animation, it should call setRefreshing(true). To disable the gesture and progress
- * animation, call setEnabled(false) on the view.
- *
- * <p> This layout should be made the parent of the view that will be refreshed as a
- * result of the gesture and can only support one direct child. This view will
- * also be made the target of the gesture and will be forced to match both the
- * width and the height supplied in this layout. The SwipeRefreshLayout does not
- * provide accessibility events; instead, a menu item must be provided to allow
- * refresh of the content wherever this gesture is used.</p>
- */
- publicclass SwipeRefreshLayout extends ViewGroup {
- privatestaticfinal String LOG_TAG = SwipeRefreshLayout.class.getSimpleName();
- privatestaticfinallong RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300;
- privatestaticfinalfloat ACCELERATE_INTERPOLATION_FACTOR = 1.5f;
- privatestaticfinalfloat DECELERATE_INTERPOLATION_FACTOR = 2f;
- privatestaticfinalfloat PROGRESS_BAR_HEIGHT = 4;
- privatestaticfinalfloat MAX_SWIPE_DISTANCE_FACTOR = .6f;
- privatestaticfinalint REFRESH_TRIGGER_DISTANCE = 120;
- privatestaticfinalint INVALID_POINTER = -1;
- private SwipeProgressBar mProgressBar; //the thing that shows progress is going
- private SwipeProgressBar mProgressBarBottom;
- private View mTarget; //the content that gets pulled down
- privateint mOriginalOffsetTop;
- private OnRefreshListener mRefreshListener;
- private OnLoadListener mLoadListener;
- privateint mFrom;
- privateboolean mRefreshing = false;
- privateboolean mLoading = false;
- privateint mTouchSlop;
- privatefloat mDistanceToTriggerSync = -1;
- privateint mMediumAnimationDuration;
- privatefloat mFromPercentage = 0;
- privatefloat mCurrPercentage = 0;
- privateint mProgressBarHeight;
- privateint mCurrentTargetOffsetTop;
- privatefloat mInitialMotionY;
- privatefloat mLastMotionY;
- privateboolean mIsBeingDragged;
- privateint mActivePointerId = INVALID_POINTER;
- // Target is returning to its start offset because it was cancelled or a
- // refresh was triggered.
- privateboolean mReturningToStart;
- privatefinal DecelerateInterpolator mDecelerateInterpolator;
- privatefinal AccelerateInterpolator mAccelerateInterpolator;
- privatestaticfinalint[] LAYOUT_ATTRS = newint[] {
- android.R.attr.enabled
- };
- private Mode mMode = Mode.getDefault();
- //之前手勢的方向,為了解決同一個觸點前後移動方向不同導致後一個方向會重新整理的問題,
- //這裡Mode.DISABLED無意義,只是一個初始值,和上拉/下拉方向進行區分
- private Mode mLastDirection = Mode.DISABLED;
- privateint mDirection = 0;
- //當子控制元件移動到盡頭時才開始計算初始點的位置
- privatefloat mStartPoint;
- privateboolean up;
- privateboolean down;
- //資料不足一屏時是否開啟上拉載入模式
- privateboolean loadNoFull = false;
- //對下拉或上拉進行復位
- privatefinal Animation mAnimateToStartPosition = new Animation() {
- @Override
- publicvoid applyTransformation(float interpolatedTime, Transformation t) {
- int targetTop = 0;
- if (mFrom != mOriginalOffsetTop) {
- targetTop = (mFrom + (int)((mOriginalOffsetTop - mFrom) * interpolatedTime));
- }
- int offset = targetTop - mTarget.getTop();
- //註釋掉這裡,不然上拉後回覆原位置會很快,不平滑
- // final int currentTop = mTarget.getTop();
- // if (offset + currentTop < 0) {
- // offset = 0 - currentTop;
- // }
- setTargetOffsetTopAndBottom(offset);
- }
- };
- //設定上方進度條的完成度百分比
- private Animation mShrinkTrigger = new Animation() {
- @Override
- publicvoid applyTransformation(float interpolatedTime, Transformation t) {
- float percent = mFromPercentage + ((0 - mFromPercentage) * interpolatedTime);
- mProgressBar.setTriggerPercentage(percent);
- }
- };
- //設定下方進度條的完成度百分比
- private Animation mShrinkTriggerBottom = new Animation() {
- @Override
- publicvoid applyTransformation(float interpolatedTime, Transformation t) {
- float percent = mFromPercentage + ((0 - mFromPercentage) * interpolatedTime);
- mProgressBarBottom.setTriggerPercentage(percent);
- }
- };
- //監聽,回覆初始位置
- privatefinal AnimationListener mReturnToStartPositionListener = new BaseAnimationListener() {
- @Override
- publicvoid onAnimationEnd(Animation animation) {
- // Once the target content has returned to its start position, reset
- // the target offset to 0
- mCurrentTargetOffsetTop = 0;
- mLastDirection = Mode.DISABLED;
- }
- };
- //回覆進度條百分比
- privatefinal AnimationListener mShrinkAnimationListener = new BaseAnimationListener() {
- @Override
- publicvoid onAnimationEnd(Animation animation) {
- mCurrPercentage = 0;
- }
- };
- //回覆初始位置
- privatefinal Runnable mReturnToStartPosition = new Runnable() {
- @Override
- publicvoid run() {
- mReturningToStart = true;
- animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(),
- mReturnToStartPositionListener);
- }
- };
- // Cancel the refresh gesture and animate everything back to its original state.
- privatefinal Runnable mCancel = new Runnable() {
- @Override
- publicvoid run() {
- mReturningToStart = true;
- // Timeout fired since the user last moved their finger; animate the
- // trigger to 0 and put the target back at its original position
- if (mProgressBar != null || mProgressBarBottom != null) {
- mFromPercentage = mCurrPercentage;
- if(mDirection > 0 && ((mMode == Mode.PULL_FROM_START) || (mMode == Mode.BOTH)))
- {
- mShrinkTrigger.setDuration(mMediumAnimationDuration);
- mShrinkTrigger.setAnimationListener(mShrinkAnimationListener);
- mShrinkTrigger.reset();
- mShrinkTrigger.setInterpolator(mDecelerateInterpolator);
- startAnimation(mShrinkTrigger);
- }
- elseif(mDirection < 0 && ((mMode == Mode.PULL_FROM_END) || (mMode == Mode.BOTH)))
- {
- mShrinkTriggerBottom.setDuration(mMediumAnimationDuration);
- mShrinkTriggerBottom.setAnimationListener(mShrinkAnimationListener);
- mShrinkTriggerBottom.reset();
- mShrinkTriggerBottom.setInterpolator(mDecelerateInterpolator);
- startAnimation(mShrinkTriggerBottom);
- }
- }
- mDirection = 0;
- animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(),
- mReturnToStartPositionListener);
- }
- };
- /**
- * Simple constructor to use when creating a SwipeRefreshLayout from code.
- * @param context
- */
- public SwipeRefreshLayout(Context context) {
- this(context, null);
- }
- /**
- * Constructor that is called when inflating SwipeRefreshLayout from XML.
- * @param context
- * @param attrs
- */
- public SwipeRefreshLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
- mMediumAnimationDuration = getResources().getInteger(
- android.R.integer.config_mediumAnimTime);