如何在从左到右滑动时显示DrawerLayout,无论在哪里?

8

背景

Google引入了DrawerLayout,当您点击操作栏上的“向上”按钮时,在屏幕左侧区域显示菜单。

由于该库尚未在ActionBarSherlock上受到支持,因此已经有一种方法可以克服使用此项目

它已经在许多应用程序上有变体:当前、Gmail、Hangouts、YouTube等。

问题

在“Currents”应用程序(以及YouTube中),当用户将(最左)页面从左向右滑动时,DrawerLayout出现,无论手指开始触摸的位置在哪里

我该如何实现相同的效果?也许我应该使用onInterceptTouchEvent

此链接(好的和这个)之外,没有太多关于可以完成的很酷的事情的文档和教程。他们说(在“给用户一个快速的瞥见”部分)左侧约20dp用于此功能,但我可以看到“Currents”使用更大的区域。

似乎该库还没有完全完成,因此甚至无法在可视化UI编辑器中显示布局XML文件...


编辑:似乎该库是开源的。代码可在以下位置找到:

.../android-sdk\sources\android-18\android\support\v4\widget\DrawerLayout.java
.../android-sdk\sources\android-18\android\support\v4\widget\SlidingPaneLayout.java
.../android-sdk\sources\android-18\android\support\v4\app\ActionBarDrawerToggle.java

现在的问题是如何使它按照我所写的方式工作,就像在YouTube上一样,允许我们自定义它的外观并从哪里允许滚动。


我不确定我是否理解正确,我也在使用新的导航抽屉,并且它总是可以正常工作,无需任何额外的设置。我正在一个项目中使用:ABS + 导航抽屉 + 片段 + GMaps,我从整个屏幕的最左侧或最右侧滑动抽屉时没有遇到任何问题。你是否按照推荐方式在xml中声明了抽屉? - unmultimedio
我非常确定ABS +新的导航抽屉可以无需任何额外帮助(如您发布的项目)进行集成。就滑动以显示抽屉的屏幕区域而言,它相当窄,我不确定是否可以像滑动库一样进行修改(无、边缘、整个屏幕)。您提供的示例(YouTube)无效,它没有使用导航抽屉,我想他们使用了滑动菜单库。Google+确实使用了新的导航抽屉,如果您尝试,滑动的屏幕区域是相同的左侧边框。 - unmultimedio
好的,我已经找到答案了。不幸的是,这是不可能的。距离边缘只有20dp:`让用户快速预览如果用户触摸屏幕的最左边缘(在距离左侧20dp以内),则在手指接触显示器时立即让抽屉弹出。这可以促进意外发现并提供更丰富的反馈。`请参考此处。在这种情况下,您需要另一种方法来确定您想要的区域的触摸,并使用mDrawerLayout.openDrawer(mDrawer)触发抽屉。 - unmultimedio
@khale912 是的,那就是我写的。不管怎样,如果他们说我们应该留出这个空间,那么这意味着它是可定制的,对吧?这不是指南的一部分吗?至少允许这个宽度?20dp太窄了,我不想让整个屏幕都滚动... - android developer
答案不如预期那样有效。我注意到它在滚动时没有像YouTube上那样产生相同的滚动效果。在YouTube上,右侧区域位于左抽屉上方,当您滚动时,它会显示抽屉的一部分。使用普通库则相反。我想知道为什么导航抽屉不是开源的。 - android developer
显示剩余9条评论
8个回答

7

SlidingMenu 是我所发现的最好的滑动库,它是非常优秀的库。
您可以设置 getSlidingMenu().setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN) 以启用全屏滑动。


我希望能够自定义允许的区域。此外,这个库支持actionBarSherlock吗? - android developer
是的!它与actionbarsherlock库集成,就像我在我的答案中所说,您可以使用setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN),它控制SlidingMenu是否可以通过滑动手势打开。 - Sadegh
其他自定义选项包括mSlidingMenu.setShadowWidthRes()mSlidingMenu.setShadowDrawable()mSlidingMenu.setBehindWidthRes()mSlidingMenu.setFadeDegree()等等。我已经在Github上尝试了几乎所有的滑动菜单库,SlidingMenu是迄今为止最好的。 - Sadegh
那么我该怎么做,才能让当手指触摸屏幕左半部分时显示抽屉,而当手指触摸屏幕右半部分时不显示呢? - android developer
对于屏幕的一半,我认为您应该计算屏幕大小和 DisplayMetrics metrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(metrics); int width = metrics.widthPixels; 然后 mSlidingMenu..setTouchModeAbove(SlidingMenu.TOUCHMODE_MARGIN);mSlidingMenu.setTouchmodeMarginThreshold(width/2); - Sadegh
显示剩余4条评论

