跳至主要內容

12.侧滑删除,slideView例子


分析

最近在群里有同学问我怎么写侧滑删除的控件,其实这个东西,我在公司里给做其他开发的同学培训过,现场写代码,所以现在写这篇文章应该也是可以直接就写代码的了!

我们要做一个侧滑删除的控件,第一步,我们就是要确定这个控件是View还是ViewGroup.有些同学可能不知道什么是View,什么是ViewGroup。

View:android里的控件都是View,包括ViewGroup,而ViewGroup则是继承自View,Group就是组的意思,所以ViewGroup其实是容器View,用于放置View的,比如说LinearLayout,RelativeLayout,这些Layout里面可以放置其他的ViewGroup和View吧,举个例子:你LinearLayout里可以继续放置LinearLayout吧,也可以放置Button,textView之类的。而View,里面不能再放View了,比如说,button里就不能再放东西了吧,TextView也不能做为一个容器里头再放其他的控件了吧!

关于命名,View的命名方式一般是:XxxView,Button除外,呵呵!比如说,TextView,ImageView。而ViewGroup的命名则是XxxLayout,比如说:LinearLayout,RelativeLayout

OK,到这里理解了View跟ViewGroup了吗?

我们继续分析我们的这个滑动删除控件,其实你把它看成View也行,看成ViewGroup也行。如果是看成View,那功能比较单一,也就是个滑动删除了。如果是个ViewGroup,那里面的东西由你自己放,那天你放图片进去也是可以滑动的嘛!

接下来的例子,我们将以ViewGroup的形式来实现!

实现过程

前面我们说到了,我们把它看成一个ViewGroup,第一步,我们就是要写一个类,继承自ViewGroup:

Snip20180805_15.png
Snip20180805_15.png

实现三个构造函数,覆写onLayout方法,这个是必须的。同学们有没有发现,我的构造方法都调用了this,其实就是不不管使用的地方是通过哪个构造方法调用进来的,都走到第三个构造方法,这样子我们可以统一地去处理一些逻辑代码,否则要独立出来,在三个方法里面单独调用。

为什么要强制实现onLayout呢?因为你要摆放孩子呀,你是一个ViewGroup呀,对不对!

这一步完成了,我们接下来干嘛呢!

大家要知道,写自定义控件的套路是固定的,我们这里实现的是ViewGrop,我们不需要把孩子画出来,这是由孩子自己完成的。ViewGroup的本质是什么呢?ViewGroup的本质是管理孩子。

所以接下来,我们是不是要摆放孩子呢,但是怎么摆放,我们是不是要先知道孩子的大小,并且摆放到什么位置上。

因 此,我们要获取到孩子的大小!

@Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        int childCount = getChildCount();
        Log.d(TAG, "childCount -- > " + childCount);
        //我们限定孩子的个数为2个,多了不行,少了不行。
        if (childCount != 2) {
            throw new IllegalArgumentException("It must be two child in this layout.");
        }
        //孩子判断OK,
        //那么就拿到孩子的大小
        View contentView = getChildAt(0);
        View deleteView = getChildAt(1);
        LayoutParams contentLayoutParams = contentView.getLayoutParams();
        int contentHeight = contentLayoutParams.height;
        Log.d(TAG, "contentHeight -- > " + contentHeight);
        //内容的宽度不需要,我们直接让它跟老爸一样宽就可以了
        //删除内容的高度和内容的高度一样,所以我们可以不拿了
        //但是删除的宽度我们需要吧
        LayoutParams deleteViewLayoutParams = deleteView.getLayoutParams();
        int deleteWidth = deleteViewLayoutParams.width;
        Log.d(TAG, "deleteWidth -- > " + deleteWidth);
    }

于是我们引用一下,并且写上两个孩子在这个控件的里头:

Snip20180805_16.png
Snip20180805_16.png
Snip20180805_17.png
Snip20180805_17.png

然后跑起来:

Snip20180805_18.png
Snip20180805_18.png

可以看到打出来结果,正如我们所需要!

接下来,我们覆写测量方法


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量第一个孩子
        //宽度直接跟老爸一样大,所以直接使用老爸的宽度即可
        //高度使用我们取到的值,但需要构造一下
        int contentHeightMeasureSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.EXACTLY);
        mContentView.measure(widthMeasureSpec, contentHeightMeasureSpec);

        //测量第二个孩子
        //高度跟第一个孩子的高度一样高,但是宽度则是第二个孩子的宽度,所以
        int deleteWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mDeleteWidth, MeasureSpec.EXACTLY);
        mDeleteView.measure(deleteWidthMeasureSpec,contentHeightMeasureSpec);
    }

关于测量的方法,可以去翻看以前的博客了,呜呜,我这里就不去找了!

测量好了,当然就是摆放孩子的位置啦!

到onLayout方法了!

@Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        //这四个参数的意思是
        //第一个,布局是否有改变
        //第二个,左边,
        //第三个,上边,
        //第四个,右边,
        //第五个,下边

        //摆放第一个孩子:
        int contentLeft = 0;
        int contentTop = 0;
        int contentRight = contentLeft + mContentView.getMeasuredWidth();
        int contentBottom = contentTop + mContentView.getMeasuredHeight();
        mContentView.layout(contentLeft, contentTop, contentRight, contentBottom);
        //摆放第二个孩子:
        int delLeft = contentRight;
        int delTop = 0;
        int delRight = delLeft + mDeleteView.getMeasuredWidth();
        int delBottom = mDeleteView.getMeasuredHeight() + delTop;
        mDeleteView.layout(delLeft, delTop, delRight, delBottom);

    }

写好以后,我们可以预览一下: Snip20180805_19.png 可以看到,两个孩子都已经摆放好了!到这里, 同学们是不是已经知道了LinearLayout是怎么实现了呢?

孩子摆放好了,接下来要进行事件处理吧:比如说怎么样拖出来,拖出来的范围处理!

所以我们要覆写onTouch方法了吧!

处理触摸事件!

Snip20180805_20.png
Snip20180805_20.png

先完成移动的功能,边界和释放的位置我们稍后处理,运行结果如下图:

Untitled6.gif
Untitled6.gif

接下来,要处理越界问题了吧:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //触摸到屏幕
                //拿到触摸下来的点,这里我们暂时不管角度的问题,只管x方向的移动,其实我们在实际开发的时候,还要处理角度问题的。
                mDownX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                //手指滑动
                float moveX = event.getX();
                //计算出dx
                int dx = (int) (moveX - mDownX);
                // Log.d(TAG, "dx -- > " + dx);
                //进行移动,要考虑到方向问题,这里我们移动的是屏幕,所以方向相反,加个负号。
                int scrollX = getScrollX();
                // Log.d(TAG, "scrollX -- > " + scrollX);
                //边界处理,方式不唯一,可以有很多种方法处理呢。
                if (-dx + scrollX < 0) {
                    scrollTo(0, 0);
                } else if (-dx + scrollX > mDeleteView.getMeasuredWidth()) {
                    scrollTo(mDeleteView.getMeasuredWidth(), 0);
                } else {
                    scrollBy(-dx, 0);
                }
                mDownX = moveX;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                //手指离开屏幕
                //决定是打开,还是还原到原来的位置
                //判断是打开,还是关闭
                int scrolledX = getScrollX();
                if (scrolledX > mDeleteView.getMeasuredWidth() / 2) {
                    //显示出来了超过了一半的位置,所以显示
                    scrollTo(mDeleteView.getMeasuredWidth(), 0);
                } else {
                    scrollTo(0, 0);
                }
                break;
        }
        return true;
    }

这么处理以后呢,就变成以下的结果了:

Untitled6.gif
Untitled6.gif

但是,回滚动回去的时候,看起来很生硬,所以我们要做一个阻尼的效果,手释放的时候,慢慢地回去

package com.sunofbeaches.slideviewdemo;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

/**
 * Created by TrillGates on 18/8/5.
 * God bless my code!
 */
public class SlideLayout extends ViewGroup {

    private static final String TAG = "SlideLayout";
    private int mContentHeight;
    private int mDeleteWidth;
    private View mContentView;
    private View mDeleteView;
    private float mDownX;
    private Scroller mScroller;

    public SlideLayout(Context context) {
        this(context, null);
    }

