转载请标明出处:
http://blog.csdn.net/u010386612/article/details/51372696
本文出自:【AItsuki的博客】

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

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

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

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

继承ListView/RecyclerView 继承ViewGroup或其子类
适用范围 ListView/Recycler 理论支持所有View和ViewGroup
加载更多 实现简单,体验好 可以实现,看需求了,做不出ListView那种加载效果的,体验比较一般
多点触控 可以完美支持 header下拉状态中是完美支持的,但是回去之后,很难将多点触控事件传递给child
案例 QQ好友列表 美团、京东等

而今天,我打算先讲第二种方式实现方式,继承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添加默认的layoutParamsViewGroup.LayoutParams layoutParams = view.getLayoutParams();if (layoutParams == null) {layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);view.setLayoutParams(layoutParams);}refreshHeader = view;addView(refreshHeader);}}@Overrideprotected 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这个距离才进入松手刷新状态}}@Overrideprotected 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>

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

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事件)

代码如下

@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {if (!isEnabled() || target == null) {return super.dispatchTouchEvent(ev);}final int actionMasked = ev.getActionMasked(); // support Multi-touchswitch (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事件给childif (!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中复制过来的。
我们来看看代码运行后的效果,很不错,就是模拟器录屏有点卡=。=

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

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

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());}@Overridepublic void run() {boolean finished = !scroller.computeScrollOffset() || scroller.isFinished();if (!finished) {int currY = scroller.getCurrY(); int offset = currY - lastY;lastY = currY;moveSpinner(offset); // 调用此方法移动header和targetpost(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);}//...

运行效果如下图

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 runnableif (!isTouch) {postDelayed(delayToScrollTopRunnable, SHOW_COMPLETED_TIME);}}
}// 刷新成功,显示500ms成功状态再滚动回顶部,这个runnalbe需要在ActionDown事件中Remove
private Runnable delayToScrollTopRunnable = new Runnable() {@Overridepublic 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事件给childif (!hasSendCancelEvent && isTouch && currentTargetOffsetTop > START_POSITION) {sendCancelEvent();hasSendCancelEvent = true;}int targetY = Math.max(0, currentTargetOffsetTop + offset); // target不能移动到小于0的位置……offset = targetY - currentTargetOffsetTop;// 1. 在RESET状态时,第一次下拉出现header的时候,设置状态变成PULLif (state == State.RESET && currentTargetOffsetTop == START_POSITION && targetY > 0) {changeState(State.PULL);}// 2. 在PULL或者COMPLETE状态时,header回到顶部的时候,状态变回RESETif (currentTargetOffsetTop > START_POSITION && targetY <= START_POSITION) {if (state == State.PULL || state == State.COMPLETE) {changeState(State.RESET);}}// 3. 如果是从底部回到顶部的过程(往上滚动),并且手指是松开状态, 并且当前是PULL状态,状态变成LOADING,这时候我们需要强制停止autoScrollif (state == State.PULL && !isTouch && currentTargetOffsetTop > totalDragDistance && targetY <= totalDragDistance) {autoScroll.stop();changeState(State.LOADING);if (refreshListener != null) {refreshListener.onRefresh();}// 因为判断条件targetY <= totalDragDistance,会导致不能回到正确的刷新高度(有那么一丁点偏差),调整changeint 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() {@Overridepublic void onRefresh() {// 延迟3秒后刷新成功refreshLayout.postDelayed(new Runnable() {@Overridepublic void run() {refreshLayout.refreshComplete();}}, 3000);}});
}

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

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

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

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

三、自定义默认的Header

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

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

先来定义几个旋转动画

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);}@Overridepublic void reset() {textView.setText(getResources().getText(R.string.qq_header_reset));successIcon.setVisibility(INVISIBLE);arrowIcon.setVisibility(VISIBLE);arrowIcon.clearAnimation();loadingIcon.setVisibility(INVISIBLE);loadingIcon.clearAnimation();}@Overridepublic void pull() {}@Overridepublic void refreshing() {arrowIcon.setVisibility(INVISIBLE);loadingIcon.setVisibility(VISIBLE);textView.setText(getResources().getText(R.string.qq_header_refreshing));arrowIcon.clearAnimation();loadingIcon.startAnimation(rotate_infinite);}@Overridepublic 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);}}}@Overridepublic void complete() {loadingIcon.setVisibility(INVISIBLE);loadingIcon.clearAnimation();successIcon.setVisibility(VISIBLE);textView.setText(getResources().getText(R.string.qq_header_completed));}
}

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

四、自动下拉刷新

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

分析:
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() {@Overridepublic void run() {// 标记当前是自动刷新状态,finishScroll调用时需要判断// 在actionDown事件中重新标记为falseisAutoRefresh = 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();

五、添加滑动阻力

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

我们可以用到这个方程

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);
}

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