4
问题在于DrawerLayout使用了ViewDragHelper,它的默认EDGE_SIZE为20dp,用于计算mEdgeSize,如下所示:
mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);

这是一个函数,它将mEdgeSize设置为显示宽度的百分比:
public static void setDrawerLeftEdgeSize(Activity activity, DrawerLayout drawerLayout, float displayWidthPercentage) {
    if (activity == null || drawerLayout == null)
        return;

    try {
        // find ViewDragHelper and set it accessible
        Field leftDraggerField = drawerLayout.getClass().getDeclaredField("mLeftDragger");
        leftDraggerField.setAccessible(true);
        ViewDragHelper leftDragger = (ViewDragHelper) leftDraggerField.get(drawerLayout);
        // find edgesize and set is accessible
        Field edgeSizeField = leftDragger.getClass().getDeclaredField("mEdgeSize");
        edgeSizeField.setAccessible(true);
        int edgeSize = edgeSizeField.getInt(leftDragger);
        // set new edgesize
        Point displaySize = new Point();
        activity.getWindowManager().getDefaultDisplay().getSize(displaySize);
        edgeSizeField.setInt(leftDragger, Math.max(edgeSize, (int) (displaySize.x * displayWidthPercentage)));
    } catch (NoSuchFieldException e) {
        // ignore
    } catch (IllegalArgumentException e) {
        // ignore
    } catch (IllegalAccessException e) {
        // ignore
    }
}

假设您希望左侧边缘的30%对滑动事件作出反应并打开导航抽屉,那么只需调用:

mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
setDrawerLeftEdgeSize(this, mDrawerLayout, 0.3f);

它仍然以古怪的方式工作。尝试将大小设置为0.5f,然后只在左侧长按。它仍然会触发显示抽屉,并且以非常快的方式显示。顺便说一句,您使用了一个过高的API来获取屏幕宽度。相反,您可以使用:activity.getResources().getDisplayMetrics().widthPixels - android developer
我在这里尝试了0.7f和长按。对我来说,动画速度还是“不错的”(我用的是S3手机)。但是,我猜测动画的持续时间是固定的,因此要在同样的时间内更多地移动抽屉 - 它只能更快地移动。注意:我故意使用了'getSize(..)',因为我使用的是较新的API级别。 - Espen Riskedal
长按不应触发显示滑动抽屉。抽屉的移动方式很奇怪,我很难解释。我也有SGS3。关于getSize(),你应该在保存时启用Lint检查,这样它会警告你使用了太新的函数/类。清单中的targetSdk应该是最新的(现在是19),但minSdk可以是您希望支持的任何版本(8或9几乎覆盖了100%)。 - android developer
是的,长按应该弹出抽屉 - 例如在此处提到 https://plus.google.com/+AdamWPowell/posts/8j2GVw72i1E。或者,您是指只有在点击边缘时才会弹出?是的,我故意设置了“minSdkVersion = 14”。与市场份额相比,这些天设置API 8(或9)会产生太多的兼容性问题。 - Espen Riskedal

3

我尝试更改默认的EDGE_SIZE值,结果也遇到了长按问题。最终,我发现通过计算X轴的偏移量来覆盖dispatchTouchEvent并根据滑动方向打开抽屉可以解决这个问题。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    int action = ev.getAction();
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            startX = ev.getX();
            startY = ev.getY();
            break;
        case MotionEvent.ACTION_UP:
            endX = ev.getX();
            endY = ev.getY();

            float sensitivity = 5;
            // From left to right
            if (endX - startX >= sensitivity) {
                if (mDrawerLayout.isDrawerOpen(Gravity.RIGHT)) {
                    mDrawerLayout.closeDrawer(Gravity.RIGHT);
                } else {
                    mDrawerLayout.openDrawer(Gravity.LEFT);
                }
            }

            // From right to left
            if (startX - endX >= sensitivity) {
                if (mDrawerLayout.isDrawerOpen(Gravity.LEFT)) {
                    mDrawerLayout.closeDrawer(Gravity.LEFT);
                } else {
                    mDrawerLayout.openDrawer(Gravity.RIGHT);
                }
            }

            break;
    }

