欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android实现支持所有View的通用的下拉刷新控件

程序员文章站 2024-03-02 21:20:22
下拉刷新对于一个app来说是必不可少的一个功能,在早期大多数使用的是chrisbanes的pulltorefresh,或是修改自该框架的其他库。而到现在已经有了更多的选择,...

下拉刷新对于一个app来说是必不可少的一个功能,在早期大多数使用的是chrisbanes的pulltorefresh,或是修改自该框架的其他库。而到现在已经有了更多的选择,github上还是有很多体验不错的下拉刷新。

而下拉刷新主要有两种实现方式:
1. 在listview中添加header和footer,监听listview的滑动事件,动态设置header/footer的高度,但是这种方式只适用于listview,recyclerview。
2. 第二种方式则是继承viewgroup或其子类,监听事件,通过scroll或layout的方式移动child。如图(又分两种情况)

layout时将header放到屏幕外面,target则填充满屏幕。这个也是swiperefreshlayout的实现原理(第二种,只下拉header)

Android实现支持所有View的通用的下拉刷新控件

这两种(指的是继承listview或继承viewgroup)下拉刷新的实现方式主要有以下区别

Android实现支持所有View的通用的下拉刷新控件

而今天,我打算先讲第二种方式实现方式,继承viewgroup,代码可以直接参考swiperefreshlayout,或者pulltorefresh,或者ultra-pull-to-refresh

一、思考和需求

下拉刷新需要几个状态:reset–> pull – > refreshing – >completed –>reset

为了应对各式各样的下拉刷新设计,我们应该提供设置自定义的header,开发者可以通过实现接口从而自定义自己的header。

而且header可以有两种显示方式,一种是只下拉header,另外一种则是header和target一起下拉。

二、着手实现代码

2.1 定义header的接口,创建自定义layout

/**
 * created by aitsuki on 2016/6/13.
 * 
 */
public enum state {
 reset, pull, loading, complete
}

/**
 * created by aitsuki on 2016/6/13.
 *
 */
public interface refreshheader {

 /**
 * 松手,头部隐藏后会回调这个方法
 */
 void reset();

 /**
 * 下拉出头部的一瞬间调用
 */
 void pull();

 /**
 * 正在刷新的时候调用
 */
 void refreshing();

 /**
 * 头部滚动的时候持续调用
 * @param currentpos target当前偏移高度
 * @param lastpos target上一次的偏移高度
 * @param refreshpos 可以松手刷新的高度
 * @param istouch 手指是否按下状态(通过scroll自动滚动时需要判断)
 * @param state 当前状态
 */
 void onpositionchange(float currentpos, float lastpos, float refreshpos, boolean istouch, state state);

 /**
 * 刷新成功的时候调用
 */
 void complete();
}

package com.aitsuki.custompulltorefresh;

import android.content.context;
import android.graphics.color;
import android.util.attributeset;
import android.view.view;
import android.view.viewconfiguration;
import android.view.viewgroup;
import android.widget.imageview;

/**
 * created by aitsuki on 2016/6/13.
 * -
 */
public class refreshlayout extends viewgroup {

 private view refreshheader;
 private view target;
 private int currenttargetoffsettop; // target偏移距离
 private boolean hasmeasureheader; // 是否已经计算头部高度
 private int touchslop; 
 private int headerheight; // header高度
 private int totaldragdistance; // 需要下拉这个距离才进入松手刷新状态,默认和header高度一致

 public refreshlayout(context context) {
 this(context, null);
 }

 public refreshlayout(context context, attributeset attrs) {
 super(context, attrs);

 touchslop = viewconfiguration.get(context).getscaledtouchslop();

 // 添加默认的头部,先简单的用一个imageview代替头部
 imageview imageview = new imageview(context);
 imageview.setimageresource(r.drawable.one_piece);
 imageview.setbackgroundcolor(color.black);
 setrefreshheader(imageview);
 }

