这种下拉刷新控件的原理不难,基本就是监听手指的运动,获取手指的坐标,通过计算判断出是哪种操作,然后就是回调相应的接口了。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。
@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,到这里就可以猜出,执行刷新的逻辑主要就在这两个方法中。
<span style="font-size:18px;">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 circleif (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 alphastartProgressAlphaStartAnimation(); }} else { if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) {// Animate the alphastartProgressAlphaMaxAnimation(); }}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 style="font-size:18px;">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 style="font-size:18px;">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 style="font-size:18px;">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>
<span style="font-size:18px;">private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {@Overridepublic void onAnimationStart(Animation animation) {}@Overridepublic void onAnimationRepeat(Animation animation) {}@Overridepublic void onAnimationEnd(Animation animation) { if (mRefreshing) {// Make sure the progress view is fully visiblemProgress.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 style="font-size:18px;">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,也就是消失的动画了,到这里整个刷新流程就结束了。