自定义侧拉删除(没有嵌套ListView)

先上效果图:


先看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;
    }
}



观看后,可以继续看博主相关的下一遍文章: 

自定义侧拉删除(嵌套ListView)


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值