日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > Android >内容正文

Android

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

發(fā)布時(shí)間:2024/3/13 Android 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 android多个下拉控件,Android实现支持所有View的通用的下拉刷新控件 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

下拉刷新對于一個(gè)app來說是必不可少的一個(gè)功能,在早期大多數(shù)使用的是chrisbanes的PullToRefresh,或是修改自該框架的其他庫。而到現(xiàn)在已經(jīng)有了更多的選擇,github上還是有很多體驗(yàn)不錯(cuò)的下拉刷新。

而下拉刷新主要有兩種實(shí)現(xiàn)方式:1. 在ListView中添加header和footer,監(jiān)聽ListView的滑動(dòng)事件,動(dòng)態(tài)設(shè)置header/footer的高度,但是這種方式只適用于ListView,RecyclerView。

2. 第二種方式則是繼承ViewGroup或其子類,監(jiān)聽事件,通過scroll或Layout的方式移動(dòng)child。如圖(又分兩種情況)

Layout時(shí)將header放到屏幕外面,target則填充滿屏幕。這個(gè)也是SwipeRefreshLayout的實(shí)現(xiàn)原理(第二種,只下拉h(huán)eader)

這兩種(指的是繼承ListView或繼承ViewGroup)下拉刷新的實(shí)現(xiàn)方式主要有以下區(qū)別

而今天,我打算先講第二種方式實(shí)現(xiàn)方式,繼承ViewGroup,代碼可以直接參考SwipeRefreshLayout,或者pullToRefresh,或者ultra-pull-to-refresh

一、思考和需求

下拉刷新需要幾個(gè)狀態(tài):Reset–> Pull – > Refreshing – >Completed –>Reset

為了應(yīng)對各式各樣的下拉刷新設(shè)計(jì),我們應(yīng)該提供設(shè)置自定義的Header,開發(fā)者可以通過實(shí)現(xiàn)接口從而自定義自己的header。

而且header可以有兩種顯示方式,一種是只下拉h(huán)eader,另外一種則是header和target一起下拉。

二、著手實(shí)現(xiàn)代碼

2.1 定義Header的接口,創(chuàng)建自定義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 {

/**

* 松手,頭部隱藏后會(huì)回調(diào)這個(gè)方法

*/

void reset();

/**

* 下拉出頭部的一瞬間調(diào)用

*/

void pull();

/**

* 正在刷新的時(shí)候調(diào)用

*/

void refreshing();

/**

* 頭部滾動(dòng)的時(shí)候持續(xù)調(diào)用

* @param currentPos target當(dāng)前偏移高度

* @param lastPos target上一次的偏移高度

* @param refreshPos 可以松手刷新的高度

* @param isTouch 手指是否按下狀態(tài)(通過scroll自動(dòng)滾動(dòng)時(shí)需要判斷)

* @param state 當(dāng)前狀態(tài)

*/

void onPositionChange(float currentPos, float lastPos, float refreshPos, boolean isTouch, State state);

/**

* 刷新成功的時(shí)候調(diào)用

*/

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; // 是否已經(jīng)計(jì)算頭部高度

private int touchSlop;

private int headerHeight; // header高度

private int totalDragDistance; // 需要下拉這個(gè)距離才進(jìn)入松手刷新狀態(tài),默認(rèn)和header高度一致

public RefreshLayout(Context context) {

this(context, null);

}

public RefreshLayout(Context context, AttributeSet attrs) {

super(context, attrs);

touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

// 添加默認(rèn)的頭部,先簡單的用一個(gè)ImageView代替頭部

ImageView imageView = new ImageView(context);

imageView.setImageResource(R.drawable.one_piece);

imageView.setBackgroundColor(Color.BLACK);

setRefreshHeader(imageView);

}

/**

* 設(shè)置自定義header

*/

public void setRefreshHeader(View view) {

if (view != null && view != refreshHeader) {

removeView(refreshHeader);

// 為header添加默認(rèn)的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重復(fù)測量

hasMeasureHeader = true;

headerHeight = refreshHeader.getMeasuredHeight(); // header高度

totalDragDistance = headerHeight; // 需要pull這個(gè)距離才進(jìn)入松手刷新狀態(tài)

}

}

@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執(zhí)行的時(shí)候,要讓target和header加上偏移距離(初始0),因?yàn)橛锌赡茉跐L動(dòng)它們的時(shí)候,child請求重新布局,從而導(dǎo)致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);

}