 /**
 * 设置自定义header
 */
 public void setrefreshheader(view view) {
 if (view != null && view != refreshheader) {
 removeview(refreshheader);

 // 为header添加默认的layoutparams
 viewgroup.layoutparams layoutparams = view.getlayoutparams();
 if (layoutparams == null) {
 layoutparams = new layoutparams(layoutparams.match_parent, layoutparams.wrap_content);
 view.setlayoutparams(layoutparams);
 }
 refreshheader = view;
 addview(refreshheader);
 }
 }



 @override
 protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
 super.onmeasure(widthmeasurespec, heightmeasurespec);
 if (target == null) {
 ensuretarget();
 }

 if (target == null) {
 return;
 }

 // ----- measure target -----
 // target占满整屏
 target.measure(measurespec.makemeasurespec(
 getmeasuredwidth() - getpaddingleft() - getpaddingright(),
 measurespec.exactly), measurespec.makemeasurespec(
 getmeasuredheight() - getpaddingtop() - getpaddingbottom(), measurespec.exactly));

 // ----- measure refreshview-----
 measurechild(refreshheader, widthmeasurespec, heightmeasurespec);
 if (!hasmeasureheader) { // 防止header重复测量
 hasmeasureheader = true;
 headerheight = refreshheader.getmeasuredheight(); // header高度
 totaldragdistance = headerheight; // 需要pull这个距离才进入松手刷新状态
 }
 }

 @override
 protected void onlayout(boolean changed, int l, int t, int r, int b) {
 final int width = getmeasuredwidth();
 final int height = getmeasuredheight();
 if (getchildcount() == 0) {
 return;
 }

 if (target == null) {
 ensuretarget();
 }
 if (target == null) {
 return;
 }

 // onlayout执行的时候,要让target和header加上偏移距离(初始0),因为有可能在滚动它们的时候,child请求重新布局,从而导致target和header瞬间回到原位。

 // target铺满屏幕
 final view child = target;
 final int childleft = getpaddingleft();
 final int childtop = getpaddingtop() + currenttargetoffsettop; 
 final int childwidth = width - getpaddingleft() - getpaddingright();
 final int childheight = height - getpaddingtop() - getpaddingbottom();
 child.layout(childleft, childtop, childleft + childwidth, childtop + childheight);

 // header放到target的上方,水平居中
 int refreshviewwidth = refreshheader.getmeasuredwidth();
 refreshheader.layout((width / 2 - refreshviewwidth / 2),
 -headerheight + currenttargetoffsettop,
 (width / 2 + refreshviewwidth / 2),
 currenttargetoffsettop);
 }

 /**
 * 将第一个child作为target
 */
 private void ensuretarget() {
 // don't bother getting the parent height if the parent hasn't been laid
 // out yet.
 if (target == null) {
 for (int i = 0; i < getchildcount(); i++) {
 view child = getchildat(i);
 if (!child.equals(refreshheader)) {
 target = child;
 break;
 }
 }
 }
 }
}

mainactivity中的布局如下,先用一个textview作为target

<?xml version="1.0" encoding="utf-8"?>
<framelayout 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.aitsuki.custompulltorefresh.mainactivity">

 <com.aitsuki.custompulltorefresh.refreshlayout
 android:layout_width="match_parent"
 android:layout_height="match_parent">
 <textview
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:text="target"
 android:textsize="30sp"
 android:gravity="center"
 android:background="#ffdab9"
 />
 </com.aitsuki.custompulltorefresh.refreshlayout>
</framelayout>

运行后结果如图如下,但是我们还没有监听事件,所以此时还无法滑动。

Android实现支持所有View的通用的下拉刷新控件

2.2 处理事件分发

控件已经测量布局好了,现在就开始处理事件分发,对于事件分发还不了解的应该先去复习下……

对于多点触控的处理:
记录活动手指的id(activepointerid),通过此id获取move事件的坐标。
 1.在手指按下的时候,记录下activepointerid
 2.第二根手指按下的时候,更新activepointerid。(我们让第二根手指作为活动手指,忽略第一个手指的move)
 3.当其中一根手指抬起时,如果是第一根手指,那么不做处理,如果是第二根手指抬起,也就是活动手指抬起的话,将活动手指改回第一根。

