侧滑删除菜单 SwipeMenuLayout

来源:互联网 发布:蔡司编程视频教程 编辑:程序博客网 时间:2024/05/22 00:20

侧滑菜单在列表布局中越来越常见,其良好的交互为 App 增色了不好,在 Android 中,其实现方式也有很多种,本文是基于自定义 ViewGroup 方式实现,使用时在列表 item 布局中引入该 Layout 即可。

实现效果图:
rightMenu


leftMenu

所用知识点:
- 自定义 ViewGroup
- ScrollTo() 和 ScrollTo() 区别及用法
- getScrollX(),getScrollY() 表示的意义及用法
- Scroller用法
- Android 事件分发机制


1. ScrollTo() 和 ScrollBy() 区别及用法

scrollTo() : 滑动到指定坐标位置点,是绝对滑动。
scrollBy() : 相对于当前位置滑动一段距离,是相对滑动,其内部实现是基于 scrollTo() 实现的,在当前位置坐标点加上滑动距离。

注意:滑动的是 View 的内容,并不是滑动 View 本身。

2. getScrollX() 和 getScrollY() 表示意义和用法

这里写图片描述

图上面,褐色的框,其实就是我们眼睛看到的手机界面,就是一个窗口。
而绿色的长方体呢,就是一块可以左右拉动的幕布啦,其实也就是我们要显示在窗口上面的内容,它其实是可以很大的,大到无限大,只是没在窗口中间的,所以我们就看不到。
而getScrollX 其实获取的值,就是这块 幕布在窗口左边界时候的值了,而幕布上面哪个点是原点(0,0)呢?就是初始化时内容显示的位置。
所以当我们将幕布往右推动的时候,幕布在窗口左边界的值就会在0的左边(-100),而向左推动,则其值会是在0的右边(100)。

举例:
这里写图片描述

效果:
getScrollX()

3.Scroller 用法

(1).原理介绍:
scrollTo() 和 scrollBy() 实现的是一个结果,即是说,当调用scrollTo(100,0) 时,再重新绘制时,内容已经出现在(100,0)位置上,缺少一个移动的过程,而 Scroller 就是帮助我们实现这个滚动的过程的。

动画的原理其实不停的重绘位置变化的内容,在视觉效果上,就产生了动画的效果。

这里写图片描述

(2) 使用步骤:

  • 创建 Scroller 对象,一般是在 构造方法中创建。
private Scroller mScroller;public SwipeMenuLayout(Context context) {    this(context, null);}public SwipeMenuLayout(Context context, AttributeSet attrs) {    this(context, attrs, 0);}public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr);    mScroller = new Scroller(context);}
  • 重写 自定义 View 的 computeScroll() 方法。下面代码基本不会变化。