下一篇博文不出意外应该会实现ListView和Recycler的下拉刷新和加载更多的功能,主要特点就是,他们都可以直接使用本篇博文中实现的QQheader。

Android 怎么实现支持所有View的通用的下拉刷新控件相关推荐

  1. android多个下拉控件,Android实现支持所有View的通用的下拉刷新控件

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

  2. Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件

    前言: 因为公司人员变动原因,导致了博主四个月没有动安卓,一直在做IOS开发,如今接近年前,终于可以花一定的时间放在安卓上了.好了,废话不多说,今天我们要带来的效果是苹果版本的QQ下拉刷新.首先看一下 ...

  3. Android自定义控件实战——下拉刷新控件终结者:PullToRefreshLayout

    说到下拉刷新控件,网上版本有很多,很多软件也都有下拉刷新功能.有一个叫XListView的,我看别人用过,没看过是咋实现的,看这名字估计是继承自ListView修改的,不过效果看起来挺丑的,也没什么扩 ...

  4. android禁止下拉刷新,Android开发之无痕过渡下拉刷新控件的实现思路详解

    相信大家已经对下拉刷新熟悉得不能再熟悉了,市面上的下拉刷新琳琅满目,然而有很多在我看来略有缺陷,接下来我将说明一下存在的缺陷问题,然后提供一种思路来解决这一缺陷,废话不多说!往下看嘞! 1.市面一些下 ...

  5. Android SwipeRefreshLayout下拉刷新控件源码简单分析

    咱们在做Android APP开发的时候经常碰到有下拉刷新和上拉加载跟多的需求,这篇文章咱们先说说下来刷新,咱们就以google的原生的下拉刷新控件SwipeRefreshLayout来看看大概的实现 ...

  6. android webview 下拉刷新页面,Android 下拉刷新控件SwipeRefreshLayout结合WebView使用

    SwipeRefreshLayout 是谷歌官方下拉刷新控件,4.0如下的版本须要用到 android-support-v4.jar包才能用到html android-support-v4.jar 包 ...

  7. Android 解决下拉刷新控件和ScrollVIew的滑动冲突问题。

    最近项目要实现ScrollView中嵌套广告轮播图+RecyleView卡片布局,并且RecyleView按照header和内容的排列样式,因为RecyleView的可扩展性很强,所以我毫无疑问的选择 ...

  8. android google 下拉刷新 csdn,android SwipeRefreshLayout google官方下拉刷新控件

    下拉刷新功能之前一直使用的是XlistView很方便我前面的博客有介绍 SwipeRefreshLayout是google官方推出的下拉刷新控件使用方法也比较简单 今天就来使用下SwipeRefres ...

  9. Android SwipeRefreshLayout 官方下拉刷新控件介绍

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24521483 下面App基本都有下拉刷新的功能,以前基本都使用XListView ...

最新文章

  1. ExtJS 的工具条及菜单
  2. 环境变量PATH 、mv 命令 、cp 命令和 查看文档相关的命令
  3. CXF客户端配置请求超时限制-SocketTimeoutException(Spring配置文件中配置和通过代码进行配置)
  4. 发布HTML 7-3
  5. Flink 1.7.2 dataset transformation 示例
  6. domino流程图_OA实施工程师一线解读Java与Domino优劣
  7. 如何更快获取想要的设计资源?
  8. Strtus2入门简单框架搭建
  9. Table View Programming Guide for iOS 官方文档翻译
  10. 飞鱼星路由器如何限制外网访问服务器网站,飞鱼星路由器如何限制禁止指定IP上网...
  11. sd卡怎么格式化?5个步骤轻松教会你
  12. 数据库期末复习:选择题汇总
  13. java espresso_java-Espresso-如何检查是否显示了其中一个视图
  14. Apple, Steve Jobs, iCon
  15. 申报绿色工厂对企业有什么好处
  16. 其实性格决定命运,细节决定成败,知识改变命运,学习成就未来,观念决定行动,思维决定出路,态度决定一切
  17. 牛客网 KY6 手机键盘
  18. 3.1 Web前端:实战电商页面1:静态布局
  19. 最后一次作业-- 总结报告
  20. 控制bing搜索爬取的方法

热门文章

  1. h5 苹果IOS端 播放mp3 没声音
  2. NPN和PNP 的电流方向 、大小关系 、电压偏置
  3. 开学季征文 | 百尺竿头,我们都要更进一步
  4. 数据结构和算法学习指南
  5. android10开发io接口,Android Things:外设I/O接口-GPIO
  6. nmp i报错git --no-replace-objects ls-remote
  7. 【mac】【转发】Mac系统升级后,按大小写键没反应了,切换大小写的灯不亮了
  8. USB 协议 (三) 基础知识
  9. APP安装与卸载测试点
  10. html怎样写出x的平方,x的平方怎么打出来