对于事件分发一般有两种处理方式
1. 在onintercept + ontouchevnet中处理
2. 在dispatchtouchevent中处理
在这里我选择了第二种方式

首先了解dispatchtouchevent返回值的含义
重写dispatchtouchevent的时候,无论你是return true,亦或是return false都会导致child接受不到事件。
return true : 告诉parent,这个事件我消费了。如果这个是down事件,那么我就会作为一个target或者说handle(事件持有者),后续的move事件或者up事件等,都会直接分发到我这里,不继续往下分发。
return false:告诉parent,这个事件我不需要,那么会交回给parent的ontouchevnet处理
只有return super.dispatchtouchevent的时候才会将事件继续往下传递。

上面只说了最简单的一点,如果对事件分发不了解的话需要看看,真的很重要。

分析
在dispatch中,即使child响应了事件,我们也能拿到所有事件。
这样我们就可以很简单的控制头部是否能下拉,那么如何拦截child的事件呢?
可以在合适的时候分发一个cancel事件给child,那么就相当于拦截了!

虽然我们一直都响应着事件,但肯定是不能所有事件都接收的,以下情况是需要我们处理的
 1.如果是下拉,并且child不能往上滚动
 2.如果上划,并且target不在顶部的时候
 3.如果是这些时候,我们拦截child的事件(派发cancel事件)

代码如下

@override
 public boolean dispatchtouchevent(motionevent ev) {
 if (!isenabled() || target == null) {
 return super.dispatchtouchevent(ev);
 }

 final int actionmasked = ev.getactionmasked(); // support multi-touch
 switch (actionmasked) {
 case motionevent.action_down:
 log.e(tag, "action_down");
 activepointerid = ev.getpointerid(0);
 istouch = true; // 手指是否按下
 hassendcancelevent = false;
 misbegindragged = false; // 是否开始下拉
 lasttargetoffsettop = currenttargetoffsettop; // 上一次target的偏移高度
 currenttargetoffsettop = target.gettop(); // 当前target偏移高度
 initdownx = lastmotionx = ev.getx(0); // 手指按下时的坐标
 initdowny = lastmotiony = ev.gety(0);
 super.dispatchtouchevent(ev);
 return true; // return true,否则可能接收不到move和up事件
 case motionevent.action_move:
 if (activepointerid == invalid_pointer) {
 log.e(tag, "got action_move event but don't have an active pointer id.");
 return super.dispatchtouchevent(ev);
 }
 lastevent = ev; // 最后一次move事件
 float x = ev.getx(motioneventcompat.findpointerindex(ev,activepointerid));
 float y = ev.gety(motioneventcompat.findpointerindex(ev,activepointerid));
 float xdiff = x - lastmotionx;
 float ydiff = y - lastmotiony;
 float offsety = ydiff * drag_rate;
 lastmotionx = x;
 lastmotiony = y;

 if(!misbegindragged && math.abs(y - initdowny) > touchslop) {
 misbegindragged = true;
 }

 if (misbegindragged) {
 boolean movedown = offsety > 0; // ↓
 boolean canmovedown = canchildscrollup();
 boolean moveup = !movedown; // ↑
 boolean canmoveup = currenttargetoffsettop > start_position;

 // 判断是否拦截事件
 if ((movedown && !canmovedown) || (moveup && canmoveup)) {
 movespinner(offsety);
 return true;
 }
 }
 break;

 case motionevent.action_cancel:
 case motionevent.action_up:
 istouch = false;
 activepointerid = invalid_pointer;
 break;

 case motionevent.action_pointer_down:
 int pointerindex = motioneventcompat.getactionindex(ev);
 if (pointerindex < 0) {
 log.e(tag, "got action_pointer_down event but have an invalid action index.");
 return super.dispatchtouchevent(ev);
 }
 lastmotionx = ev.getx(pointerindex);
 lastmotiony = ev.gety(pointerindex);
 activepointerid = motioneventcompat.getpointerid(ev, pointerindex);
 break;

 case motionevent.action_pointer_up:
 onsecondarypointerup(ev);
 lastmotiony = ev.gety(ev.findpointerindex(activepointerid));
 lastmotionx = ev.getx(ev.findpointerindex(activepointerid));
 break;
 }
 return super.dispatchtouchevent(ev);
 }

