引言
在上篇博客Android进阶之自定义View实战(四)ViewDragHelper入门中,我们学习了ViewDragHelper的常用方法,它帮助我们处理的View的触摸事件的分发与拦截,我们仅仅只需实现它的Callback接口的几个方法就可以方便的实现诸如边界检测、惯性滚动、View联动之类的逻辑。这篇博客就通过仿YouTubeLayout来熟悉它的用法:
YouTubeLayout实现
本节内容主要来自http://flavienlaurent.com/blog/2013/08/28/each-navigation-drawer-hides-a-viewdraghelper/,
效果图:
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<com.qicode.draghelperdr.YouTubeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/youtubeLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="visible">
<TextView
android:id="@+id/viewHeader"
android:layout_width="match_parent"
android:layout_height="128dp"
android:background="#AD78CC"
android:clickable="true"
android:fontFamily="sans-serif-thin"
android:gravity="center"
android:tag="text"
android:text="drag me to bottom"
android:textColor="@android:color/white"
android:textSize="25sp" />
<TextView
android:id="@+id/viewDesc"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF00FF"
android:clickable="true"
android:gravity="center"
android:tag="desc"
android:text="Loreum Loreum"
android:textColor="@android:color/white"
android:textSize="35sp" />
</com.qicode.draghelperdr.YouTubeLayout>
原文还是在拦截和消费事件上做了大量处理,个人认为没有必要,ViewDragHelper的精髓就是已经帮你处理好了手势检测和惯性滚动...下面是我简化后的代码:
成员变量:
private ViewDragHelper mDragHelper;
private View mHeaderView;//头部View,唯一需要被捕获的View
private View mDescView;
private int mDragRange;//滚动范围:总高度-header的高度
private int mTop;//滚动过程中的当期top位置,用于View重绘
private float mDragOffset;//拖动距离百分比,用于判断松手行为
构造方法及成员变量初始化:
public YouTubeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mDragHelper = ViewDragHelper.create(this, 1f, new DragHelperCallback());
}
public YouTubeLayout(Context context) {
this(context, null);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mHeaderView = findViewById(R.id.viewHeader);
mDescView = findViewById(R.id.viewDesc);
//点击事件测试
mDescView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(), "mDescView", Toast.LENGTH_SHORT).show();
}
});
mHeaderView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(), "mHeaderView", Toast.LENGTH_SHORT).show();
}
});
}
DragHelperCallback接口:
private class DragHelperCallback extends ViewDragHelper.Callback{
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == mHeaderView;
}
//捕获的view位置移动时,View的变化处理
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
mTop = top;
mDragOffset = (float)top/mDragRange;//滑动percent
mHeaderView.setPivotX(mHeaderView.getWidth());//右下角锚点缩放
mHeaderView.setPivotY(mHeaderView.getHeight());
mHeaderView.setScaleX(1 - mDragOffset / 2);
mHeaderView.setScaleY(1 - mDragOffset / 2);
mDescView.setAlpha(1 - mDragOffset);
requestLayout();
Log.e("TAG", "onViewPositionChanged");
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
int top = getPaddingTop();
if (yvel > 0 || (yvel == 0 && mDragOffset > 0.5f)) {//向下沉底条件:速度向下,或者滚动比例超过一半
top += mDragRange;//目标位置
}
mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);//设定滚动目标
invalidate();//开启惯性滑动
Log.e("TAG", "onViewReleased");
}
@Override
public int getViewVerticalDragRange(View child) {//使子View的点击事件生效
return mDragRange;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - mHeaderView.getHeight() - mHeaderView.getPaddingBottom();
final int newTop = Math.min(Math.max(top, topBound), bottomBound);
return newTop;
}
}
说明:
- onViewPositionChanged方法实现在HeaderView滚动过程中View的缩放与透明度变化,mTop参数确定了两个子View的垂直布局位置,更新完需要调用requestLayout重布局;
- clampViewPositionVertical确定垂直滚动边界, getViewVerticalDragRange是子View可以获取到点击事件
- onViewReleased根据滚动位置和手势速度来指定HeaderView回滚还是滚动到底部。
既然是个ViewGroup,避免不了onMeasure和onLayout方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);//测量子布局
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mDragRange = getHeight() - mHeaderView.getHeight();
mHeaderView.layout(
0,
mTop,
r,
mTop + mHeaderView.getMeasuredHeight());
mDescView.layout(
0,
mTop + mHeaderView.getMeasuredHeight(),
r,
mTop + b);
}
onMeasure方法里只加了句measureChildren方法,目的是计算子View的宽高,然后onLayout方法中才能拿到测量后的宽高。
手势处理和惯性滚动:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mDragHelper.cancel();
return false;
}
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
mDragHelper.processTouchEvent(ev);
return true;
}
//惯性滚动
@Override
public void computeScroll() {
if(mDragHelper.continueSettling(true)){
invalidate();
}
}
短短代码150多行就可以实现相对复杂的交互,真真是谁用谁知道!代码传送门:GayHub地址