/**

* 將第一個(gè)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中的布局如下,先用一個(gè)TextView作為Target

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

tools:context="com.aitsuki.custompulltorefresh.MainActivity">

android:layout_width="match_parent"

android:layout_height="match_parent">

android:layout_width="match_parent"

android:layout_height="match_parent"

android:text="Target"

android:textSize="30sp"

android:gravity="center"

android:background="#FFDAB9"

/>

運(yùn)行后結(jié)果如圖如下,但是我們還沒有監(jiān)聽事件,所以此時(shí)還無法滑動(dòng)。

2.2 處理事件分發(fā)

控件已經(jīng)測量布局好了,現(xiàn)在就開始處理事件分發(fā),對于事件分發(fā)還不了解的應(yīng)該先去復(fù)習(xí)下……

對于多點(diǎn)觸控的處理:記錄活動(dòng)手指的id(activePointerId),通過此ID獲取move事件的坐標(biāo)。

1.在手指按下的時(shí)候,記錄下activePointerId

2.第二根手指按下的時(shí)候,更新activePointerId。(我們讓第二根手指作為活動(dòng)手指,忽略第一個(gè)手指的move)

3.當(dāng)其中一根手指抬起時(shí),如果是第一根手指,那么不做處理,如果是第二根手指抬起,也就是活動(dòng)手指抬起的話,將活動(dòng)手指改回第一根。

對于事件分發(fā)一般有兩種處理方式1. 在onIntercept + onTouchEvnet中處理

2. 在dispatchTouchEvent中處理

在這里我選擇了第二種方式

首先了解DispatchTouchEvent返回值的含義重寫dispatchTouchEvent的時(shí)候,無論你是return true,亦或是return false都會(huì)導(dǎo)致child接受不到事件。

return true : 告訴parent,這個(gè)事件我消費(fèi)了。如果這個(gè)是down事件,那么我就會(huì)作為一個(gè)target或者說handle(事件持有者),后續(xù)的move事件或者up事件等,都會(huì)直接分發(fā)到我這里,不繼續(xù)往下分發(fā)。

return false:告訴parent,這個(gè)事件我不需要,那么會(huì)交回給parent的onTouchEvnet處理

只有return super.dispatchTouchEvent的時(shí)候才會(huì)將事件繼續(xù)往下傳遞。

上面只說了最簡單的一點(diǎn),如果對事件分發(fā)不了解的話需要看看,真的很重要。

分析

在dispatch中,即使child響應(yīng)了事件,我們也能拿到所有事件。

這樣我們就可以很簡單的控制頭部是否能下拉,那么如何攔截child的事件呢?

可以在合適的時(shí)候分發(fā)一個(gè)cancel事件給child,那么就相當(dāng)于攔截了!

雖然我們一直都響應(yīng)著事件,但肯定是不能所有事件都接收的,以下情況是需要我們處理的

1.如果是下拉,并且child不能往上滾動(dòng)

2.如果上劃,并且target不在頂部的時(shí)候

3.如果是這些時(shí)候,我們攔截child的事件(派發(fā)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(); // 當(dāng)前target偏移高度

initDownX = lastMotionX = ev.getX(0); // 手指按下時(shí)的坐標(biāo)

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下拉之前發(fā)送cancel事件給child

private void moveSpinner(float diff) {

int offset = Math.round(diff);

if (offset == 0) {

return;

}

// 發(fā)送cancel事件給child

if (!hasSendCancelEvent && isTouch && currentTargetOffsetTop > START_POSITION) {

sendCancelEvent();

hasSendCancelEvent = true;

}

int targetY = Math.max(0, currentTargetOffsetTop + offset); // target不能移動(dòng)到小于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);

}

代碼有點(diǎn)多,不過沒關(guān)系,其實(shí)很多都是從SwipeRefreshLayout中復(fù)制過來的。

我們來看看代碼運(yùn)行后的效果,很不錯(cuò),就是模擬器錄屏有點(diǎn)卡=。=

換成ListView試試, 也沒有問題。

多點(diǎn)觸控也是可以的,但是模擬器我沒法演示了。

2.3 添加自動(dòng)滾動(dòng)

頭雖然可以下拉了, 但是拉下來后就不會(huì)回去了啊,我們需要在手指松開讓頭部自動(dòng)回到原位。

可以使用動(dòng)畫,可以使用ValueAnimator計(jì)算距離移動(dòng),也可以使用Scroller計(jì)算距離移動(dòng)。

但是選擇第三種是比較好的,為什么呢。

首先如果使用動(dòng)畫,在回去的過程中我們無法下拉,我們想做的是一個(gè)可以在任何時(shí)候都能上下拉的,就像ListView添加頭的哪種效果。

valueAnimator也是,不好停止。

但是scroller卻可以使用forceFinish強(qiáng)行停止計(jì)算。

松開手指時(shí),我們通過scroller計(jì)算每次移動(dòng)的offset,然后調(diào)用moveSpinner即可。

在手指按下的時(shí)候,需要停止scroller。

我們先寫一個(gè)內(nèi)部類,封裝一下滾動(dòng)功能

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); // 調(diào)用此方法移動(dòng)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;

}

}

然后這個(gè)是回調(diào),暫時(shí)用戶不上,但還是先寫好吧。

/**

* 在scroll結(jié)束的時(shí)候會(huì)回調(diào)這個(gè)方法

* @param isForceFinish 是否是強(qiáng)制結(jié)束的

*/