2

通过使用他人的帮助:(第一个) (第二个)

我发现可以使用反射来修改默认的20dp屏幕边缘,以便让抽屉菜单滑动。

在声明内容和抽屉后,您可以这样做:

public class MainActivity extends Activity {
private DrawerLayout mDrawerLayout;
private ListView mDrawerList;
private ActionBarDrawerToggle mDrawerToggle;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
    mDrawerList = (ListView) findViewById(R.id.left_drawer);

    // set a custom shadow that overlays the main content when the drawer opens
    mDrawerLayout.setDrawerShadow(R.drawable.your_drawer_shadow, GravityCompat.START);
    // set up the drawer's list view with items and click listener
    mDrawerList.setAdapter(new ArrayAdapter<String>(this,
            R.layout.your_drawer_list, yourItems));
    mDrawerList.setOnItemClickListener(new DrawerItemClickListener());

    // enable ActionBar app icon to behave as action to toggle nav drawer
    getActionBar().setDisplayHomeAsUpEnabled(true);
    getActionBar().setHomeButtonEnabled(true);

    Field mDragger = null;
    try {
        mDragger = mDrawerLayout.getClass().getDeclaredField(
                "mLeftDragger"); //mRightDragger for right obviously
    } catch (NoSuchFieldException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    mDragger.setAccessible(true);
    ViewDragHelper draggerObj = null;
    try {
        draggerObj = (ViewDragHelper) mDragger
                .get(mDrawerLayout);
    } catch (IllegalArgumentException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    Field mEdgeSize = null;
    try {
        mEdgeSize = draggerObj.getClass().getDeclaredField(
                "mEdgeSize");
    } catch (NoSuchFieldException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    mEdgeSize.setAccessible(true);
    int edge = 0;
    try {
        edge = mEdgeSize.getInt(draggerObj);
    } catch (IllegalArgumentException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    try {
        mEdgeSize.setInt(draggerObj, edge * 5); //optimal value as for me, you may set any constant in dp
        //You can set it even to the value you want like mEdgeSize.setInt(draggerObj, 150); for 150dp
    } catch (IllegalArgumentException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    // ActionBarDrawerToggle ties together the the proper interactions
    // between the sliding drawer and the action bar app icon
    mDrawerToggle = new ActionBarDrawerToggle(
            this,                  /* host Activity */
            mDrawerLayout,         /* DrawerLayout object */
            R.drawable.ic_drawer,  /* nav drawer image to replace 'Up' caret */
            R.string.drawer_open,  /* "open drawer" description for accessibility */
            R.string.drawer_close  /* "close drawer" description for accessibility */
            ) {
        public void onDrawerClosed(View view) {

        }

        public void onDrawerOpened(View drawerView) {

        }
    };
    mDrawerLayout.setDrawerListener(mDrawerToggle);

}
}

嗯,这对我有效啊!


1
干得好,但是:1.使用反射有点冒险,难道不能扩展类并使用已有的内容吗?也许只需复制其代码(代码在哪里?)?2.它的工作效果不佳,因为它在完全显示内容之前就停止滚动了。3.你可以把整个新代码放在一个try-catch中,因为如果一个失败了,其他的也会失败。 - android developer
好的,1. 是的,只要你没有正确捕获所有异常,这是有风险的,万一出现意外情况,你需要准备一个B计划(在这种情况下默认为20-dp,或者只是主页按钮)。扩展和覆盖方法会很棒,问题是我们没有完全访问它,不是开源的。:( 2. 我也注意到了,它在所有滚动后显示一些额外的白色空间。3. 你可以尝试一下,但是你将失去内存中初始化所有你将在try{}部分中使用的变量的赋值空间,虽然不是很大的一部分。 - unmultimedio
  1. 实际上它是开源的。我已更新问题,以包括文件路径。
  2. 一定有更好的方法。
  3. 在这个小样本中,如果任何步骤失败了,您不需要使用变量。
- android developer
这段示例代码在我的KitKat HTC One上直接跳转到NoSuchFieldException。 - Timothy Miller

2
您可以在导航抽屉中使用此功能。
DrawerLayout mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
Field mDragger = mDrawerLayout.getClass().getDeclaredField(
    "mLeftDragger");//mRightDragger or mLeftDragger based on Drawer Gravity
mDragger.setAccessible(true);
ViewDragHelper draggerObj = (ViewDragHelper) mDragger
    .get(mDrawerLayout);

Field mEdgeSize = draggerObj.getClass().getDeclaredField(
    "mEdgeSize");
mEdgeSize.setAccessible(true);
int edge = mEdgeSize.getInt(draggerObj);

mEdgeSize.setInt(draggerObj, edge * 3); 

2
它能运行,但有点古怪。如果我把3改成30,它的表现就非常奇怪。 - android developer
谢谢这个提示!我在下面的另一个答案中稍微改进了一下。 - Espen Riskedal

1
当我们覆盖默认的边缘时,屏幕上与之重叠的部分将停止工作。我已经在viewpager中使用了上述代码,但现在当我从右向左滚动时,它不会显示下一页,而是保持不变。有什么解决办法吗?我已经使用了setDrawerLeftEdgeSize(this,mDrawerLayout,distance );
public static void setDrawerLeftEdgeSize(Activity activity,
            DrawerLayout drawerLayout, float displayWidthPercentage) {
        if (activity == null || drawerLayout == null)
            return;

        try {
            // find ViewDragHelper and set it accessible
            Field leftDraggerField = drawerLayout.getClass().getDeclaredField(
                    "mLeftDragger");
            leftDraggerField.setAccessible(true);
            ViewDragHelper leftDragger = (ViewDragHelper) leftDraggerField
                    .get(drawerLayout);
            // find edgesize and set is accessible
            Field edgeSizeField = leftDragger.getClass().getDeclaredField(
                    "mEdgeSize");
            edgeSizeField.setAccessible(true);
            int edgeSize = edgeSizeField.getInt(leftDragger);
            // set new edgesize
            Point displaySize = new Point();
            activity.getWindowManager().getDefaultDisplay()
                    .getSize(displaySize);
            edgeSizeField.setInt(leftDragger, Math.max(edgeSize,
                    (int) (displaySize.x * displayWidthPercentage)));
        } catch (NoSuchFieldException e) {
            // ignore
        } catch (IllegalArgumentException e) {
            // ignore
        } catch (IllegalAccessException e) {
            // ignore
        }
    }

和上述相同...

1

首先,我不确定您所说的ActionBarSherlock不支持DrawerLayout是什么意思。您可以很愉快地使用DrawerLayout和ActionBarSherlock,我有一些应用程序正在使用这种组合。您需要更改支持jar版本,但我发现ActionbarSherlock对此没有问题。

按照通常的方式实现DrawerLayout。然后在您的片段中,实现onTouchEvent或interceptTouchEvent,并且当事件类型为“移动”类型事件,从左到右滑动时,您可以调用DrawerLayout上的openDrawer方法。您需要添加对手指移动距离的处理,以确保打开抽屉的调用不太敏感。


请展示一个示例代码,演示如何实现此功能。请注意,我不仅希望打开抽屉,还希望像我提到的应用程序一样移动它,并且如果用户停止触摸,则决定是向后滚动还是滚动以显示抽屉。 - android developer
这个功能与DrawerLayout的功能不同。您不想使用Drawer Layout有什么特别的原因吗?尝试实现自定义抽屉只会让您的生活更加困难。关于如何处理这个问题,可以查看Cyril Mottier在Prixing中实现抽屉的一系列文章:http://cyrilmottier.com/2012/05/22/the-making-of-prixing-fly-in-app-menu-part-1/ - Phil H
我并没有说我不想使用它。事实上,我是在问如何使用它,并且拥有选择开始拖动的功能。 - android developer

0
使用此項目SlidingMenu,您可以實現該效果並與ActionBarSherlock集成。
要將菜單從左滑到右,只需在創建菜單時自定義菜單並添加:
slidingMenu.setTouchModeAbove(SlidingMenu.TOUCHMODE_MARGIN);

希望这有所帮助。

我不明白。在哪里可以设置区域大小,以便拖动DrawerLayout?而且,我认为这是完全不同的库。 - android developer
该区域是屏幕的边缘,只需在边缘附近或屏幕外轻扫,它就能像魔法一样工作。 这确实是另一个库,我在Google推出DrawerLayout之前就一直在使用它,非常好用。 你说你的库还没有完全完成,所以我为你提供了一个备选方案。 - Mikel
我想要自定义它,例如像当前和YouTube一样。 - android developer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接