上一篇文章介绍了 事件分发机制 和 滑动冲突的解决方案,本篇文章开启自定义下拉刷新之旅。首先,我们看效果图。
在自定义下拉刷新时,我们通过使用Scroller 来滑动布局。接下来,我们先了解Scroller的使用。
Scroller
这篇文章郭霖 完全解析Scroller,详细地介绍了Scroller。
使用Scroller的步骤非常简单:
- 创建Scroller的实例
- 调用startScroll()方法来初始化滚动数据并刷新界面
- 重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
同时我们要注意到ScrollTo 表示滚动到指定位置,ScrollBy表示每次滚动一段距离。
我们自定义一个ScrollerLayout来模拟ViewPager的滑动切换效果。
布局如下,在ScrollerLayout中嵌套了三个Button
1 | <?xml version="1.0" encoding="utf-8"?> |
由于Button是可点击的,它会消费点击事件,导致ScrollerLayout 不能调用OnTouchEvent。根据上篇文章介绍的 事件拦截机制和滑动冲突解决方案,我们必须在自定义ScrollerLayout添加拦截事件,即在ScrollerLayout滑动时进行拦截
1 | @Override |
左边界
当我们处于第一个界面,向右滑动
很明显
scrolledX = LastX - X < 0
getScroller()+ScrolledX < 0 <LeftBorder
此时我们让界面滑动到左边距
1 | if (getScrollX() + scrolledX < mLeftBorder) { //左边界 |
右边界
当我们处于最后一个界面,向左滑动
scrolledX = LastX - X > 0
我们要控制
getScroller + scrolledX + getWidth > rightBorder
1 | if (getScrollX() + getWidth() + scrolledX > mRightBorder) {// 右边界 |
完整代码
1 | public class ScrollerLayout extends ViewGroup { |
getScrollerX/Y
使用Scroller的过程中反复地使用到了getScrollerX()/getScrollerY()
下面我们以getScrollerY为例进行解释,getScrollerY获取的到底是什么值。
1 | /** |
源码中给了其中的解释,意思是 View顶部和显示界面的距离。下面我们通过一张图片更直观地理解到底getScrollY获取的是什么值。
当上滑时,超出屏幕的距离就是 getScroller的值,为正数
当下滑时,超出屏幕的距离也是 getScroller的值,为负数
那么Scroller.startScroll(0, getScrollerY(), 0, dy),这里 dy 起到的作用是什么呢?
通过这句代码,我们实现的操作是
getScrollerY+dy
假设 getScrollerY 值为 200, dy的值为200 ,执行这句代码后我们的变化如下:
注意
执行Scroller.startScroll(0, getScrollerY(), 0, dy)后要 调用invalidate进行刷新。
computeScroll()
这个函数的作用是什么呢?为什么要重写? 实际上它才是决定 我们调用Scroller.startScroll(0, getScrollerY(), 0, dy) 实现滑动的决定因素。
1 | @Override |
注意这行代码,1
scrollTo(0, mScroller.getCurrY());
mScroller.getCurrY 的值为前面我们计算的getScrollerY+dy 值。 scrollTo()表示移动到指定位置。 所以当我们使用Scroller.startScroll() 后会自动调用computeScroll() 来实现我们的滑动效果。
因此,我们在使用Scroller的时候要重写computeScroll(),在使用后一定要记得 invalid 进行重绘
自定义简单的下拉刷新组件
思路
初始化时,我们的屏幕显示的是 带颜色的这块内容。当我们向下滑动的时候显示头部内容,向上滑动时显示底部内容。
所以在自定的 SimpleRefreshLayout时,我们动态添加了头部和底部。
1 | public SimpleRefreshLayout(Context context, AttributeSet attrs) { |
注意我们添加头部和顶部是在 onFinishInflate()这个函数中。
onFinishInflate()何时调用?为什么要用onFinishInflate()?
在我们使用View.inflate(context,R.layout.view_layout,null); View中的所有控件被映射成xml,在加载完成xml后,就会执行这个方法。也就是初始化布局后执行。
在此处使用OnFinishInflate() 是为了保证 头部和底部 布局已经被初始化后再添加到 SimplerRefreshLayout中。
测量
重写OnMeasure 来测量子类1
2
3
4
5
6
7
8
9@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//测量子类
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
布局
为了在初始状态只显示我们的 内容界面,
header的位置为 (0,-height,getWidth,0)
footer的位置为 (0,getHeight,getWidth, getHeight+height)
1 | //布局 |
滑动
根据滑动的方向,我们来 切换滑动效果
1 | int dy = mLastMoveY - y; |
向下滑动时,dy < 0;
向上滑动时,dy > 0;
为了控制顶部最多只能滑动到头部高度的一半 我们使用了下面判断
1 | if (Math.abs(getScrollY()) <= mHeader.getMeasuredHeight() / 2) { |
我们还可以设置 有效距离effectiveScrollY,当未超过effectiveScrollY, 不显示头部,这个操作主要是在ACTION_UP做处理:
1 | if (Math.abs(getScrollY()) >= effectiveScrollY) { |
设置回调监听
1 | public interface onRefreshListener { |
完整代码
1 | public class SimpleRefreshLayout extends ViewGroup { |
头部布局item_header_layout
1 | <?xml version="1.0" encoding="utf-8"?> |
底部布局item_footer_layout
1 | <?xml version="1.0" encoding="utf-8"?> |
主布局activity_refresh_layout
1 | <?xml version="1.0" encoding="utf-8"?> |
主代码
1 | public class RefreshActivity extends AppCompatActivity { |
本篇给出了自定义下拉刷新组件的思路,并给出了非常简单的小例子。实现的只是简单的 全屏图片时,下拉刷新上拉加载的效果。下一篇,我们将会 加入 嵌套布局时,滑动冲突判断,实现更加有意义的下拉刷新组件。
参考文章:
自定义下拉刷新组件