private void onScrollFinish(boolean isForceFinish) {

}

我們在構(gòu)造中初始化AutoScroll,然后分別在ActionDown和ActionUp中分別調(diào)用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);

}

//...

運(yùn)行效果如下圖

2.4 添加刷新狀態(tài)

最開始的時(shí)候我們也新建了一個(gè)枚舉,設(shè)置了幾種狀態(tài),分別是 RESET, PULL, LOADING, COMPLETE

而我們的初始狀態(tài)應(yīng)該為RESET

private State state = State.RESET;

再分析一下,這幾種狀態(tài)什么時(shí)候互相切換:

1. 在RESET狀態(tài)時(shí),第一次下拉出現(xiàn)header的時(shí)候,設(shè)置狀態(tài)變成PULL

2. 在PULL或者COMPLETE狀態(tài)時(shí),header回到頂部的時(shí)候,狀態(tài)變回RESET

3. 如果是從底部回到頂部的過程(往上滾動(dòng)),并且手指是松開狀態(tài), 并且當(dāng)前是PULL狀態(tài),狀態(tài)變成LOADING,這時(shí)候我們需要強(qiáng)制停止autoScroll。并且正在刷新中的偵聽器也在這里調(diào)用(onRefresh())

4. 在LOADING狀態(tài)中,想變成其他狀態(tài),需要提供公共方法給外部調(diào)用

首先,我們先寫一個(gè)改變狀態(tài)的方法,在狀態(tài)改變的同時(shí)要回調(diào)給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;

}

}

}

還有,提供外部設(shè)置刷新成功的方法。

因?yàn)樗⑿鲁晒笮枰獙eader滾動(dòng)回原位,所以需要做以下判斷

1. 如果已經(jīng)在原位,那么直接將狀態(tài)改成Reset

2. 如果不在原位,延時(shí)500毫秒后自動(dòng)滾動(dòng)回原位。這里延時(shí)500毫秒是為了展示刷新成功的提示,否則在網(wǎng)速很快的情況下,刷新成功后header立即回到原位體驗(yàn)性不好,感覺就像是下拉后立即就自動(dòng)回去了。

3. 在自動(dòng)回滾時(shí)還需要判斷當(dāng)前手指是否在觸摸狀態(tài),如果正在觸摸,代表用戶可能并不想header回去,所以這時(shí)候我們不能讓頭部滾動(dòng)。

4. 再者就是,如果在延時(shí)的500內(nèi),用戶按下了手指,我們需要將這個(gè)runnable取消,在ActionDown中RemoveCallBack即可??偟膩碚f一句話就是,用戶必須持有header的絕對控制權(quán),在手指按下時(shí),header不應(yīng)該出現(xiàn)自動(dòng)滾動(dòng)的情況。

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成功狀態(tài)再滾動(dòng)回頂部,這個(gè)runnalbe需要在ActionDown事件中Remove

private Runnable delayToScrollTopRunnable = new Runnable() {

@Override

public void run() {

autoScroll.scrollTo(START_POSITION, SCROLL_TO_TOP_DURATION);

}

};

提供設(shè)置正在刷新回調(diào)的方法

當(dāng)用戶松開手指,進(jìn)入刷新狀態(tài)時(shí)我們需要回調(diào)這個(gè)方法。

// 定義一個(gè)偵聽器

public interface OnRefreshListener {

void onRefresh();

}

// 提供外部設(shè)置方法

public void setRefreshListener(OnRefreshListener refreshListener) {

this.refreshListener = refreshListener;

}

做完以上幾部,我們算是完成了LOADING到COMPLETE的狀態(tài)切換,余下的幾個(gè)狀態(tài)我們則需要在movespinner這個(gè)方法中控制,上面也已經(jīng)分析過了邏輯,那么可以直接看代碼了。