private void onsecondarypointerup(motionevent ev) {
 final int pointerindex = motioneventcompat.getactionindex(ev);
 final int pointerid = motioneventcompat.getpointerid(ev, pointerindex);
 if (pointerid == activepointerid) {
 // this was our active pointer going up. choose a new
 // active pointer and adjust accordingly.
 final int newpointerindex = pointerindex == 0 ? 1 : 0;
 lastmotiony = ev.gety(newpointerindex);
 lastmotionx = ev.getx(newpointerindex);
 activepointerid = motioneventcompat.getpointerid(ev, newpointerindex);
 }
 }

public boolean canchildscrollup() {
 if (android.os.build.version.sdk_int < 14) {
 if (target instanceof abslistview) {
 final abslistview abslistview = (abslistview) target;
 return abslistview.getchildcount() > 0
 && (abslistview.getfirstvisibleposition() > 0 || abslistview.getchildat(0)
 .gettop() < abslistview.getpaddingtop());
 } else {
 return viewcompat.canscrollvertically(target, -1) || target.getscrolly() > 0;
 }
 } else {
 return viewcompat.canscrollvertically(target, -1);
 }
 }

以上就是事件的处理,我们还需要在header下拉之前发送cancel事件给child

private void movespinner(float diff) {
 int offset = math.round(diff);
 if (offset == 0) {
 return;
 }
 // 发送cancel事件给child
 if (!hassendcancelevent && istouch && currenttargetoffsettop > start_position) {
 sendcancelevent();
 hassendcancelevent = true;
 }

 int targety = math.max(0, currenttargetoffsettop + offset); // target不能移动到小于0的位置……
 offset = targety - currenttargetoffsettop;
 settargetoffsettopandbottom(offset);
 }

private void settargetoffsettopandbottom(int offset) {
 if (offset == 0) {
 return;
 }
 target.offsettopandbottom(offset);
 refreshheader.offsettopandbottom(offset);
 lasttargetoffsettop = currenttargetoffsettop;
 currenttargetoffsettop = target.gettop();
 invalidate();
 }

 private void sendcancelevent() {
 if (lastevent == null) {
 return;
 }
 motionevent ev = motionevent.obtain(lastevent);
 ev.setaction(motionevent.action_cancel);
 super.dispatchtouchevent(ev);
 }

代码有点多,不过没关系,其实很多都是从swiperefreshlayout中复制过来的。
我们来看看代码运行后的效果,很不错,就是模拟器录屏有点卡=。=

Android实现支持所有View的通用的下拉刷新控件

换成listview试试, 也没有问题。

Android实现支持所有View的通用的下拉刷新控件

多点触控也是可以的,但是模拟器我没法演示了。

2.3 添加自动滚动

头虽然可以下拉了, 但是拉下来后就不会回去了啊,我们需要在手指松开让头部自动回到原位。
可以使用动画,可以使用valueanimator计算距离移动,也可以使用scroller计算距离移动。

但是选择第三种是比较好的,为什么呢。
首先如果使用动画,在回去的过程中我们无法下拉,我们想做的是一个可以在任何时候都能上下拉的,就像listview添加头的哪种效果。
valueanimator也是,不好停止。
但是scroller却可以使用forcefinish强行停止计算。

松开手指时,我们通过scroller计算每次移动的offset,然后调用movespinner即可。
在手指按下的时候,需要停止scroller。

我们先写一个内部类,封装一下滚动功能

private class autoscroll implements runnable {
 private scroller scroller;
 private int lasty;

 public autoscroll() {
 scroller = new scroller(getcontext());
 }

 @override
 public void run() {
 boolean finished = !scroller.computescrolloffset() || scroller.isfinished();
 if (!finished) {
 int curry = scroller.getcurry(); 
 int offset = curry - lasty;
 lasty = curry;
 movespinner(offset); // 调用此方法移动header和target
 post(this);
 onscrollfinish(false);
 } else {
 stop();
 onscrollfinish(true);
 }
 }

