編輯:關於Android編程
SwipeRefreshLayout是Android官方的下拉刷新控件,使用簡單,界面美觀,不熟悉的朋友可以隨便搜索了解一下,這裡就不廢話了,直接進入正題。
首先給張流程圖吧,標出了幾個主要方法的作用,可以結合著看一下哈。
這種下拉刷新控件的原理不難,基本就是監聽手指的運動,獲取手指的坐標,通過計算判斷出是哪種操作,然後就是回調相應的接口了。SwipeRefreshLayout是繼承自ViewGroup的,根據Android的事件分發機制,觸摸事件應該是先傳遞到ViewGroup,根據onInterceptTouchEvent的返回值決定是否攔截事件的,那麼就onInterceptTouchEvent出發:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { ensureTarget(); final int action = MotionEventCompat.getActionMasked(ev); if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing || mNestedScrollInProgress) { // Fail fast if we're not in a state where a swipe is possible return false; } switch (action) { case MotionEvent.ACTION_DOWN: 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; if (yDiff > mTouchSlop && !mIsBeingDragged) { mInitialMotionY = mInitialDownY + mTouchSlop; 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; }
是否攔截的情況有很多種,這裡如果滿足五個條件之一就直接返回false,使用時觸摸事件發生沖突的話就可以從這裡出發分析,這裡也不具體展開了。簡單看一下,在ACTION_DOWN中記錄下手指坐標,ACTION_MOVE中計算出移動的距離,並且判斷是否大於阈值,是的話就將mIsBeingDragged標志位設為true,ACTION_UP中則將mIsBeingDragged設為false。最後返回的是mIsBeingDragged。
SwipeRefreshLayout一般是嵌套可滾動的View使用的,正常滾動時會滿足前面的條件,這時不進行攔截,只有當滾動到頂部才會進入後面action的判斷。在手指按下和抬起期間mIsBeingDragged為true,也就是說進行攔截,接下來就是如何處理了,看看onTouchEvent:
@Override public boolean onTouchEvent(MotionEvent ev) { .... switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; break; case MotionEvent.ACTION_MOVE: { 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) { moveSpinner(overscrollTop); } else { return false; } } break; } .... case MotionEvent.ACTION_UP: { pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); return false; } final float y = MotionEventCompat.getY(ev, pointerIndex); final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; mIsBeingDragged = false; finishSpinner(overscrollTop); mActivePointerId = INVALID_POINTER; return false; } case MotionEvent.ACTION_CANCEL: return false; } return true; }
這裡省略了一些代碼,前面還有幾行跟上面的類似,也是在滿足其中一個條件時直接返回;switch中也還有幾行處理多指觸控的,這些都略過了。看一下ACTION_MOVE中計算了手指移動的距離,這時的mIsBeingDragged正常情況下應為true,當距離大於零就會執行moveSpinner。在ACTION_UP中則會執行finishSpinner,到這裡就可以猜出,執行刷新的邏輯主要就在這兩個方法中。
看這兩個方法前,要知道兩個重要的成員變量:一個是mCircleView,是CircleImageView的實例,繼承了ImageView,主要繪制進度圈的背景;另一個是mProgress,是MaterialProgressDrawable的實例,繼承自Drawable且實現Animatable接口,主要繪制進度圈,SwipeRefreshLayout正是通過調用其方法來繪制動畫。接下來就先看一下moveSpinner:
<span >private void moveSpinner(float overscrollTop) { mProgress.showArrow(true); float originalDragPercent = overscrollTop / mTotalDragDistance; float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; 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); // where 1.0f is a full circle if (mCircleView.getVisibility() != View.VISIBLE) { mCircleView.setVisibility(View.VISIBLE); } if (!mScale) { ViewCompat.setScaleX(mCircleView, 1f); ViewCompat.setScaleY(mCircleView, 1f); } if (mScale) { setAnimationProgress(Math.min(1f, overscrollTop / mTotalDragDistance)); } if (overscrollTop < mTotalDragDistance) { if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA && !isAnimationRunning(mAlphaStartAnimation)) { // Animate the alpha startProgressAlphaStartAnimation(); } } else { if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) { // Animate the alpha startProgressAlphaMaxAnimation(); } } float strokeStart = adjustedPercent * .8f; mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); mProgress.setArrowScale(Math.min(1f, adjustedPercent)); float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; mProgress.setProgressRotation(rotation); setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */); }</span>
showArrow是顯示箭頭,中間那一坨主要也是一些math和設置進度圈的樣式,倒數第二行執行了setProgressRotation,傳入的是經過一堆計算後的rotation,這堆計算主要是優化效果,比如在剛開始移動時增長比較快,超過刷新的距離後就增長比較慢。傳入該方法後,mProgress就根據它來繪制進度圈,因此主要的動畫就應該在這個方法內。最後一行執行setTargetOffsetTopAndBottom,我們來看一下:
<span >private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { mCircleView.bringToFront(); mCircleView.offsetTopAndBottom(offset); mCurrentTargetOffsetTop = mCircleView.getTop(); if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { invalidate(); } }</span>
比較簡單,就是調整進度圈的位置並進行記錄。最後來看一下finishSpinner:
<span >private void finishSpinner(float overscrollTop) { if (overscrollTop > mTotalDragDistance) { setRefreshing(true, true /* notify */); } else { // cancel refresh 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) { startScaleDownAnimation(null); } } @Override public void onAnimationRepeat(Animation animation) { } }; } animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); mProgress.showArrow(false); } }</span>
邏輯也很簡單,當移動的距離超過設定值時就執行setRefreshing(true,true),在該方法裡更新一些成員變量的值後會執行animateOffsetToCorrectPosition,由名字就知道是執行動畫將進度圈移動到正確位置的(也就是頭部)。如果移動的距離沒有超過設定值,就會執行animateOffsetToStartPosition。一起看一下animateOffsetToCorrectPosition和animateOffsetToStartPosition這兩個方法:
<span >private void animateOffsetToCorrectPosition(int from, AnimationListener listener) { mFrom = from; mAnimateToCorrectPosition.reset(); mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); if (listener != null) { mCircleView.setAnimationListener(listener); } mCircleView.clearAnimation(); mCircleView.startAnimation(mAnimateToCorrectPosition); } private void animateOffsetToStartPosition(int from, AnimationListener listener) { if (mScale) { // Scale the item back down startScaleDownReturnToStartAnimation(from, listener); } else { mFrom = from; mAnimateToStartPosition.reset(); mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); if (listener != null) { mCircleView.setAnimationListener(listener); } mCircleView.clearAnimation(); mCircleView.startAnimation(mAnimateToStartPosition); } }</span>
邏輯基本相同,進行一些設置後,最後都會執行mCircleView的startAnimation,只是傳入的值以及監聽器不同。
如果是要執行刷新的操作,傳入的值是頭部高度,監聽器為:
<span >private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (mRefreshing) { // Make sure the progress view is fully visible mProgress.setAlpha(MAX_ALPHA); mProgress.start(); if (mNotify) { if (mListener != null) { mListener.onRefresh(); } } mCurrentTargetOffsetTop = mCircleView.getTop(); } else { reset(); } } };</span>
動畫完成後,也就是進度圈移動到頭部後,會執行mProgress.start();這裡執行的就是在刷新時進度圈轉啊轉的動畫。接下來注意到如果mListener不為空就會執行onRefresh方法,這個mListener其實就是執行setOnRefreshListener所設置的監聽器,因此在這裡完成刷新。如果是執行回到初始位置的操作,傳入的值為初始高度(也就是頂部之上),監聽器為
<span >listener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (!mScale) { startScaleDownAnimation(null); } } @Override public void onAnimationRepeat(Animation animation) { } };</span>
移動到初始位置後會執行startScaleDownAnimation,也就是消失的動畫了,到這裡整個刷新流程就結束了。
這樣就基本把SwipeRefreshLayout的流程過了一遍,但是要實現這樣一個控件還是有很多小問題需要考慮的,這裡主要是把思路理清,知道如果出現問題該怎樣解決。另外從源碼也可以看出swipeRefreshLayout的定制性是比較差的,也不知道google是不是故意這樣希望以後全都用這種統一樣式的下拉刷新。。當然有一些第三方下拉刷新的定制性還是比較好的,使用上也不難。但是有些人(比如我)是比較傾向於使用官方的控件的,不到萬不得已都不想用第三方工具。下次會寫一篇探討一下用swipeRefreshLayout實現自定義樣式的文章~
後續還有一篇從修改swipeRefreshLayout的源碼出發自定義樣式高仿微信朋友圈的下拉刷新效果的文章,有興趣可以看一下哈http://www.jb51.net/article/89311.htm
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持本站。
menu部分xml代碼<?xml version=1.0 encoding=utf-8?><menu xmlns:android=htt
0. 前言 Android動畫是面試的時候經常被問到的話題。我們都知道Android動畫分為三類:View動畫、幀動畫和屬性動畫。先對這三種動畫做一個概述:V
Service簡介:Service 是Android的四大組件之一,一般用於沒有UI界面,長期執行的後台任務,即使程序退出時,後台任務還在執行。比如:音樂播放。Servi
Layout_Margin與padding的區別以及Layout_gravity與gravity的區別平時開發中這幾個屬性是我們經常使用的幾個屬性,偶爾腦子一糊塗,就容易