第一次嘗試寫一個下拉刷新控件,一開始的目的只是想了解dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent這幾個事件的分別,沒想到最後居然寫了一個刷新控件。好的,廢話很少說,先來看看效果圖: git
首先,咱們仍是須要搞清楚我上面說的這三個事件:github
咱們先來理一理下拉刷新的邏輯。首先若是用戶手指向上滑動,咱們不須要進行事件的攔截,交給子View處理。若是用戶手指是向下滑動的時候就要進行處理了。首先要看看刷新控件中的子View能不能繼續向上滾動,也就是說子View有沒有滾動到頂,若是到頂了,用戶繼續向下滑動的話就開始顯示頭部的刷新視圖。至因而到頂後,拿開手指後再下拉顯示刷新視圖仍是到頂後直接繼續下拉就能夠顯示刷新視圖就看項目須要了。我是實現的前者。而後就是顯示刷新視圖後,用戶下拉多少,頂部就有多少留白,而後提示文字始終在留白的最中間位置。下拉到必定位置提示鬆手開始刷新。當下拉到最大距離,留白再也不加大。最後鬆手,留白減小,而且提示正在刷新,最後提示刷新結果。bash
首先我定義了一些變量來記錄一些須要用到的值,變量說明都在註釋中:ide
// 每次觸摸事件中第一次接觸屏幕的Y座標
private float downY;
// 手指在Y軸的滑動距離
private float dY;
// 在刷新佈局中的子View
private View mTarget;
// 最大Y軸滑動距離
private float maxDY = 300;
// 頭部View
private View headerView;
// 下拉開始時顯示的文字
private String readyText = "下拉開始刷新";
// 下拉到觸發刷新的下拉距離以後的提示文字
private String refreshOkText = "鬆開開始刷新";
// 正在刷新時候提醒的文字
private String refreshingText = "正在刷新";
// 刷新成功的提示文字
private String refreshSuc = "刷新成功!";
// 刷新失敗提醒的文字
private String refreshFail = "刷新失敗!";
// 觸發刷新的距離
private float refreshDist = maxDY / 2;
// 滑動多少距離纔算是滑動,不然有時候是點擊也會誤觸發滑動
private int minDist;
// 是否觸發了刷新
private boolean canRefresh = false;
// 正在刷新?
private boolean isRefreshing = false;
// 控件狀態監聽
private RefreshStateListender listener;
// 狀態表示代碼
private final int READY_REFRESH = 0; // 剛開始下拉時候的狀態
private final int CAN_REFRESH = 1; // 已經能夠觸發刷新的狀態
private final int ON_REFRESH = 2; // 正在刷新的狀態
private final int ON_FINISH = 3; // 刷新完成的狀態
複製代碼
接着咱們就要來處理一下事件的攔截,從咱們上面理好的邏輯中知道,咱們主要處理用戶下拉手勢。首先咱們就應當知道用戶究竟是在上拉仍是在下拉。個人作法是,當用戶第一次觸摸到屏幕的時候,我記錄下這個點的Y軸位置爲初始Y軸位置,而後在用戶的滑動過程當中,獲取滑動的點的Y軸位置減去初始Y軸位置。若是結果爲負數,表明是上拉,若是是正數就表明下拉而後對事件進行攔截。可是,我在實現過程當中發現不能經過判斷正負攔截,由於點擊也是屬於touch事件的一種,可是你不能確保在用戶的點擊過程當中會發生一點點的滑動,這樣就會形成子View的點擊事件也可能會被攔截。所以Android提供了一個值,滑動距離小於這個值會被系統認爲是點擊,大於這個值系統會認爲這是滑動。根據ROM的不一樣,這個值也會不一樣。就像上面代碼中,我用minDist
這個變量將值保存下來,獲取這個值的方法是:minDist = ViewConfiguration.get(context).getScaledTouchSlop()
。而後咱們經過判斷滑動的點的Y軸位置減去初始Y軸位置是否大於minDist來判斷上拉仍是下拉。最後說一下,Android好像提供了判斷用戶是上拉仍是下拉的方法,我還沒去研究,暫時先這樣處理。還一個問題,咱們怎麼知道子View是否滑動到了頂部呢?我爲此特地看了一下Android中SwipeRefreshLayout的源碼,其中有一串代碼以下:佈局
private boolean canChildScrollUp() {
return this.mTarget instanceof ListView ? ListViewCompat.canScrollList((ListView)this.mTarget, -1) : this.mTarget.canScrollVertically(-1);
}
複製代碼
這串代碼就是判斷子View是否能向上滾動,源碼中還有一層我沒摘錄下來,我就以爲這段對我有用。 而後,咱們還須要把子View保存下來,否則this.mTarget
就是空指針,代碼以下(SwipeRefreshLayout也是相似作法):測試
private void ensureTarget() {
if (this.mTarget == null) {
final int count = this.getChildCount();
for (int i = 0; i < count; i++) {
View childView = getChildAt(i);
if (!headerView.equals(childView)) {
this.mTarget = childView;
if (mTarget.getBackground() == null) {
mTarget.setBackgroundColor(Color.WHITE);
}
}
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
this.ensureTarget();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
this.ensureTarget();
}
複製代碼
截下來攔截下來的事件的處理了,從這一段開頭理的邏輯中知道,下拉到必定位置才能觸發刷新,若是處於刷新狀態再下拉什麼的就所有由子View處理:ui
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_MOVE) {
dY = event.getY() - downY;
if ((dY >= minDist && dY <= maxDY) && !isRefreshing) { // 若是沒有正在刷新而且是下拉狀態,而且沒有超過最大下拉距離
mTarget.setTranslationY(dY);
if (dY > headerView.getMeasuredHeight()) { // 下拉距離超過headerView的高度,headerView在Y軸就要開始移動
headerView.setTranslationY((dY - headerView.getMeasuredHeight()) / 2);
}
if (dY > refreshDist) { // 已經到了能夠觸發刷新的下拉距離
configHeaderView(refreshOkText, CAN_REFRESH);
canRefresh = true;
} else { // 已經下拉可是還沒到能夠觸發刷新的距離
configHeaderView(readyText, READY_REFRESH);
canRefresh = false;
}
}
} else if (action == MotionEvent.ACTION_UP) { // 鬆手觸發刷新
if (dY > maxDY) {
dY = maxDY;
}
if (dY > minDist) {
if (!canRefresh && !isRefreshing) { // 若是還不能觸發刷新而且沒有正在刷新,鬆手的話就回彈回去
ObjectAnimator.ofFloat(mTarget, "translationY", dY, 0).setDuration(500).start();
ObjectAnimator.ofFloat(headerView, "translationY",
(dY - headerView.getMeasuredHeight()) / 2, 0)
.setDuration(500).start();
} else if (!isRefreshing){ // 若是已經能觸發刷新而且沒有正在刷新,鬆手的話就回彈到最大距離的一半而且提示正在刷新
ObjectAnimator.ofFloat(mTarget, "translationY", dY, refreshDist).setDuration(500).start();
ObjectAnimator animator = ObjectAnimator.ofFloat(headerView, "translationY",
(dY - headerView.getMeasuredHeight()) / 2, (refreshDist - headerView.getMeasuredHeight()) / 2)
.setDuration(500);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
configHeaderView(refreshingText, ON_REFRESH);
isRefreshing = true;
canRefresh = false;
}
});
animator.start();
}
}
dY = 0;
}
return true;
}
複製代碼
最後,對外提供刷新完成的接口:this
public void refreshFinish(boolean suc) {
if (suc) {
configHeaderView(refreshSuc, ON_FINISH, suc);
} else {
configHeaderView(refreshFail, ON_FINISH, suc);
}
// 刷新完成,提示刷新結果後
ObjectAnimator animator1 = ObjectAnimator.ofFloat(mTarget, "translationY", refreshDist, 0);
animator1.setDuration(200).setStartDelay(500);
animator1.start();
ObjectAnimator animator2 = ObjectAnimator.ofFloat(headerView, "translationY",
(refreshDist - headerView.getMeasuredHeight()) / 2, 0);
animator2.setDuration(200).setStartDelay(500);
animator2.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isRefreshing = false;
canRefresh = false;
}
});
animator2.start();
}
複製代碼
還有一些設置刷新監聽的代碼並無放到本文中講解,這一部分感受很簡單,能夠到源碼中查看更多詳細內容,註釋也都很詳細。源碼地址:github.com/cyixlq/View…spa