 public void scrollto(int to, int duration) {
 int from = currenttargetoffsettop;
 int distance = to - from;
 stop();
 if (distance == 0) {
 return;
 }
 scroller.startscroll(0, 0, 0, distance, duration);
 post(this);
 }

 private void stop() {
 removecallbacks(this);
 if (!scroller.isfinished()) {
 scroller.forcefinished(true);
 }
 lasty = 0;
 }
 }

然后这个是回调,暂时用户不上,但还是先写好吧。

/**
 * 在scroll结束的时候会回调这个方法
 * @param isforcefinish 是否是强制结束的
 */
 private void onscrollfinish(boolean isforcefinish) {

 }

我们在构造中初始化autoscroll,然后分别在actiondown和actionup中分别调用stop和scrollto即可,如下

case motionevent.action_down:
 //...
 autoscroll.stop();
 //...
 break

case motionevent.action_cancel:
case motionevent.action_up:
 //...
 if(currenttargetoffsettop > start_position) {
 autoscroll.scrollto(start_position, scroll_to_top_duration);
 }
 //...

运行效果如下图

Android实现支持所有View的通用的下拉刷新控件

2.4 添加刷新状态

最开始的时候我们也新建了一个枚举,设置了几种状态,分别是 reset, pull, loading, complete
而我们的初始状态应该为reset
private state state = state.reset;

再分析一下,这几种状态什么时候互相切换:
1. 在reset状态时,第一次下拉出现header的时候,设置状态变成pull
2. 在pull或者complete状态时,header回到顶部的时候,状态变回reset
3. 如果是从底部回到顶部的过程(往上滚动),并且手指是松开状态, 并且当前是pull状态,状态变成loading,这时候我们需要强制停止autoscroll。并且正在刷新中的侦听器也在这里调用(onrefresh())
4. 在loading状态中,想变成其他状态,需要提供公共方法给外部调用

首先,我们先写一个改变状态的方法,在状态改变的同时要回调给header。

private void changestate(state state) {
 this.state = state;

 refreshheader refreshheader = this.refreshheader instanceof refreshheader ? ((refreshheader) this.refreshheader) : null;
 if (refreshheader != null) {
 switch (state) {
 case reset:
 refreshheader.reset();
 break;
 case pull:
 refreshheader.pull();
 break;
 case loading:
 refreshheader.refreshing();
 break;
 case complete:
 refreshheader.complete();
 break;
 }
 }
 }

还有,提供外部设置刷新成功的方法。
因为刷新成功后需要将header滚动回原位,所以需要做以下判断
1. 如果已经在原位,那么直接将状态改成reset
2. 如果不在原位,延时500毫秒后自动滚动回原位。这里延时500毫秒是为了展示刷新成功的提示,否则在网速很快的情况下,刷新成功后header立即回到原位体验性不好,感觉就像是下拉后立即就自动回去了。
3. 在自动回滚时还需要判断当前手指是否在触摸状态,如果正在触摸,代表用户可能并不想header回去,所以这时候我们不能让头部滚动。
4. 再者就是,如果在延时的500内,用户按下了手指,我们需要将这个runnable取消,在actiondown中removecallback即可。总的来说一句话就是,用户必须持有header的绝对控制权,在手指按下时,header不应该出现自动滚动的情况。

public void refreshcomplete() {
 changestate(state.complete);
 // if refresh completed and the target at top, change state to reset.
 if (currenttargetoffsettop == start_position) {
 changestate(state.reset);
 } else {
 // waiting for a time to show refreshview completed state.
 // at next touch event, remove this runnable
 if (!istouch) {
 postdelayed(delaytoscrolltoprunnable, show_completed_time);
 }
 }
}

// 刷新成功,显示500ms成功状态再滚动回顶部,这个runnalbe需要在actiondown事件中remove
private runnable delaytoscrolltoprunnable = new runnable() {
 @override
 public void run() {
 autoscroll.scrollto(start_position, scroll_to_top_duration);
 }
};

提供设置正在刷新回调的方法
当用户松开手指,进入刷新状态时我们需要回调这个方法。

