当用户按下返回按钮时隐藏导航抽屉

27

我按照Google官方开发者教程这里创建了一个导航抽屉。

目前,一切都很正常,除了当用户使用Android底部提供的本机返回按钮(与主屏幕和最近使用的应用程序按钮一起)时。如果用户使用此本机返回按钮进行后退导航,则导航抽屉仍将保持打开状态。如果用户改为使用ActionBar进行后退导航,则导航抽屉将会关闭,就像我想要的那样。

我的代码几乎与官方教程相同,除了我如何处理用户选择抽屉上的项目:

   mDrawerList.setOnItemClickListener(new ListView.OnItemClickListener()
    {
        @Override
        public void onItemClick(AdapterView parent, View view, int position, long id)
        {
            switch(position)
            {
                case 0:
                {
                    Intent intent = new Intent(MainActivity.this, NextActivity.class);
                    startActivity(intent);
                }
            }
        }
    });
如何在用户使用原生返回按钮导航回去时关闭导航抽屉?欢迎任何建议。谢谢!
12个回答

70

您需要覆盖 onBackPressed() 方法。根据文档:

当活动检测到用户按下后退键时调用。默认实现仅结束当前活动,但您可以覆盖该方法以执行任何您想要的操作。

所以您可以编写如下代码:

@Override
public void onBackPressed() {
    if (this.drawerLayout.isDrawerOpen(GravityCompat.START)) {
        this.drawerLayout.closeDrawer(GravityCompat.START);
    } else {
        super.onBackPressed();
    }
}

如果已经打开,则此方法将关闭它,否则回退到默认行为。


5
如果你希望在按下返回按钮时关闭抽屉而不是关闭应用程序,那么请使用isDrawerVisible而不是isDrawerOpen。记住,修改内容时需要保持原意,并使语言更加通俗易懂。 - Terel
可能更容易在打开抽屉项目后关闭抽屉(并节省动画):https://dev59.com/2l8d5IYBdhLWcg3waxvo#43559228 - Damnum

10

你需要在你的activity中重写onBackPressed()方法并检查导航抽屉是否打开。如果它是打开的,那么关闭它,否则执行正常的返回按下方法。以下是一些混合了一些伪代码的代码来帮助你:

@Override
public void onBackPressed(){
  if(drawer.isDrawerOpen()){ //replace this with actual function which returns if the drawer is open
   drawer.close();     // replace this with actual function which closes drawer
  }
  else{
   super.onBackPressed();
  }
}

查看文档中的抽屉以替换伪代码。我知道这两种方法都存在。


5
这里有一个替代方案来解决你的问题。
@Override    
public void onBackPressed(){    
    if(drawerLayout.isDrawerOpen(navigationView)){    
        drawerLayout.closeDrawer(navigationView);    
    }else {    
        finish();    
    }    
}    

4

更新:

从支持库 24.0.0 开始,无需任何变通方法即可实现此操作。两种新的方法 openDrawercloseDrawer 已经添加到 DrawerLayout 中,允许抽屉在没有动画的情况下打开或关闭。

您现在可以使用 openDrawer(drawerView, false)closeDrawer(drawerView, false) 来无延迟地打开和关闭抽屉。


如果您在使用后退按钮返回到该实例的活动时,未调用closeDrawer()而直接调用startActivity(),则抽屉将保持打开状态。在调用startActivity()时调用closeDrawer()会出现一些问题,从卡顿的动画到长时间的感知延迟,具体取决于您使用的解决方法。因此,我认为最好的方法是只调用startActivity(),然后在返回时关闭抽屉。
为了使其正常工作,您需要一种在使用后退按钮导航回该活动时无需关闭动画即可关闭抽屉的方法。(一个相对浪费的解决方法是在导航回来时强制重新创建活动,但可以不这样做解决这个问题。)
您还需要确保仅在导航后返回时关闭抽屉,而不是在方向更改后关闭抽屉,但这很容易实现。

