开发动机
现在的APP里面十个有八个肯定会有下拉刷新组件,但是有的时候这些第三方Jar并不能满足我们的需求定制。我所在的项目就遇到了这种情况,需要在刷新成功后加一个停留动画,并且需要区分成功和失败,因为我们项目组是分模块的开发,同事采用了Hardcode的方式满足了自身的需求,但是其他模块都通用不了。所以,我决定自己写一个下拉刷新来替代原有的变成一个通用的BaseView。
下面,开始我们的表演~
开发思路
在写代码之前,我们简单想一想,下拉刷新需要怎么实现?
~请原谅我不能画图来表示,画图真的很花时间~
第一步 整体框架
我首先想到的滑动,在一个ViewGroup内部滑动的话可以通过
scoller
来管理scroll值。所以思路就是一个自定义的ViewGroup,考虑到自定义ViewGroup需要自己去onMeasure
和onLayout
,有点麻烦呀。怎么办?先继承LinearLayout吧,先从简单的开始。顶部的View命名为HeaderView,底部的View命名为FooterView,中间夹着一个实体View,通过scoller
控制HeaderView 和FooterView的显示和隐藏第二步 HeaderView和FooterView 的实现
整体框架出来之后,就需要考虑HeaderView怎么去实现。(FooterView的实现和HeaderView一模一样,所以后面就只写HeaderView。我写代码有一个习惯,就是先用接口去写想要的结果,然后再去实现这个接口。)因为HeaderView中需要显示下拉刷新、刷新中、刷新成功等提示,所以接口需要begin,progress,end最基本的三个方法。第三步 到顶计算
有的子View也能滑动,我们在什么情况下可以下拉刷新,需要去计算子View是否到顶,还有一种复杂布局的情况下,需要多个子View一起配合,才能下拉刷新。所以需要提供一个方法,将需要计算到顶的View,传入。然后统一判断。
第四步 合并实现
好了。整体框架和顶部底部的思路都出来,还差什么?还差了一个触摸控制。触摸控制就要明白android的Touch事件分发了,这个就是细节实现问题了。所以,开始写代码吧。
知识储备
等等,我们先总结下开发下拉刷新需要什么知识点
按优先级排序为:
Scroller
其实不使用Scroller
也可以,我们通过View内部自带的getScrollY()
拿到滑动值,然后ACTION_UP的时候,启动一个动画,不断调用scrollTo(int x, int y)
改变scroll值一样能达到目的。
那为什么要使用Scroller
呢?
就像汽车的自动挡和手动挡一样。Scroller
就是自动挡,已经帮我们把业务之外的工作全部做了。知道当前的起点Y,目的地Y2,就可以调用Scroller
的public void startScroll(int startX, int startY, int dx, int dy, int duration)
实现松手滑动动画。
Scroller
最重要的一点是 Scroller
只改变View的scroll值,但是不滑动,需要我们主动刷新,切记切记。如果调用了startScroll
结果发现没有效果,不要惊讶,请主动复写如下代码:
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset() && !mScroller.isFinished()) {
scrollTo(0, mScroller.getCurrY());
postInvalidate();
}
}
Touch事件分发
接收Touch时间主要有三个方法,优先级从大到小排序依次为,分发、拦截、使用。
这里简单说一下三个方法的区别
这里要注意一下:onInterceptTouchEvent在dispatchTouchEvent里会判断,所以会是这样传递。
[定义:父layout在最下层,子View在最上层]
下层dispatchTouchEvent->下层onInterceptTouchEvent ->
——该层dispatchTouchEvent-> 该层onInterceptTouchEvent ->
————- 上层dispatchTouchEvent-> 上层onInterceptTouchEvent
分发(ViewGroup独有)
默认能收到所以经过此viewGroup的Touch事件,包括子View,Touch事件从父View一级一级往上传递,此方法要慎用。
当在dispatchTouchEvent
中返回true
后,表示事件被收起来,不会继续往上一层传递。
并且该层的onInterceptTouchEvent
和onTouchEvent
将不会再接收到任何Touch事件,上层的三个方法都不会接收到任何Touch事件。
当在dispatchTouchEvent
中返回false
后,该层的和上层的所有方法都将接收不到任何Touch事件。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
拦截(ViewGroup独有)
当在onInterceptTouchEvent
中返回true
后,表示所有经过的事件被此View**强制接收**,上层View会收到一次ACTION_CANCEL
,然后上层View的三个方法将不会收到任何事件。
当在onInterceptTouchEvent
中返回false
后,该层的onTouchEvent不会收到任何事件
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
使用(ViewGroup和View都有)
onTouchEvent
就比较简单了。
在ViewGroup中,不在拦截中拦截事件的话,不会走进onTouchEvent
。
在onTouchEvent
中返回true
表示使用该事件,其他层的onTouchEvent将不会收到事件。
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
基本的事件传递描述完毕。三个方法返回值相互配合,情况又会不一样。ACTION_DOWN、ACTION_MOVE、ACTION_UP在事件传递中还有一些细节,用文字描述的话可能有点绕,大家写一个Demo,跑一下会有更好的认识。
自定义ViewGroup
主要有三个地方需要注意:
onMeasure
用于计算ViewGroup大小。使用setMeasuredDimension(mTotalWidth, mTotalHeight)
设置ViewGroup的大小。onLayout
用于布局子View,设置子View的位置
generateLayoutParams
设置自己的LayoutParams,通常继承MarginLayoutParams才能使用Margin属性
@Override
public RefreshLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new RefreshLayout.LayoutParams(getContext(), attrs);
}
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
代码
源码和Demo GitHub:[https://github.com/xindasunday/easyrefreshlayout]
导入方式:
第一步:增加maven地址
allprojects {
repositories {
jcenter()
repositories {
maven { url 'https://www.jitpack.io' }
}
}
}
第二步:
模块build.gradle中增加
dependencies {
compile 'com.github.xindasunday:easyrefreshlayout:1.0.1'
}
gradle sync之后即可开始使用easyrefreshlayout。
和Demo见GitHub:https://github.com/xindasunday/easyrefreshlayout
一个周的时间,调试的差不多,并且在实际项目中测试解决过BUG了。
HeaderView:
public interface HeaderView {
void begin();
/**
* 相对于HeaderView高度的倍数
* **/
void progress(float progress);
void loading();
//初始化和结束后调用
void reset();
View getView();
//刷新成功后暂停几秒用于显示动画效果后
void showPause(boolean success);
boolean isPauseTime();
long getPauseMillTime();
}
接口的意义在与可以自定义多种多样的HeaderVIew。
public interface RefreshListener {
void refresh();
void loadMore();
}
RefreshLayout的属性
private HeaderView mHeaderView;
private FootView mFootView;
private float mLastX;
private float mLastY;
private int mHeadViewHeight;
private int mFootViewHeight;
private Scroller mScroller;
private RefreshListener mRefreshListener;
//是否可以拉动超出HeaderView的高度
private boolean isFullPull = true;
//是否可以拉动超出FooterView的高度
private boolean isFullPush = true;
//把HeaderView拉出的最大高度,isFullPull = true时 生效
private int maxPullHeight;
//把FooterView拉出的最大高度,isFullPush = true时 生效
private int maxPushHeight;
//滑动差值,值越小,滑动速度越慢
private float mMoveRate = 0.3f;
//headerView或者footerView 在可全屏滑动下,松手后滑动进入刷新/加载状态的时间
private int mOutRangeScrollTime = 800;
//headerView或者footerView 在刷新/加载结束后,隐藏的滑动时间
public int hideHeadFootViewTime = 800;//ms
//是否处于刷新状态
private boolean isRefresh;
//是否处于加载状态
private boolean isLoadMore;
//是否允许刷新
private boolean isCanRefresh = true;
//是否允许加载
private boolean isCanLoadMore = true;
//用于计算子View是否到底/到顶,轻松解决嵌套/组合 可滑动控件
private Set<View> mChildCalcList;
//覆盖mBaseView的错误提示view;
private View mErrorView;
private View mBaseView;