本文主要是基于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();
}
}
总结
- 弹性滑动是对长距离滑动的多次分割
- 触发重绘可以把多次微小的滑动串联起来
- 处理子元素和自身的滑动冲突(难点)
- 子元素跳跃性的滑动