    public SlideLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScroller = new Scroller(context);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        int childCount = getChildCount();
        Log.d(TAG, "childCount -- > " + childCount);
        //我们限定孩子的个数为2个,多了不行,少了不行。
        if (childCount != 2) {
            throw new IllegalArgumentException("It must be two child in this layout.");
        }
        //孩子判断OK,
        //那么就拿到孩子的大小
        mContentView = getChildAt(0);
        mDeleteView = getChildAt(1);
        LayoutParams contentLayoutParams = mContentView.getLayoutParams();
        mContentHeight = contentLayoutParams.height;
        Log.d(TAG, "contentHeight -- > " + mContentHeight);
        //内容的宽度不需要,我们直接让它跟老爸一样宽就可以了
        //删除内容的高度和内容的高度一样,所以我们可以不拿了
        //但是删除的宽度我们需要吧
        LayoutParams deleteViewLayoutParams = mDeleteView.getLayoutParams();
        mDeleteWidth = deleteViewLayoutParams.width;
        Log.d(TAG, "deleteWidth -- > " + mDeleteWidth);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量第一个孩子
        //宽度直接跟老爸一样大,所以直接使用老爸的宽度即可
        //高度使用我们取到的值,但需要构造一下
        int contentHeightMeasureSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.EXACTLY);
        mContentView.measure(widthMeasureSpec, contentHeightMeasureSpec);

        //测量第二个孩子
        //高度跟第一个孩子的高度一样高,但是宽度则是第二个孩子的宽度,所以
        int deleteWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mDeleteWidth, MeasureSpec.EXACTLY);
        mDeleteView.measure(deleteWidthMeasureSpec, contentHeightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        //这四个参数的意思是
        //第一个,布局是否有改变
        //第二个,左边,
        //第三个,上边,
        //第四个,右边,
        //第五个,下边

        //摆放第一个孩子:
        int contentLeft = 0;
        int contentTop = 0;
        int contentRight = contentLeft + mContentView.getMeasuredWidth();
        int contentBottom = contentTop + mContentView.getMeasuredHeight();
        mContentView.layout(contentLeft, contentTop, contentRight, contentBottom);
        //摆放第二个孩子:
        int delLeft = contentRight;
        int delTop = 0;
        int delRight = delLeft + mDeleteView.getMeasuredWidth();
        int delBottom = mDeleteView.getMeasuredHeight() + delTop;
        mDeleteView.layout(delLeft, delTop, delRight, delBottom);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //触摸到屏幕
                //拿到触摸下来的点,这里我们暂时不管角度的问题,只管x方向的移动,其实我们在实际开发的时候,还要处理角度问题的。
                mDownX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                //手指滑动
                float moveX = event.getX();
                //计算出dx
                int dx = (int) (moveX - mDownX);
                // Log.d(TAG, "dx -- > " + dx);
                //进行移动,要考虑到方向问题,这里我们移动的是屏幕,所以方向相反,加个负号。
                int scrollX = getScrollX();
                // Log.d(TAG, "scrollX -- > " + scrollX);
                //边界处理,方式不唯一,可以有很多种方法处理呢。
                if (-dx + scrollX < 0) {
                    scrollTo(0, 0);
                } else if (-dx + scrollX > mDeleteView.getMeasuredWidth()) {
                    scrollTo(mDeleteView.getMeasuredWidth(), 0);
                } else {
                    scrollBy(-dx, 0);
                }
                mDownX = moveX;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                //手指离开屏幕
                //决定是打开,还是还原到原来的位置
                //判断是打开,还是关闭
                //一般来说,这个时间是动态计算的,这里就交给同学们去做啦,如果距离长一点,那么就时间 长一点,距离段,时间 可以短一点。
                if (getScrollX() > mDeleteView.getMeasuredWidth() / 2) {
                    // 全部显示菜单部分
                    mScroller.startScroll(getScrollX(), 0, mDeleteView.getMeasuredWidth() - getScrollX(), 0, 500);
                } else {
                    // 显示 内容部分
                    mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 500);
                }
                invalidate();
                break;
        }
        return true;
    }

    /**
     * 这个方法用于生成剃度数据,就是让我们连续scroll到一个位置
     * 看起来是流畅的滑动,达到一个阻尼的效果。
     */
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
            invalidate();
        }
    }
}

效果是这样子的:

Untitled62.gif
Untitled62.gif

好啦,到这里的话,控件的效果就实现了,接下来,我们应该是暴露接口和相关的方法了吧!

封装控件

你这个控件是给别人用的嘛,所以你是不是要暴露出一些接口和判断的方法出来,比如说,判断当前的状态,是打开,还是关闭。还有就是设置通知接口,对吧!

所以我们定义以下接口:

public boolean isOpen() {
        return mIsOpen;
    }


    private OnSlideStateListener mOnSlideStateListener = null;

    /**
     * 暴露给外面设置
     *
     * @param listener
     */
    public void setOnSlideStateListener(OnSlideStateListener listener) {
        this.mOnSlideStateListener = listener;
    }

    /**
     * 状态接口,如果外面使用的人需要监听的话,那么就设置一个实现进来即可。
     */
    public interface OnSlideStateListener {

        void open();

        void close();
    }

在什么地方改变呢?当然是当我们的手离开的时候啦:

case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                //手指离开屏幕
                //决定是打开,还是还原到原来的位置
                //判断是打开,还是关闭
                //一般来说,这个时间是动态计算的,这里就交给同学们去做啦,如果距离长一点,那么就时间 长一点,距离段,时间 可以短一点。
                if (getScrollX() > mDeleteView.getMeasuredWidth() / 2) {
                    // 全部显示菜单部分
                    mScroller.startScroll(getScrollX(), 0, mDeleteView.getMeasuredWidth() - getScrollX(), 0, 500);
                    mOnSlideStateListener.open();
                    mIsOpen = true;
                } else {
                    // 显示 内容部分
                    mOnSlideStateListener.close();
                    mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 500);
                    mIsOpen = false;
                }
                invalidate();
                break;

好啦,到这里我们这控件基本写完了,后面还要添加删除的点击事件,外面实现,这样子就可以监听删除被点击了。

总结

自定义控件的几个步骤,今天这篇文章是ViewGroup的定制,本质就是对子控件进行管理。

也就是进行摆放,要摆放我们先要进行测量,要测量我们先要获得各个控件的大小。

一步一步逆推回去,如果 孩子摆放好了, 接下来就是处理动作事件了,什么情况下,控件的摆放怎么变化,实现的方式有很多种,思想才是重要的。

除了这种实现方式以外,也可以动态地改变布局位置,也可以挪动控件的位置。

好啦,祝大家学习快乐!有问题到群里或者网站上提问吧!