在《ViewDragHelper的简单分析(一)》这篇博客颇为详细的说明了 ViewDragHelper的工作原理及在拖动过程中重写CallBack的几个相关方法对拖动过程加以控制,其核心拖动原理就是对offsetLeftAndRight/offsetTopAndBottom的应用。ViewDragHelper的控件其实很好用,网上查查资料或者自己静下来研读一下它的源码很快就可以上手,当我基本看完了它的源码并完成了第一篇博客后就琢磨着怎么利用它写个自定义的简单控件。记得我琢磨过qq聊天列表的侧滑功能是怎么实现的,并试着完成一下,在没有了解ViewDragHelper的时候基本上没什么思路,不过看了ViewDragHelper的实现原理之后或许是灵机一动,就尝试着用它来实现侧滑功能,一点一点完善算是完成了吧。本篇博客就来详细说明一下它的实现过程,估计博文有点长,如果觉得麻烦或者啰嗦的话可以直接下载博文最后的demo链接自己研究和修改。闲言少叙,书归正传!
先说说核心思路吧,通篇围绕着就是核心思路在做扩展来完善侧滑功能:因为是水平移动的,所以核心原理就是在合适的时机调用offsetLeftAndRight。基本思路两个水平左右排列的View(左边的为leftView,右边的为rightView),当拖动leftView移动dx距离的时候,让rightView.offsetLeftAndRight(dx);同理当拖拽rightView移动dx的距离的时候,让leftView.offsetLeftAdnRight(dx);很简单吧?说了基本原理和思路下面就一步一步来实现怎么处理的吧。
1)重写onLayout,实现leftView和rightView的左右水平排列
在我的实现了,我的自定义控件直接扩展或者继承RelativeLayout,并没有直接继承ViewGoup,因为这样我就不需要在实现它的Measure了。为了让leftView和rightView实现左右并排布局,我重写了RelativeLayout的onLayout方法。之所以重写此方法,是因为在布局文件里用toRightOf的时候实现不了侧滑的效果,具体原因我尚未清楚,为了懒省事,所以直接重写之!
-
/**
-
* 重写onLayout,让leftView和rightView分左右排列布局
-
*/
-
@Override
-
protected void onLayout(boolean changed, int l, int t, int r, int b) {
-
leftView.layout(0, 0, leftView.getMeasuredWidth(), leftView.getMeasuredHeight());
-
rightView.layout(leftView.getMeasuredWidth(), 0,
-
leftView.getMeasuredWidth() + rightView.getMeasuredWidth(), rightView.getMeasuredHeight());
-
}
2)初始化ViewDragHelper和CallBack
具体怎么初始化的见博文一,在此就不多说.Callback的tryCaptureView方法是决定ViewGroup或者parentView里面哪一个childView可以被拖拽的关键,因为在我们这个控件里要拖动的有两个View,即leftView和rightView,所以tryCaptureView的实现很简单了:
-
/**
-
* 是否允许是指按住的那个View被拖拽
-
* @param captureView 要被检测是否可以拖拽的View
-
* @param pointerId
-
* @return true 说明captureView允许拖拽,false说明captureView不允许拖拽
-
*/
-
@Override
-
public boolean tryCaptureView(View captureView, int pointerId) {
-
return captureView==leftView||captureView==rightView;
-
}
正如《ViewDragHelper的简单分析(一)》这篇博客说的,如果要让某个childView在水平或者竖直方向上拖拽必须重写clampViewPositionHorizontal或者clampViewPositionVertical方法,本篇只涉及到水平拖拽所以我们直接重写clampViewPositionHorizontal即可(注意该方法返回值的意思是 childView随着拖拽的进行水平方向上即将到达的下一个坐标位置):
-
/**
-
* child 水平滚动的最终的距离
-
* @param child 拖拽的View,即captureView
-
* @param left child即将滚动到的新的位置 left = child.getLeft()+dx
-
* @param dx 拖拽的距离
-
* @return chid水平方向上即将达到的下一个坐标位置
-
*/
-
@Override
-
public int clampViewPositionHorizontal(View child, int left, int dx) {
-
return left;
-
}
3)重写onViewPositionChanged方法,这是实现上文中实现基本思路的核心!
在我们拖拽的某一个childView,随着childView位置不断发生改变,会持续的调用onViewPositionChanged这个方法。所以很明显我们要在这个方法里面实现拖动leftView/rightView让rightView/leftView也跟着拖动;
-
/**
-
* @param changedView 被拖拽的View,即captureView
-
* @param left 滚动过后最新的x的位置
-
* @param top
-
* @param dx 最终滚动的距离
-
* @param dy
-
*/
-
@Override
-
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
-
if(changedView==leftView){//如果拖拽的是左边的View
-
//让右边的view滚动的同样的距离
-
rightView.offsetLeftAndRight(dx);
-
}else if(changedView==rightView){//如果手指拖动的是右边的view
-
//让左边的view滚动同样的距离
-
leftView.offsetLeftAndRight(dx);
-
}
-
}
Ok,经过步骤一二三,就大致完成了拖拽的效果:
为了区别方便leftView背景我设置了粉红色,rightView布局就很简单了,就是上那个TextView:置顶,标为已读,删除
估计你已经发现了问题就是向左或者向右拖拽的时候并没有做边界处理:当向左滚动的时候,rightView完全显示的时候必须限制不能在向左拖拽;让当右拖拽的时候,如果rightView已经完全看不见,那么不能继续向右拖拽。
下面就来详细的说明怎么处理。
4)左右边界处理问题:
总的来说向左向右拖动的完成流程可以用下图来表示:
如上图所示 :当rightView完全显示的时候,即上图中的B图位置的时候,正常逻辑是此时在向左拖拽leftView或者rightView都不会在进行拖拽。 注意几个关键坐标点,此时leftView的左边点为(-rightView,0).也就是说如果拖拽的是leftView的话,clampViewPositionHorizontal这个方法的第二个参数left满足left<-rightViewWidth,为了不让leftView继续向左运动,只需要clampViewPositionHorizontal返回-rightViewWidht即可(clampViewPositionHorizontal该方法返回的参数代表着当前chidView随着拖拽的进行即将达到的下一个位置,既如此我们就让它在B图的情况下一直返回-rightWidht的坐标位置就是);当向右拖拽leftView的时候如果位于图C的位置,此时继续拖拽leftView的话,leftView的left>0.所以为了入职继续向右拖拽,clampViewPositionHorizontal直接返回0;同理可以分析出拖拽rightView的边界处理的情况,再次就不在赘述,所以clampViewPositionHorizontal方法我们不能像上面那样直接返回left,修改如下:
-
/**
-
* child 水平滚动的最终的距离
-
* @param child 拖拽的View,即captureView
-
* @param left child即将滚动到的新的位置 left = child.getLeft()+dx
-
* @param dx 拖拽的距离
-
* @return
-
*/
-
@Override
-
public int clampViewPositionHorizontal(View child, int left, int dx) {
-
int rightViewWidth = rightView.getMeasuredWidth();
-
//如果拖拽的是左边的View
-
//注意此时left指的是leftView即将到达的左上角的坐标
-
if(child==leftView){
-
//如果rightView完全显示,即图B
-
if(left<-rightViewWidth){
-
left=-rightViewWidth;
-
}else if(left>0){//如果leftView完全显示,即图A或者图C
-
left = 0;
-
}
-
}else if(child==rightView){//如果拖拽的是右边的view 注意此时left指的是rightView即将到达的左上角的坐标
-
int parentWidth = getMeasuredWidth();
-
//如果rihtView完全显示,即图B
-
if(left<parentWidth-rightViewWidth){
-
left = parentWidth-rightViewWidth;
-
}else if(left>rightViewWidth){//如果rightView完全隐藏,即图C位置
-
left = rightViewWidth;
-
}
-
}
-
return left;
-
}
这样就完美的解决了边际处理问题,具体效果就不在贴图了
5)手指抬起时继续滑动效果,完善体验,实现惯性滑动
经过上面四个步骤实现的侧滑发现手指抬起的时候就不会继续运行了,那么怎么让手指抬起的时候继续让leftView或者rightView向左或者向右滑动制定的位置呢?ViewDragHelper提供了smoothSlideViewTo和continueSettling方法来可以解决这个方法:
源码里对smoothSlideViewTo这个方法的解释是,如果返回true就调用continueSettling直到continueSettling返回了false,表明拖拽结束,所以用这个方法完全可以实现惯性滑动效果,观察continueSettling的源码里面也即是调用了captureView的offsetLeftAndRight实现的。我们知道在callback里的onViewReleased方法可以处理手指抬起时候对childView进行处理。结合《 View的滚动原理简单解析》这篇博客中实现TextView滚动次数的思路,所以简单粗略的实现了如下代码:
-
@Override
-
public void onViewReleased(View releasedChild, float xvel, float yvel) {
-
if(releasedChild==leftView){
-
//if(xvel==0){
-
if(xvel<0){//手指向左滑动
-
if(leftView.getLeft()>-rightView.getMeasuredWidth()){//rightView还没有显示完
-
if(viewDragHelper.smoothSlideViewTo(leftView,-rightView.getMeasuredWidth(),0)) {
-
post(QqLayoutView.this);
-
}
-
}
-
}else if(xvel>0){//手指向右滑动
-
if(leftView.getLeft()<0){//rightView还没有显示完
-
if(viewDragHelper.smoothSlideViewTo(leftView,0,0)) {
-
post(QqLayoutView.this);
-
}
-
}
-
}
-
}else if(releasedChild==rightView){
-
if(xvel<0){//手指向左滑动
-
if(rightView.getLeft()>getWidth()-rightView.getWidth()){
-
if(viewDragHelper.smoothSlideViewTo(rightView,getWidth()-rightView.getWidth() ,0)){
-
post(QqLayoutView.this);
-
}
-
}
-
}else if(xvel>0){//如果手指向左运动
-
if(rightView.getRight()>getWidth()){
-
if(viewDragHelper.smoothSlideViewTo(rightView,getWidth() ,0)){
-
post(QqLayoutView.this);
-
}
-
}
-
}
-
}
-
}
另外让自定义控件实现了一个Runnable,在run方法里简单发生了如下调用:
-
@Override
-
public void run() {
-
//如果滚动还没有结束
-
if(viewDragHelper.continueSettling(true)){
-
post(this);
-
}
-
}
就这样简单的惯性滑动效果就实现了,熟悉android绘制流程的都知道,其实重写computeScroll方法是也是的解决惯性滑动方式,所以步骤五也可以这么实现:
原来的post(this)替换成ViewCompat.postInvalidateOnAnimation(QqLayoutView.this);并且重写的computeScroll方法为:
-
@Override
-
public void computeScroll() {
-
if(viewDragHelper.continueSettling(true)){
-
ViewCompat.postInvalidateOnAnimation(this);
-
}
-
}
另外需要注意的是这种方式需要在onViewReleased和onViewPositionChanged方法最后写上invalidate()否则运行效果rightView的那部分偶尔出现白色。