详情

(如果您只想查看代码,请跳过此解释。)

虽然从onCreate()调用closeDrawer()将使抽屉关闭,没有任何动画,但从onResume()不是这样。从onResume()调用closeDrawer()会通过一个短暂可见的动画关闭抽屉。 DrawerLayout不提供任何关闭抽屉而没有动画的方法,但可以扩展它以添加一个。

实际上,关闭抽屉只是将其滑出屏幕,因此您可以通过直接将抽屉移动到其“关闭”位置来有效地跳过动画。翻译方向将根据重力(左侧或右侧抽屉)而变化,确切位置取决于抽屉一旦布置了所有子项后的大小。

但仅仅移动它是不够的,因为DrawerLayout在扩展的LayoutParams中保留了一些内部状态,用于判断抽屉是否打开。如果您只是将抽屉移动到屏幕外,它将不知道它已关闭,这会引起其他问题。(例如,在下一个方向更改时,抽屉将重新出现。)

由于您正在将支持库编译到您的应用程序中,因此可以在android.support.v4.widget包中创建一个类以访问其默认(软件包私有)部分,或扩展DrawerLayout而不复制它需要的任何其他类。这也将减轻更新代码以适应支持库未来变化的负担。(尽可能地隔离您的代码与实现细节最好)。您可以使用moveDrawerToOffset()来移动抽屉,并设置LayoutParams以使其知道抽屉已关闭。


代码

这是跳过动画的代码:

        // move drawer directly to the closed position
        moveDrawerToOffset(drawerView, 0.f); 
        
        // set internal state so DrawerLayout knows it's closed
        final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
        lp.onScreen = 0.f;
        lp.knownOpen = false;

        invalidate();

注意:如果您只调用moveDrawerToOffset()而不更改LayoutParams,则抽屉将在下一次方向更改时移回其打开位置。

选项1(使用现有的DrawerLayout)

这种方法将一个实用类添加到support.v4包中,以便获取我们需要在DrawerLayout内部使用的package-private部分。

将这个类放入/src/android/support/v4/widget/目录下:

package android.support.v4.widget;

import android.support.annotation.IntDef;
import android.support.v4.view.GravityCompat;
import android.view.Gravity;
import android.view.View;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

public class Support4Widget {

    /** @hide */
    @IntDef({Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END})
    @Retention(RetentionPolicy.SOURCE)
    private @interface EdgeGravity {}

    public static void setDrawerClosed(DrawerLayout drawerLayout, @EdgeGravity int gravity) {
        final View drawerView = drawerLayout.findDrawerWithGravity(gravity);
        if (drawerView == null) {
            throw new IllegalArgumentException("No drawer view found with gravity " +
                    DrawerLayout.gravityToString(gravity));
        }

        // move drawer directly to the closed position
        drawerLayout.moveDrawerToOffset(drawerView, 0.f); 
        
        // set internal state so DrawerLayout knows it's closed
        final DrawerLayout.LayoutParams lp = (DrawerLayout.LayoutParams) drawerView.getLayoutParams();
        lp.onScreen = 0.f;
        lp.knownOpen = false;

        drawerLayout.invalidate();
    }
}

在您离开页面时,在Activity中设置一个布尔值,表示应该关闭抽屉菜单:
public static final String CLOSE_NAV_DRAWER = "CLOSE_NAV_DRAWER";
private boolean mCloseNavDrawer;

@Override
public void onCreate(Bundle savedInstanceState) {
    // ...
    if (savedInstanceState != null) {
        mCloseNavDrawer = savedInstanceState.getBoolean(CLOSE_NAV_DRAWER);
    }
}

@Override
public boolean onNavigationItemSelected(MenuItem menuItem) {

    // ...

    startActivity(intent);
    mCloseNavDrawer = true;
}

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    savedInstanceState.putBoolean(CLOSE_NAV_DRAWER, mCloseNavDrawer);
    super.onSaveInstanceState(savedInstanceState);
}   