private void moveSpinner(float diff) {

int offset = Math.round(diff);

if (offset == 0) {

return;

}

// 發(fā)送cancel事件給child

if (!hasSendCancelEvent && isTouch && currentTargetOffsetTop > START_POSITION) {

sendCancelEvent();

hasSendCancelEvent = true;

}

int targetY = Math.max(0, currentTargetOffsetTop + offset); // target不能移動(dòng)到小于0的位置……

offset = targetY - currentTargetOffsetTop;

// 1. 在RESET狀態(tài)時(shí),第一次下拉出現(xiàn)header的時(shí)候,設(shè)置狀態(tài)變成PULL

if (state == State.RESET && currentTargetOffsetTop == START_POSITION && targetY > 0) {

changeState(State.PULL);

}

// 2. 在PULL或者COMPLETE狀態(tài)時(shí),header回到頂部的時(shí)候,狀態(tài)變回RESET

if (currentTargetOffsetTop > START_POSITION && targetY <= START_POSITION) {

if (state == State.PULL || state == State.COMPLETE) {

changeState(State.RESET);

}

}

// 3. 如果是從底部回到頂部的過程(往上滾動(dòng)),并且手指是松開狀態(tài), 并且當(dāng)前是PULL狀態(tài),狀態(tài)變成LOADING,這時(shí)候我們需要強(qiáng)制停止autoScroll

if (state == State.PULL && !isTouch && currentTargetOffsetTop > totalDragDistance && targetY <= totalDragDistance) {

autoScroll.stop();

changeState(State.LOADING);

if (refreshListener != null) {

refreshListener.onRefresh();

}

// 因?yàn)榕袛鄺l件targetY <= totalDragDistance,會(huì)導(dǎo)致不能回到正確的刷新高度(有那么一丁點(diǎn)偏差),調(diào)整change

int adjustOffset = totalDragDistance - targetY;

offset += adjustOffset;

}

setTargetOffsetTopAndBottom(offset);

// 別忘了回調(diào)header的位置改變方法。

if(refreshHeader instanceof RefreshHeader) {

((RefreshHeader) refreshHeader)

.onPositionChange(currentTargetOffsetTop, lastTargetOffsetTop, totalDragDistance, isTouch,state);

}

}

而ActionUp的時(shí)候也不能單純的讓header回到頂部了,而是需要通過判斷狀態(tài),回到刷新高度亦或是回到頂部。

1. 刷新狀態(tài),回到刷新高度

2. 否則,回到頂部

我們將原本在ActionUp中的autoScroll.scrollto(…)抽取成一個(gè)方法再調(diào)用,如下

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打印一下狀態(tài),來運(yùn)行下!

Toast.makeText(getContext(), state.toString(), Toast.LENGTH_SHORT).show();

別忘記在Activity中調(diào)用refreshComplete方法,我們延時(shí)三秒后設(shè)置刷新成功!

以下是Activity中的調(diào)用:

final RefreshLayout refreshLayout = (RefreshLayout) findViewById(R.id.refreshLayout);

if (refreshLayout != null) {

// 刷新狀態(tài)的回調(diào)

refreshLayout.setRefreshListener(new RefreshLayout.OnRefreshListener() {

@Override

public void onRefresh() {

// 延遲3秒后刷新成功

refreshLayout.postDelayed(new Runnable() {

@Override

public void run() {

refreshLayout.refreshComplete();

}

}, 3000);

}

});

}

運(yùn)行結(jié)果:我們演示幾種情況

下拉 – >回到頂部 (pull –> reset)

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

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

完全沒有問題,體驗(yàn)還是可以的!這樣我們就完成了一個(gè)下拉刷新控件了!

三、自定義默認(rèn)的Header

下拉刷新是弄好了,但是我們的header也太寒磣太敷衍了吧!

現(xiàn)在我們就來自定義一個(gè)header,包含一個(gè)旋轉(zhuǎn)的箭頭,還有文字提示!但是我不準(zhǔn)備提供時(shí)間提示了~普通點(diǎn),和QQ一樣的

首先我們需要一些圖片資源,從QQ的apk解壓獲取到

先來定義幾個(gè)旋轉(zhuǎn)動(dòng)畫

rotate_down.xml

android:duration="150"

android:fillAfter="true"

android:fromDegrees="-180"

android:pivotX="50%"

android:pivotY="50%"

android:repeatCount="0"

android:toDegrees="0" />

rotate_up.xml

android:duration="150"

android:fillAfter="true"

