前言
第三篇下拉刷新的博客来的稍微有点晚,因为前两篇的博客访问量一直不是很高,所以博主花了点时间修改了整体的Demo效果,处理了很多极端下拉情况下的显示问题,给大家呈现一个完美的下拉刷新控件.因为本文不介绍贝塞尔曲线的实现,所以如有对贝塞尔曲线感兴趣的读者,可以阅读博主的上一篇博客( Android仿苹果版QQ下拉刷新实现(二) ——贝塞尔曲线开发"鼻涕"下拉粘连效果)即可.好了,按照惯例,我们先来看下我们今天要实现的最终效果:
图片稍微有点大,还请读者们稍微等待一下-_-!,总的来说,效果和QQ还是很相似的,废话不多说了,下面就跟随博主去实现吧!
一、自定义LinearLayout并初始化布局
下拉头部的布局实现其实很简单,包含了一套重叠布局,即上层为贝塞尔小球,下层为我们的刷新成功的提示布局,通过对布局的隐藏和显示来动态显示状态,下面贴出我们的header的xml布局代码:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center|top"
android:background="#ffffff">
<LinearLayout
android:id="@+id/ll_ok"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal"
android:visibility="gone">
<ImageView
android:id="@+id/iv_ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/pull_ok" />
<TextView
android:id="@+id/tv_ok"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:gravity="center"
android:text="刷新成功"
android:textSize="14sp"
android:textAppearance="?android:attr/textAppearance"
android:textColor="#999999"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center|top">
<ProgressBar
android:id="@+id/pb_refresh"
style="?android:attr/progressBarStyleSmall"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_centerVertical="true"
android:layout_gravity="center"
android:indeterminate="true"
android:indeterminateDrawable="@drawable/pulling"
android:visibility="gone" />
<com.ypx.jiehunle.ypx_bezierqqrefreshdemo.YPXQQRefresh.YPXBezierView
android:id="@+id/bview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</RelativeLayout>
其中的YPXBezierView就是我们的贝塞尔小球,主要原理是通过自定义View来绘制路径得到的(上一篇博客有介绍),可以看到,我们的贝塞尔小球什么都不需要设置,宽高全部填满父控件即可.
接下来,就是自定义LinearLayout了,首先我们看一下申明的变量:
/**
* 下拉刷新状态
*/
public static final int REFRESH_BY_PULLDOWN = 0;
/**
* 松开刷新状态
*/
public static final int REFRESH_BY_RELEASE = 1;
/**
* 正在刷新状态
*/
public static final int REFRESHING = 2;
/**
* 刷新成功状态
*/
public static final int REFRESHING_SUCCESS = 3;
/**
* 刷新失败状态
*/
public static final int REFRESHING_FAILD = 4;
/**
* 收回到刷新位置状态
*/
public static final int TAKEBACK_REFRESH = -1;
/**
* 收回到初始位置状态
*/
public static final int TAKEBACK_RESET = -2;
/**
* 从头收到尾,不考虑中间状态
*/
public static final int TAKEBACK_ALL = -3;
private int refreshTargetTop=dp(-60);//刷新头部高度
ObjectAnimator anim;
//下拉刷新布局
private View refreshView;
LinearLayout ll_ok;
LinearLayout ll_refresh;
ImageView iv_ok;
TextView tv_ok;
ProgressBar pb_refresh;
YPXBezierView bezierView;
private RefreshListener refreshListener;
private int lastY;
private int lastTop;
/**
* 刷新状态
*/
int refreshState = REFRESH_BY_PULLDOWN;
/**
* 收回状态
*/
int takeBackState = TAKEBACK_RESET;
/**
* 是否可刷新标记
*/
private boolean isRefreshEnabled = true;
private float topCircleRadius;//默认上面圆形半径
private float topCircleX;//默认上面圆形x
private float topCircleY;//默认上面圆形y
private int refreshMaxHeight;//刷新小球可滑动的最大距离
boolean bezierLock = false;
private Context mContext;
public QQRefreshView(Context context) {
this(context, null);
}
public QQRefreshView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}
变量比较多,略过一些状态常量,咱们挑重点的说:
refreshTargetTop:要刷新的高度,即刷新头控件的高度,就是我们的topMagin,因为默认在View的顶部,用户看不到,所以我们设置为默认-60dp
lastTop:手指从开始触摸屏幕到滑动结束的高度
takeBacksState:刷新结束后动画应该收回到的高度状态
bezierLock:贝塞尔小球锁,因为用户移动会产生多次状态变化,为了不多执行,添加锁
最主要的变量就是这四个变量了,也是本文实现下拉刷新的关键字段.后面会着重介绍它们的作用.
介绍完我们的初始变量,接下来就是初始化我们的刷新header布局了:
private void initRefreshView() {
//刷新视图顶端的的view
refreshView = LayoutInflater.from(mContext).inflate(R.layout.layout_qqrefresh_header, null);
ll_ok = (LinearLayout) refreshView.findViewById(R.id.ll_ok);
ll_refresh = (LinearLayout) refreshView.findViewById(R.id.ll_refresh);
bezierView = (YPXBezierView) refreshView.findViewById(R.id.bview);
iv_ok = (ImageView) refreshView.findViewById(R.id.iv_ok);
tv_ok = (TextView) refreshView.findViewById(R.id.tv_ok);
pb_refresh = (ProgressBar) refreshView.findViewById(R.id.pb_refresh);
LayoutParams lp = new LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, -refreshTargetTop);
lp.topMargin = refreshTargetTop;
addView(refreshView, lp);
resetData();
bezierView.setOnAnimResetListener(new YPXBezierView.OnAnimResetListener() {
@Override
public void onReset() {
animRefreshView(200, TAKEBACK_REFRESH);
if (refreshListener != null && refreshState == REFRESH_BY_RELEASE) {
refreshing();
refreshListener.onRefresh();
setRefreshState(REFRESHING);
}
}
});
}
private void resetData(){
lastTop = refreshTargetTop;
refreshMaxHeight=-refreshTargetTop;
topCircleX=ScreenUtils.getScreenWidth(mContext)/2;
topCircleY=-refreshTargetTop/2;
topCircleRadius=-refreshTargetTop/4;
bezierView.setTopCircleX(topCircleX);
bezierView.setTopCircleY(topCircleY);
bezierView.setTopCircleRadius(topCircleRadius);
bezierView.setMaxHeight(refreshMaxHeight);
bezierView.resetBottomCricle();
}
我们把我们的刷新头高度设为我们的-refreshTargetTop,因为我们的刷新头默认在View的顶上,看不见,所以我们设置的是负值,高度就是-refreshTargetTop(负负得正),然后就是设置我们的刷新布局的topMargin,其实这里的topMargin就是我们的refreshTargetTop值,resetData中我们初始化了我们的贝塞尔View的一些参数,这些都可以交给用户去设置.最重要的是贝塞尔View的动画收回的回调监听了,主要是切换到刷新状态,具体的代码后面会给大家详细介绍.到此,我们基本可以实现了一个带贝塞尔View的刷新头布局.
接下来就是对应的刷新状态了:
/**
* 下拉刷新状态
*/
public void pullDownToRefresh() {
setRefreshState(REFRESH_BY_PULLDOWN);
ll_refresh.setVisibility(View.VISIBLE);
ll_ok.setVisibility(View.GONE);
pb_refresh.setVisibility(View.GONE);
bezierView.setVisibility(View.VISIBLE);
}
/**
* 正在刷新状态
*/
public void refreshing() {
setRefreshState(REFRESHING);
ll_refresh.setVisibility(View.VISIBLE);
ll_ok.setVisibility(View.GONE);
bezierView.setVisibility(View.GONE);
pb_refresh.setVisibility(View.VISIBLE);
}
/**
* 刷新成功状态
*/
public void refreshOK() {
setRefreshState(REFRESHING_SUCCESS);
ll_refresh.setVisibility(View.GONE);
ll_ok.setVisibility(View.VISIBLE);
tv_ok.setText("刷新成功");
iv_ok.setImageDrawable(getResources().getDrawable(R.mipmap.pull_ok));
}
/**
* 刷新失败状态
*/
public void refreshFailed() {
setRefreshState(REFRESHING_FAILD);
ll_refresh.setVisibility(View.GONE);
ll_ok.setVisibility(View.VISIBLE);
tv_ok.setText("刷新失败");
iv_ok.setImageDrawable(getResources().getDrawable(R.mipmap.pull_failure));
}
看过我之前博客的读者也许能发现以前的五种刷新状态布局现在就只有四种了,是状态减少了吗?其实并不是的,从上面的gif图片其实可以分析出,我们的刷新控件其实并没有松开刷新的状态,从贝塞尔小球被拉伸到触发刷新,中间过程的松开刷新状态其实和下拉刷新状态的显示是一样的,那么会有人问,如果显示的一样,我们就可以取消掉松开刷新的状态了吗?去除掉REFRESH_BY_RELEASE的变量.答案是不可以的,虽然显示的一样,但是代表的意义大不相同,在后面的触发事件中我们就会知道下拉刷新和松开刷新其实是两种完全不相干的状态了.
二、刷新原理分析
其实在上上篇博客(Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件)中博主就已经介绍了关于通用下拉刷新的原理了,下面就贴一下上次分析的截图:
其实原理上相差不是很多,一开始我们依旧是通过改变topMargin来实现滑动,但是,因为我们的贝塞尔小球到了一定的位置后就需要触发曲线下拉,所以我们的topMargin并不能完全决定我们的控件的全部滑动,这时候,我们就需要定好几个滑动分界线:
第一状态:贝塞尔小球被拉出并完全显示:
对应的原理图如下:
可以看到,在第一状态中,我们的lastTop从refreshTargetTop(-60dp)到0点,对应的屏幕上的显示就是贝塞尔小球从隐藏到全部显示的过程,这个过程中,我们只需要改变我们的刷新头(refreshView)d的topMargin就可以,此时的刷新头高度不变,为-refreshTargetTop(负负得正).对应的代码如下:
LayoutParams lp = (LayoutParams) refreshView.getLayoutParams();
lastTop += moveY * 0.5;
if (lastTop < 0) {
lp.topMargin = lastTop;
lp.height = -refreshTargetTop;
setRefreshState(REFRESH_BY_PULLDOWN);
pullDownToRefresh();
}
其中lastTop就是我们手指滑动的距离,在这里,我们给它设置了一个0.5的滑动阻力值,给人的感觉就会显得平滑一点.
第二状态:贝塞尔小球完全显示到拉伸最大距离
对应的原理图:
忽略左图,直接看右图,可以看到当我们的贝塞尔小球到了刷新临界点后,就开始被向下拉伸,当被拉伸到我们设置的最大拉伸距离后,开始收回触发刷新.在拉伸的过程中,我们通过改变刷新头(refreshView)的高度来动态更新我们的贝塞尔小球形状.其中我们需要得到刷新距离和最大拉伸距离的比例,即offset,这个offset就是小球的拉伸变化率了,从上一篇文章可以知道,当我们的offset越小的时候,代表我们的贝塞尔小球被拉伸的越远,几何意义上就是拉伸距离越接近于最大距离.了解完这些,我们再来看代码:
lp.topMargin = 0;
lp.height = lastTop - refreshTargetTop;
float offset = 1 - (lastTop * 1.0f) /refreshMaxHeight;//1~0
if (offset >= 0.2) {
bezierView.setBottomCircleY(bezierView.getTopCircleY() + (lastTop));
bezierView.setBottomCircleRadius(bezierView.getDefaultRadius() * offset);
bezierView.setOffset(offset);
bezierView.setTopCircleRadius((float) (bezierView.getDefaultRadius() * (Math.pow(offset, 1 / 3.0))));
}
lp.topMargin = 0;
lp.height = lastTop - refreshTargetTop;
float offset = 1 - (lastTop * 1.0f) /refreshMaxHeight;//1~0
if (offset >= 0.2) {
bezierView.setBottomCircleY(bezierView.getTopCircleY() + (lastTop));
bezierView.setBottomCircleRadius(bezierView.getDefaultRadius() * offset);
bezierView.setOffset(offset);
bezierView.setTopCircleRadius((float) (bezierView.getDefaultRadius() * (Math.pow(offset, 1 / 3.0))));
}
其中判断是否到达回收状态的时候我们用offset是否小于0.2来判断,如果大于等于0.2,就把我们的贝塞尔小球拉伸,如果读者看过博主第二篇博客的话,可以发现我们这里的代码其实就是贝塞尔小球触摸事件的代码,如果没有看过,在这里我也贴上贝塞尔小球的触摸事件代码:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
delayY=event.getRawY() - lastY;//滑动高度的偏移量
if(delayY<0){
return true;
}
offset=1-delayY/maxHeight;//滑动的偏移量offset 范围 offset∈(1,0)
//如果偏移量大于等于0.2的时候我们就让它开始重绘,
// 这样可以给下面的圆留下一点可见半径,要不然offset为0的时候下面的圆就成了点
if(offset>=0.2){
bottomCircleRadius = defaultRadius * offset;
bottomCircleX = topCircleX;
bottomCircleY = topCircleY + delayY;
topCircleRadius = (float) (defaultRadius * (Math.pow(offset, 1 / 3.0)));
postInvalidate();
}
break;
case MotionEvent.ACTION_UP:
animToReset(false);
break;
case MotionEvent.ACTION_CANCEL:
animToReset(false);
break;
}
return true;
}
怎么样,对比一下是不是发现一模一样.
接下来我们看一下最后的状态
第三状态:触发刷新回收状态
效果图在这里就不贴上了,就是收回我们的贝塞尔小球到刷新状态的显示.根据状态二我们知道,当我们的offset小于0.2的时候.我们就要收回我们的贝塞尔小球,收回的时候是触发了一段属性动画,在上一篇博客中已经介绍了我们的收回动画,接下来直接上状态三的代码:
if(offset<0){//lastTop>refreshMaxHeight
return;
}
if (!bezierLock&&takeBackState!=TAKEBACK_ALL) {
bezierView.animToReset(bezierLock);
refreshState = REFRESH_BY_RELEASE;//松开刷新状态
bezierLock = true;
}
if(offset<0){//lastTop>refreshMaxHeight
return;
}
if (!bezierLock&&takeBackState!=TAKEBACK_ALL) {
bezierView.animToReset(bezierLock);
refreshState = REFRESH_BY_RELEASE;//松开刷新状态
bezierLock = true;
}
代码很简单,其中用到了bezierLock(贝塞尔锁),这个锁前面已经大概介绍过了,这里我们细说一下,当我们的手指触发屏幕的时候,其实会存在重复点,就会触发很多次我们的动画,就会造成运行内存多余,体现的效果肯定也非常糟糕,那么我们如何限制只执行一次呢,那就是定义一个布尔类型变量,在我们的状态没有发生变化时候就不更新它的值,只有在变化的时候才会改变,这样就可以保证一种状态下只执行一次我们的代码.这个思路其实很多地方都可以用到,比如当你想监听listview滑动的时候,上滑的时候触发一个函数,如果不加锁,你会发现我们的事件会执行多次,这个时候我们就可以引用这个思路.具体的我就不去赘述了,后面的博客里会着重介绍一下这个方法.
看完了收回动画代码,细心的朋友可能会发现,咦?下拉刷新在什么时候执行呢?在文章上面第一节中其实我们就已经贴出了我们的刷新完成代码,现在我们再回过头看一下代码:
bezierView.setOnAnimResetListener(new YPXBezierView.OnAnimResetListener() {
@Override
public void onReset() {
animRefreshView(200, TAKEBACK_REFRESH);
if (refreshListener != null && refreshState == REFRESH_BY_RELEASE) {
refreshing();
refreshListener.onRefresh();
setRefreshState(REFRESHING);
}
}
});
是不是清晰了很多.
到此,我们下拉刷新的三种状态已经介绍完成,下面上一下触摸事件的全部代码:
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录下y坐标
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//y移动坐标
int m = y - lastY;
doMovement(m);
//记录下此刻y坐标
this.lastY = y;
break;
case MotionEvent.ACTION_UP:
fling();
break;
}
return true;
}
/**
* 下拉move事件处理
*
* @param moveY
*/
private void doMovement(float moveY) {
if ((refreshState != REFRESH_BY_RELEASE && refreshState != REFRESH_BY_PULLDOWN)
||anim.isRunning()) {
return;
}
LayoutParams lp = (LayoutParams) refreshView.getLayoutParams();
lastTop += moveY * 0.5;
if (lastTop < 0) {
lp.topMargin = lastTop;
lp.height = -refreshTargetTop;
setRefreshState(REFRESH_BY_PULLDOWN);
pullDownToRefresh();
} else {
lp.topMargin = 0;
lp.height = lastTop - refreshTargetTop;
float offset = 1 - (lastTop * 1.0f) /refreshMaxHeight;//1~0
if (offset >= 0.2) {
bezierView.setBottomCircleY(bezierView.getTopCircleY() + (lastTop));
bezierView.setBottomCircleRadius(bezierView.getDefaultRadius() * offset);
bezierView.setOffset(offset);
bezierView.setTopCircleRadius((float) (bezierView.getDefaultRadius() * (Math.pow(offset, 1 / 3.0))));
} else {
if(offset<0){//lastTop>refreshMaxHeight
return;
}
if (!bezierLock&&takeBackState!=TAKEBACK_ALL) {
bezierView.animToReset(bezierLock);
refreshState = REFRESH_BY_RELEASE;//松开刷新状态
bezierLock = true;
}
}
bezierView.postInvalidate();
}
refreshView.setLayoutParams(lp);
refreshView.invalidate();
invalidate();
}
代码比较长,但是核心部分我们刚刚都已经分析完毕,其中有很多细节的处理,比如在我们触发下拉之前,先要判读一下当前是否还没有刷新完毕,如果没有刷新完毕,那么我们的lastTop的默认高度就不对,是我们上一次刷新拉到的高度,就会产生很多问题,所以在这里,博主多了一些细节上的判断,让我们的控件流畅度和抗压度都得到了不错的提升.
三、收回原理分析
其实收回状态应该属于第二节的第四种状态才对,这里博主为什么要单独拿出来作为一个章节呢?因为收回的原理并不简单,从效果图上我们可以看到,我们的收回其实是分为两段式收回,从贝塞尔小球被拉伸到触发刷新到收回到刷新状态的时候,其实我们触发了
animRefreshView(200, TAKEBACK_REFRESH);
这个方法,这个方法是用来干嘛的呢?字面意思上就是给我们的刷新头布局添加动画,收回状态为TAKEBACK_REFRESH,即收回到刷新位置:
下面上一下我们刷新收回的几个位置点的效果图:
可以看到我们的收回状态其实有三种,每一种收回的起点或终点都有所改变,那么我们需要定义三个动画来收回吗?答案并不是这样的,一个动画的监听回调就好,下面我们来上一下最核心的收回动画代码:
private void init() {
initRefreshView();
// initLoadMoreView();
anim = ObjectAnimator.ofFloat(refreshView, "ypx", 0.0f, 1.0f);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float cVal = (Float) valueAnimator.getAnimatedValue();
LayoutParams lp = (LayoutParams) refreshView.getLayoutParams();
switch (takeBackState) {
case TAKEBACK_REFRESH:
lp.height = lp.height + (int) (cVal * (-refreshTargetTop - lp.height));
lp.topMargin = 0;
break;
case TAKEBACK_RESET:
lp.topMargin = lp.topMargin + (int) (cVal * (refreshTargetTop - lp.topMargin));
lp.height = -refreshTargetTop;
break;
case TAKEBACK_ALL:
lp.topMargin = lp.topMargin + (int) (cVal * (refreshTargetTop - lp.topMargin));
lp.height = lp.height + (int) (cVal * (-refreshTargetTop - lp.height));
//bezierView.reset((float) Math.pow(cVal, 2 / 5.0));
break;
}
refreshView.setLayoutParams(lp);
refreshView.invalidate();
invalidate();
if (lp.height == -refreshTargetTop
&& lp.topMargin == refreshTargetTop) {//动画完成
resetRefreshView();
}
}
});
}
代码很简单,就是对三种收回状态单独判断,如果是收回到刷新位(TAKEBACK_REFRESH),那么我们的高度就需要动态的收回到刷新位(-refreshTargetHeight)的高度,topMargin就是0点,当前的几何意义就是我们的刷新头完整显示.剩下的两种状态我就不带着大家分析了,因为原理都一样,如果你对于为什么会有收回状态三有所疑问,因为我们正常的刷新收回是先回到刷新位,再从刷新位回到初始位置,但是,我们还遗漏了一个状态,就是下拉到触发贝塞尔小球变形但是未触发到贝塞尔小球最大距离时手指离开的状态.简单的说,我们的贝塞尔小球如果没有触发到刷新状态,这时候手指离开就需要收回我们的小球,这就触发了收回状态三.
下面我们来看对于这种情况下的代码处理:
/**
* up事件处理
*/
private void fling() {
LayoutParams lp = (LayoutParams) refreshView.getLayoutParams();
//当前状态不是下拉刷新状态且刷新头为初始状态,此时可以认为是已经触发了刷新完成后离开手指
if (refreshState != REFRESH_BY_PULLDOWN
|| (lp.topMargin == refreshTargetTop && lp.height == -refreshTargetTop)) {
return;
}
bezierView.setTopCircleRadius(topCircleRadius);
bezierView.resetBottomCricle();
//没刷新收回状态
animRefreshView(500, TAKEBACK_ALL);
}
当我们的手指离开屏幕时,如果当前状态不是下拉刷新状态且刷新头为初始状态,此时可以认为是已经触发了刷新完成后离开手指,我们需要屏蔽掉这种情况的处理,因为我们已经完成了一套完整的下拉刷新.如果用户在触发刷新前松开手指,我们就把贝塞尔小球初始化,触发收回动画三.
到此我们的刷新控件基本已经完成了,接下来就是如何定制和使用我们的自定义控件了.
四、定制及使用
我们的自定义控件有了骨头,自然少不了"肉",我们来看一下ScrollView下的使用方法:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.ypx.jiehunle.ypx_bezierqqrefreshdemo.YPXQQRefresh.QQRefreshView
android:id="@+id/refreshableView1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="visible">
<ScrollView
android:id="@+id/scrollView1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never">
<LinearLayout
android:id="@+id/ll_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
</LinearLayout>
</ScrollView>
</com.ypx.jiehunle.ypx_bezierqqrefreshdemo.YPXQQRefresh.QQRefreshView>
</RelativeLayout>
使用代码:
public class ScrollViewFragment extends Fragment{
QQRefreshView refreshableView;
LinearLayout layout;
final int SUCCESS = 1;
final int FAILED = 0;
View view;
@SuppressLint("HandlerLeak")
Handler handler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case SUCCESS:
refreshableView.finishRefresh(true);
TextView textView = new TextView(getActivity());
textView.setTextColor(Color.parseColor("#666666"));
textView.setTextSize(18);
textView.setText("这是刷新的文本");
textView.setPadding(dp(15),dp(10),dp(15),dp(10));
layout.addView(textView,0);
break;
case FAILED:
refreshableView.finishRefresh(false);
break;
default:
break;
}
};
};
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
view= LayoutInflater.from(getContext()).inflate(R.layout.fragment_scrollview,null);
initView();
initData();
return view;
}
private void initView() {
refreshableView = (QQRefreshView) view.findViewById(R.id.refreshableView1);
layout = (LinearLayout) view.findViewById(R.id.ll_layout);
//设置是否可以刷新,默认可以刷新
refreshableView.setRefreshEnabled(true);
//设置刷新头的高度,此高度会决定小球的默认半径和坐标
/* refreshableView.setRefreshViewHeight(refreshableView.dp(120));
//设置刷新颜色,默认颜色值#999999
refreshableView.setRefreshColor(Color.parseColor("#26B8F2"));
//设置刷新图标,默认刷新图标
refreshableView.setRefreshIcon(R.mipmap.ic_launcher);
//设置刷新球最大拉伸距离,默认为刷新头部高度
refreshableView.setRefreshMaxHeight(refreshableView.dp(150));
//设置刷新球半径,默认15dp
refreshableView.setTopCircleRadius(refreshableView.dp(30));
//设置刷新球圆心X值,默认屏宽一半
refreshableView.setTopCircleX(refreshableView.dp(50));
//设置刷新球圆心Y值,默认30dp
refreshableView.setTopCircleY(refreshableView.dp(30)); */
}
private void initData() {
layout.removeAllViews();
for (int i = 0; i < 50; i++) {
final TextView textView = new TextView(getActivity());
textView.setTextColor(Color.parseColor("#666666"));
textView.setTextSize(18);
textView.setPadding(dp(15),dp(10),dp(15),dp(10));
textView.setText("这是第" + i + "个文本");
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(getActivity(),textView.getText(),Toast.LENGTH_SHORT).show();
}
});
layout.addView(textView);
}
refreshableView.setRefreshListener(new QQRefreshView.RefreshListener() {
@Override
public void onRefresh() {
handler.postDelayed(new Runnable() {
@Override
public void run() {
handler.sendEmptyMessage(SUCCESS);
}
}, 500);
}
});
}
public int dp(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dp,getResources().getDisplayMetrics());
}
}
我们的控件其中包含的方法有很多,大致罗列了一下:
refreshableView.setRefreshEnabled(true);
//设置刷新头的高度,此高度会决定小球的默认半径和坐标
refreshableView.setRefreshViewHeight(refreshableView.dp(120));
//设置刷新颜色,默认颜色值#999999
refreshableView.setRefreshColor(Color.parseColor("#26B8F2"));
//设置刷新图标,默认刷新图标
refreshableView.setRefreshIcon(R.mipmap.ic_launcher);
//设置刷新球最大拉伸距离,默认为刷新头部高度
refreshableView.setRefreshMaxHeight(refreshableView.dp(150));
//设置刷新球半径,默认15dp
refreshableView.setTopCircleRadius(refreshableView.dp(30));
//设置刷新球圆心X值,默认屏宽一半
refreshableView.setTopCircleX(refreshableView.dp(50));
//设置刷新球圆心Y值,默认30dp
refreshableView.setTopCircleY(refreshableView.dp(30));
注释写的很清楚,其中有一个方法是叫做setRefreshViewHeight,这个方法单独拿出来说一下,因为当用户设置死了我们的刷新头高度,我们的贝塞尔小球默认半径和拉动最大距离也会改变,如果想和原来的保持一致,我们就需要设置下面的几种方法.总的来说,用户可以自定义成任何样式的贝塞尔小球,包括拉动距离、以及半径等,下面上一个如上代码所产生的效果:
五、总结
总的来说,打造一个这样的下拉刷新控件思路其实并不复杂,重要的在于每一个状态的把握.当我们在自定义View的时候,我们应该先确定好我们的每一个状态变化,针对每种状态编码,就会使我们的效率大大提高.记得在实现这个效果前,我也做了不少的功课,也尝试了很多有趣的失败品,虽然效果不尽人意,但是过程中可以学到的确实很多很多.如果有人问博主你觉得你的下拉刷新是最完整的吗?答案肯定是否定的,因为我自己知道,其实还少了一些功能,比如上拉加载.虽然上拉加载的逻辑并不是很复杂,只要给滑动布局添加footView就好,但是本篇博客中并没有实现,所以接下来的博客,博主将会抽离出下拉刷新的主干框架,包含上拉加载,打造一些五颜六色,杂七杂八的刷新效果,比如淘宝、京东、Boss直聘、美团等等效果集于一身,还请读者们多多支持和关注哦~
感谢大家的支持,谢谢!喜爱本博客的读者们帮忙多多点赞哦~
作者:yangpeixing
QQ:313930500
下载地址:https://github.com/yangpeixing/YPXRefreshLayout/tree/develop
转载请注明出处~谢谢~