// 定义一个侦听器
public interface onrefreshlistener {
 void onrefresh();
 }

// 提供外部设置方法
public void setrefreshlistener(onrefreshlistener refreshlistener) {
 this.refreshlistener = refreshlistener;
 }

做完以上几部,我们算是完成了loading到complete的状态切换,余下的几个状态我们则需要在movespinner这个方法中控制,上面也已经分析过了逻辑,那么可以直接看代码了。

private void movespinner(float diff) {
 int offset = math.round(diff);
 if (offset == 0) {
 return;
 }
 // 发送cancel事件给child
 if (!hassendcancelevent && istouch && currenttargetoffsettop > start_position) {
 sendcancelevent();
 hassendcancelevent = true;
 }

 int targety = math.max(0, currenttargetoffsettop + offset); // target不能移动到小于0的位置……
 offset = targety - currenttargetoffsettop;

 // 1. 在reset状态时,第一次下拉出现header的时候,设置状态变成pull
 if (state == state.reset && currenttargetoffsettop == start_position && targety > 0) {
 changestate(state.pull);
 }

 // 2. 在pull或者complete状态时,header回到顶部的时候,状态变回reset
 if (currenttargetoffsettop > start_position && targety <= start_position) {
 if (state == state.pull || state == state.complete) {
 changestate(state.reset);
 }
 }

 // 3. 如果是从底部回到顶部的过程(往上滚动),并且手指是松开状态, 并且当前是pull状态,状态变成loading,这时候我们需要强制停止autoscroll
 if (state == state.pull && !istouch && currenttargetoffsettop > totaldragdistance && targety <= totaldragdistance) {
 autoscroll.stop();
 changestate(state.loading);
 if (refreshlistener != null) {
 refreshlistener.onrefresh();
 }
 // 因为判断条件targety <= totaldragdistance,会导致不能回到正确的刷新高度(有那么一丁点偏差),调整change
 int adjustoffset = totaldragdistance - targety;
 offset += adjustoffset;
 }

 settargetoffsettopandbottom(offset);

 // 别忘了回调header的位置改变方法。
 if(refreshheader instanceof refreshheader) {
 ((refreshheader) refreshheader)
 .onpositionchange(currenttargetoffsettop, lasttargetoffsettop, totaldragdistance, istouch,state);
 }
 }

而actionup的时候也不能单纯的让header回到顶部了,而是需要通过判断状态,回到刷新高度亦或是回到顶部。
1. 刷新状态,回到刷新高度
2. 否则,回到顶部
我们将原本在actionup中的autoscroll.scrollto(…)抽取成一个方法再调用,如下

private void finishspinner() {
 if (state == state.loading) {
 if (currenttargetoffsettop > totaldragdistance) {
 autoscroll.scrollto(totaldragdistance, scroll_to_refresh_duration);
 }
 } else {
 autoscroll.scrollto(start_position, scroll_to_top_duration);
 }
 }

好了,大功告成!在changestate方法中添加toast打印一下状态,来运行下!

toast.maketext(getcontext(), state.tostring(), toast.length_short).show();

别忘记在activity中调用refreshcomplete方法,我们延时三秒后设置刷新成功!
以下是activity中的调用:

final refreshlayout refreshlayout = (refreshlayout) findviewbyid(r.id.refreshlayout);
if (refreshlayout != null) {
 // 刷新状态的回调
 refreshlayout.setrefreshlistener(new refreshlayout.onrefreshlistener() {
 @override
 public void onrefresh() {
 // 延迟3秒后刷新成功
 refreshlayout.postdelayed(new runnable() {
 @override
 public void run() {
 refreshlayout.refreshcomplete();
 }
 }, 3000);
 }
 });
}

运行结果:我们演示几种情况
下拉 – >回到顶部 (pull –> reset)

Android实现支持所有View的通用的下拉刷新控件

下拉 –>刷新 –> 刷新成功 –> 回到顶部(pull–>loading–>complete–>reset)

Android实现支持所有View的通用的下拉刷新控件

下拉 –>刷新 –> 刷新成功 –> 回到顶部(手指按下,不让header回到顶部)

