侧滑删除菜单 SwipeMenuLayout

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

实现效果图:
侧滑删除菜单 SwipeMenuLayout_第1张图片


侧滑删除菜单 SwipeMenuLayout_第2张图片

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


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

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

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

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

侧滑删除菜单 SwipeMenuLayout_第3张图片

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

举例:
侧滑删除菜单 SwipeMenuLayout_第4张图片

效果:
侧滑删除菜单 SwipeMenuLayout_第5张图片

3.Scroller 用法

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

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

侧滑删除菜单 SwipeMenuLayout_第6张图片

(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() 方法。下面代码基本不会变化。
@Override
public 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


<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>

你可能感兴趣的:(Android,自定义,View)