先上效果图:
先看xml文件
<?xml version="1.0" encoding="utf-8"?>
<com.test.listviewdragdemo.view.ViewWithDraged
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/vwd"
android:layout_width="match_parent"
android:layout_height="50dp">
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="第一项"
android:textSize="25sp"/>
<Button
android:id="@+id/btn_1"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="按钮1"
android:textSize="25sp"/>
<Button
android:id="@+id/btn_2"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="按钮2"
android:textSize="25sp"/>
</com.test.listviewdragdemo.view.ViewWithDraged>
在这里,我自定义了一个FrameLayout(其实这个不重要,只要不是LinearLayout就行了)
因为当自定义LinearLayout时,btn1和btn2会被顶在屏幕外(顶在屏幕外的控件会消失)
如果有童鞋问,我想用帧布局啊,表格布局啊,我只能这么说,我没试过,要不你去试试啊(所以,还是不要奇葩的好,正常一点好嘛!!!!)
先看看类头部
/**
* 自定义可侧拉控件,第一个view占满宽度,第n+1个在第n个后面
* Created by 13798 on 2016/6/8.
*/
public class ViewWithDraged extends FrameLayout implements View.OnTouchListener {
可以看得出来,这个ViewGroup会将第二个子View放在第一个后面,第n+1个在第n个后面。
继续附上一波成员变量和构造器。
private Context mContext;
/**
* 子view数组
*/
private View[] views;
private GestureDetector gestureDetector;
/**
* 定义滑动的时间
*/
private static final int SCROLL_TIME = 500;
/**
* 除第一个子View宽度
*/
private int otherWidth;
/**
* 定义一条中线(偏右则打开侧栏控件)
*/
private int middleLine;
/**
* 定义侧拉状态(默认为侧拉关闭)
*/
private enum SlideState {
OPENED, SLIDING, CLOSED
}
private SlideState slideState = SlideState.CLOSED;
public ViewWithDraged(Context context) {
this(context, null);
}
public ViewWithDraged(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ViewWithDraged(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
setOnTouchListener(this);
gestureDetector = new GestureDetector(context, new MyGesture() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (velocityX < -750 && slideState != SlideState.OPENED) {//左滑
doAnimation(views[0].getLeft(), -otherWidth - views[0].getLeft());
} else if (velocityX > 750 && slideState != SlideState.CLOSED) {//右滑
doAnimation(views[0].getLeft(), -views[0].getLeft());
}
invalidate();
return false;
}
});
}
使用的是少参数调用多参数的战略。
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() < 2) {
throw new RuntimeException("子控件必须大于等于2个!!");
}
views = new View[getChildCount()];
for (int i = 0; i < views.length; i++) {
views[i] = getChildAt(i);
}
}
当执行这个方法的时候,xml文件已经被加载完毕了,此时已经知道有多少个子View,因为这个自定义的控件的目的是侧拉删除,因此少于2个控件则不可能,所以在少于2个子view的时候抛出一个运行时异常。
然后,在初始化views,和将每一个子view赋值到对应的views元素(即views[0],views[1]......)
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
views[0].layout(0,getTop(),getWidth(),getBottom());
for (int i = 1; i < views.length; i++) {
if (i == 1)
otherWidth = 0;
views[i].layout(views[i - 1].getRight(), getTop(), views[i - 1].getRight() + views[i].getWidth(), getTop() + getHeight());
otherWidth += views[i].getWidth();
}
middleLine = getWidth() - otherWidth / 2;
}
当执行这个方法的时候,每一个子view的N维属性也出现了(left,top,bottom,right,width和height)。同时,我们强制让第一个子view占满全控件,通过otehrWidth记录除了第一个子view的宽度和。然后让第n+1个控件在第n个控件后面。middleLine是一条中线。看下图。
计算出middleLine的位置,假设是在图中很长的线的位置,如果控件0(views[0])的右边(getRight)在它(middleLine)右边,但没占满全屏,此时已松开手指(onTouch后面说到),views[0]会占满全屏, 如下
/**
* 设置关闭状态
*/
public void setClose() {
slideState = SlideState.CLOSED;
views[0].layout(0, getTop(), getWidth(), getTop() + getHeight());
for (int i = 1; i < views.length; i++)
views[i].layout(views[i - 1].getRight(), getTop(), views[i - 1].getRight() + views[i].getWidth(), getTop() + getHeight());
}
public void setOpen() {
slideState = SlideState.OPENED;
// 设置最后一个的位置
views[getChildCount() - 1].layout(getWidth() - views[getChildCount() - 1].getWidth(), getTop(), getWidth(), getTop() + getHeight());
for (int i = views.length - 2; i >= 0; i--) {
views[i].layout(views[i + 1].getLeft() - views[i].getWidth(), getTop(), views[i + 1].getLeft(), getTop() + getHeight());
}
}
如果让侧拉控件可以显示,就是打开状态,否则就是关闭状态。(在以后的嵌套ListView上会用到,本次木有
)
private int downX;
private int lastX;
private int downY;
private int lastY;
@Override
public boolean onTouch(View v, MotionEvent event) {
gestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = (int) event.getX();
downY = (int) event.getY();
lastX = downX;
lastY = downY;
break;
case MotionEvent.ACTION_MOVE:
final int startX = (int) event.getX();
final int startY = (int) event.getY();
final int dX = startX - lastX;
if (Math.abs(startX - downX) > Math.abs(startY - downY)) {
if (valueAnimator!=null && valueAnimator.isRunning())
valueAnimator.cancel();
if (!canSliding(dX))
return true;
if (!isOutBorder()) {
setLayoutByx(dX);
}
isOutBorder(dX);
}
lastX = startX;
break;
case MotionEvent.ACTION_UP:
if (slideState == SlideState.SLIDING && !isDoingAnimation) {
// 超过中线实现右滑效果
if (views[0].getRight() > middleLine) {
doAnimation(views[0].getLeft(), -views[0].getLeft());
} else {
doAnimation(views[0].getLeft(), -otherWidth - views[0].getLeft());
}
}
break;
}
return true;
}
来个本次博客的重头戏了,onTouch事件,Down就是手指压下的时候,Move就是手指一动的时候,Up就是手指抬起的时候。
滑动的时候,计算出两次move之间的偏移量dX,Math.abs(startX - downX) > Math.abs(startY - downY)是用于判断手指是横向还是纵向滑动(根据比较滑动x的距离和滑动y的距离作比较)。
if (valueAnimator!=null && valueAnimator.isRunning())
valueAnimator.cancel();
滑动的时候,可能会触发动画效果,此时就把动画给取消了。
if (!canSliding(dX))
return true;
判断可不可以滑动
看看canSliding(int dx)。
private boolean canSliding(int dX) {
if (dX >= 0 && slideState == SlideState.CLOSED || dX <= 0 && slideState == SlideState.OPENED)
return false;
return true;
}
在一开始贴上的成员变量的时候,已经初始化
private SlideState slideState = SlideState.CLOSED;
当手指向右滑动,但状态已经是关闭的时候
就好像这样,此时是不能让它滑动(返回false),当手指向左滑动,但状态已经是打开的时候
就好像这样,同样不让它滑动(false)。
(第一张图漏了2个红色矩形,心中有它就行了)
if (!isOutBorder()) {
setLayoutByx(dX);
}
如果判断可以滑动的时候,会进行判断是否越界,没有越界就执行
setLayoutByx(dX);
先看看isOutBorder();
/**
* 判断是否越界
*
* @return
*/
private boolean isOutBorder() {
// 关闭状态
if (views[0].getLeft() >= 0 && slideState != SlideState.CLOSED) {
getParent().requestDisallowInterceptTouchEvent(false);
if (listener != null) {
listener.close();
}
if (slideState != SlideState.CLOSED) {
slideState = SlideState.CLOSED;
}
return true;
// 打开状态
} else if (views[getChildCount() - 1].getRight() <= getWidth() && slideState != SlideState.OPENED) {
if (listener != null)
listener.open();
getParent().requestDisallowInterceptTouchEvent(false);
if (slideState != SlideState.OPENED) {
slideState = SlideState.OPENED;
}
return true;
}
// 滑动状态
if (slideState != SlideState.SLIDING) {
getParent().requestDisallowInterceptTouchEvent(true);
slideState = SlideState.SLIDING;
}
return false;
}
在这里会处理一些状态SlidingState更改,事件回调,请求父布局(通常指ListView)拦不拦截???
如果越界了就会返回true,否则返回false。
然后再看看
setLayoutByx(dX);
/**
* 通过滑动移动x
*
* @param dx 偏移量
*/
public void setLayoutByx(int dx) {
for (int i = 0; i < views.length; i++) {
final int newLeft = views[i].getLeft() + dx;
views[i].layout(newLeft, getTop(), newLeft + views[i].getWidth(), getTop() + getHeight());
}
}
明显看出,这段代码就是通过偏移量来一点一点的实现控件左右滑动。
判断完是否越界后(否就滑动)
会进行最后一次的滑动后判断是否越界。
/**
* 判断是否越界(自动纠正)
*
* @param dx 偏移量
*/
private void isOutBorder(int dx) {
if (views[0].getLeft() > 0 || views[views.length - 1].getRight() < getWidth())
setLayoutByx(-dx);
}
如果越界了,就会通过setLayoutByx(int dx);将多出的部分滑动回去。
当手指抬起的时候,先看看判断代码
case MotionEvent.ACTION_UP:
if (slideState == SlideState.SLIDING && !isDoingAnimation) {
// 超过中线实现右滑效果
if (views[0].getRight() > middleLine) {
doAnimation(views[0].getLeft(), -views[0].getLeft());
} else {
doAnimation(views[0].getLeft(), -otherWidth - views[0].getLeft());
}
}
break;
先过滤掉isDoingAnimation(后面讲)
走进doAnimation方法看看。
/**
* 判断是否正在滑动
*/
private boolean isDoingAnimation;
private ValueAnimator valueAnimator;
/**
* @param startLeft views[0]的getLeft(固定)
* @param dx 偏移量
*/
public void doAnimation(final int startLeft, int dx) {
valueAnimator = ValueAnimator.ofInt(0, dx);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (!isDoingAnimation)
isDoingAnimation = true;
int animatedValue = (int) animation.getAnimatedValue();
for (int i = 0; i < views.length; i++) {
if (i == 0)
views[i].layout(startLeft + animatedValue, getTop(), startLeft + animatedValue + views[i].getWidth(), getTop() + getHeight());
else
views[i].layout(views[i - 1].getRight(), getTop(), views[i - 1].getRight() + views[i].getWidth(), getTop() + getHeight());
}
}
});
valueAnimator.addListener(new SimpleAnimationListener() {
@Override
public void onAnimationEnd(Animator animation) {
isDoingAnimation = false;
isOutBorder();
}
});
valueAnimator.setDuration(SCROLL_TIME);
valueAnimator.start();
}
在这里用到了ValueAnimator的知识,不懂戳进:推荐大神的文章
isDoingAnimation是用于判断控件有没在滑动。
一开头有这么一段片段
gestureDetector = new GestureDetector(context, new MyGesture() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (velocityX < -750 && slideState != SlideState.OPENED) {//左滑
doAnimation(views[0].getLeft(), -otherWidth - views[0].getLeft());
} else if (velocityX > 750 && slideState != SlideState.CLOSED) {//右滑
doAnimation(views[0].getLeft(), -views[0].getLeft());
}
return false;
}
});
这个手势监听的,先贴出MyGGesture()这个类
public class MyGesture implements GestureDetector.OnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}
然后就可以重写一些自己想调用的方法了。
在onTouch一开始就调用了手势
@Override
public boolean onTouch(View v, MotionEvent event) {
gestureDetector.onTouchEvent(event);
手势用到了doAnimation,然而手指抬起也用了doAnimation,如果不作滑动判断,后者会抢占了前者的动画效果!!!
附上本类全部代码:
package com.test.listviewdragdemo.view;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import com.test.listviewdragdemo.anim.SimpleAnimationListener;
import com.test.listviewdragdemo.ges.MyGesture;
/**
* 自定义可侧拉控件,第一个view占满宽度,第n+1个在第n个后面
* Created by 13798 on 2016/6/8.
*/
public class ViewWithDraged extends FrameLayout implements View.OnTouchListener {
private Context mContext;
/**
* 子view数组
*/
private View[] views;
private GestureDetector gestureDetector;
/**
* 定义滑动的时间
*/
private static final int SCROLL_TIME = 500;
/**
* 除第一个子View宽度
*/
private int otherWidth;
/**
* 定义一条中线(偏右则打开侧栏控件)
*/
private int middleLine;
/**
* 定义侧拉状态(默认为侧拉关闭)
*/
private enum SlideState {
OPENED, SLIDING, CLOSED
}
private SlideState slideState = SlideState.CLOSED;
public ViewWithDraged(Context context) {
this(context, null);
}
public ViewWithDraged(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ViewWithDraged(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
setOnTouchListener(this);
gestureDetector = new GestureDetector(context, new MyGesture() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (velocityX < -750 && slideState != SlideState.OPENED) {//左滑
doAnimation(views[0].getLeft(), -otherWidth - views[0].getLeft());
} else if (velocityX > 750 && slideState != SlideState.CLOSED) {//右滑
doAnimation(views[0].getLeft(), -views[0].getLeft());
}
return false;
}
});
}
/**
* 判断是否正在滑动
*/
private boolean isDoingAnimation;
private ValueAnimator valueAnimator;
/**
* @param startLeft views[0]的getLeft(固定)
* @param dx 偏移量
*/
public void doAnimation(final int startLeft, int dx) {
valueAnimator = ValueAnimator.ofInt(0, dx);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (!isDoingAnimation)
isDoingAnimation = true;
int animatedValue = (int) animation.getAnimatedValue();
for (int i = 0; i < views.length; i++) {
if (i == 0)
views[i].layout(startLeft + animatedValue, getTop(), startLeft + animatedValue + views[i].getWidth(), getTop() + getHeight());
else
views[i].layout(views[i - 1].getRight(), getTop(), views[i - 1].getRight() + views[i].getWidth(), getTop() + getHeight());
}
}
});
valueAnimator.addListener(new SimpleAnimationListener() {
@Override
public void onAnimationEnd(Animator animation) {
isDoingAnimation = false;
isOutBorder();
}
});
valueAnimator.setDuration(SCROLL_TIME);
valueAnimator.start();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() < 2) {
throw new RuntimeException("子控件必须大于等于2个!!");
}
views = new View[getChildCount()];
for (int i = 0; i < views.length; i++) {
views[i] = getChildAt(i);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
for (int i = 1; i < views.length; i++) {
if (i == 1)
otherWidth = 0;
views[i].layout(views[i - 1].getRight(), getTop(), views[i - 1].getRight() + views[i].getWidth(), getTop() + getHeight());
otherWidth += views[i].getWidth();
}
middleLine = getWidth() - otherWidth / 2;
}
/**
* 设置关闭状态
*/
public void setClose() {
slideState = SlideState.CLOSED;
views[0].layout(0, getTop(), getWidth(), getTop() + getHeight());
for (int i = 1; i < views.length; i++)
views[i].layout(views[i - 1].getRight(), getTop(), views[i - 1].getRight() + views[i].getWidth(), getTop() + getHeight());
}
public void setOpen() {
slideState = SlideState.OPENED;
// 设置最后一个的位置
views[getChildCount() - 1].layout(getWidth() - views[getChildCount() - 1].getWidth(), getTop(), getWidth(), getTop() + getHeight());
for (int i = views.length - 2; i >= 0; i--) {
views[i].layout(views[i + 1].getLeft() - views[i].getWidth(), getTop(), views[i + 1].getLeft(), getTop() + getHeight());
}
}
private int downX;
private int lastX;
private int downY;
private int lastY;
@Override
public boolean onTouch(View v, MotionEvent event) {
gestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = (int) event.getX();
downY = (int) event.getY();
lastX = downX;
lastY = downY;
break;
case MotionEvent.ACTION_MOVE:
final int startX = (int) event.getX();
final int startY = (int) event.getY();
final int dX = startX - lastX;
if (Math.abs(startX - downX) > Math.abs(startY - downY)) {
if (valueAnimator!=null && valueAnimator.isRunning())
valueAnimator.cancel();
if (!canSliding(dX))
return true;
if (!isOutBorder()) {
setLayoutByx(dX);
}
isOutBorder(dX);
}
lastX = startX;
break;
case MotionEvent.ACTION_UP:
if (slideState == SlideState.SLIDING && !isDoingAnimation) {
// 超过中线实现右滑效果
if (views[0].getRight() > middleLine) {
doAnimation(views[0].getLeft(), -views[0].getLeft());
} else {
doAnimation(views[0].getLeft(), -otherWidth - views[0].getLeft());
}
}
break;
}
return true;
}
private boolean canSliding(int dX) {
if (dX >= 0 && slideState == SlideState.CLOSED || dX <= 0 && slideState == SlideState.OPENED)
return false;
return true;
}
/**
* 通过滑动移动x
*
* @param dx 偏移量
*/
public void setLayoutByx(int dx) {
for (int i = 0; i < views.length; i++) {
final int newLeft = views[i].getLeft() + dx;
views[i].layout(newLeft, getTop(), newLeft + views[i].getWidth(), getTop() + getHeight());
}
}
/**
* 判断是否越界
*
* @return
*/
private boolean isOutBorder() {
// 关闭状态
if (views[0].getLeft() >= 0 && slideState != SlideState.CLOSED) {
getParent().requestDisallowInterceptTouchEvent(false);
if (listener != null) {
listener.close();
}
if (slideState != SlideState.CLOSED) {
slideState = SlideState.CLOSED;
}
return true;
// 打开状态
} else if (views[getChildCount() - 1].getRight() <= getWidth() && slideState != SlideState.OPENED) {
if (listener != null)
listener.open();
getParent().requestDisallowInterceptTouchEvent(false);
if (slideState != SlideState.OPENED) {
slideState = SlideState.OPENED;
}
return true;
}
// 滑动状态
if (slideState != SlideState.SLIDING) {
getParent().requestDisallowInterceptTouchEvent(true);
slideState = SlideState.SLIDING;
}
return false;
}
/**
* 判断是否越界(自动纠正)
*
* @param dx 偏移量
*/
private void isOutBorder(int dx) {
if (views[0].getLeft() > 0 || views[views.length - 1].getRight() < getWidth())
setLayoutByx(-dx);
}
private StateListener listener;
public interface StateListener {
void open();
void close();
}
public void setStateListener(StateListener listener) {
this.listener = listener;
}
}