解决SlidingPaneLayout的滑动冲突

来源:互联网 发布:淘宝网sns平台 编辑:程序博客网 时间:2024/05/19 12:18

最近2天在写自己的小项目,系图书馆管理系统。打开APP后的界面的布局是左边一个侧拉菜单,右边主界面的内容为一个ViewPager,可以经行滑动和点击选项卡切换。
最后的截图如图:
这里写图片描述
so,xml的布局代码大致如下:

activity_main.xml

<views.PagerEnableSlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <include layout="@layout/main_menu"/>    <include layout="@layout/content_main"/></views.PagerEnableSlidingPaneLayout>

这里的PagerEnableSlidingPaneLayout就是重写过的SlidingPaneLayout,因为如果直接使用SlidingPaneLayout的话,右侧区域的主视图里面的ViewPager会和它发生滑动冲突。手指从右往左滑动没有问题,但是手指从左向右滑动的时候,会将侧拉菜单拉出,ViewPager的切页效果消失。所以,为了消除滑动冲突,我们需要重写SlindingPaneLayout这个类。
由于自己的实力有限,所以参考了Stack Overflow上面的一个相同问题的回答。他对SlindingPaneLayout的重写如下:

PagerEnabledSlidingPaneLayout.java

import android.content.Context;import android.support.v4.view.MotionEventCompat;import android.support.v4.widget.SlidingPaneLayout;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.ViewConfiguration;/** * SlidingPaneLayout that, if closed, checks if children can scroll before it intercepts * touch events.  This allows it to contain horizontally scrollable children without * intercepting all of their touches. * * To handle cases where the user is scrolled very far to the right, but should still be * able to open the pane without the need to scroll all the way back to the start, this * view also adds edge touch detection, so it will intercept edge swipes to open the pane. */public class PagerEnabledSlidingPaneLayout extends SlidingPaneLayout {    private float mInitialMotionX;    private float mInitialMotionY;    private float mEdgeSlop;    public PagerEnabledSlidingPaneLayout(Context context) {        this(context, null);    }    public PagerEnabledSlidingPaneLayout(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public PagerEnabledSlidingPaneLayout(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);        ViewConfiguration config = ViewConfiguration.get(context);        mEdgeSlop = config.getScaledEdgeSlop();    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        switch (MotionEventCompat.getActionMasked(ev)) {            case MotionEvent.ACTION_DOWN: {                mInitialMotionX = ev.getX();                mInitialMotionY = ev.getY();                break;            }            case MotionEvent.ACTION_MOVE: {                final float x = ev.getX();                final float y = ev.getY();                // The user should always be able to "close" the pane, so we only check                // for child scrollability if the pane is currently closed.                if (mInitialMotionX > mEdgeSlop && !isOpen() && canScroll(this, false,                        Math.round(x - mInitialMotionX), Math.round(x), Math.round(y))) {                    // How do we set super.mIsUnableToDrag = true?                    // send the parent a cancel event                    MotionEvent cancelEvent = MotionEvent.obtain(ev);                    cancelEvent.setAction(MotionEvent.ACTION_CANCEL);                    return super.onInterceptTouchEvent(cancelEvent);                }            }        }        return super.onInterceptTouchEvent(ev);    }}

如果嫌代码长不好读,那么我们把核心的部分抽出来

  1. 获取ViewConfiguration的一个参数
ViewConfiguration configuration = ViewConfiguration.get(context);mEdgeSlop = configuration.getScaledEdgeSlop();

源码对getScaledEdgeSlop()方法的描述如下:
Inset in pixels to look for touchable content when the user touches the edge of the screen
大概意思就是滑动的距离满足系统定义的从溢出屏幕处开始水平滑动的一段距离。这让我想到了DrawerLayout的侧拉菜单的拉出方式。也就是从屏幕的最最左侧拉。

2. 在触摸事件触发的时候进行判断核心代码如下:
    switch (MotionEventCompat.getActionMasked(ev)) {            case MotionEvent.ACTION_DOWN:                mInitialMotionX = ev.getX();                break;            case MotionEvent.ACTION_MOVE:                final float x = ev.getX();                final float y = ev.getY();                if (mInitialMotionX > mEdgeSlop && !isOpen() && canScroll(this, false, Math.round(x - mInitialMotionX),                        Math.round(x), Math.round(y))) {                    MotionEvent cancelEvent = MotionEvent.obtain(ev);                    cancelEvent.setAction(MotionEvent.ACTION_DOWN);                    return super.onInterceptTouchEvent(cancelEvent);                }                break;            default:                break; 
这段代码的意思就是在MOVE事件的时候,如果同时满足一下3种情况,就继续执代码中的MotionEvent.ACTION_CANCEL)的事件传递。不再传递MOVE事件,就把SlidingPaneLayout的滑动事件给忽略了。① 手指按下的点的横坐标超过系统定义的getScaledEdgeSlop(),但还是觉得这点没有理解透。大概理解的“是未能从屏幕最左侧开始滑动”。② 侧拉菜单处在关闭状态(这点比较容易理解)③ 这点也理解的很勉强,因为没看懂canScroll这个函数的作用。大概理解的就是判断子view是否能滑动。

总之,将原先的SlidingPaneLayout替换成这个自定义的SlidingPaneLayout之后,就有了本偏博客开头的那个效果。在ViewPager处在第一页的时候,才可以右滑打开侧拉菜单。如果不处于第一页,从屏幕最左侧开始滑动也可以打开侧拉菜单。

下面总结一下我不懂的几点:
① 到底如何理解configuration.getScaledEdgeSlop()获取到的那个常量的意义?源码中简单的一句话我没有理解过来。我现在理解的是如果手指开始触摸到的坐标比这个量大,那就不能拉出菜单,手指从屏幕最左侧开始的时候是可以打开菜单的。就想知乎客户端的DrawerLayout一样,不知道理解是否正确?
② SldingPaneLayout里面的canScroll()的意义我不太理解。我对这个方法每个参数的理解就是:第一个参数View代表要测试子view是否能滑动的view,第二boolean类型的参数在源码中的解释是”Whether the view v passed should itself be checked for scrollability (true),or just its children (false)”,这句话的意思好像是在说如果是参数是true,就从这个view本身开始检测,如果是false,就只检查它的子view,在本例中也就是测试viewpager和菜单的内容的view。后面三个参数分别代表手指滑动在水平方向上的坐标变化,以及当前的xy坐标。如果函数返回true,代表view的子view可以滑动,如果返回false,则不能滑动。
这个是此函数中源码的实现:

 /**     * Tests scrollability within child views of v given a delta of dx.     *     * @param v View to test for horizontal scrollability     * @param checkV Whether the view v passed should itself be checked for scrollability (true),     *               or just its children (false).     * @param dx Delta scrolled in pixels     * @param x X coordinate of the active touch point     * @param y Y coordinate of the active touch point     * @return true if child views of v can be scrolled by delta of dx.     */    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {        if (v instanceof ViewGroup) {            final ViewGroup group = (ViewGroup) v;            final int scrollX = v.getScrollX();            final int scrollY = v.getScrollY();            final int count = group.getChildCount();            // Count backwards - let topmost views consume scroll distance first.            for (int i = count - 1; i >= 0; i--) {                // TODO: Add versioned support here for transformed views.                // This will not work for transformed views in Honeycomb+                final View child = group.getChildAt(i);                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&                        y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&                        canScroll(child, true, dx, x + scrollX - child.getLeft(),                                y + scrollY - child.getTop())) {                    return true;                }            }        }        return checkV && ViewCompat.canScrollHorizontally(v, (isLayoutRtlSupport() ? dx : -dx));    }

我理解的是这个函数是判断子view是否能滑动,但是源码里面的执行过程看不太懂。
确定view的每个子view,然后每个子view都递归执行这个函数,返回true的条件是当前手指滑动的坐标x和y加上滑动距离都不超出view的范围,这个到底是什么意思呢?

③ 这个类的实现用到了MotionEventCompat,这个是Android里面的MotionEvent的兼容实现类,随手看了下MotionEventCompat.getActionMasked(ev)的实现,实现如下:

public static int getActionMasked(MotionEvent event) {    return event.getAction() & ACTION_MASK;}

也就是最终还是使用了event.getAction()函数的结果,但是为什么这个结果要和ACTION_MASK进行与运算呢?
查看源码可知ACTION_MASK的值为0xff,也就是二进制的1111 1111,和这个数进行与运算就是本身,Android的源码为什么要做一次与运算?如果不做与运算会在什么时候发生错误呢?一直想不太明白….

这三个问题,希望大神能给我指出,感激不尽!

0 0
原创粉丝点击