前言
想必大家也发现,时下的很多App都应用了这个Google出品的SwipeRefreshLayout下拉刷新控件,它以Material Design风格、适用场景广泛,简单易用等特性而独步江湖。但在我们使用的过程中,不可避免地会发现一些bug,或者需要添加某些特性来满足需求。出现这些问题,最好的方法就是解读源码,理解它实现的原理,并且在理解源码的基础上修改源码,达成需求。然而不知为何,至今还没有一篇关于SwipeRefreshLayout源码解析的文章,所以萌发了要写一篇这样的文章。鉴于阅读技术博文的枯燥,加之还是篇源码解析的文章,我不打算一下子扔出来一大段代码让读者去啃,而是一步一步往下走,揭开SwipeRefreshLayout的神秘面纱。
阅读源码的小技巧
为什么源码普遍都很难读,有人甚至谈之色变?其实代码(出自大神之手)生来是易读的,但代码多了就变得难读了。所以阅读源码时,要把握住主干,细枝末节可以暂时忽略,一路下来理解了程序工作流程后再回过头来会有一种豁然开朗的感觉。
阅读源码我还是选择Android Studio。这个强大的工具提供了很多快捷键,大大地方便了源码的阅读。
- Ctrl+F :在当页查找关键字
- Alt+F7: 查看方法或变量在哪里被使用过
- Ctrl+Q:查看java doc,如果该方法或变量有的话javadoc的话就可以更快知道该它的相关信息
- Ctrl+左击:这个不用说了吧,进入方法体或者查看定义或者查看被使用的地方
- Ctrl+Shift+i:可以不离开当前阅读的位置,查看指定方法的方法体
- Ctrl+F11:加BookMark,简直是非常有用的功能,不过需要去设置添加一下跳转下一个书签或上一个书签的快捷键才能发挥出该功能真正强大。
- Ctrl+F12 : 输入关键字快速定位指定的变量或方法,支持模糊搜索。
- Ctrl +Alt+左箭头或右箭头:返回前一个或下一个光标的位置,在想回溯阅读位置的时候非常有用
- 关于阅读源码的快捷键就这些吧,以后想到了再补充…
你应该知道:
在看往下看之前,我希望你了解:
- 事件分发机制
- ViewGroup的测量绘制过程
准备工作
所幸该控件没有跟系统api耦合,所以可以直接copy一份代码到自己的demo工程中,尽情地改。但是hint会理解报出一些错误。首先包名要改一下,类名最好也改吧,以免混淆~其次把CircleImageView和MaterialProgressDrawable这两个类都copy过来,放在同一个包里。如图:
如果嫌麻烦可以直接fork我的项目。
探究之旅
我们朝着未知的黑暗出发。打开SwipeRefreshTestLayout的类文件,看到左边这么小的滑块,其实我一开始是拒绝的~ 感觉无从下手啊有没有… 沉下心来,想想看看它是继承于ViewGroup的,所以想想它一定有两个很关键的方法:onMeasure和onLayout,分别解决了它和它的子View占多大地和搁到哪。因为它是一个下拉刷新控件,它必定要涉及到事件分发的处理,同样是两个关键方法:onInterceptTouchEvent和onTouchEvent,分别用于决定是否拦截点击事件和进行点击事件的处理。天空瞬间亮了许多…
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;
}
}
}
我们看到,这个方法代码不长,但却很关键。重写该方法的作用是设置子View的尺寸。出现mTarget是什么未知生物?其实就是一个它包裹的子View,通常是ListView等一些可滚动的控件。ensureTarget();保证它非空并存在。如果不小心包裹了多个VIew呢?则mTarget就是其中的最后一个子View。mCircleView又是什么生物呢?顾名思义,下拉的白色小圆圈,一个ImageView而已。mCurrentTargetOffsetTop 和mOriginalOffsetTop 是两个非常关键的变量,分别表示当前mCircleView的位置(top值)和初始时mCircleView的位置(top值),当然它们初始化都等于mCircleView高度的负数。还有一个mUsingCustomStart 是什么呢?我当时也不知道。没关系,Ctrl+F11打个书签,等读完再回头看。或者我们可以通过Alt+F7看看它的在哪里被引用过。
可以看到,它在setProgressViewOffset被赋值为true,而该方法是用于设置CircleView初始的位置和刷新停留的位置,Custom是自定义的意思,所以mUsingCustomStart就是一个标志,表示是否用自定义的起始位置,而默认的起始位置就是CircleView高度的负数。
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