Android实现支持所有View的通用的下拉刷新控件

完全没有问题,体验还是可以的!这样我们就完成了一个下拉刷新控件了!

三、自定义默认的header

下拉刷新是弄好了,但是我们的header也太寒碜太敷衍了吧!
现在我们就来自定义一个header,包含一个旋转的箭头,还有文字提示!但是我不准备提供时间提示了~普通点,和qq一样的Android实现支持所有View的通用的下拉刷新控件

首先我们需要一些图片资源,从qq的apk解压获取到

Android实现支持所有View的通用的下拉刷新控件

先来定义几个旋转动画

rotate_down.xml

<rotate xmlns:android="http://schemas.android.com/apk/res/android"
 android:duration="150"
 android:fillafter="true"
 android:fromdegrees="-180"
 android:pivotx="50%"
 android:pivoty="50%"
 android:repeatcount="0"
 android:todegrees="0" />

rotate_up.xml

<rotate xmlns:android="http://schemas.android.com/apk/res/android"
 android:duration="150"
 android:fillafter="true"
 android:fromdegrees="0"
 android:pivotx="50%"
 android:pivoty="50%"
 android:todegrees="180" />

rotate_infinite.xml

<rotate xmlns:android="http://schemas.android.com/apk/res/android"
 android:duration="150"
 android:fillafter="true"
 android:fromdegrees="180"
 android:interpolator="@android:anim/linear_interpolator"
 android:pivotx="50%"
 android:pivoty="50%"
 android:repeatcount="0"
 android:todegrees="0" />

header代码如下

import android.content.context;
import android.util.attributeset;
import android.view.view;
import android.view.animation.animation;
import android.view.animation.animationutils;
import android.widget.framelayout;
import android.widget.textview;

/**
 * created by aitsuki on 2016/6/15.
 *
 */
public class qqrefreshheader extends framelayout implements refreshheader {


 private animation rotate_up;
 private animation rotate_down;
 private animation rotate_infinite;
 private textview textview;
 private view arrowicon;
 private view successicon;
 private view loadingicon;

 public qqrefreshheader(context context) {
 this(context, null);
 }

 public qqrefreshheader(context context, attributeset attrs) {
 super(context, attrs);

 // 初始化动画
 rotate_up = animationutils.loadanimation(context , r.anim.rotate_up);
 rotate_down = animationutils.loadanimation(context , r.anim.rotate_down);
 rotate_infinite = animationutils.loadanimation(context , r.anim.rotate_infinite);

 inflate(context, r.layout.header_qq, this);

 textview = (textview) findviewbyid(r.id.text);
 arrowicon = findviewbyid(r.id.arrowicon);
 successicon = findviewbyid(r.id.successicon);
 loadingicon = findviewbyid(r.id.loadingicon);
 }

 @override
 public void reset() {
 textview.settext(getresources().gettext(r.string.qq_header_reset));
 successicon.setvisibility(invisible);
 arrowicon.setvisibility(visible);
 arrowicon.clearanimation();
 loadingicon.setvisibility(invisible);
 loadingicon.clearanimation();
 }

 @override
 public void pull() {

 }

 @override
 public void refreshing() {
 arrowicon.setvisibility(invisible);
 loadingicon.setvisibility(visible);
 textview.settext(getresources().gettext(r.string.qq_header_refreshing));
 arrowicon.clearanimation();
 loadingicon.startanimation(rotate_infinite);
 }

 @override
 public void onpositionchange(float currentpos, float lastpos, float refreshpos, boolean istouch, state state) {
 // 往上拉
 if (currentpos < refreshpos && lastpos >= refreshpos) {
 if (istouch && state == state.pull) {
 textview.settext(getresources().gettext(r.string.qq_header_pull));
 arrowicon.clearanimation();
 arrowicon.startanimation(rotate_down);
 }
 // 往下拉
 } else if (currentpos > refreshpos && lastpos <= refreshpos) {
 if (istouch && state == state.pull) {
 textview.settext(getresources().gettext(r.string.qq_header_pull_over));
 arrowicon.clearanimation();
 arrowicon.startanimation(rotate_up);
 }
 }
 }

