先给大家看效果图吧、
需求:
将复杂的内容布局 通过向右拖拽或者是快速向右滑动将其移动到最右边
当在向左拖动或者是快速向左滑动会将移除的布局恢复到原位
使用方法
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;
}