本文主要是基于ViewGroup來實作平滑下拉和彈性回歸的PTR控件
總體思想
根布局選擇ViewGroup,添加header、body、footer三個子元素。通過重寫onMeasure、onLayout來确定控件大小以及位置,通過dispatchTouchEvent來控制事件傳遞以及滑動,通過Scroller和computeScroll來實作彈性回歸
本文僅從下拉重新整理的角度分析
待解決的問題
- header的滑動
- 彈性滑動
- 解決滑動沖突
ViewGroup的選擇
本文沒有通過繼承已有的ViewGroup實作類,而是直接繼承ViewGroup來實作PTR控件的,這樣有利于控件的擴充以及個性化
繼承ViewGroup需要重寫幾個重要的方法。
onMeasure
确定自身以及每個子元素的大小
onLayout
确定每個子元素的位置
dispatchTouchEvent
擷取事件,來控制滑動
computeScroll
配合Scroller實作彈性滑動
事件分發
事件分發基本原則僞代碼
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if (onInterceptTouchEvent(event)) {
consume = onTouchEvent(event);
}else {
consume = child.dispatchTouchEvent(event);
}
return consume;
}
想要控制自身的滑動,就必須擷取到所有的move事件,同時,不能影響子元素有效的事件接收。其中很顯然onInterceptTouchEvent以及onTouchEvent不一定會執行到(onInterceptTouchEvent是攔截一次後,改事件序列的後續事件都會被攔截,進而不會觸發onInterceptTouchEvent的調用),是以本文選擇的是dispatchTouchEvent
View滑動
- scrollTo
- scrollBy
以上兩種方式都可以實作View的滑動,但是是瞬間的滑動。
如何解決彈性滑動?
彈性滑動其實就是把長距離的滑動分割為若幹個短距離的連續滑動,基于這種思想,我們可以在move事件時來完成短距離的滑動,進而達到平滑滑動的效果;在header回彈時也是基于這種分割的思想
具體實作
測量定位
重寫onMeasure方法來确定大小,為後續定位也做準備
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
// 增加高度,本文僅添加header的高度
int height = MeasureSpec.makeMeasureSpec(size + MAX_HEADER_SIZE, mode);
// 設定ViewGroupz自身的高度
super.onMeasure(widthMeasureSpec, height);
int headerSpec = MeasureSpec.makeMeasureSpec(MAX_HEADER_SIZE, mode);
// 設定header的高度
header.measure(widthMeasureSpec, headerSpec);
// 設定body的高度
body.measure(widthMeasureSpec, heightMeasureSpec);
}
重寫onLayout方法來确定位置,同時需要将Headery隐藏,相當于設定header的marginTop屬性
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int measuredHeight = getMeasuredHeight();
// 相當于設定header margin = -MAX_HEADER_SIZE
header.layout(l + paddingLeft, -MAX_HEADER_SIZE, r - paddingRight, 0);
body.layout(l + paddingLeft, paddingTop, r - paddingRight,
measuredHeight - paddingBottom);
}
事件消費和傳遞
重寫dispatchTouchEvent事件來捕捉move事件,由于有自己來控制子元素的事件接收,是以重寫了
onInterceptTouchEvent
方法,傳回true,攔截所有的事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
在dispatchTouchEvent方法中完成滑動,具體了以參考:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int scrollY = getScrollY();
float rawY = event.getRawY();
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastMovePoint = mTouchDownPoint = rawY;
isDragging = true;
break;
case MotionEvent.ACTION_MOVE:
// body控件是否能夠向下滑動
boolean canScrollVertically = mBodyView.getView().canScrollVertically(-1);
float v = rawY - mLastMovePoint;
if (v > 0) {
mDirection = ORIENTATION_DOWN;
} else {
mDirection = ORIENTATION_UP;
}
// 觸發可識别的最小滑動距離
if (Math.abs(rawY - mTouchDownPoint) > mTouchSlot) {
if (mDirection == ORIENTATION_UP) {
if (scrollY < 0) {
boolean reachTop = scrollY + Math.abs(v) >= 0;
float dis = reachTop ? -scrollY : Math.abs(v);
scrollBy(0, (int) dis);
mLastMovePoint = rawY;
isDragging = true;
if (reachTop) {
// 重新定位Body的DownEvent,防止跳躍性的滑動
MotionEvent obtain = MotionEvent.obtain(event);
obtain.setAction(MotionEvent.ACTION_DOWN);
dispatchEventToBody(obtain);
}
return true;
}
} else if (mDirection == ORIENTATION_DOWN && scrollY > -MAX_SCROLL_SIZE) {
if (!canScrollVertically) {
float dis = scrollY - Math.abs(v) > -MAX_SCROLL_SIZE ? -Math.abs(v) : -MAX_SCROLL_SIZE - scrollY;
scrollBy(0, (int) dis);
mLastMovePoint = rawY;
isDragging = true;
return true;
}
}
}
mLastMovePoint = rawY;
isDragging = true;
dispatchEventToBody(event);
break;
case MotionEvent.ACTION_UP:
mLastMovePoint = 0;
isDragging = false;
scrollToPosition();
mTouchDownPoint = 0;
break;
case MotionEvent.ACTION_CANCEL:
mLastMovePoint = 0;
isDragging = false;
mTouchDownPoint = 0;
break;
default:
break;
}
// 分發事件
if (action != MotionEvent.ACTION_MOVE) {
dispatchEventToBody(event);
}
return true;
}
以上代碼中,主要涉及到幾點說明:
- 觸發滑動的規則:滑動距離大于
ViewConfiguration.getScaledTouchSlop()
- 滑動方向的判定
mDirection
- 是否拖拽的判定
isDragging
- 滑動距離的判定
v
- Body控件能否向下滑動的判定,處理滑動優先級
canScrollVertically
- 子元素的事件分發邏輯
dispatchEventToBody
其中是否下發事件到Body,主要依據點選事件是否發生在Body區域
private void dispatchEventToBody(MotionEvent event) {
float evX = event.getRawX();
float evY = event.getRawY();
int[] location = new int[2];
body.getLocationOnScreen(location);
float x = location[0];
float y = location[1];
Log.e("body", "x= " + x + ", y= " + y);
if (evX >= x && evX <= (x + body.getWidth()) && evY > y && evY < (y + body.getHeight())) {
body.dispatchTouchEvent(event);
}
}
松手後的回彈
主要通過
Scroller
和回調方法
computeScroll
來實作
其中平滑滑動的觸發條件為:
/**
* @param start 目前位置的scrollY
* @param end 目的位置的scrollY
* @param duration 時長
*/
private void smoothScroll(int start, int end, int duration) {
int dis = end - start;
mScroller.startScroll(0, start, 0, dis, duration);
invalidate();
}
Scroller相當于一個內插補點器,控制滑動距離的細分,通過
invalidate
來觸發自身的重繪,在
computeScroll
中進行連續的滑動
/**
* 所有滑動中的狀态均在此處理
*/
@Override
public void computeScroll() {
//手指已經離開,并且觸發過內插補點器,則回彈到指定位置
if (!isDragging && mScroller.computeScrollOffset()) {
// 通過以下三行代碼實作彈性回彈
int currY = mScroller.getCurrY();
scrollTo(0, currY);
postInvalidate();
}
}
總結
- 彈性滑動是對長距離滑動的多次分割
- 觸發重繪可以把多次微小的滑動串聯起來
- 處理子元素和自身的滑動沖突(難點)
- 子元素跳躍性的滑動
連結
安卓下拉重新整理的一種實作GitHub