实现效果
使用方法
可以指定控件大小,默认的RecyclerView会填充指定的大小
自定义属性就三条
<!--控制header/footer自动关闭的速度-->
<attr name="duration" format="integer"/>
<!--指定自己的header/footer布局-->
<attr name="header_layout" format="reference"/>
<attr name="footer_layout" format="reference"/>
<!--可以自行控制需要显示header/还是footer-->
<attr name="header_visible" format="boolean"/>
<attr name="footer_visible" format="boolean"/>
资料文件
项目目录
项目结构分三部分,自己引用即可
项目地址:github连接点这里
造轮子的经验总结
因为控件结构简单,子View数量也比较小,因此初始化/测量/摆放都很简单
难点在于滑动事件的处理
设计思路
在学习笔记里写到
触摸事件的处理,首先判断需要拦截的情况,在onInterceptTouchEvent(MotionEvent ev)中对应情况下返回true
然后在onTounch()中处理具体的滑动和手指抬起事件
但是自己去写的时候不知道如何下手,经常写的时候信心满满,测试的时候心态就崩了.
后来自己整理了下思路
1.按照正常的自定义View流程,首先分配触摸事件,拦截需要自定义的触摸事件(在onInterceptTouchEvent()方法中为预期的触摸行为返回true);然后在处理拦截到的事件(在onTouchEvent()中对拦截到的事件进行处理)
事件分发处理
因为我们的列表zview自身是可以滑动的,所以如果不对事件分发进行处理,界面效果就是一个单纯的滑动View,header/fpooter不会弹出
因此我们需要处理事件分发,什么样的情况下,需要把屏幕滑动事件分配给ViewGroup整体滑动
header的弹出/取消
header的作用是提示用户刷新.何时header该弹出,何时header该消失呢
弹出
当列表View滑动到顶端的时候,如果用户还在向上滑动,我们就认为是想刷新,此时弹出header,
取消
当header已经弹出,用户向下拉的时候,是想要取消刷新,我们就取消header
另外,当后台刷新逻辑处理完以后,也需要我们取消header
footer的原理类似
列表View滑动到顶端/底端的监听如何开始呢
我是下面这样设计的
int distance = (int) (lastY - ev.getRawY());
if (Math.abs(distance) > mSlop) {
if (contentView instanceof RecyclerView) {
LinearLayoutManager manager = (LinearLayoutManager) ((RecyclerView) contentView).getLayoutManager();
if (manager != null) {
// 判断滑动到顶端,开始下拉
if (headerVisible&&manager.findFirstCompletelyVisibleItemPosition() == 0 && distance < 0) {
return true;
}
// 判断滑动到底端,开始上拉
if (footerVisible&&(manager.findLastCompletelyVisibleItemPosition() + 1) == Objects.requireNonNull(((RecyclerView) contentView).getAdapter()).getItemCount() && distance > 0) {
return true;
}
// 只要当前显示了header/footer,就拦截事件
if (headerRefreshCompleted&&headerVisible){
return true;
}
if (footerRefreshCompleted&&footerVisible){
return true;
}
}
}
}
首先判断是否在顶端,根据完全露出的item是否是第一条决定,但是view加载的时候默认显示的就是第一条.所以,我们需要排除默认显示的情况,默认显示的时候,如果滑动方向向下,那自然就是view自己的滑动,所以加上方向的限制.就能把滑动到顶端/底端跟正常显示到顶端/底端的事件区分开
然后,footerRefreshCompleted || headerRefreshCompleted是什么意思呢
想象一下,如果header正常显示了,用户希望取消header这个时候,开始下拉(distance>0),这个时候也需要处理,因此我们定义了一个标志值,当header显示的时候,headerRefreshCompleted为true/footer显示的时候footerRefreshCompleted 为true.只要这两个标志有一个为真,就继续分发事件
这样事件分发就搞定了
然后就是难点,滑动事件处理
我们按照滑动距离来分类,我们用Scroller类来辅助滑动
scroller类的实现如下
@Override
...
//初始化
mScroller = new Scroller(context);
autoScrollRange = 0.6;
...
//实现自动滑动的方法(格式可以是固定的)
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
//使用,实现滑动返回原位
mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY());
invalidate();
在onTouchEvent()中首先支持滑动
public boolean onTouchEvent(MotionEvent event) {
float distance = lastY - event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
lastY = event.getRawY();
// 默认可滑动,在move中处理视图内滑动事件,在up中处理视图外滑动事件
scrollBy(0, (int) distance);
...
}
然后还是在case MotionEvent.ACTION_MOVE中,当视图在我们的viewGroup范围内滑动时
如果滑动方向向下,那么分两种情况考虑,一个是用户想要下拉显示header,另一种是想要取消footer
我们开始添加效果,如果滑动的距离超过dheader/footer的高度的一定范围,那么久调用Scroller类,来辅助滑动,显示/隐藏完整的header/footer
如果滑动方向向上,那么也是分两种情况,一个是用户想要上拉显示footer,另一种是想要取消header
// 在视图内滑动处理
if (getScrollY() >= -headerHeight && getScrollY() <= footerHeight) {
// 向上滑动,a想要上拉显示footer,b想要上拉取消header
if (distance > 0) {
// a要上拉显示footer,超过角标的autoScrollRange就自动下拉显示
if (!footerRefreshCompleted && getScrollY() >= footerHeight * autoScrollRange) {
Log.i(TAG, "onTouchEvent: 自动上拉");
mScroller.startScroll(getScrollX(), getScrollY(), 0, footerHeight - getScrollY());
footerRefreshCompleted = true;
if (mListener != null) {
mListener.footerRefreshStart(footer, contentView);
} else {
Log.e(TAG, "onTouchEvent: mListener=null");
}
}
// b想要上拉取消header,超过角标的autoScrollRange就自动下拉显示
if (headerRefreshCompleted && getScrollY() < 0) {
Log.i(TAG, "onTouchEvent: 取消下拉");
mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY(), 1000);
headerRefreshCompleted = false;
if (mListener != null) {
mListener.headerRefreshCancel();
} else {
Log.e(TAG, "onTouchEvent: mListener=null");
}
}
}
// 向下滑动,a想要下拉显示header,b想要下拉取消footer
if (distance < 0) {
// a判定下拉显示header,超过角标的autoScrollRange就自动下拉显示
if (!headerRefreshCompleted && getScrollY() <= -headerHeight * autoScrollRange) {
Log.i(TAG, "onTouchEvent: 自动下拉");
mScroller.startScroll(getScrollX(), getScrollY(), 0, -headerHeight - getScrollY());
headerRefreshCompleted = true;
headerRefreshStart();
}
// b判定想要上拉,取消上拉footer,超过角标的autoScrollRange就自动取消下拉footer
if (footerRefreshCompleted && getScrollY() > 0) {
Log.i(TAG, "onTouchEvent: 取消上拉");
mScroller.startScroll(getScrollX(), getScrollY(), 0, -footerHeight, 1000);
footerRefreshCompleted = false;
if (mListener != null) {
mListener.footerRefreshCancel();
} else {
Log.e(TAG, "onTouchEvent: mListener=null");
}
}
}
}
invalidate();
记得在处理完的最后,添加invalidate(),Scroller类才生效
这样处理完了以后,已经可以自己弹出/隐藏header/footer了,但是还有两点不足的地方需要改进
1当滑动的距离超过自动显示/隐藏范围时,自动显示/隐藏,那么当没有超过的时候,显示的就是不完整的header/footer怎么办
2当滑动的范围超过了我们定义的GroupView的范围时,会在header/footer的外围露出大片的空白
解决办法,在手指抬起事件中,如果滑动的范围超过了我们定义的GroupView的范围,那么久默认显示边界为header顶端或者footer底端,调用Scroller滚动即可;滑动的距离没有超过自动显示/隐藏范围时,我们直接调用Scroller类隐藏即可
case MotionEvent.ACTION_UP:
// 如果移动范围超过视图顶端范围,那么在手指抬起时,返回到视图最顶端
if (getScrollY() < -headerHeight) {
mScroller.startScroll(getScrollX(), getScrollY(), 0, -headerHeight - getScrollY(), 500);
}
// 如果移动范围超过视图底端范围,那么在手指抬起时,返回到视图最底端
if (getScrollY() > footerHeight) {
mScroller.startScroll(getScrollX(), getScrollY(), 0, footerHeight - getScrollY());
}
// 如果在视图范围内,手指抬起时,没有触发自动显示header/footer,就自动隐藏
if (getScrollY() >= -headerHeight && getScrollY() <= footerHeight) {
// 自动隐藏header
if (!headerRefreshCompleted && getScrollY() > -headerHeight * autoScrollRange) {
mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY());
}
// 自动隐藏footer
if (!footerRefreshCompleted && getScrollY() < footerHeight * autoScrollRange) {
mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY());
}
}
invalidate();
break;
##添加逻辑处理完后,取消header/footer的方法
public void onHeaderRefreshCompleted() {
headerRefreshCompleted = false;
mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY());
invalidate();
}
public void onFooterRefreshCompleted() {
footerRefreshCompleted = false;
mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY());
invalidate();
}