 @override
 public void complete() {
 loadingicon.setvisibility(invisible);
 loadingicon.clearanimation();
 successicon.setvisibility(visible);
 textview.settext(getresources().gettext(r.string.qq_header_completed));
 }
}

我们来看看运行结果,完美~

Android实现支持所有View的通用的下拉刷新控件

四、自动下拉刷新

是不是觉得还少了点什么?没错,就是自动刷新了!
很多时候,我们进入某个页面,初始化是需要自动刷新数据,这时候就需要用到自动刷新了,不需要用户手动。

分析:
1. 刷新状态都是在movespinner中变更的,而autoscroll正好是调用movespinner实现滚动
2. 我们可以调用autoscroll方法,让它滚动到刷新高度,然后再调用finishspinner方法,让控件进入loading状态
3. 自动刷新一般是在activity的oncreate的这个生命周期执行,此时界面可能还没有绘制完毕,可以通过postdelay方法延迟个几百毫秒,保证界面显示正常。
4. 而如果在postdelay的延迟时间中,用户如果点击了界面,我们应该将自动刷新功能移除。

首先我们定义公共方法:

 public void autorefresh() {
 autorefresh(500);
 }

 /**
 * 在oncreate中调用autorefresh,此时view可能还没有初始化好,需要延长一段时间执行。
 *
 * @param duration 延时执行的毫秒值
 */
 public void autorefresh(long duration) {
 if (state != state.reset) {
 return;
 }
 postdelayed(autorefreshrunnable, duration);
 }

runnable

 // 自动刷新,需要等view初始化完毕才调用,否则头部不会滚动出现
 private runnable autorefreshrunnable = new runnable() {
 @override
 public void run() {
 // 标记当前是自动刷新状态,finishscroll调用时需要判断
 // 在actiondown事件中重新标记为false
 isautorefresh = true;
 changestate(state.pull);
 autoscroll.scrollto(totaldragdistance, scroll_to_refresh_duration);
 }
 };

当autoscroll滚动结束的时候,会回调这个方法,判断如果是自动刷新,将状态设置为loading,并且调用finishspinner方法。

/**
 * 滚动结束回调
 *
 * @param isforcefinish 是否强制停止
 */
 private void onscrollfinish(boolean isforcefinish) {
 if (isautorefresh && !isforcefinish) {
 isautorefresh = false;
 changestate(state.loading);
 if (refreshlistener != null) {
 refreshlistener.onrefresh();
 }
 finishspinner();
 }
 }

搞定,在activity中调用
refreshlayout.autorefresh();

Android实现支持所有View的通用的下拉刷新控件

五、添加滑动阻力

目前还有个问题,控件可以无限下拉(多点触控),我们应该让阻力随着滑动距离的增大而逐渐增加,直到划不动为止。

我们可以用到这个方程

Android实现支持所有View的通用的下拉刷新控件

y是阻力,控制在0~1。
x是target偏移量超出刷新高度的百分比,控制在0~2。

代码如下,写在movespinnner中。

// y = x - (x/2)^2
float extraos = targety - totaldragdistance;
float slingshotdist = totaldragdistance;
float tensionslingshotpercent = math.max(0, math.min(extraos, slingshotdist * 2) / slingshotdist);
float tensionpercent = (float) (tensionslingshotpercent - math.pow(tensionslingshotpercent / 2, 2));

if(offset > 0) { // 下拉的时候才添加阻力
 offset = (int) (offset * (1f - tensionpercent));
 targety = math.max(0, currenttargetoffsettop + offset);
}

Android实现支持所有View的通用的下拉刷新控件

那么,一个体验还算不错的下拉刷新控件就这么完成了
部分代码参考自swiperefreshlayout和ultrapulltorefresh
这是demo下载地址:
https://github.com/aitsuki/custompulltorefresh

下一篇博文不出意外应该会实现listview和recycler的下拉刷新和加载更多的功能,主要特点就是,他们都可以直接使用本篇文中实现的qqheader。
出处:
本文出自:【aitsuki的博客】
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。