@Overridepublic void computeScroll() {    super.computeScroll();    if (mScroller.computeScrollOffset()) {  // 动画没有结束        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());        postInvalidate();    }}
  • 调用 Scroller 的 startScroll()方法,并 invalidate() 重绘 View.
mScroller.startScroll(int startX,int startY,int dx,int dy);  // startX起始坐标,dx 偏移量invalidate();

4.自定义 ViewGroup - SwipeMenuLayout

  • 自定义属性 attrs.xml
<resources>    <declare-styleable name="SwipeMenuLayout">        <attr name="leftMenuId" format="reference" />        <attr name="rightMenuId" format="reference" />        <attr name="contentId" format="reference" />    </declare-styleable></resources>

SwipeMenuLayout.java

public class SwipeMenuLayout extends ViewGroup {    private static final String TAG = "SwipeMenuLayout";    private Scroller mScroller;    private int mScaledTouchSlop;    private int leftMenuId;    private int rightMenuId;    private View leftMenuView;    private View rightMenuView;    private View contentView;    private int contentId;    private boolean isSwipeing;    // 静态类写入内存共享。用来判断当前界面是否有menu打开    private static SwipeMenuLayout swipeMenuLayout;    private static State curState;    private static boolean isTouching = false;    public SwipeMenuLayout(Context context) {        this(context, null);    }    public SwipeMenuLayout(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        readAttrs(context, attrs);        // 1.创建 Scroller 对象        mScroller = new Scroller(context);        ViewConfiguration viewConfiguration = ViewConfiguration.get(context);        mScaledTouchSlop = viewConfiguration.getScaledTouchSlop();    }    private void readAttrs(Context context, AttributeSet attrs) {        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SwipeMenuLayout);        try {            leftMenuId = typedArray.getResourceId(R.styleable.SwipeMenuLayout_leftMenuId, 0);            rightMenuId = typedArray.getResourceId(R.styleable.SwipeMenuLayout_rightMenuId, 0);            contentId = typedArray.getResourceId(R.styleable.SwipeMenuLayout_contentId, 0);        } finally {            typedArray.recycle();        }    }    /**     * 测量方法可能会被调用多次     *     * @param widthMeasureSpec     * @param heightMeasureSpec     */    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        setClickable(true);        int viewHeight = 0;        int viewWidth = 0;        int childCount = getChildCount();        for (int i = 0; i < childCount; i++) {            View childView = getChildAt(i);            childView.setClickable(true);            if (childView.getVisibility() == View.GONE) {                continue;            }            measureChild(childView, widthMeasureSpec, heightMeasureSpec);            viewHeight = Math.max(viewHeight, childView.getMeasuredHeight());            Log.d(TAG, "onMeasure: getMeasureWidth() = " + i + "," + +childView.getMeasuredWidth());            viewWidth += childView.getMeasuredWidth();        }        setMeasuredDimension(viewWidth, viewHeight);    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        Log.d(TAG, "onLayout: l = " + l + ",t = " + t + ",r = " + r + ",b = " + b);        int childCount = getChildCount();        for (int i = 0; i < childCount; i++) {            View childView = getChildAt(i);            if (leftMenuView == null && childView.getId() == leftMenuId) {                leftMenuView = childView;                continue;            }            if (rightMenuView == null && childView.getId() == rightMenuId) {                rightMenuView = childView;                continue;            }            if (contentView == null && childView.getId() == contentId) {                contentView = childView;            }        }        Log.d(TAG, "onLayout: leftMenuView.getMeasureWidth() = " + leftMenuView.getMeasuredWidth());        Log.d(TAG, "onLayout: contentView.getMeasureWidth() = " + contentView.getMeasuredWidth());        Log.d(TAG, "onLayout: rightMenuView.getMeasureWidth() = " + rightMenuView.getMeasuredWidth());        // 布局 leftMenu        if (leftMenuView != null) {            leftMenuView.layout(-leftMenuView.getMeasuredWidth(), t, 0, b);        }        // 布局 contentView        if (contentView != null) {            contentView.layout(0, t, contentView.getMeasuredWidth(), b);        }        // 布局 rightMenu        if (rightMenuView != null) {            rightMenuView.layout(contentView.getMeasuredWidth(), t,                    contentView.getMeasuredWidth() + rightMenuView.getMeasuredWidth(), b);        }    }    // 2. 重写 computeScroll()    @Override    public void computeScroll() {        super.computeScroll();        if (mScroller.computeScrollOffset()) {  // 动画没有结束            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());            //通知View重绘-invalidate()->onDraw()->computeScroll()            postInvalidate();        }    }    private PointF lastPoint;    // 记录第一次触摸点的坐标,方便计算手指抬起时,总的滑动距离    private PointF firstPoint;    // 手指按下到抬起,总的滑动距离    float finalDistance;    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        switch (ev.getAction()) {            case MotionEvent.ACTION_DOWN:                if (isTouching) {                    return false;                }                isTouching = true;                isSwipeing = false;                if (firstPoint == null) {                    firstPoint = new PointF();                }                if (lastPoint == null) {                    lastPoint = new PointF();                }                // 当前触摸的不是已经打开的那个 SwipeMenuLayout,则需要将打开的那个关闭掉。                if (swipeMenuLayout != null) {                    if (swipeMenuLayout != this) {                        // 调用已经打开的那个 SwipeMenuLayout 关闭方法                        swipeMenuLayout.handleSwipeMenu(State.CLOSE);//                        getParent().requestDisallowInterceptTouchEvent(true);                    }                }                firstPoint.set(ev.getX(), ev.getY());                lastPoint.set(ev.getX(), ev.getY());                break;            case MotionEvent.ACTION_MOVE:                // 偏移量 = 当前坐标值 - 上次坐标值                int dx = (int) (ev.getX() - lastPoint.x);                int dy = (int) (ev.getY() - lastPoint.y);                // scrollBy 移动,正值内容向左移动,负值内容向右移动                if (Math.abs(dx) > Math.abs(dy)) {                    scrollBy(-dx, 0);                }                // 边界限定                if (getScrollX() > 0) {  // 向左滑动,滑出 rightMenuView                    if (rightMenuView != null) {  // 存在 rightMenuView,滑动距离不能超过 rightMenuView 宽度                        if (getScrollX() > rightMenuView.getMeasuredWidth()) {                            scrollTo(rightMenuView.getMeasuredWidth(), 0);                        }                    } else {  // 不存在 rightMenuView,禁止向左滑动                        scrollTo(0, 0);                    }                } else if (getScrollX() < 0) {  // 向右滑动,滑出 leftMenuView                    if (leftMenuView != null) {  // 存在 leftMenuView,滑动距离不能大于 leftMenuView 宽度                        if (getScrollX() < -leftMenuView.getMeasuredWidth()) {  // getScrollX()是负值,                            scrollTo(-leftMenuView.getMeasuredWidth(), 0);                        }                    } else {                        scrollTo(0, 0);                    }                }                // 当水平滑动时,请求父控件不要拦截事件                if (Math.abs(dx) > mScaledTouchSlop) {                    getParent().requestDisallowInterceptTouchEvent(true);                }                lastPoint.set(ev.getX(), ev.getY());                break;            case MotionEvent.ACTION_UP:            case MotionEvent.ACTION_CANCEL:                isTouching = false;                finalDistance = ev.getX() - firstPoint.x;                if (Math.abs(finalDistance) > mScaledTouchSlop) {                    isSwipeing = true;                }                State state = isShouldOpenMenu(getScrollX());                handleSwipeMenu(state);                break;            case MotionEvent.ACTION_POINTER_UP:                break;        }        return super.dispatchTouchEvent(ev);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        switch (ev.getAction()) {            case MotionEvent.ACTION_DOWN:                break;            case MotionEvent.ACTION_MOVE:                //滑动时拦截点击时间                if (Math.abs(finalDistance) > mScaledTouchSlop) {                    // 当手指拖动值大于mScaledTouchSlop值时,认为应该进行滚动,拦截子控件的事件                    return true;                }                break;            case MotionEvent.ACTION_UP:            case MotionEvent.ACTION_CANCEL:                //滑动后不触发contentView的点击事件                if (isSwipeing) {                    isSwipeing = false;                    finalDistance = 0;                    return true;                }////                if (getX() < getScreenWidth() - rightMenuView.getMeasuredWidth()) {//                    return true;//                }                break;        }        return super.onInterceptTouchEvent(ev);    }    private void handleSwipeMenu(State state) {        if (state == State.RIGHT_MENU_OPEN) {            swipeMenuLayout = this;            mScroller.startScroll(getScrollX(), 0, rightMenuView.getMeasuredWidth() - getScrollX(), 0);            curState = state;        } else if (state == State.LEFT_MENU_OPEN) {            swipeMenuLayout = this;            // getScrollX() 为负值            mScroller.startScroll(getScrollX(), 0, -getScrollX() - leftMenuView.getMeasuredWidth(), 0);            curState = state;        } else if (state == State.CLOSE) {            mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0);            swipeMenuLayout = null;            curState = null;        }        //通知View重绘-invalidate()->onDraw()->computeScroll()        invalidate();    }    private State isShouldOpenMenu(int scrollX) {        // (1) scrollX > 0 : 表明现在处于 rightMenuView 打开状态,根据临界值决定是关闭,还是继续打开        if (scrollX > 0) {            if (finalDistance < 0) {        // 左滑                if (rightMenuView != null && scrollX > mScaledTouchSlop) {                    return State.RIGHT_MENU_OPEN;                }            } else if (finalDistance > 0) {  // 右滑                if (rightMenuView != null && scrollX < rightMenuView.getMeasuredWidth() - mScaledTouchSlop) {                    return State.CLOSE;                }            }        } else if (scrollX < 0) { // (2)scrollX < 0:表明现在处于 leftMenuView 打开状态,根据临界值是否打开,还是关闭            if (finalDistance < 0) { //  左滑                if (leftMenuView != null && Math.abs(scrollX) > mScaledTouchSlop) {                    return State.CLOSE;                }            } else if (finalDistance > 0) {  // 右滑                if (leftMenuView != null && Math.abs(scrollX) > mScaledTouchSlop) {                    return State.LEFT_MENU_OPEN;                }            }        }        return State.CLOSE;    }    @Override    protected void onAttachedToWindow() {        super.onAttachedToWindow();        if (this == swipeMenuLayout) {            swipeMenuLayout.handleSwipeMenu(curState);        }    }    @Override    protected void onDetachedFromWindow() {        super.onDetachedFromWindow();        if (this == swipeMenuLayout) {            swipeMenuLayout.handleSwipeMenu(State.CLOSE);        }    }    public int getScreenWidth() {        return getResources().getDisplayMetrics().widthPixels;    }    enum State {        LEFT_MENU_OPEN,        RIGHT_MENU_OPEN,        CLOSE    }}

ListView item 布局 item_list_view.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:layout_height="70dp"    android:orientation="vertical">    <com.xing.swipemenulayoutlibrary.SwipeMenuLayout        android:id="@+id/swipeMenuLayout"        android:layout_width="match_parent"        android:layout_height="70dp"        app:contentId="@+id/content_view"        app:leftMenuId="@+id/left_menu"        app:rightMenuId="@+id/right_menu">        <LinearLayout            android:id="@+id/left_menu"            android:layout_width="80dp"            android:layout_height="match_parent"            android:orientation="horizontal">            <TextView                android:layout_width="80dp"                android:layout_height="match_parent"                android:background="@android:color/holo_blue_dark"                android:gravity="center"                android:text="LeftMenu"                android:textColor="@android:color/white" />        </LinearLayout>        <TextView            android:id="@+id/content_view"            android:layout_width="match_parent"            android:layout_height="match_parent"            android:gravity="center_vertical"            android:paddingLeft="16dp"            android:text="Android 8.0 奥利奥来了"            android:textColor="@android:color/black" />        <LinearLayout            android:id="@+id/right_menu"            android:layout_width="240dp"            android:layout_height="match_parent"            android:orientation="horizontal">            <TextView                android:layout_width="80dp"                android:layout_height="match_parent"                android:background="#D9DEE4"                android:gravity="center"                android:text="Top"                android:textColor="@android:color/white" />            <TextView                android:id="@+id/tv_add"                android:layout_width="80dp"                android:layout_height="match_parent"                android:background="#ECD50A"                android:gravity="center"                android:text="Add"                android:textColor="@android:color/white"                android:textSize="16sp" />            <TextView                android:id="@+id/tv_delete"                android:layout_width="80dp"                android:layout_height="match_parent"                android:background="#FF4A57"                android:gravity="center"                android:text="Delete"                android:textColor="@android:color/white"                android:textSize="16sp" />        </LinearLayout>    </com.xing.swipemenulayoutlibrary.SwipeMenuLayout></LinearLayout>
原创粉丝点击