onResume()中使用setDrawerClosed()方法关闭抽屉,不带动画:

@Overrid6e
protected void onResume() {
    super.onResume();

    if(mCloseNavDrawer && mDrawerLayout != null && mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
        Support4Widget.setDrawerClosed(mDrawerLayout, GravityCompat.START);
        mCloseNavDrawer = false;
    }
}

选项2(从DrawerLayout扩展)

这种方法是通过扩展DrawerLayout来添加setDrawerClosed()方法。

将此类放入/src/android/support/v4/widget/中:

package android.support.v4.widget;

import android.content.Context;
import android.support.annotation.IntDef;
import android.support.v4.view.GravityCompat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

public class CustomDrawerLayout extends DrawerLayout {

    /** @hide */
    @IntDef({Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END})
    @Retention(RetentionPolicy.SOURCE)
    private @interface EdgeGravity {}
    
    public CustomDrawerLayout(Context context) {
        super(context);
    }

    public CustomDrawerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomDrawerLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    
    public void setDrawerClosed(View drawerView) {
        if (!isDrawerView(drawerView)) {
            throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
        }
        
        // move drawer directly to the closed position
        moveDrawerToOffset(drawerView, 0.f); 
        
        // set internal state so DrawerLayout knows it's closed
        final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
        lp.onScreen = 0.f;
        lp.knownOpen = false;
        
        invalidate();
    }

    public void setDrawerClosed(@EdgeGravity int gravity) {
        final View drawerView = findDrawerWithGravity(gravity);
        if (drawerView == null) {
            throw new IllegalArgumentException("No drawer view found with gravity " +
                    gravityToString(gravity));
        }

        // move drawer directly to the closed position
        moveDrawerToOffset(drawerView, 0.f); 
        
        // set internal state so DrawerLayout knows it's closed
        final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
        lp.onScreen = 0.f;
        lp.knownOpen = false;

        invalidate();
    }
}

在你的活动布局中使用 CustomDrawerLayout 替代 DrawerLayout

<android.support.v4.widget.CustomDrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    >

当您导航离开时,在您的活动中设置一个布尔值,指示抽屉应该被关闭:

public static final String CLOSE_NAV_DRAWER = "CLOSE_NAV_DRAWER";
private boolean mCloseNavDrawer;

@Override
public void onCreate(Bundle savedInstanceState) {
    // ...
    if (savedInstanceState != null) {
        mCloseNavDrawer = savedInstanceState.getBoolean(CLOSE_NAV_DRAWER);
    }
}

@Override
public boolean onNavigationItemSelected(MenuItem menuItem) {

    // ...

    startActivity(intent);
    mCloseNavDrawer = true;
}

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    savedInstanceState.putBoolean(CLOSE_NAV_DRAWER, mCloseNavDrawer);
    super.onSaveInstanceState(savedInstanceState);
}   

onResume()中使用setDrawerClosed()方法关闭抽屉,不带动画:

@Overrid6e
protected void onResume() {
    super.onResume();

    if(mCloseNavDrawer && mDrawerLayout != null && mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
        mDrawerLayout.setDrawerClosed(GravityCompat.START);
        mCloseNavDrawer = false;
    }
}

3

使用@James Cross提供的答案实现是可行的,但关闭抽屉时的动画效果不理想,并且如果要修复需要花费很多精力,例如

@Override
public void onResume()
{
    super.onResume();
    mDrawerLayout.closeDrawers();
}

