一,概述
在上篇blog中我们发现使用SwipeRefreshLayout实现下拉刷新极其方便快捷,而且拉刷新控件与展示内容的控件实现了分离,非常便于代码的扩展与维护。但是其刷新UI确实很简单,很难入公司设计师的法眼,这时就需要我们修改刷新UI了。
这篇blog从源码的角度分析SwipeRefreshLayout的实现原理,即为了学习SwipeRefreshLayout的实现思想,也可以实现自己想要的刷新UI。
SwipeRefreshLayout是一个view类,继承ViewGroup。其中的核心方法有:
构造方法;
onMeasure();
onLayout();
onIntercepterTouchEvent();(重点)
onTouchEvent();(重点)
下面按照以上方法的顺序逐渐分析源码。
二,构造方法
构造方法的核心代码如下:
public SwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
/**触发移动事件的最小距离,自定义View处理touch事件的时候,有的时候需要判断用户是否真的存在movie,系统提供了这样的方法。表示滑动的时候,手的移动要大于这个返回的距离值才开始移动控件。*/
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
//获取移动动画的差值器
mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density);//得到刷新View的宽度
mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density);//得到刷新View的高度
createProgressView();//创建刷新View
}
createProgressView方法的源码如下:
private void createProgressView() {
mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2);
mProgress = new MaterialProgressDrawable(getContext(), this);
mProgress.setBackgroundColor(CIRCLE_BG_LIGHT);
mCircleView.setImageDrawable(mProgress);
mCircleView.setVisibility(View.GONE);
addView(mCircleView);//将mCircleView添加到父View上。
}
注:构造方法主要做了两件事情:
1,使用addView添加刷新的view
2,初始化参数,刷新view的大小,最大滑动距离等等。
三,onMeasure方法
方法的核心代码是:
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
ensureTarget();//找到mTarget,这个view既是承载数据的view,可能是ListView,ScrollView,RecyclerView。
}
if (mTarget == null) {
return;
}
//测量mTarget
mTarget.measure(MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
//测量刷新View
mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY));
}
注:onMeasure方法中没有特别的注意点,就是找到两个子view,并测量。
四,onLayout方法
方法的核心代码是:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
final View child = mTarget;
final int childLeft = getPaddingLeft();
final int childTop = getPaddingTop();
final int childWidth = width - getPaddingLeft() - getPaddingRight();
final int childHeight = height - getPaddingTop() - getPaddingBottom();
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);//布局mTarget
int circleWidth = mCircleView.getMeasuredWidth();
int circleHeight = mCircleView.getMeasuredHeight();
mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
(width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);//布局刷新View
}
注意:刷新的view的位置与mCurrentTargetOffsetTop有关。
五,onIntercepterTouchEvent方法
这个方法是刷新功能实现的重点。如果说onMeasure 方法和onLayout方法只是自定义View的知识,那么onIntercepterTouchEvent方法就是刷新功能实现的核心。一定要注意这个方法。
方法的核心代码如下:
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();//找到mTarget对象
final int action = MotionEventCompat.getActionMasked(ev);//获取手势动作
/**下面几种情况直接返回false,表示不拦截。其中canChildScrollUp非常重要,后面会详细讲解这个方法。*/
if (!isEnabled() || mReturningToStart || canChildScrollUp()|| mRefreshing || mNestedScrollInProgress) {
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;//这行代码是关键,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;
}
注意:onIntercepterTouchEvent方法中决定事件拦截不拦截,如果不拦截就传递给mTarget,由mTarget消费事件。
下面再次看下不拦截的代码:
if (!isEnabled() || mReturningToStart || canChildScrollUp()|| mRefreshing || mNestedScrollInProgress) {
return false;
}
以上五种情况都不拦截,其中最重要的是canChildScrollUp方法。这个方法表示mTarget是否可以向下滑动,方法的源码是:
public boolean canChildScrollUp() {
if (android.os.Build.VERSION.SDK_INT < 14) {//首先做了一个判断,判断sdk的版本。
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 {//当前的sdk版本肯定是大于14的,所以只看这行代码。
return ViewCompat.canScrollVertically(mTarget, -1);//这行代码判断view在竖直方向是否可以滑动,-1表示是否可以向下滑动。
}
}
当mTarget可以向下滑动时则不拦截,只有mTarget滑动到顶部时,此时不能向下滑动了,此时拦截。拦截之后调用自己的onTouchEvent方法,交给自己处理。
六,onTouchEvent方法
我们知道当view拦截事件后,会调用onTouchEvent方法。
onTouchEvent方法的核心代码是:
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
int pointerIndex = -1;
/**下面几种情况直接返回false,表示不处理*/
if (!isEnabled() || mReturningToStart || canChildScrollUp() || mNestedScrollInProgress) {
return false;
}
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 MotionEventCompat.ACTION_POINTER_DOWN: {
pointerIndex = MotionEventCompat.getActionIndex(ev);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");
return false;
}
mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
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;
}
下面首先看重点一,moveSpinner方法,这个方法的作用时移动刷新view的位置。在move事件时被调用
方法的核心源码如下:
private void moveSpinner(float overscrollTop) {//参数overscrollTop表示这一瞬间手指移动的距离
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);//计算targetY的值,即刷新View将要到达的目标位置。
float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;
mProgress.setProgressRotation(rotation);//设置刷新view的动画
setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);//改变刷新View的位置,这个方法也是重点
}
setTargetOffsetTopAndBottom方法的源码如下:
private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) {
mCircleView.bringToFront();
mCircleView.offsetTopAndBottom(offset);//移动mCircleView的位置,这个方法会自动调用invalidate方法,所以此时onLayout方法会被调用
mCurrentTargetOffsetTop = mCircleView.getTop();
if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) {
invalidate();
}
}
下面首先看重点二,finishSpinner方法,这个方法的作用是处理手指抬起以后的操作。
方法的核心源码如下:
private void finishSpinner(float overscrollTop) {
if (overscrollTop > mTotalDragDistance) {//如果滑动的距离大于某个值则调用刷新方法
setRefreshing(true, true /* notify */);
} else {//如果滑动的距离不大于某个值,则回到原始的位置。
mRefreshing = false;
mProgress.setStartEndTrim(0f, 0f);
animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
mProgress.showArrow(false);
}
}
刷新方法是setRefreshing,下面看这个方法的源码:
private void setRefreshing(boolean refreshing, final boolean notify) {
if (mRefreshing != refreshing) {
mNotify = notify;
ensureTarget();
mRefreshing = refreshing;
if (mRefreshing) {//此时mRefreshing等于true,所以会走这一步,
animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
} else {
startScaleDownAnimation(mRefreshListener);
}
}
}
animateOffsetToCorrectPosition方法表示以动画的形式回到某个位置,动画不重要,重要的是动画监听器mRefreshListener,下面看这个对象的创建。
private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
public void onAnimationRepeat(Animation animation) {}
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();
}
}
};
这是一个动画监听器,当动画结束时调用mListener.onRefresh(),这个即是我们设置的刷新监听器。此时就调用了刷新的方法。
七,总结
经过以上源码分析,我们得到的结论如下:
1,SwipeRefreshLayout是一个viewGroup,里面有两个子view,一个是刷新view,即mCircleView,即在刷新时旋转的view。另外一个时显示内容的View,即mTarget,这个就是Listview,或者Scrollview.
2,在onIntercepterTouchEvent方法中做了事件的拦截,当listView可以向下滑动时不拦截,事件由ListViw处理,当不能向下滑动时拦截事件,事件交给SwipeRefreshLayout当onTouchEvent方法处理。
3,onTouchEvent方法分别对move事件和up事件进行了处理,当满足刷新条件时掉用刷新监听器的方法。
八,扩展使用
看过SwipeRefreshLayout源码后,就可以实现自己想要的刷新控件了。
SuperEasyRefreshLayout是一个仿照SwipeRefreshLayout的实现原理实现的一个强大的下拉刷新控件,不仅美化了刷新UI,而且可以根据需要自定义UI效果的View,且实现了上拉加载更多的功能。
SuperEasyRefreshLayout的使用介绍的地址是:http://blog.csdn.net/fightingxia/article/details/75307875
SuperEasyRefreshLayout的源码在GitHub上的地址是:https://github.com/guozhengXia/SuperEasyRefreshLayout