android:fromDegrees="0"

android:pivotX="50%"

android:pivotY="50%"

android:toDegrees="180" />

rotate_infinite.xml

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

// 初始化動(dòng)畫

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

}

}

我們來看看運(yùn)行結(jié)果,完美~

四、自動(dòng)下拉刷新

是不是覺得還少了點(diǎn)什么?沒錯(cuò),就是自動(dòng)刷新了!

很多時(shí)候,我們進(jìn)入某個(gè)頁面,初始化是需要自動(dòng)刷新數(shù)據(jù),這時(shí)候就需要用到自動(dòng)刷新了,不需要用戶手動(dòng)。

分析:

1. 刷新狀態(tài)都是在moveSpinner中變更的,而autoScroll正好是調(diào)用moveSpinner實(shí)現(xiàn)滾動(dòng)

2. 我們可以調(diào)用autoScroll方法,讓它滾動(dòng)到刷新高度,然后再調(diào)用finishSpinner方法,讓控件進(jìn)入Loading狀態(tài)

3. 自動(dòng)刷新一般是在Activity的onCreate的這個(gè)生命周期執(zhí)行,此時(shí)界面可能還沒有繪制完畢,可以通過postDelay方法延遲個(gè)幾百毫秒,保證界面顯示正常。

4. 而如果在postDelay的延遲時(shí)間中,用戶如果點(diǎn)擊了界面,我們應(yīng)該將自動(dòng)刷新功能移除。

首先我們定義公共方法:

public void autoRefresh() {

autoRefresh(500);

}

/**

* 在onCreate中調(diào)用autoRefresh,此時(shí)View可能還沒有初始化好,需要延長一段時(shí)間執(zhí)行。

*

* @param duration 延時(shí)執(zhí)行的毫秒值

*/

public void autoRefresh(long duration) {

if (state != State.RESET) {

return;

}

postDelayed(autoRefreshRunnable, duration);

}

runnable

// 自動(dòng)刷新,需要等View初始化完畢才調(diào)用,否則頭部不會(huì)滾動(dòng)出現(xiàn)

private Runnable autoRefreshRunnable = new Runnable() {

@Override

public void run() {

// 標(biāo)記當(dāng)前是自動(dòng)刷新狀態(tài),finishScroll調(diào)用時(shí)需要判斷

// 在actionDown事件中重新標(biāo)記為false

isAutoRefresh = true;

changeState(State.PULL);

autoScroll.scrollTo(totalDragDistance, SCROLL_TO_REFRESH_DURATION);

}

};

當(dāng)autoScroll滾動(dòng)結(jié)束的時(shí)候,會(huì)回調(diào)這個(gè)方法,判斷如果是自動(dòng)刷新,將狀態(tài)設(shè)置為Loading,并且調(diào)用finishSpinner方法。

/**

* 滾動(dòng)結(jié)束回調(diào)

*

* @param isForceFinish 是否強(qiáng)制停止

*/

private void onScrollFinish(boolean isForceFinish) {

if (isAutoRefresh && !isForceFinish) {

isAutoRefresh = false;

changeState(State.LOADING);

if (refreshListener != null) {

refreshListener.onRefresh();

}

finishSpinner();

}

}

搞定,在Activity中調(diào)用

refreshLayout.autoRefresh();

五、添加滑動(dòng)阻力

目前還有個(gè)問題,控件可以無限下拉(多點(diǎn)觸控),我們應(yīng)該讓阻力隨著滑動(dòng)距離的增大而逐漸增加,直到劃不動(dòng)為止。

我們可以用到這個(gè)方程

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) { // 下拉的時(shí)候才添加阻力

offset = (int) (offset * (1f - tensionPercent));

targetY = Math.max(0, currentTargetOffsetTop + offset);

}

那么,一個(gè)體驗(yàn)還算不錯(cuò)的下拉刷新控件就這么完成了

部分代碼參考自SwipeRefreshLayout和UltraPullToRefresh

這是Demo下載地址:

https://github.com/AItsuki/CustomPullToRefresh

下一篇博文不出意外應(yīng)該會(huì)實(shí)現(xiàn)ListView和Recycler的下拉刷新和加載更多的功能,主要特點(diǎn)就是,他們都可以直接使用本篇文中實(shí)現(xiàn)的QQheader。

出處: http://blog.csdn.net/u010386612/article/details/51372696

本文出自:【AItsuki的博客】

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

總結(jié)

以上是生活随笔為你收集整理的android多个下拉控件,Android实现支持所有View的通用的下拉刷新控件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。