編輯:關於Android編程
咱們在做Android APP開發的時候經常碰到有下拉刷新和上拉加載跟多的需求,這篇文章咱們先說說下來刷新,咱們就以google的原生的下拉刷新控件SwipeRefreshLayout來看看大概的實現過程。
SwipeRefreshLayout是google自己推出的下拉刷新控件。使用起來也非常的簡單,在滿足條件的情況下下拉的時候會顯示一個圓形的loading的動畫效果,然後回調到上層,上層自己做刷新的一系列的處理,處理結束後調用SwipeRefreshLayout的setRefreshing(false)告訴SwipeRefreshLayout完成刷新。具體的效果圖如下
那接下來我們就來簡單的看下SwipeRefreshLayout內部是怎麼實現的下拉刷新。准備從三個CircleImageView,MaterialProgressDrawable,SwipeRefreshLayout相關的類著手來分析下拉刷新代碼簡單實現。
繼承自ImageView,CircleImageView是一個圓形的並且底部是有一定陰影效果的ImageView。正如上圖中下拉刷新的時候顯示的那個白色的小圓。
CircleImageView的具體實現。裡面的代碼非常的少就干了兩件事一個是確定圓形,一個是圓形底部的陰影效果(包括向下兼容的情況)。具體的實現我就干脆寫在代碼的注釋裡面了,CircleImageView包所在的路徑android.support.v4.widget。
CircleImageView構造函數
public CircleImageView(Context context, int color, final float radius) { super(context); final float density = getContext().getResources().getDisplayMetrics().density; final int diameter = (int) (radius * density * 2); final int shadowYOffset = (int) (density * Y_OFFSET); final int shadowXOffset = (int) (density * X_OFFSET); mShadowRadius = (int) (density * SHADOW_RADIUS); ShapeDrawable circle; if (elevationSupported()) { // 確保是一個圓形 circle = new ShapeDrawable(new OvalShape()); // 如果版本支持陰影的設置,直接調用setElevation函數設置陰影效果 ViewCompat.setElevation(this, SHADOW_ELEVATION * density); } else { // 如果版本不支持陰影效果的設置,沒辦了只能自己去實現一個類似的效果了。 // OvalShadow是繼承自OvalShape自定義的一個類,用來實現類似的陰影效果(這個可能是我們的一個學習的點)。 OvalShape oval = new OvalShadow(mShadowRadius, diameter); circle = new ShapeDrawable(oval); // 關閉硬件加速,要不繪制的陰影沒有效果 ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, circle.getPaint()); // 設置陰影層,Y方向稍微偏移了一點點 circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset, KEY_SHADOW_COLOR); final int padding = mShadowRadius; // 保證接下的內容不會繪制到陰影上面去,但是陰影被覆蓋住。 setPadding(padding, padding, padding, padding); } circle.getPaint().setColor(color); setBackgroundDrawable(circle); }
CircleImageView測量函數
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (!elevationSupported()) { // 如果不支持陰影效果,把陰影的范圍加進去重新設置控件的大小 setMeasuredDimension(getMeasuredWidth() + mShadowRadius * 2, getMeasuredHeight() + mShadowRadius * 2); } }
為了向下兼容實現類似陰影效果而自定義的類
/** * 繼承自OvalShape,先保證圖像是圓形的。重寫draw方法實現一個類似陰影的效果 */ private class OvalShadow extends OvalShape { private RadialGradient mRadialGradient; private Paint mShadowPaint; private int mCircleDiameter; public OvalShadow(int shadowRadius, int circleDiameter) { super(); // 畫陰影的paint mShadowPaint = new Paint(); // 陰影的范圍大小 mShadowRadius = shadowRadius; // 直徑 mCircleDiameter = circleDiameter; // 環形渲染,達到陰影的效果 mRadialGradient = new RadialGradient(mCircleDiameter / 2, mCircleDiameter / 2, mShadowRadius, new int[]{FILL_SHADOW_COLOR, Color.TRANSPARENT}, null, Shader.TileMode.CLAMP); mShadowPaint.setShader(mRadialGradient); } @Override public void draw(Canvas canvas, Paint paint) { final int viewWidth = CircleImageView.this.getWidth(); final int viewHeight = CircleImageView.this.getHeight(); // 先畫上陰影效果 canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2 + mShadowRadius), mShadowPaint); // 畫上內容 canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2), paint); } }
繼承自Drawable,這個Drawable干的事情就是當下拉刷新進入加載的時候顯示一個小圓環,並且這個小圓環是可以一直轉圈的,正如上文效果圖中一直轉圈並且顏色不同變化的情況就是通過MaterialProgressDrawable來實現的。
MaterialProgressDrawable繼承自Drawable,既然是繼承自Drawable那咱們首先關注重寫的getIntrinsicHeight() getIntrinsicWidth() draw(Canvas c) setAlpha(int alpha)這些方法。其中getIntrinsicHeight和getIntrinsicWidth用來給依附的view提供測量大小,draw函數就是Drawable具體的內容了,setAlpha設置透明度。我們就直接看draw()函數了。
@Override public void draw(Canvas c) { // 自定義Drawable的時候draw函數是關鍵部分 final Rect bounds = getBounds(); // 獲取Drawable的區域 final int saveCount = c.save(); // 旋轉mRotation角度 c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); // 這個裡面就開始畫箭頭和轉圈的小圓環了 mRing.draw(c, bounds); c.restoreToCount(saveCount); }
在draw函數中mRing.draw(c, bounds);就是來繪制轉圈的那個圓環的。調用的是內部類Ring的draw()函數,進入看下咯。
public void draw(Canvas c, Rect bounds) { final RectF arcBounds = mTempBounds; arcBounds.set(bounds); // 進度條相對於外圈的一個內邊距 arcBounds.inset(mStrokeInset, mStrokeInset); final float startAngle = (mStartTrim + mRotation) * 360; final float endAngle = (mEndTrim + mRotation) * 360; float sweepAngle = endAngle - startAngle; mPaint.setColor(mCurrentColor); // 畫進度圓環(環的寬度setStrokeWidth) c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); // 如果需要的話,畫箭頭 drawTriangle(c, startAngle, sweepAngle, bounds); if (mAlpha < 255) { // 在上面覆蓋一層alpha,達到透明的效果 mCirclePaint.setColor(mBackgroundColor); mCirclePaint.setAlpha(255 - mAlpha); c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, mCirclePaint); } } private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { if (mShowArrow) { // 如果現實箭頭 if (mArrow == null) { mArrow = new android.graphics.Path(); mArrow.setFillType(android.graphics.Path.FillType.EVEN_ODD); } else { mArrow.reset(); } // 找到三角形箭頭要偏移的位置(x,y方向要偏移的位置) float inset = (int) mStrokeInset / 2 * mArrowScale; float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); // 先確定三角形箭頭的三個點,在偏移到0度角的位置,然後再旋轉進度條掃過的角度,在封閉形成三角形箭頭 mArrow.moveTo(0, 0); mArrow.lineTo(mArrowWidth * mArrowScale, 0); mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight * mArrowScale)); mArrow.offset(x - inset, y); mArrow.close(); // draw a triangle mArrowPaint.setColor(mCurrentColor); c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), bounds.exactCenterY()); c.drawPath(mArrow, mArrowPaint); } }
畫圓環,畫圓環上面的三角形箭頭有了吧。到現在圖形是有了,但是啥時候開始轉圈啥時候停止轉圈動畫呢。看MaterialProgressDrawable的start()和stop()函數,對應的就是開始結束轉圈的動畫。看看start()裡面到底做的是寫啥。
// MaterialProgressDrawable釋放的時候開始轉圈動畫,沒轉一圈換一個顏色 @Override public void start() { mAnimation.reset(); // 進度圓環保存一些mStartTrim,mEndTrim,mRotation設置信息 mRing.storeOriginals(); if (mRing.getEndTrim() != mRing.getStartTrim()) { // 有進度圓環的時候,這個時候做的事情會先慢慢的把這個現有的圓環慢慢的變小,然後在開始轉圈 mFinishing = true; mAnimation.setDuration(ANIMATION_DURATION / 2); mParent.startAnimation(mAnimation); } else { // 沒有進度圓環的時候,直接開始轉圈的動畫 mRing.setColorIndex(0); mRing.resetOriginals(); mAnimation.setDuration(ANIMATION_DURATION); mParent.startAnimation(mAnimation); } }
恩,都是在和mAnimation變量打交道。接著看下mAnimation干啥用的。
private void setupAnimators() { final MaterialProgressDrawable.Ring ring = mRing; final Animation animation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { if (mFinishing) { // 在有進度圓環的時候我們去啟動轉圈的動畫的時候是要先把這個圓環慢慢的變小消失 applyFinishTranslation(interpolatedTime, ring); } else { final float minProgressArc = getMinProgressArc(ring); final float startingEndTrim = ring.getStartingEndTrim(); final float startingTrim = ring.getStartingStartTrim(); final float startingRotation = ring.getStartingRotation(); // 每次repeat的動畫在最後的25%的過程中顏色有過渡的效果 updateRingColor(interpolatedTime, ring); // 每次repeat的動畫的前50%的時候圓環的起始角度有一個往前移的動作 if (interpolatedTime <= START_TRIM_DURATION_OFFSET) { final float scaledTime = (interpolatedTime) / (1.0f - START_TRIM_DURATION_OFFSET); final float startTrim = startingTrim + ((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime)); ring.setStartTrim(startTrim); } // 每次repeat的動畫的後50%的時候圓環的結束角度有一個往前移的動作 if (interpolatedTime > END_TRIM_START_DELAY_OFFSET) { final float minArc = MAX_PROGRESS_ARC - minProgressArc; float scaledTime = (interpolatedTime - START_TRIM_DURATION_OFFSET) / (1.0f - START_TRIM_DURATION_OFFSET); final float endTrim = startingEndTrim + (minArc * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime)); ring.setEndTrim(endTrim); } final float rotation = startingRotation + (0.25f * interpolatedTime); // 圓環旋轉的效果 ring.setRotation(rotation); float groupRotation = ((FULL_ROTATION / NUM_POINTS) * interpolatedTime) + (FULL_ROTATION * (mRotationCount / NUM_POINTS)); setRotation(groupRotation); } } }; animation.setRepeatCount(Animation.INFINITE); animation.setRepeatMode(Animation.RESTART); animation.setInterpolator(LINEAR_INTERPOLATOR); animation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { mRotationCount = 0; } @Override public void onAnimationEnd(Animation animation) { // do nothing } @Override public void onAnimationRepeat(Animation animation) { // 轉一圈圓環換一個顏色 ring.storeOriginals(); ring.goToNextColor(); ring.setStartTrim(ring.getEndTrim()); if (mFinishing) { // 在SwipeRefreshLayout中調用MaterialProgressDrawable類start函數的時候, // 如果有圓環第一次動畫就是圓環慢慢消失,這裡表示消失完成了 mFinishing = false; animation.setDuration(ANIMATION_DURATION); ring.setShowArrow(false); } else { mRotationCount = (mRotationCount + 1) % (NUM_POINTS); } } }); mAnimation = animation; } // 在有進度圓環的時候我們去啟動轉圈的動畫的時候是要先把這個圓環慢慢的變小消失 private void applyFinishTranslation(float interpolatedTime, MaterialProgressDrawable.Ring ring) { // 進度圓環的顏色有一個過渡的效果 updateRingColor(interpolatedTime, ring); // 一次動畫要轉的rotation float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC) + 1f); final float minProgressArc = getMinProgressArc(ring); final float startTrim = ring.getStartingStartTrim() + (ring.getStartingEndTrim() - minProgressArc - ring.getStartingStartTrim()) * interpolatedTime; // 在一圈的過程中進度圓環是慢慢變小的所以setEndTrim是沒變化的 ring.setStartTrim(startTrim); ring.setEndTrim(ring.getStartingEndTrim()); final float rotation = ring.getStartingRotation() + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); // 在一圈的過程中進度圓環會慢慢往前旋轉的 ring.setRotation(rotation); }
看的出來mAnimation是自定義的一個Animation(自定義Animation的時候重心在applyTransformation函數上面會隨著動畫的進行不斷的回調這個函數)。如果在我們調用MaterialProgressDrawable start()函數的時候如果有小圓圈的顯示動畫的第一次repeat的時候會把這個小圓圈慢慢的變小從applyFinishTranslation()可以分析得到。然後才開始一圈一圈的轉圈並且每次repeat的時候會換一種顏色。
本文最重要的一個類來了,這個才是下拉刷新接觸最多的一個類,下拉刷新打不的邏輯都集中在這個類當中。SwipeRefreshLayout繼承自ViewGroup是一個容器控件。既然是一個自定義的容器類那咱們就從onMeasure(),onLayout(),onInterceptTouchEvent(),onTouchEvent()四個函數入手來分析SwipeRefreshLayout的過程。
從onMeasure()函數我們可以得到SwipeRefreshLayout中控件的測量規則,看看onMeasure()的具體實現
private void ensureTarget() { // 取了第一個不是mCircleView的view作為mTarget View if (mTarget == null) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (!child.equals(mCircleView)) { mTarget = child; break; } } } } @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mTarget == null) { ensureTarget(); } if (mTarget == null) { return; } // mTarget這個就是咱們的內容控件,直接適用了SwipeRefreshLayout的整個大小 mTarget.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); // mCircleView這個就是咱們下拉和加載的時候顯示的那個小圓圈在構造函數中addView,給了確定的大小,具體可以參SwipeRefreshLayout的構造函數 mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); // 確定mCircleView初始的偏移位置和當前位置 if (!mUsingCustomStart && !mOriginalOffsetCalculated) { mOriginalOffsetCalculated = true; mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); } // mCircleView在SwipeRefreshLayout中的子View的index mCircleViewIndex = -1; // Get the index of the circleview. for (int index = 0; index < getChildCount(); index++) { if (getChildAt(index) == mCircleView) { mCircleViewIndex = index; break; } } }
從上面代碼分析咱可以看得出來SwipeRefreshLayout只關心兩個View:mTarget、mCircleView。其中mTarget是內容控件,mCircleView下拉或者刷新過程中顯示的小圓控件。同時mTarget的大小設置了整個SwipeRefreshLayout的大小所以咱們在xml中設置的大小應該是不算數的。
從onLayout()函數我們可以得到SwipeRefreshLayout中控件的布局規則
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int width = getMeasuredWidth(); final int height = getMeasuredHeight(); if (getChildCount() == 0) { return; } if (mTarget == null) { ensureTarget(); } if (mTarget == null) { return; } final View child = mTarget; final int childLeft = getPaddingLeft(); final int childTop = getPaddingTop(); final int childWidth = width - getPaddingLeft() - getPaddingRight(); final int childHeight = height - getPaddingTop() - getPaddingBottom(); // 設置mTarget的位置,正常布局沒啥看頭 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); int circleWidth = mCircleView.getMeasuredWidth(); int circleHeight = mCircleView.getMeasuredHeight(); // 設置mCircleView,也是正常布局就偏移了mCurrentTargetOffsetTop的高度,這個好理解咱mCircleView是會上下滑動的 mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight); }
onLayout()還是中規中矩的,分別布局了mTarget和mCircleView
onInterceptTouchEvent()用來對觸摸事件做攔截處理。如果攔截了就不會想子View傳遞了。關於事件的攔截想多說已經如果ACTION_DOWN被攔截下來了那麼該事件接下來的ACTION_MOV和EACTION_UP也不會往下傳遞。
/** * 就是去判斷mTarget是否有向上滑動,有一個向上的scroll。如果有這個時候肯定是不能下拉刷新的吧 */ public boolean canChildScrollUp() { if (android.os.Build.VERSION.SDK_INT < 14) { if (mTarget instanceof AbsListView) { final AbsListView absListView = (AbsListView) mTarget; return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0).getTop() < absListView.getPaddingTop()); } else { return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0; } } else { return ViewCompat.canScrollVertically(mTarget, -1); } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { ensureTarget(); final int action = MotionEventCompat.getActionMasked(ev); // mReturningToStart好像沒啥作用,一直是false if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } // 如果mTarget這個時候有向上滑動有scroll y(這個時候是不滿足下拉刷新的條件的),或者正在刷新。事件不攔截個字View去處理。 // 從這裡也可以看出當正在刷新的時候子View還是會想要按鍵事件的。 if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) { // 不攔截 return false; } switch (action) { case MotionEvent.ACTION_DOWN://ACTION_DOWN這個時間是不攔截的 // mCircleView移動到起始位置。(mOriginalOffsetTop設置的初始位置+mCircleView設置的top位置) setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); // 標記下拉是否開始了 mIsBeingDragged = false; final float initialDownY = getMotionEventY(ev, mActivePointerId); if (initialDownY == -1) { return false; } mInitialDownY = initialDownY; break; case MotionEvent.ACTION_MOVE: if (mActivePointerId == INVALID_POINTER) { Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); return false; } final float y = getMotionEventY(ev, mActivePointerId); if (y == -1) { return false; } final float yDiff = y - mInitialDownY; // y方向有滑動 if (yDiff > mTouchSlop && !mIsBeingDragged) { mInitialMotionY = mInitialDownY + mTouchSlop; // 下拉開始,從這個時候開始當前事件一直到ACTION_UP之間的事件我們是會攔截下來的 mIsBeingDragged = true; mProgress.setAlpha(STARTING_PROGRESS_ALPHA); } break; case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; break; } return mIsBeingDragged; }
先判斷是否滿足下拉刷新的條件,同時咱也看的出來ACTION_DOWN不去做攔截處理。主要的攔截在ACTION_MOVE裡面。當滿足下拉刷新的條件並且下拉了那不好意思這次的時間我SwipeRefreshLayout要強行插手處理了。接下來就得去onTouchEvent()函數了。
onTouchEvent()SwipeRefreshLayout對具體的事件都在這個函數裡面了。
@Override public boolean onTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } // 如果mTarget這個時候有向上滑動有scroll, SwipeRefreshLayout不對該事件做處理 if (!isEnabled() || mReturningToStart || canChildScrollUp()) { return false; } switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; break; case MotionEvent.ACTION_MOVE: { final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); return false; } final float y = MotionEventCompat.getY(ev, pointerIndex); final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; if (mIsBeingDragged) { if (overscrollTop > 0) { // 可以處理下拉了,mCircleView會隨著手指往下移動了 moveSpinner(overscrollTop); } else { return false; } } break; } case MotionEventCompat.ACTION_POINTER_DOWN: { final int index = MotionEventCompat.getActionIndex(ev); mActivePointerId = MotionEventCompat.getPointerId(ev, index); break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { if (mActivePointerId == INVALID_POINTER) { if (action == MotionEvent.ACTION_UP) { Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); } return false; } final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float y = MotionEventCompat.getY(ev, pointerIndex); final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; mIsBeingDragged = false; // 是否觸摸的時候,mCircleView會到指定的位置,必要的話進入刷新的狀態 finishSpinner(overscrollTop); mActivePointerId = INVALID_POINTER; return false; } } return true; }
重心在MotionEvent.ACTION_MOVE和MotionEvent.ACTION_UP上面正好對應了moveSpinner()和finishSpinner()函數。咱們先分析moveSpinner()這個函數做的事情就是隨著手指的下拉mCircleView做相應的位移操作並且mCircleView裡面的mProgress(MaterialProgressDrawable)做相應的動態變化。
/** * 下拉過程中調用該函數 * @param overscrollTop:表示y軸上下拉的距離 */ private void moveSpinner(float overscrollTop) { mProgress.showArrow(true); // 相對於刷新距離滑動了百分之多少(注意如果超過了刷新的距離這個值會大於1的) float originalDragPercent = overscrollTop / mTotalDragDistance; // 控制最大值為1 dragPercent == 1 表示滑動距離已經到了刷新的條件了 float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); // 調整下百分比(小於0.4的情況下設置為0) float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; // 相對於進入刷新的位置的偏移量,注意這個值可能是負數。負數表示還沒有達到刷新的距離 float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; // 這裡去計算小圓圈在Y軸上面可以滑動到的距離(targetY)為啥要這樣算就沒搞明白 float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset - mOriginalOffsetTop : mSpinnerFinalOffset; float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2) / slingshotDist); float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2)) * 2f; float extraMove = (slingshotDist) * tensionPercent * 2; int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove); // 在手指滑動的過程中mCircleView小圓圈是可見的 if (mCircleView.getVisibility() != View.VISIBLE) { mCircleView.setVisibility(View.VISIBLE); } if (!mScale) { // 在滑動過程中小圓圈設置不縮放,x,y scale都設置為1 ViewCompat.setScaleX(mCircleView, 1f); ViewCompat.setScaleY(mCircleView, 1f); } if (overscrollTop < mTotalDragDistance) { // 還沒達到刷新的距離的時候 if (mScale) { // 如果設置了小圓圈在滑動的過程中可以縮放,scale慢慢的變大 setAnimationProgress(overscrollTop / mTotalDragDistance); } if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA && !isAnimationRunning(mAlphaStartAnimation)) { // 其實這裡也可以看出來,在沒有達到刷新距離的時候,alpha會盡量保持是STARTING_PROGRESS_ALPHA的(相對來說模糊點) startProgressAlphaStartAnimation(); } float strokeStart = adjustedPercent * .8f; // 設置小圓圈裡面進度條的開始和結束位置(在還沒有達到刷新距離的時候小圓圈裡面進度條是慢慢變大的,最多達到80%的圈) mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); // 設置mCircleView小圓圈裡面進度條箭頭的縮放大小(在還沒有達到刷新距離的時候小圓圈進度條箭頭是慢慢變大的) mProgress.setArrowScale(Math.min(1f, adjustedPercent)); } else { // 達到了刷新的距離的時候(注意這個時候小圓圈裡面進度條占80%,並且是可見的) if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) { // 其實這裡也可以看出來,在達到刷新距離的時候,alpha會盡量保持是MAX_ALPHA的(完全顯示) startProgressAlphaMaxAnimation(); } } float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; // 設置小圓圈進度條的旋轉角度,在下拉的過程中mCircleView小圓圈是一點一點往前旋轉的 mProgress.setProgressRotation(rotation); // mCircleView會隨著手指往下移動 setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */); } /** * mCircleView做縮放操作 */ private void setAnimationProgress(float progress) { if (isAlphaUsedForScale()) { setColorViewAlpha((int) (progress * MAX_ALPHA)); } else { ViewCompat.setScaleX(mCircleView, progress); ViewCompat.setScaleY(mCircleView, progress); } } // 啟動一個alpha變化的動畫,從當前值到STARTING_PROGRESS_ALPHA的變化 private void startProgressAlphaStartAnimation() { mAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA); }
當然裡面涉及到的東西比較都,直接一筆帶過了哦。
接下來咱來看看都手指松開的時候調用的finishSpinner()函數。
/** * 下拉結束的時候調用該函數 * @param overscrollTop: 表示y軸上下拉的距離 */ private void finishSpinner(float overscrollTop) { if (overscrollTop > mTotalDragDistance) { // 下拉結束的時候達到了刷新的距離,這個時候就要告訴上層該進入刷新了 setRefreshing(true, true /* notify */); } else { // 下拉結束的時候還沒有達到刷新的距離 mRefreshing = false; // 小圓圈進度條消失 mProgress.setStartEndTrim(0f, 0f); Animation.AnimationListener listener = null; if (!mScale) { // 小圓圈沒有設置縮放 listener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (!mScale) { // 如果小圓圈沒有設置縮放,當會到了初始位置之後scale縮小為0,不可見 startScaleDownAnimation(null); } } @Override public void onAnimationRepeat(Animation animation) { } }; } // 小圓圈從當前位置返回到初始位置 animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); // 小圓圈裡面進度條不顯示箭頭了 mProgress.showArrow(false); } }
准備進入刷新狀態的時候調用的是setRefreshing()函數。
/** * 是指是否進入刷新狀態 * @param refreshing: 是否進入刷新狀態 * @param notify:是否通知上層,SwipeRefreshLayout的時候定義OnRefreshListener的監聽 */ private void setRefreshing(boolean refreshing, final boolean notify) { if (mRefreshing != refreshing) { // 當前狀態不相同 mNotify = notify; ensureTarget(); mRefreshing = refreshing; if (mRefreshing) { // 進入刷新狀態, animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); } else { // 進入非刷新狀態,直接scale縮小為0了 startScaleDownAnimation(mRefreshListener); } } }
咱還是看進入刷新狀態的情況,調用的是animateOffsetToCorrectPosition()函數。兩個參數一個是mCurrentTargetOffsetTop:mCircleView的當前top位置,一個是mRefreshListener:動畫開始,結束,重復的監聽。animateOffsetToCorrectPosition()函數的啟動一個動畫引導mCircleView到指定的位置,並且在動畫結束的時候會進入到刷新的狀態OnRefreshListener。動畫的具體實現也比較的簡單咱就不具體的貼出來了。
ps:分析的比較簡單,希望對大家能有一點幫助。
Android RecyclerView 是Android5.0推出來的,導入support-v7包即可使用。個人體驗來說,RecyclerView絕對是一款功能強大的控
導語手機直播一般都會通過移動屏幕來調節音量的大小,本篇只實現了圖例,並不能改變音量。先看效果:需要的素材:小喇叭圖片,點擊這裡獲取預熱如果你將這哥們的十幾篇帖子都看完了的
本例為模仿微信聊天界面UI設計,文字發送以及語言錄制UI。1先看效果圖: 第一:chat.xml設計 <?xml vers
筆者發現在很多應用中,都有自動獲取驗證碼的功能:點擊獲取驗證碼按鈕,收到短信,當前應用不需要退出程序就可以獲取到短信中的驗證碼,並自動填充。覺得這種用戶體驗很贊,無須用戶