Android 拖动滑出滑入的布局 自定义ViewDragHelper详解
来源:互联网 发布:淘宝日系男装店铺排行 编辑:程序博客网 时间:2024/04/30 15:23
先给大家看效果图吧、
需求:
将复杂的内容布局 通过向右拖拽或者是快速向右滑动将其移动到最右边
当在向左拖动或者是快速向左滑动会将移除的布局恢复到原位
使用方法
github源码 欢迎star fork https://github.com/shf981862482/SlideLayoutApp.git
compile 'com.slidelayout:slipe_layout_library:0.0.3'
//滑动完成监听 slide.setOnSlideStatusListener(new SlideLayout.OnSlideStatusListener() { @Override public void slideOutComplete() { Log.d("SHF", "slideOutComplete"); } @Override public void slideInComplete() { Log.d("SHF", "slideInComplete"); } });
注意:
1、SlideLayout使用相当于RelativeLayout
2、 第一个子布局就是可拖动的布局
3、至少一个子布局
<sun.com.slipelayoutlibrary.SlideLayout android:id="@+id/slide" android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true" android:background="@color/grayTran" android:gravity="center" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="4dp" android:text="@string/text" /> <TextView android:id="@+id/gone_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="消失的内容" /> </RelativeLayout> <Button android:id="@+id/btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="改布局" /> </sun.com.slipelayoutlibrary.SlideLayout>
SlideLayout布局是结合 自定义的ViewDrawHelp来实现的
还没了解ViewDrawHelp的请看鸿祥的博客 Android ViewDragHelper完全解析 自定义ViewGroup神器
SlideViewDragHelper原理
在使用ViewDragHelper的时候、我们需要将onInterceptTouchEvent 和 onTouchEvent 交给
ViewDragHelper处理 代码如下
@Override public boolean onInterceptTouchEvent(MotionEvent event) { return mDragger.shouldInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { mDragger.processTouchEvent(event); return true; }
这里主要说一下 processTouchEvent
我们看一下他的部分源码
case MotionEvent.ACTION_DOWN: { final float x = ev.getX(); final float y = ev.getY(); mCallback.onViewDown(ev); final int pointerId = MotionEventCompat.getPointerId(ev, 0); final View toCapture = findTopChildUnder((int) x, (int) y); saveInitialMotion(x, y, pointerId); // Since the parent is already directly processing this touch event, // there is no reason to delay for a slop before dragging. // Start immediately if possible. tryCaptureViewForDrag(toCapture, pointerId); final int edgesTouched = mInitialEdgesTouched[pointerId]; if ((edgesTouched & mTrackingEdges) != 0) { mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); } break; }
可以看到 在按下的时候 会通过 findTopChildUnder((int) x, (int) y); 找到需要拖动的子view
findTopChildUnder 方法源码如下
public View findTopChildUnder(int x, int y) { final int childCount = mParentView.getChildCount(); for (int i = childCount - 1; i >= 0; i--) { final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i)); if (x >= child.getLeft() && x < child.getRight() && y >= child.getTop() && y < child.getBottom()) { return child; } } return null; }
他是根据点击的位置来找到点击的子child,
那么当我们将布局拖出去之后,显然根据源码是找不到子view的 这个方法就会返回空,那么我们又要拖拽这个处于外头的子布局怎么办呢,看代码
public View findTopChildUnder(int x, int y) { final int childCount = mParentView.getChildCount(); if (childCount > 0) { return mParentView.getChildAt(mCallback.getOrderedChildIndex(0)); } return null; }
上面就是SlideViewDragHelper的findTopChildUnder()方法,他是永远获取第一个子view,我们在布局的时候将需要拖动的布局放到第一个即可
最关键的地方我们实现了,那么如何去监听他是否滑动完成呢、我们看代码
在使用的时候 我们自定义的布局要重写这个方法
@Override public void computeScroll() { super.computeScroll(); if (mDragger.continueSettling(true)) { invalidate(); } }
mDragger.continueSettling(true) 这一个我们先放一放 我们先看ViewDragHelper的实现机制
ViewDragHelper的实现机制
大家对ViewDragHelper的实现机制应该很好奇吧
1、是怎么拖动的
2、拖动到中间是怎么完成剩下的滑动动画的
ViewDragHelper是怎么拖动的
其实很简单,在processTouchEvent()方法中 VIewDragHelper是这样处理ACTION_MOVE的
case MotionEvent.ACTION_MOVE: { mCallback.onViewMove(ev); if (mDragState == STATE_DRAGGING) { final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, index); final float y = MotionEventCompat.getY(ev, index); final int idx = (int) (x - mLastMotionX[mActivePointerId]); final int idy = (int) (y - mLastMotionY[mActivePointerId]); dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy, ev); saveLastMotion(ev); } else { // Check to see if any pointer is now over a draggable view. final int pointerCount = MotionEventCompat.getPointerCount(ev); for (int i = 0; i < pointerCount; i++) { final int pointerId = MotionEventCompat.getPointerId(ev, i); final float x = MotionEventCompat.getX(ev, i); final float y = MotionEventCompat.getY(ev, i); final float dx = x - mInitialMotionX[pointerId]; final float dy = y - mInitialMotionY[pointerId]; reportNewEdgeDrags(dx, dy, pointerId); if (mDragState == STATE_DRAGGING) { // Callback might have started an edge drag. break; } final View toCapture = findTopChildUnder((int) x, (int) y); if (checkTouchSlop(toCapture, dx, dy) && tryCaptureViewForDrag(toCapture, pointerId)) { break; } } saveLastMotion(ev); } break; }
看到 dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy, ev);这个方法了吧, 这就是手动拖拽的主要实现方法 我们进去看看
private void dragTo(int left, int top, int dx, int dy, MotionEvent ev) { int clampedX = left; int clampedY = top; final int oldLeft = mCapturedView.getLeft(); final int oldTop = mCapturedView.getTop(); if (dx != 0) { clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); mCapturedView.offsetLeftAndRight(clampedX - oldLeft); } if (dy != 0) { clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); mCapturedView.offsetTopAndBottom(clampedY - oldTop); } if (dx != 0 || dy != 0) { final int clampedDx = clampedX - oldLeft; final int clampedDy = clampedY - oldTop; mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy); } mCallback.onViewDragMove(ev); }
可以看到主要是通过 offsetLeftAndRight 和 offsetTopAndBottom来实现未知的改变
ViewDragHelper拖动到中间是怎么完成剩下的滑动动画的
ViewDragHelper有个ViewDragHelper.Callback
自定义的布局 去创建ViewDragHelper的时候 需要传递ViewDragHelper.Callback的对象
Callback有一个方法是手指释放的时候回调 方法名如下
public void onViewReleased(View releasedChild, float xvel, float yvel) {}
在这个方法内部 我们做一些判断然后调用
mDragger.settleCapturedViewAt(x, y);即可实现剩下的滑动动画
我们看一下settleCapturedViewAt()
public boolean settleCapturedViewAt(int finalLeft, int finalTop) { if (!mReleaseInProgress) { throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + "Callback#onViewReleased"); } return forceSettleCapturedViewAt(finalLeft, finalTop, (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId)); }
继续看 forceSettleCapturedViewAt()
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) { final int startLeft = mCapturedView.getLeft(); final int startTop = mCapturedView.getTop(); final int dx = finalLeft - startLeft; final int dy = finalTop - startTop; if (dx == 0 && dy == 0) { // Nothing to do. Send callbacks, be done. mScroller.abortAnimation(); setDragState(STATE_IDLE); return false; } final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); mScroller.startScroll(startLeft, startTop, dx, dy, duration); setDragState(STATE_SETTLING); return true; }
我们看到熟悉的Scroller.startScroll()了,
想了解Scroller的可以看一下ViewPager的源码
大家以为这就完成滚动了吗,其实不然,每个VIew都有一个可以重写的方法 前面我们说过叫做computeScroll
我们看一下官方解释
/** * Called by a parent to request that a child update its values for mScrollX * and mScrollY if necessary. This will typically be done if the child is * animating a scroll using a {@link android.widget.Scroller Scroller} * object. */
简单来说,就是Scroller执行的时候 会调用View.computeScroll()
@Override public void computeScroll() { super.computeScroll(); if (mDragger.continueSettling(true)) { invalidate(); } }
看一下源码 continueSettling()
public boolean continueSettling(boolean deferCallbacks) { if (mDragState == STATE_SETTLING) { boolean keepGoing = mScroller.computeScrollOffset(); final int x = mScroller.getCurrX(); final int y = mScroller.getCurrY(); final int dx = x - mCapturedView.getLeft(); final int dy = y - mCapturedView.getTop(); if (dx != 0) { mCapturedView.offsetLeftAndRight(dx); } if (dy != 0) { mCapturedView.offsetTopAndBottom(dy); } if (dx != 0 || dy != 0) { mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy); } if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) { // Close enough. The interpolator/scroller might think we're still moving // but the user sure doesn't. mScroller.abortAnimation(); keepGoing = false; } if (!keepGoing) { if (deferCallbacks) { mParentView.post(mSetIdleRunnable); } else { setDragState(STATE_IDLE); } } } return mDragState == STATE_SETTLING; }
注意参数 keepGoing 是指正在滑动
位置改变还是跟dragTo一样用的offsetLeftAndRight
SlideViewDragHelper 滑动完成监听
为Callback添加如下属性和方法
public static abstract class Callback { private boolean isScroll; /** *滚动监听 */ public void onStartScrollListener(boolean isComplete) { isScroll = !isComplete; } }
在前面continueSettling的方法中 我们调用这个方法
public boolean continueSettling(boolean deferCallbacks) { if (mDragState == STATE_SETTLING) { boolean keepGoing = mScroller.computeScrollOffset(); if (mCallback != null) { mCallback.onStartScrollListener(!keepGoing); } ... } }
好了滚动监听就实现了,再复杂的滚动逻辑就需要在我们自定义的SlideLayout中实现了
SlideLayout源码解析
初始化创建SlideViewDragHelper
public SlideLayout(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { mDragger = SlideViewDragHelper.create(this, 1.0f, new SlipeCallback()); mDragger.setEdgeTrackingEnabled(SlideViewDragHelper.EDGE_LEFT); }
添加滚动完成监听接口
public interface OnSlideStatusListener { void slideOutComplete(); void slideInComplete(); }
添加全局变量
public OnSlideStatusListener slideStatusListener; private final int OUTSLIPESPEED = 3000;//可以滑动到外部滑动速度临界点 private final int INSLIPESPEED = -3000;//可以归位的滑动速度临界点 private SlideViewDragHelper mDragger; private View mDragView;//要拖动的子布局 private Point mAutoBackOriginPos = new Point();//记录子布局的初始位置 private boolean haveSavePoint = false;//有没有记录下位置 private boolean isOut = false;//有没有滑动到外头 private boolean isMove = false;//子布局是不是在 移动
重写如下方法
@Override public boolean onInterceptTouchEvent(MotionEvent event) { return mDragger.shouldInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { mDragger.processTouchEvent(event); return true; } /** * http://my.oschina.net/ososchina/blog/600281 */ @Override public void computeScroll() { super.computeScroll(); if (mDragger.continueSettling(true)) { invalidate(); } }
这个方法意思是inflate完成后 将第一个子view作为拖动布局
@Override protected void onFinishInflate() { super.onFinishInflate(); mDragView = getChildAt(0); }
下面重写onLayout方法是重点,因为offsetLeftAndRight 并没有改LayoutParam的mLeft的值
而view的onLayout()源码如下
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // The layout has actually already been performed and the positions // cached. Apply the cached values to the children. final int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { RelativeLayout.LayoutParams st = (RelativeLayout.LayoutParams) child.getLayoutParams(); child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom); } } }
所以说,当子view有Gone Visible操作时,会执行onLayout 会导致画面闪动
解决办法移动时直接用子view的getLeft 没移动时调用super.onLayout(changed, l, t, r, b);
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); if (count == 0) { throw new RuntimeException("you must have one child view!!!"); } if (!isOut && !isMove) { super.onLayout(changed, l, t, r, b); } else { for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { child.layout(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); } } } if (!haveSavePoint) { //记录初始坐标 mAutoBackOriginPos.x = mDragView.getLeft(); mAutoBackOriginPos.y = mDragView.getTop(); haveSavePoint = true; } }
注意:必须有一个子布局,不然会抛出异常
拖动布局的主要逻辑
好了重点来了、我们继承SlideViewDragHelper.Callback
添加属性
private int screenMiddle;//屏幕中间 private float downLeft;//点击 child0 的left private float beforeChildLeft;//点击 child0 的left private float childLeft;//移动时 child0 的left private float recentChildLeft;//最近执行移动的childLeft private float moveLeft; private float dragXSpeed;
重写方法
捕捉拖动布局回调
@Override public boolean tryCaptureView(View child, int pointerId) { //mEdgeTrackerView禁止直接移动 return child == mDragView; }
点击回调,用来记录拖动前的 拖动布局的位置
@Override public void onViewDown(MotionEvent event) { childLeft = getChildAt(0).getLeft(); if (!isScroll() && !isMove) {//如果正在滚动 不记录按下left beforeChildLeft = getChildAt(0).getLeft(); downLeft = event.getX(); } }
移动回调,用来记录手指移动的位置
@Override public void onViewMove(MotionEvent event) { if (!isScroll()) {//如果正在滚动 不记录移动left moveLeft = event.getX(); } }
SlipeViewDragHelper dragTo中的拖动移动回调
@Override public void onViewDragMove(MotionEvent event) { childLeft = getChildAt(0).getLeft(); }
拖动布局改变位置的回调
@Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { isMove = true; }
用来返回子view的位置
@Override public int clampViewPositionHorizontal(View child, int left, int dx) { return left; } @Override public int clampViewPositionVertical(View child, int top, int dy) { return super.clampViewPositionVertical(child, top, dy); }
//手指释放的时候回调 @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { dragXSpeed = xvel; screenMiddle = getWidth() / 2; //mAutoBackView手指释放时可以自动回去 if (releasedChild == mDragView) { float length = moveLeft - downLeft;//手指移动的距离 recentChildLeft = childLeft; Log.d(TAG, "onViewReleased--left or right-->" + (length >= 0 ? "right" : "left") + "--速度xvel-->" + xvel + "--当前位置childLeft-->" + childLeft + "--length-->" + length); if (xvel > OUTSLIPESPEED) {//右滑 速度快 不管是左滑右滑直接到外部 slideOut(); invalidate(); return; } if (xvel < INSLIPESPEED) {//左滑 速度快 不管左滑右滑直接到内部 slideIn(); invalidate(); return; } if (length >= 0) {//根据手指移动距离判断右滑 if (childLeft > getWidth()) { slideOut(); invalidate(); return; } if (xvel <= 0 && childLeft < screenMiddle) { slideIn(); invalidate(); return; } if (xvel <= 0 && childLeft > screenMiddle) {//右滑 但是滑动时快速点击 将滑动速度变成了负数 slideOut(); invalidate(); return; } if (xvel >= 0 && xvel <= OUTSLIPESPEED//右滑 速度慢 没过最大滑动距离 && (childLeft <= screenMiddle && childLeft >= 0)) { slideIn(); invalidate(); return; } if (xvel >= 0 && xvel <= OUTSLIPESPEED && (childLeft > screenMiddle)) {//右滑 速度慢 超过最大滑动距离 slideOut(); invalidate(); return; } } else {//左滑 if (childLeft < 0) {//滑到左屏幕外头 slideIn(); invalidate(); return; } if (xvel >= 0 && childLeft < screenMiddle) { slideIn(); invalidate(); return; } if (xvel >= 0 && childLeft > screenMiddle) {//左滑 但是滑动时快速点击 将滑动速度变成了整的 slideOut(); invalidate(); return; } if (xvel >= INSLIPESPEED && xvel <= 0 && (childLeft > screenMiddle)) {//左滑 速度慢 没过最大滑动距离 slideOut(); invalidate(); return; } if (xvel >= INSLIPESPEED && xvel <= 0 && (childLeft <= screenMiddle && childLeft >= 0)) {//左滑 速度慢 超过最大滑动距离 slideIn(); invalidate(); return; } } } }
/** * 滚动完成监听 * * @param isComplete 滚动是否完成 */ @Override public void onStartScrollListener(boolean isComplete) { if (isComplete && slideStatusListener != null) {//滚动完成 float length = moveLeft - downLeft;//手指移动的距离 isMove = false; if (recentChildLeft < 0) {//抬起后子view的left小于零说明没有滑动 return; } if (recentChildLeft > getWidth()) { return; } Log.d(TAG, "onstartScrollListener--left or right-->" + (length >= 0 ? "right" : "left") + "--dragXSpeed-->" + dragXSpeed + "--当前位置recentChildLeft-->" + recentChildLeft + "--length-->" + length + "--beforeChildLeft-->" + beforeChildLeft); if (beforeChildLeft <= mAutoBackOriginPos.x) { if (dragXSpeed > OUTSLIPESPEED) {//右滑 速度快 直接执行滚动外部方法 slideStatusListener.slideOutComplete(); return; } if (dragXSpeed < INSLIPESPEED) { return; } //抬起手时 子view的left大于screenMiddle 执行滚动外部方法 if (dragXSpeed >= INSLIPESPEED && recentChildLeft > screenMiddle) { slideStatusListener.slideOutComplete(); return; } } if (beforeChildLeft >= getWidth()) { if (dragXSpeed < INSLIPESPEED) { slideStatusListener.slideInComplete(); return; } if (dragXSpeed > OUTSLIPESPEED) { return; } if (dragXSpeed <= OUTSLIPESPEED && recentChildLeft < screenMiddle) { slideStatusListener.slideInComplete(); return; } } } return; }
执行剩下的滚动动画
private void slideOut() { mDragger.settleCapturedViewAt(getWidth(), mAutoBackOriginPos.y); isOut = true; } private void slideIn() { mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y); isOut = false; }
- Android 拖动滑出滑入的布局 自定义ViewDragHelper详解
- ViewDragHelper详解- 可拖动的view
- ViewDragHelper详解- 可拖动的view
- ViewDragHelper详解,简化拖动操作
- ViewDragHelper详解(一)- 可拖动的view
- ViewDragHelper详解(一)- 可拖动的view
- ViewDragHelper详解(一)- 可拖动的view
- Android开发,自定义ViewGroup的神器,ViewDragHelper
- Android 之 ViewDragHelper 详解
- android ViewDragHelper详解
- Android ViewDragHelper 使用详解
- Android ViewDragHelper详解
- ViewDragHelper的使用(一):自定义DragFrameLayout(childView可拖动)
- ViewDragHelper的使用详解
- Android自定义ViewGroup神器-ViewDragHelper
- 基于Android图标拖动布局的实现
- Android拖动控件的实现,自定义可拖动的LinearLayout
- android带返回按钮的自定义标题栏布局文件详解
- java servlet的tomcat
- mysql sql语句优化细节
- iconv 文件编码转换
- 1033 旧键盘打字
- Period--KMP,最小循环节
- Android 拖动滑出滑入的布局 自定义ViewDragHelper详解
- 作者(zhang854429783) eclipse通过tomcat热部署web项目
- python第五天学习记录——模块
- 计算几何 BAPC 14 C itadel Construction (Gym 100526C )
- HashMap封装查询到的数据
- 1034 有理数四则运算
- JavaScript学习笔记--语法
- 百度地图配置使用笔记(AndroidStudio)
- 五十道编程小题目 --- 19 打印菱形 java