Google在其support库中为我们提供了DrawerLayout和SlidingPaneLayout两个布局来帮助我们实现侧边栏滑动的效果。在这两个功能强大的布局后面,有一个鲜为人知却功能强大的类——ViewDragHelper。通过ViewDragHelper,基本可以实现各种不同的滑动、拖放需求。
如何使用ViewDragHelper创建一个滑动布局,在下面的例子中,准备实现类似QQ滑动侧边栏的布局,初始时显示内容页面,当用户手指滑动超过一段距离时,内容界面侧滑显示菜单界面,效果如下图:
具体的代码如何实现:
初始化ViewDragHelper
ViewDragHelper通常定义在一个ViewGroup的内部,并通过其静态工厂方法进行初始化,代码如下:
mViewDragHelper = ViewDragHelper.create(this, mCallback);
它的第一个参数是要监听的View,通常需要是一个ViewGroup,即parentView;第二个参数是一个Callback回调,这个回调就是整个ViewDragHelper的逻辑核心。
拦截事件
接下来需要重写事件的拦截方法,将事件传递给ViewDragHelper进行处理,代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递给ViewDragHelper,此操作必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}
处理computeScroll()
使用ViewDragHelper同样需要重写computeScroll()方法,因为ViewDragHelper内部也是通过Scroller来实现平滑移动的。模板代码如下:
@Override
public void computeScroll() {
if(mViewDragHelper.continueSettling(true))
ViewCompat.postInvalidateOnAnimation(this);
}
处理回调Callback
首先创建一个ViewDragHelper.Callback,代码如下:
private ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
/**
* 何时开始检测触摸事件
* 该方法确定该ViewGroup中哪一个子View可以被移动
* 该ViewGroup中有两个子View——MenuView与MainView
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果当前触摸的child是mMainView时开始检测,也就是只有mMainView可以被拖动
return child == mMainView;
}
// 触摸到View后回调
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}
// 当拖拽状态改变后回调,比如idle,dragging
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}
// 当位置改变的时候回调,常用于滑动时更改scale进行缩放等效果
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
/**
* 处理垂直滑动。如果要实现垂直方向滑动效果,该方法必须重写。因为它的默认值为0,即不发生滑动。
* @param child
* @param top 垂直方向上child移动的距离
* @param dy 比较前一次的增量
* @return
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
/**
* 处理水平滑动。如果要实现水平方向滑动效果,该方法必须重写。因为它的默认值为0,即不发生滑动。
* 通常情况下,只需返回left即可,当但需要更加精确地计算padding等属性的时候,就需要对left进行处理,
* 并返回合适大小的值。
* @param child
* @param left 水平方向上child移动的距离
* @param dx 比较前一次的增量
* @return
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
/**
* 该方法拖动结束后调用,通过重写该方法,可以非常简单地实现当手指离开屏幕后要实现的操作,这个方法内部是
* 通过Scroller类来实现的,这也是重写computeScroll()方法的原因
* @param releasedChild
* @param xvel
* @param yvel
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//手指抬起后缓慢移动到指定位置
if(mMainView.getLeft() < 500){
//关闭菜单
//相当于Scroller的startScroll方法
mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}else{
//打开菜单
mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
};
最后完善
在自定义ViewGroup的onFinishInflate()方法中,按顺序将子View分别定义成MenuView与MainView,并在onSizeChanged()方法中获得View的宽度。当需要根据View的宽度来处理滑动后的效果,就可以使用这个值来进行判断,代码如下:
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = mMenuView.getMeasuredWidth();
}