当设备的返回按钮被按下时,一种解决方法是重新启动活动。虽然这对我来说并不理想,但它确实有效。可以重写onBackPressed(),如@mt0s和@Qazi Ahmed所建议的,并传递一个额外参数来确定调用的活动:

    mDrawerList.setOnItemClickListener(new ListView.OnItemClickListener()
    {
        @Override
        public void onItemClick(AdapterView parent, View view, int position, long id)
        {
            switch(position)
            {
                case 0:
                {
                    Intent intent = new Intent(MainActivity.this, NextActivity.class);
                    //pass int extra to determine calling activity
                    intent.putExtra(EXTRA_CALLING_ACTIVITY, CallingActivityInterface.MAIN_ACTIVITY);
                    startActivity(intent);
                }
            }
        }
    });

NextActivity.class中,检查调用的活动:
@Override
public void onBackPressed()
{
    int callingActivity = getIntent().getIntExtra(EXTRA_CALLING_ACTIVITY, CallingActivityInterface.MAIN_ACTIVITY);
    switch(callingActivity)
    {
        case CallingActivityInterface.MAIN_ACTIVITY:
        {
            Intent intent = new Intent(this, MainActivity.class);
            startActivity(intent);
            finish();
        }
        ...
    }
}

无论使用向上按钮还是返回按钮,当我返回到MainActivity时,抽屉将不带动画关闭。可能有更好的方法来实现这一点。目前我的应用程序相对简单,这个方法可以工作,但如果有更有效的方法,我会等待它。

2

为什么这么麻烦?单击抽屉中的一个项目时,只需关闭抽屉即可。这是官方Google Play应用程序的做法。

private class DrawerItemClickListener implements ListView.OnItemClickListener {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
         drawerLayout.closeDrawer(GravityCompat.START, false);
         selectItem(position); 
    }
}

为什么这么麻烦?因为closeDrawer在https://issuetracker.google.com/issues/37087029之前并不存在。 - Lorne Laliberte
@Damnum “为什么要麻烦呢?只需在单击抽屉项时关闭抽屉”,这是最好的答案,对我有用,尽管我的代码没有第二个参数(false),需要将其删除。点赞伙计! - user3560827

2
如果你正在使用最新版本的Android 13,并且使用Kotlin编写代码,想要在按下返回按钮时关闭抽屉菜单,但是遇到了一个问题,即系统没有调用onBackPressed()方法。
val callback = onBackPressedDispatcher.addCallback(this, false) {
        drawerLayout.closeDrawer(GravityCompat.START)
    }

    drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener {

        override fun onDrawerOpened(drawerView: View) {
            callback.isEnabled = true
        }

        override fun onDrawerClosed(drawerView: View) {
            callback.isEnabled = false
        }

        override fun onDrawerSlide(drawerView: View, slideOffset: Float) = Unit
        override fun onDrawerStateChanged(newState: Int) = Unit
    })

1

JETPACK COMPOSE

如果你正在使用Jetpack Compose。

在你的scaffold中使用以下代码:

BackHandler(enabled = drawerState.isOpen) {

     scope.launch { drawerState.close() }
}

完整版本:

val scope = rememberCoroutineScope()
val drawerState = rememberDrawerState(DrawerValue.Closed)

Scaffold(

    topBar = {},

    bottomBar = {},

    snackbarHost = {},

    content = {

      ...

        BackHandler(enabled = drawerState.isOpen) {

            scope.launch { drawerState.close() }
        }
    },
    ...

)

1

当打开活动时,您可能希望确保导航抽屉始终关闭。使用以下代码实现:

@Override
public void onResume(){
    mDrawerList.closeDrawer(Gravity.LEFT);
}

1
谢谢,这个方法可行,但是有一个小问题。从ActionBar返回时,我会回到启动时看到的屏幕,根本没有抽屉可见。使用返回按钮将关闭抽屉,但在关闭时仍然可以看到它的一瞥。文档中说它被“动画移出视图”。如何摆脱动画,使其完全不可见? - pez

0

简单示例:

Drawer resultDrawer;

public void onBackPressed(){
    if (this.resultDrawer.isDrawerOpen()) {    
        this.resultDrawer.closeDrawer();    
    } else {    
        super.onBackPressed();    
    }
}

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