上一篇博客:《连按两次Back键退出应用程序》,我们从源码的角度分析了一下,虽然这个功能实现起来很简单,但是我们还是能通过系统源码学到很多知识。今天我们从源码的角度来分析一下SwipeRefreshLayout这个神奇的控件吧~
SwipeRefreshLayout是当之无愧的下拉刷新全能王,它继承了ViewGroup,因此,我们的分析角度就可以这么来:构造函数->onMeasure->onLayout->onInterceptTouchEvent->onTouchEvent
首先,我们来分析一下构造函数
/**
* 在代码中创建SwipeRefreshLayout的时候,会调用这个方法
* Simple constructor to use when creating a SwipeRefreshLayout from code.
*
* @param context
*/
public SwipeRefreshLayout(Context context) {
this(context, null);
}
/**
* 在布局文件中创建SwipeRefreshLayout的时候,会调用这个方法
* Constructor that is called when inflating SwipeRefreshLayout from XML.
*
* @param context
* @param attrs
*/
public SwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
//ViewConfigurationi:包含了方法和标准的常量用来设置UI的超时、大小和距离
//mTouchSlop:是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMediumAnimationDuration = getResources().getInteger(
android.R.integer.config_mediumAnimTime);
setWillNotDraw(false);
mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
setEnabled(a.getBoolean(0, true));
a.recycle();
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);
//创建了下拉刷新中那个会旋转的圈圈,并添加到整个布局里面
createProgressView();
ViewCompat.setChildrenDrawingOrderEnabled(this, true);
// the absolute offset has to take into account that the circle starts at an offset
mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density;
mTotalDragDistance = mSpinnerFinalOffset;
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;
//把那个旋转的小圈圈放到初始位置
moveToStart(1.0f);
}
它只实现了两个构造方法,并且在第二个构造方法中对UI进行了一定的初始化
然后我们来看看onMeasure方法
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
//mTarget的尺寸为match_parent,除去内边距
mTarget.measure(MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
//设置mCircleView的尺寸
mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY));
//如果mOriginalOffsetTop未被初始化并且mUsingCustomStart ?,则将下拉小圆的初始位置设置成默认值
if (!mUsingCustomStart && !mOriginalOffsetCalculated) {
mOriginalOffsetCalculated = true;
mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight();
}
mCircleViewIndex = -1;
// Get the index of the circleview.获取circleview的索引值,主要是为了后面重载getChildDrawingOrder时要用
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mCircleView) {
mCircleViewIndex = index;
break;
}
}
}
大概就是计算一下各个控件需要的宽高,计算好之后就要考虑如何摆放这些控件了,我们来看看onLayout方法
@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放在覆盖parent的位置(除去内边距)
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
//将mCircleView放在mTarget的平面位置上面居中,初始化时是完全隐藏在屏幕外的
int circleWidth = mCircleView.getMeasuredWidth();
int circleHeight = mCircleView.getMeasuredHeight();
mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
(width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
}
控件都摆放好了,接下来就要处理用户的下拉动作了
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
final int action = MotionEventCompat.getActionMasked(ev);
//如果当mCircleView正在返回初始位置的同时手指按下了,将标志mReturningToStart复位
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
//如果下拉被禁用、mCircleView正在返回初始位置、mTarget没有到达顶部、
//正在刷新、mNestedScrollInProgress
// 不拦截,不处理点击事件,处理权交还mTarget
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;
//如果是滑动动作,将mIsBeingDragged置为true
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;
}
//如果正在被拖拽,拦截该系列的点击事件,并调用自己的onTouchEvent()来处理
return mIsBeingDragged;
}
这里是对用户的操作进行拦截,首先调用了第3步中的ensureTarget来确保滑动控件被初始化了,完后可以看到获取了ACTION,这里跟平时直接用getAction方法来获取还是有些区别的,这里用到的getActionMasked方法可以捕获到多个手指的事件,这个后面会说,下面的mReturningToStart这个是布尔变量,初始为false,用来判断滑动控件是否在执行滑动回顶点的动画,在动画开始的时候,会将这个值赋值为true,完后这里判断如果值为true,并且为按下事件,则赋值为false,表示是一次新的滑动了。
if (!isEnabled() || mReturningToStart || canChildScrollUp())
这3个分别是判断是否可用,是否正在执行动画,以及是否能滚动,如果不满足条件,直接返回false,就不往下执行了。
ACTION_DOWN,mLastMotionY是最后滑动的y坐标,mInitialMotionY是按下时初始的y坐标,mActivePointerId是第一个按下手指的id,mIsBeingDragged是一个布尔变量,表示是否正在拖动,mCurrPercentage是默认动画相关的百分比,改造后用不到ACTION_MOVE,首先判断了id是否为-1,是的话直接报错返回(UP或CANCEL时会赋值为-1),完后获取现在的y坐标,并计算与初始y坐标的差是否大于mTouchSlop,这个值是ViewConfiguration类中定义的一个常量,它定义了最小滑动距离,小于这个值表示不是正常的滑动,一般你随便滑动一下肯定超过这个值,ViewConfiguration中定义了很多这种标准值,有兴趣的可以自己去看看,如果大于mTouchSlop,则将mLastMotionY赋值为新值,并且将mIsBeingDragged赋值为true,表示控件开始随手指滑动了ACTION_UP/ACTION_CANCEL,这里很简单,不说了最后还有一个ACTION_POINTER_UP,这里调用了onSecondaryPointerUp方法,这个具体到onTouchEvent中再说
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
int pointerIndex = -1;
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
//如果被禁用、CircleView正在复位、没到达顶部、mNestedScrollInProgress,直接返回,不处理该事件
if (!isEnabled() || mReturningToStart || canChildScrollUp() || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
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) {
//spinner可理解为下拉组件,将spinner移到指定的高度
//很关键的方法,进入看看
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;
}
这个控件还没有分析完,等待我的更新吧。我的微信公众号:代码也是人。每天都会分享一篇技术干货,赶紧关注吧!觉得我写得好的话,记得收藏一下哈

代码也是人