前言
我们都知道,自带的下拉刷新控件SwipeRefreshLayout非常好用,而且在很多app中都使用了,但是有些时候公司的设计觉得这样子不好看,他需要像QQ一样的下拉刷新的效果,这时候你就会去找各种支持下拉刷新的列表控件,什么pullToRefresh啦这些控件实现的效果确实挺好的,但是注意了,这里你就非得使用它改造过的列表控件啦,有时候你需要再弄点其他效果你可能就需要修改别人的实现.这就非常的不好了。而原生的下拉刷新就很优雅了,虽然效果基本不能定制,但是别人和列表控件解耦啊,一点关系没有啊!你列表是ListView还是RecyclerView一点关系都没有啊,于是乎,博主就带你写一个像自带刷新控件一样优雅的,但是又和列表控件解耦的自定义控件 ———小金子
阅读本篇博客需要一定的自定义控件的基础,如果没有,请自行查阅相关博文
另外本篇博客实现的刷新效果完全是由用户自定义xml的,所以你想实现qq的,饿了么的,或者其他任何app的下拉刷新的效果都是可以的,这里举例实现类似qq的下拉刷新的效果
需要实现的效果图
分析,首先我们可以看到的是,有一个我们的列表,就是不刷新的时候的界面
还有一个就是我们用手拖拽下来的界面
而使用过自带的下拉刷新的童鞋你们都清楚,使用的时候是用SwipeRefreshLayout包住了你的列表,换句话说,列表就是SwipeRefreshLayout控件的一个孩子,SwipeRefreshLayout就是一个ViewGroup
而我们现在需要实现自定义刷新的界面,所以我们必须能有用户自定义的xml提供出来.
所以这里我们可以这样子,我们编写一个CommonRefreshLayout
也是一个ViewGroup
我们在这里面放两个孩子,第一个孩子(View)当成刷新的部分,第二个孩子(View)当成列表
然后我们通过拦截事件等一系列的手势操作来实现这个效果
实现效果
编写一个名称为CommonRefreshLayout的类,继承ViewGroup
public class CommonRefreshLayout extends ViewGroup
重写测量的方法onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取孩子个数
int childCount = getChildCount();
if (childCount != 2) {
throw new RuntimeException("the child count must be 2");
}
//拿到推荐的宽和高
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//父容器给我多少我就要多少,不能放在列表中使用,因为列表中推荐过来的值heightSize为0
setMeasuredDimension(widthSize, heightSize);
//推荐给第二个孩子的宽和高,和自身宽高一样
View mainView = getChildAt(1);
int widthSpec = MeasureSpec.makeMeasureSpec(widthSize - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY);
int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
mainView.measure(widthSpec, heightSpec);
//推荐给第一个孩子的宽和高,宽度是自身的4分之一
View menuView = getChildAt(0);
headerMenuHeight = (heightSize) / headerRatio;
heightSpec = MeasureSpec.makeMeasureSpec(headerMenuHeight, MeasureSpec.AT_MOST);
menuView.measure(widthSpec, heightSpec);
}
/**
* 列表的高度和刷新的View的高度比
*/
private int headerRatio = 4;
上述代码基本的意思就是
博主给第一个孩子(也就是我们的刷新的视图)推荐了和自身一样的宽
高度是自身的一个比例,这个比例值就是headerRatio
4表示刷新的View是自身高度的1/4
5表示刷新的View是自身高度的1/5
同时博主处理了一下左右两边的内边距
给第二个孩子(也就是我们的列表)推荐了和自身一样的宽和高
同时博主处理了一下左右两边的内边距
实现布局方法onLayout
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//刷新控件View
View reFreshView = getChildAt(0);
int measuredWidth = reFreshView.getMeasuredWidth();
int measuredHeight = reFreshView.getMeasuredHeight();
//记录下刷新控件的高度,后续有用
headerMenuHeight = measuredHeight;
reFreshView.layout(0 + getPaddingLeft(), 0 - measuredHeight, measuredWidth, 0);
//列表控件
View mainView = getChildAt(1);
measuredWidth = mainView.getMeasuredWidth();
measuredHeight = mainView.getMeasuredHeight();
//测量出来的宽高如果没有内边距的时候应该和自身宽高是一样
mainView.layout(0 + getPaddingLeft(), 0, measuredWidth, measuredHeight);
}
这段代码更简单了,就是安排刷新的视图和列表视图的位置,博主画个图来表示一下
这里说刷新视图部分不可见不是说设置了不可见的属性,而是安排孩子的时候我们把他安放到了眼睛看不到的地方
超出了CommonRefreshLayout控件的边界
到此为止我们运行话我们可以正常的看到我们的列表了,而看不到我们的刷新的视图,所以我们继续
使用拖拽的神器ViewDragHelper
没用到的童鞋自行搜索博文学习下
首先我们重写拦截事件的方法
public boolean onInterceptTouchEvent(MotionEvent event) {
//拿到事件的动作
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) { //按下的时候记录下位置
downY = event.getY();
downX = event.getX();
} else if (action == MotionEvent.ACTION_MOVE) {
View view = getChildAt(1);
//如果当前的y是大于按下时候的y,说明是在向下拉
if (event.getY() > downY && !canChildScrollUp(view)) {
//改成按下的事件
event.setAction(MotionEvent.ACTION_DOWN);
//传递给onTouchevent
onTouchEvent(event);
event.setAction(MotionEvent.ACTION_MOVE);
return true;
}
}
return false;
}
上述的几句奇怪的代码
event.setAction(MotionEvent.ACTION_DOWN);
onTouchEvent(event);
event.setAction(MotionEvent.ACTION_MOVE);
这是因为拖拽神器第一个事件必须是ACTION_DOWN
否则会挂掉
所以这里把事件动作改成按下,并且主动调用了一次onTouchEvent方法,这就相当于传入了一个按下的动作给onTouchEvent
而之后的事件由于拦截了都会流经onTouchEvent
这里我们对事件就行了判断和拦截
首先我们在手指按下的时候记录下当前的坐标点
//按下的时候的坐标点
private float downX;
private float downY;
然后如果是移动的时候,我们先拿到了第二个孩子,也就是我们的列表控件
然后再判断当前的点的纵坐标和刚刚记录下的纵坐标比较,如果大于,说明用户手指正在往下滑动
然后我们又判断了一下列表控件View是否可以向下滚动
public boolean canChildScrollUp(View mTarget) {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mTarget, -1);
}
}
这段代码基本上从SwipeRefreshLayout控件源码中拿出来的,所以有时候写代码不能死板
这段代码能判断出一个View是否可以继续往上滚动了,也就是这个列表控件item0,item1等是否滚上去了
依靠这两点条件,我们就可以让我们的刷新控件出现啦,所以拦截下事件,交给自身的onTouchEvent方法处理.因为满足手指往下滑动,而且列表item0也是可见的,这就证明用户是要刷新
而我们的onTouchEvent方法又交给了拖拽神器ViewDragHelper处理
public boolean onTouchEvent(MotionEvent event) {
mDragger.processTouchEvent(event);
return true;
}
/**
* 滑动的工具类
*/
private ViewDragHelper mDragger;
//初始化方法,在构造函数中被调用,主要用于初始化ViewDragHelper
private void init() {
mDragger = ViewDragHelper.create(this, callback);
}
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
if (child == getChildAt(1)) {
return true;
}
return false;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//返回0表示被拖动的View不进行滑动,呆在原地
return 0;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
//此处是为了让滑动的变得越来越困难
if (dy > 0) {
int mScrollY = Math.abs(getScrollY());
//计算滑动出来的高度和头部的高度的比值
float percent = ((Number) mScrollY).floatValue() / (headerMenuHeight * 2);
dy = (int) ((1 - (percent)) * dy);
}
if (getScrollY() - dy > 0) {
//执行滑动
scrollBy(0, 0);
} else {
//执行滑动
scrollBy(0, -dy);
}
float pullPercent = 1f;
//如果菜单没有完全滑动出来
if (getScrollY() > -headerMenuHeight) {
//计算拉出来的百分比
pullPercent = ((Number) Math.abs(getScrollY())).floatValue() / ((Number) Math.abs(headerMenuHeight)).floatValue();
//如果之前是菜单整个显示的状态,切换为菜单不显示的状态,并通知监听者
if (currentHeaderMenuState == STATE_MENU_SHOWED) {
currentHeaderMenuState = STATE_MENU_UNSHOW;
if (onRefreshListener != null) {
onRefreshListener.onHeaderCamcelPrepareRefresh();
}
}
} else { //菜单整个出来了
//计算百分比
pullPercent = 1f;
//如果之前是菜单不显示的状态,切换为菜单显示的状态,并通知监听者
if (currentHeaderMenuState == STATE_MENU_UNSHOW) {
currentHeaderMenuState = STATE_MENU_SHOWED;
if (onRefreshListener != null) {
onRefreshListener.onHeaderPrepareRefresh();
}
}
}
if (onRefreshListener != null) {
onRefreshListener.onPullPercentage(pullPercent);
}
//返回0表示被拖动的View不进行滑动,呆在原地
return 0;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//声明目标y
int toValue;
//如果没有滑出整个菜单,那么还原状态
if (getScrollY() > -headerMenuHeight) {
toValue = 0;
currentHeaderMenuState = STATE_MENU_UNSHOW;
currentHeaderMenuRefreshState = STATE_MENU_REFRESH_NORMAL;
} else {
//如果释放的时候整个菜单都滑动出来了,那么目标y就是菜单的top的位置
toValue = -headerMenuHeight;
currentHeaderMenuState = STATE_MENU_SHOWED;
//记录正在刷新
currentHeaderMenuRefreshState = STATE_MENU_REFRESH_NOW;
//通知监听者
if (onRefreshListener != null) {
onRefreshListener.onHeaderRefresh();
}
}
smothTo(toValue);
}
};
在实现CallBack接口的实现方法中,前两个方法这里不说了,请自行参阅其他博文来学习ViewDragHelper
我们就着重分析一下clampViewPositionVertical方法和onViewReleased方法,因为我们只关心竖直方向的滑动和手指释放的时候
clampViewPositionVertical方法
if (dy > 0) {
int mScrollY = Math.abs(getScrollY());
//计算滑动出来的高度和头部的高度的比值
float percent = ((Number) mScrollY).floatValue() / (headerMenuHeight * 2);
dy = (int) ((1 - (percent)) * dy);
}
dy>0表示正在往下拽
**这段代码是为了让滑动的变得越来越困难,你可以看到,如果percent越大
那么1-percent的值就越小,那么(1 - (percent)) * dy就越小
而percent是当前纵向滑动的距离和headerMenuHeight * 2的比值,这里的headerMenuHeight * 2就是最高能滑动到的高度**
这里实现的就是一开始qq的效果那种越往下拉越难拉的情况
if (getScrollY() - dy > 0) {
//执行滑动
scrollTo(0, 0);
} else {
//执行滑动
scrollBy(0, -dy);
}
这一段是为了防止上下拖拽的时候,把列表往上拽了,所以有一个判断,如果成立我们就滚动回开始的位置(刷新视图看不见,列表滑动到最上面的状态)
在接下去的代码就是一些逻辑的处理了
比如当刷新视图没有完全显示的时候–>完全显示,我们记录下当前的状态,并通知监听者
当刷新视图完全显示的时候–>不完全显示,我们记录下当前的状态,并通知监听者
还计算了一个刷新视图滑动出来的百分比,也通知给监听者
onViewReleased
此方法就是在手指抬起的时候的被调用的
主要处理以下的细节
当刷新视图滑动出来的部分小于视图高度一半的时候,我们还原到最初始的状态
如果大于一半,我们滑动到刷新视图完全显示的位置,然后通知监听者
/**
* 设置刷新完成
*/
public void setOnRefreshComplete() {
smothTo(0);
}
然后监听者做了加载数据的操作之后,调用
setOnRefreshComplete方法还原刷新视图的位置
一个完成的流程是怎么样的
1.用户手指按下
2.如果确定是可以下拉刷新,就拦截子类事件
3.自身处理事件都交给拖拽神器处理
4.在拖拽神器的回调方法中处理竖直方向的滑动数据用于滚动自身视图来达到效果上的滚动
5.在手指释放的时候,根据刷新视图显示是否超过一半来决定是否走刷新还是还原状态
6.如果走了刷新,就通知监听者,刷新视图一直保持显示
7.监听者调用结束刷新方法来还原刷新视图控件
下面是完整的代码
/**
* Created by cxj on 2016/8/21.
* 这是一个通用的头部刷新自定义控件
* 必须有两个孩子!
* 第一个孩子作为刷新的头部,头部里面的控件完全自己放置
* 第二个孩子是你自己的界面
*/
public class CommonRefreshLayout extends ViewGroup {
/**
* 菜单没有完全显示
*/
public static final int STATE_MENU_UNSHOW = 0;
/**
* 菜单完全显示
*/
public static final int STATE_MENU_SHOWED = 1;
/**
* 不在刷新状态
*/
public static final int STATE_MENU_REFRESH_NORMAL = 11;
/**
* 正在刷新
*/
public static final int STATE_MENU_REFRESH_NOW = 12;
public CommonRefreshLayout(Context context) {
this(context, null);
}
public CommonRefreshLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CommonRefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* 初始化
*/
private void init() {
mDragger = ViewDragHelper.create(this, callback);
}
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
if (child == getChildAt(1)) {
return true;
}
return false;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//返回0表示被拖动的View不进行滑动,呆在原地
return 0;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
if (dy > 0) {
int mScrollY = Math.abs(getScrollY());
//计算滑动出来的高度和头部的高度的比值
float percent = ((Number) mScrollY).floatValue() / (headerMenuHeight * 2);
dy = (int) ((1 - (percent)) * dy);
}
//这一段是为了防止上下拖拽的时候,把列表往上拽了,所以有一个判断,如果成立我们就滚动回开始的位置(刷新视图看不见,列表滑动到最上面的状态)
if (getScrollY() - dy > 0) {
//执行滑动
scrollTo(0, 0);
} else {
//执行滑动
scrollBy(0, -dy);
}
float pullPercent = 1f;
//如果菜单没有完全滑动出来
if (getScrollY() > -headerMenuHeight) {
//计算拉出来的百分比
pullPercent = ((Number) Math.abs(getScrollY())).floatValue() / ((Number) Math.abs(headerMenuHeight)).floatValue();
//如果之前是菜单整个显示的状态,切换为菜单不显示的状态,并通知监听者
if (currentHeaderMenuState == STATE_MENU_SHOWED) {
currentHeaderMenuState = STATE_MENU_UNSHOW;
if (onRefreshListener != null) {
onRefreshListener.onHeaderCamcelPrepareRefresh();
}
}
} else { //菜单整个出来了
//计算百分比
pullPercent = 1f;
//如果之前是菜单不显示的状态,切换为菜单显示的状态,并通知监听者
if (currentHeaderMenuState == STATE_MENU_UNSHOW) {
currentHeaderMenuState = STATE_MENU_SHOWED;
if (onRefreshListener != null) {
onRefreshListener.onHeaderPrepareRefresh();
}
}
}
if (onRefreshListener != null) {
onRefreshListener.onPullPercentage(pullPercent);
}
//返回0表示被拖动的View不进行滑动,呆在原地
return 0;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//声明目标y
int toValue;
//如果没有滑出整个菜单,那么还原状态
if (getScrollY() > -headerMenuHeight) {
toValue = 0;
currentHeaderMenuState = STATE_MENU_UNSHOW;
currentHeaderMenuRefreshState = STATE_MENU_REFRESH_NORMAL;
} else {
//如果释放的时候整个菜单都滑动出来了,那么目标y就是菜单的top的位置
toValue = -headerMenuHeight;
currentHeaderMenuState = STATE_MENU_SHOWED;
//记录正在刷新
currentHeaderMenuRefreshState = STATE_MENU_REFRESH_NOW;
//通知监听者
if (onRefreshListener != null) {
onRefreshListener.onHeaderRefresh();
}
}
smothTo(toValue);
}
};
/**
* 滑动的工具类
*/
private ViewDragHelper mDragger;
/**
* 当前的头部菜单的状态
* 0:菜单没有滑动出来
* 1:菜单滑动出来了
*/
private int currentHeaderMenuState = STATE_MENU_UNSHOW;
/**
* 当前的头部是不是在状态的状态记录
*/
private int currentHeaderMenuRefreshState = STATE_MENU_REFRESH_NORMAL;
/**
* 列表的高度和刷新的View的高度比
*/
private int headerRatio = 4;
//按下的时候的坐标点
private float downX;
private float downY;
/**
* 是否拦截事件
*/
private boolean isInterceptTouchEvent = true;
public void setInterceptTouchEvent(boolean interceptTouchEvent) {
isInterceptTouchEvent = interceptTouchEvent;
}
/**
* 拦截子类事件,在按下的时候记录下坐标点
* 移动的时候判断当前点的y是否比按下的时候的y大
* 如果大了并且标识符isInterceptTouchEvent为true,就表示拦截事件,下拉出我们的菜单
*
* @param event
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
//如果正在刷新,那就什么都别说了,直接拦截,
if (currentHeaderMenuRefreshState == STATE_MENU_REFRESH_NOW) {
return true;
}
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) { //按下的时候记录下位置
downY = event.getY();
downX = event.getX();
} else if (action == MotionEvent.ACTION_MOVE) {
View view = getChildAt(1);
//如果当前的y是大于按下时候的y,说明是在向下拉
if (isInterceptTouchEvent && event.getY() > downY && !canChildScrollUp(view)) {
//改成按下的事件
event.setAction(MotionEvent.ACTION_DOWN);
//传递给onTouchevent
onTouchEvent(event);
event.setAction(MotionEvent.ACTION_MOVE);
return true;
}
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (currentHeaderMenuRefreshState == STATE_MENU_REFRESH_NOW) {
return false;
}
mDragger.processTouchEvent(event);
return true;
}
/**
* 头部菜单的高度
*/
private int headerMenuHeight;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取孩子个数
int childCount = getChildCount();
if (childCount != 2) {
throw new RuntimeException("the child count must be 2");
}
//拿到推荐的宽和高
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//父容器给我多少我就要多少,不能放在列表中使用,因为列表中推荐过来的值heightSize为0
setMeasuredDimension(widthSize, heightSize);
//推荐给第二个孩子的宽和高,和自身宽高一样
View mainView = getChildAt(1);
int widthSpec = MeasureSpec.makeMeasureSpec(widthSize - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY);
int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
mainView.measure(widthSpec, heightSpec);
//推荐给第一个孩子的宽和高,宽度是自身的4分之一
View menuView = getChildAt(0);
headerMenuHeight = (heightSize) / headerRatio;
heightSpec = MeasureSpec.makeMeasureSpec(headerMenuHeight, MeasureSpec.AT_MOST);
menuView.measure(widthSpec, heightSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//刷新控件View
View reFreshView = getChildAt(0);
int measuredWidth = reFreshView.getMeasuredWidth();
int measuredHeight = reFreshView.getMeasuredHeight();
//记录下刷新控件的高度,后续有用
headerMenuHeight = measuredHeight;
reFreshView.layout(0 + getPaddingLeft(), 0 - measuredHeight, measuredWidth, 0);
View mainView = getChildAt(1);
measuredWidth = mainView.getMeasuredWidth();
measuredHeight = mainView.getMeasuredHeight();
mainView.layout(0 + getPaddingLeft(), 0, measuredWidth, measuredHeight);
}
/**
* 设置刷新完成
*/
public void setOnRefreshComplete() {
currentHeaderMenuRefreshState = STATE_MENU_REFRESH_NORMAL;
smothTo(0);
}
/**
* 判断这个View是不是可以向上滑动
*
* @param mTarget
* @return
*/
public boolean canChildScrollUp(View mTarget) {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mTarget, -1);
}
}
/**
* 平滑的滚动到某个位置,这里针对竖直方向
*
* @param toValue 目标y
*/
private void smothTo(int toValue) {
ValueAnimator objectAnimator = //
ofInt(getScrollY(), toValue)//
.setDuration(300);
//设置更新数据的监听
objectAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
scrollTo(0, value);
}
});
objectAnimator.start();
}
/**
* 刷新的监听
*/
public interface OnRefreshListener {
/**
* 拉拽的时候,菜单拉出来的部分占用整个菜单高度的百分比
*
* @param percent
*/
void onPullPercentage(float percent);
/**
* 头部刷新啦
*/
void onHeaderRefresh();
/**
* 准备刷新,在菜单整个滑动出来被调用
*/
void onHeaderPrepareRefresh();
/**
* 取消准备刷新,在菜单整个滑动出来之后,又滑动回去了
*/
void onHeaderCamcelPrepareRefresh();
}
private OnRefreshListener onRefreshListener;
/**
* 设置监听
*
* @param onRefreshListener
*/
public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
this.onRefreshListener = onRefreshListener;
}
}
那么自定义控件写完了,我们该如何去使用呢?
xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.cxj.mvpdemo.MainActivity">
<com.example.cxj.mvpdemo.CommonRefreshLayout
android:id="@+id/crl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/rl_refresh"
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="#456789">
<ImageView
android:id="@+id/iv"
android:layout_width="32dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:layout_marginLeft="110dp"
android:src="@mipmap/arrow_down" />
<ProgressBar
android:id="@+id/pb"
android:layout_width="32dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:layout_marginLeft="110dp"
android:src="@mipmap/arrow_down"
android:visibility="gone" />
<TextView
android:id="@+id/tv_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginLeft="10dp"
android:layout_toRightOf="@+id/iv"
android:text="提示的文本\ud83c\udc00"
android:textColor="#DDDDDD" />
</RelativeLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.example.cxj.mvpdemo.CommonRefreshLayout>
</RelativeLayout>
预览图中可以看出我们的列表是自定义控件一样大的,而刷新视图在上面看不见了,也就是我们的标题栏的位置,看不见啦
Activity
public class MainActivity extends AppCompatActivity {
//下拉刷新控件
private CommonRefreshLayout crl;
//列表控件
private RecyclerView rv = null;
//显示的数据
private List<String> data = new ArrayList<>();
private HeaderReFresh HeaderReFresh;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
crl = (CommonRefreshLayout) findViewById(R.id.crl);
rv = (RecyclerView) findViewById(R.id.rv);
HeaderReFresh = new HeaderReFresh(findViewById(R.id.rl_refresh), crl);
//数据造假
for (int i = 0; i < 100; i++) {
data.add("测试" + i);
}
//设置下拉刷新监听
crl.setOnRefreshListener(HeaderReFresh);
//初始化列表控件的布局管理器
LinearLayoutManager layout = new LinearLayoutManager(this);
layout.setOrientation(LinearLayoutManager.VERTICAL);
//设置布局管理器
rv.setLayoutManager(layout);
//设置适配器
rv.setAdapter(new CommonRecyclerViewAdapter<String>(this, data) {
@Override
public void convert(CommonRecyclerViewHolder h, String entity, int position) {
h.setText(android.R.id.text1, entity);
}
@Override
public int getLayoutViewId(int viewType) {
return android.R.layout.simple_list_item_1;
}
});
}
}
这里为了快速,用了通用的适配器,在我的博文中又介绍,可以自行查找一下
HeaderReFresh 类的功能看注释吧,写在注释上了
/**
* Created by cxj on 2016/11/22.
* 抽取出下拉刷新更新刷新视图ui的代码
*/
public class HeaderReFresh implements CommonRefreshLayout.OnRefreshListener {
private ImageView iv = null;
private TextView tv = null;
private ProgressBar pb;
private CommonRefreshLayout crl;
private View contentView;
public HeaderReFresh(View contentView, CommonRefreshLayout crl) {
this.contentView = contentView;
this.crl = crl;
iv = (ImageView) contentView.findViewById(R.id.iv);
tv = (TextView) contentView.findViewById(R.id.tv_tip);
pb = (ProgressBar) contentView.findViewById(R.id.pb);
}
private Handler h = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//设置刷新控件完成刷新
crl.setOnRefreshComplete();
//还原ui
pb.setVisibility(View.GONE);
iv.setVisibility(View.VISIBLE);
iv.setRotation(0);
}
};
@Override
public void onPullPercentage(float percent) {
if (percent < 0) {
percent = 0f;
}
if (percent > 1) {
percent = 1f;
}
if (percent > 0.7f) {
percent = (percent - 0.7f) * 10 / 3;
iv.setRotation(180 * percent);
}else{
iv.setRotation(0);
}
}
@Override
public void onHeaderRefresh() {
tv.setText("正在刷新.......");
iv.setVisibility(View.INVISIBLE);
pb.setVisibility(View.VISIBLE);
//模拟加载数据,延迟0.5秒发送消息
h.sendEmptyMessageDelayed(0, 500);
}
@Override
public void onHeaderPrepareRefresh() {
tv.setText("释放立即刷新");
}
@Override
public void onHeaderCamcelPrepareRefresh() {
tv.setText("下拉刷新");
}
}
这个类中基本上就是在不同的方法回调中更新了ui
看看效果吧
效果还是不错哒