在Android中使用工具栏实现正确的后退导航和主页按钮处理

19

我正在使用单个活动和多个片段(附有截图)在同一活动中提供无缝导航。但是在实施最新的工具栏和导航视图后,似乎很难处理导航和主页按钮。以下是我的问题:

  • 管理左上角的汉堡包/返回按钮。将图标和功能切换为菜单和返回导航。
    • 页面标题-每当推送和弹出碎片时更改页面标题。

我尝试了几种方法,如覆盖onBackPressed()、setHomeAsUpIndicator、手动弹出片段等。以前我使用ActionBarDrawer按钮来处理它,但现在它会出现错误。我检查了谷歌示例,他们似乎大多数位置都使用单独的活动。

有人能指导我如何实现适当的后退导航以处理NavigationView、内部片段中的后退按钮和页面标题吗?我正在使用 AppCompatActivityandroid.app.FragmentNavigationViewToolbar

Fragment 1 -> Fragment 2 -> Fragment 3


之前我使用ActionBarDrawer切换来处理这个问题,但是NavigationView不支持它。为什么你不能在NavigationView中使用ActionBarDrawerToggle呢? - Mike M.
1
请看这里,一定要阅读答案中关于抽屉切换的最后一条评论:https://dev59.com/l5Hea4cB1Zd3GeqPrp9g - Daniel Nugent
我已经尝试过这个。它没有按预期工作。在内部片段中,主页按钮变成了返回按钮,但仍然像以前的菜单按钮一样运行。当你按下系统返回键并返回到主屏幕时,主页图标消失了。我遇到了这两个问题。我已经尝试根据评论中提到的方法添加抽屉开关,但那也没有起作用。 - Ajith M A
请在问题中添加您已经编写的相关代码。 - Daniel Nugent
请尝试我的答案,它可以处理您想要的所有用例。 - Rohit Arya
@AjithMemana 我更新了我的答案,我也不再使用 ActionBarDrawerToggle 了。 - Yoann Hercouet
6个回答

16

使用一些责任分配的方式来说明你的ActivityFragment的职责划分会更加容易。

Activity和Fragment的职责划分 问题1:管理左上角的汉堡/返回按钮。 切换图标和功能以进行菜单和返回导航。

从这张图片中可以看出,解决方案应该由Activity封装,类似于以下代码:

public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {

    private ActionBarDrawerToggle mDrawerToggle;
    private DrawerLayout mDrawer;
    private ActionBar mActionBar;

    private boolean mToolBarNavigationListenerIsRegistered = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mActionBar = getSupportActionBar();

        mDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        mDrawer.addDrawerListener(mDrawerToggle);
        mDrawerToggle.syncState();

        NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
        navigationView.setNavigationItemSelectedListener(this);

        // On orientation change savedInstanceState will not be null.
        // Use this to show hamburger or up icon based on fragment back stack.
        if(savedInstanceState != null){
            resolveUpButtonWithFragmentStack();
        } else {
            // You probably want to add your ListFragment here.
        }
    }

    @Override
    public void onBackPressed() {

        if (mDrawer.isDrawerOpen(GravityCompat.START)) {
            mDrawer.closeDrawer(GravityCompat.START);

        } else {
            int backStackCount = getSupportFragmentManager().getBackStackEntryCount();

            if (backStackCount >= 1) {
                getSupportFragmentManager().popBackStack();
                // Change to hamburger icon if at bottom of stack
                if(backStackCount == 1){
                    showUpButton(false);
                }
            } else {
                super.onBackPressed();
            }
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;

        } else if (id == android.R.id.home) {
            // Home/Up logic handled by onBackPressed implementation
            onBackPressed();
        }

        return super.onOptionsItemSelected(item);
    }

    @SuppressWarnings("StatementWithEmptyBody")
    @Override
    public boolean onNavigationItemSelected(MenuItem item) {
        // Handle navigation view item clicks here.
        int id = item.getItemId();

        // Navigation drawer item selection logic goes here

        mDrawer.closeDrawer(GravityCompat.START);
        return true;
    }

    private void replaceFragment() {
        /**
        * Your fragment replacement logic goes here
        * e.g.
        * FragmentTransaction ft = getFragmentManager().beginTransaction();
        * String tag = "MyFragment";
        * ft.replace(R.id.content, MyFragment.newInstance(tag), tag).addToBackStack(null).commit();
        */

        // The part that changes the hamburger icon to the up icon
        showUpButton(true);
    }

    private void resolveUpButtonWithFragmentStack() {
        showUpButton(getSupportFragmentManager().getBackStackEntryCount() > 0);
    }

    private void showUpButton(boolean show) {
        // To keep states of ActionBar and ActionBarDrawerToggle synchronized,
        // when you enable on one, you disable on the other.
        // And as you may notice, the order for this operation is disable first, then enable - VERY VERY IMPORTANT.
        if(show) {
            // Remove hamburger
            mDrawerToggle.setDrawerIndicatorEnabled(false);
            // Show back button
            mActionBar.setDisplayHomeAsUpEnabled(true);
            // when DrawerToggle is disabled i.e. setDrawerIndicatorEnabled(false), navigation icon
            // clicks are disabled i.e. the UP button will not work.
            // We need to add a listener, as in below, so DrawerToggle will forward
            // click events to this listener.
            if(!mToolBarNavigationListenerIsRegistered) {
                mDrawerToggle.setToolbarNavigationClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        onBackPressed();
                    }
                });

                mToolBarNavigationListenerIsRegistered = true;
            }

        } else {
            // Remove back button
            mActionBar.setDisplayHomeAsUpEnabled(false);
            // Show hamburger
            mDrawerToggle.setDrawerIndicatorEnabled(true);
            // Remove the/any drawer toggle listener 
            mDrawerToggle.setToolbarNavigationClickListener(null);
            mToolBarNavigationListenerIsRegistered = false;
        }

        // So, one may think "Hmm why not simplify to:
        // .....
        // getSupportActionBar().setDisplayHomeAsUpEnabled(enable);
        // mDrawer.setDrawerIndicatorEnabled(!enable);
        // ......
        // To re-iterate, the order in which you enable and disable views IS important #dontSimplify.
    }
}

问题2: 页面标题 - 当推入和弹出片段时更改页面标题。

基本上,这可以在每个FragmentonStart中处理,即您的ListFragment、DetailsFragment和CommentsFragment看起来像这样:

@Override
public void onStart() {
    super.onStart();
    // where mText is the title you want on your toolbar/actionBar
    getActivity().setTitle(mText);
}

在片段的onCreate中也值得使用setRetainInstance(true)


谢谢,它几乎可以工作了。现在我唯一的问题是,在详细片段(第二级)中,菜单变成了返回按钮,但返回操作不起作用。在活动中的OnBackPressed没有被调用?有没有标准方法来处理后退按键(工具栏中的<-按钮)并弹出片段。 - Ajith M A
我还假设你的Activity主题扩展了Theme.AppCompat.Light.NoActionBar - ade.akinyede
不错的解决方案!对我有用。 - Ryan Newsom

5

简述:

观看视频:https://youtu.be/ANpBWIT3vlU

克隆项目:https://github.com/shredderskelton/androidtemplate

这是一个非常普遍的问题,我通过创建一种模板项目来解决它,每当我开始一个新的Android项目时使用。其思想是将处理后退按钮、'汉堡包'指示器和片段管理的大部分逻辑抽象成可重用的类:

首先创建一个BaseActivity和BaseFragment类。在这里,您将尽可能地放置可重用的代码。

让我们从您的BaseActivity开始。

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    fragmentManager = getSupportFragmentManager();
    fragmentHandler = new AddFragmentHandler(fragmentManager);
    fragmentManager.addOnBackStackChangedListener(backStackListener);
}

FragmentManager是掌管后退栈的关键,因此您需要从这里监听后退栈的更改。AddFramentHandler是我设计的一个小类,以便更轻松地从Fragment中添加Fragment。稍后将详细介绍。

@Override
public void onBackPressed() {
    if (sendBackPressToDrawer()) {
        //the drawer consumed the backpress
        return;
    }

    if (sendBackPressToFragmentOnTop()) {
        // fragment on top consumed the back press
        return;
    }

    //let the android system handle the back press, usually by popping the fragment
    super.onBackPressed();

    //close the activity if back is pressed on the root fragment
    if (fragmentManager.getBackStackEntryCount() == 0) {
        finish();
    }
}

onBackPressed是大部分魔法发生的地方。你注意到方法的普通文本格式...我是一位热爱Clean Code的粉丝——如果你需要编写注释,说明你的代码不够干净整洁。基本上,你需要有一个中心位置,在那里当你不确定为什么后退按钮按下时没有按照预期的方式工作时可以前去。这个方法就是那个位置。

private void syncDrawerToggleState() {
    ActionBarDrawerToggle drawerToggle = getDrawerToggle();
    if (getDrawerToggle() == null) {
        return;
    }
    if (fragmentManager.getBackStackEntryCount() > 1) {
        drawerToggle.setDrawerIndicatorEnabled(false);
        drawerToggle.setToolbarNavigationClickListener(navigationBackPressListener); //pop backstack
    } else {
        drawerToggle.setDrawerIndicatorEnabled(true);
        drawerToggle.setToolbarNavigationClickListener(drawerToggle.getToolbarNavigationClickListener()); //open nav menu drawer
    }
}

这是BaseActivity的另一个关键部分。基本上,这个方法检查您是否在根片段,并相应地设置指示器。请注意,它根据后台堆栈中有多少个片段来更改侦听器。
然后是BaseFragment:
@Override
public void onResume() {
    super.onResume();
    getActivity().setTitle(getTitle());
}

protected abstract String getTitle();

上述代码展示了片段如何处理标题。

请在您的答案中更新完整的代码。sendBackPressToDrawer和sendBackPressToFragmentOnTop丢失。 - Johny19

1

"页面标题 - 在推入和弹出片段时更改页面标题"

当您删除一个片段时,有一个名为 isRemoving() 的方法。它有助于将标题改回原来的状态。

@Override
public void onStop() {
    super.onStop();
    if (isRemoving()) {
        // Change your title here
    }
}

"功能性菜单和返回导航"

建议:我们需要依赖于默认的安卓导航系统。如果我们在片段中使用addToBackStack(),理论上我们根本不需要重写onBackPressed()。

  1. "应用程序不重新定义系统图标(例如返回按钮)的预期功能。"
  2. "应用支持标准的系统返回按钮导航,并不使用任何自定义的屏幕“返回按钮”提示。"

核心应用质量:https://developer.android.com/distribute/essentials/quality/core.html

"管理左上角的汉堡/返回按钮"

我建议使用活动而不是'MainActivityDetailFragment'来避免复杂化。


使用MainActivityDetailFragment的想法很好。但是除此之外,是否真的有一种方法可以处理问题,而不必实际上干扰系统的onBackPressed?当我检查Gmail应用程序时,他们在单个活动中处理消息列表和活动。 - Ajith M A
如何通过getSupportFragmentMananger()或getFragmentManager()添加您的(嵌套)片段?为片段实现后退导航:http://developer.android.com/intl/ru/training/implementing-navigation/temporal.html - Dmitry

1
尝试类似以下的内容:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    if (getSupportActionBar()!=null) {
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    }

    drawer = (DrawerLayout) findViewById(R.id.drawer_layout);

    final ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle(
            this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
    drawer.addDrawerListener(drawerToggle);
    drawerToggle.syncState();

    final View.OnClickListener originalToolbarListener = drawerToggle.getToolbarNavigationClickListener();

    final View.OnClickListener navigationBackPressListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            getFragmentManager().popBackStack();
        }
    };

    getFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
        @Override
        public void onBackStackChanged() {
            if (getFragmentManager().getBackStackEntryCount() > 0) {
                drawerToggle.setDrawerIndicatorEnabled(false);
                drawerToggle.setToolbarNavigationClickListener(navigationBackPressListener);
            } else {
                drawerToggle.setDrawerIndicatorEnabled(true);
                drawerToggle.setToolbarNavigationClickListener(originalToolbarListener);
            }
        }
    });

    // Though below steps are not related but I have included to show drawer close on Navigation Item click. 

    navigationView = (NavigationView) findViewById(R.id.nav_view);
    navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(MenuItem item) {
            int id = item.getItemId();
            /**
             * handle item clicks using id
             */
            drawer.closeDrawer(GravityCompat.START);
            return true;
        }
    });
}

处理抽屉状态 onBackPressed:
@Override
public void onBackPressed() {
    if (drawer.isDrawerOpen(GravityCompat.START)) {
        drawer.closeDrawer(GravityCompat.START);
    } else {
        super.onBackPressed();
    }
}

为了在后退时重新加载先前的“片段”,请始终像这样将片段事务添加到返回堆栈中:
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
SomeFragment fragmentToBeLoaded = new SomeFragment();
fragmentTransaction.replace(R.id.fragment_container, fragmentToBeLoaded,
                fragmentToBeLoaded.getName());
fragmentTransaction.addToBackStack(fragmentToBeLoaded.getName());
fragmentTransaction.commit();

为了动态更改页面标题,您可以在每个FragmentonStartonResume方法中调用此方法:
@Override
public void onStart() {
   super.onStart();
   getActivity().setTitle("Title for fragment");
}

注意:我已经考虑了标准布局声明,因此没有包含任何布局。

0
在你调用Fragments的MainActivity中添加以下内容。getBackStackEntryCount() 返回后台堆栈中Fragment的数量,其中堆栈底部的Fragment索引为0。 popBackStack() 弹出后台堆栈中的顶部Fragment。
 @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == android.R.id.home) {
            if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
                getSupportFragmentManager().popBackStack();
            } else {
                super.onBackPressed();
            }
        }
        return true;
    }

在你想要返回的Fragment中使用这个函数

  @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == android.R.id.home) {
            getActivity().onBackPressed();
        }
        return true;
    }

1
谢谢Umar。我也在寻找菜单/返回按钮的处理方法。 - Ajith M A

0

好的,经过许多测试,我终于成功地设置了一个良好的导航。我需要的正是你所需要的,唯一的区别是我正在使用v4片段,但我不认为这会改变任何东西。

我没有使用ActionBarDrawerToggle,因为谷歌最新的示例不再使用此组件。

以下解决方案也适用于深度导航:父活动-->片段-->片段等。

片段中唯一需要更改的是标题:

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    getActivity().setTitle(R.string.targets);
}

在父类 Activity 的 onCreate 方法中,我初始化了以下内容:
    mNavigationView = (NavigationView) findViewById(R.id.navigation_view);
    setupDrawerContent(mNavigationView);

    final Toolbar toolbar = (Toolbar) findViewById(R.id.drawer_toolbar);
    setSupportActionBar(toolbar);

    getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_menu_24);// Set the hamburger icon
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);// Set home button pressable

    // Handle the changes on the actionbar
    getSupportFragmentManager().addOnBackStackChangedListener(
            new FragmentManager.OnBackStackChangedListener() {
                public void onBackStackChanged() {
                    // When no more fragments to remove, we display back the hamburger icon and the original activity title
                    if (getSupportFragmentManager().getBackStackEntryCount() <= 0) {
                        getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_menu_24);
                        setTitle(R.string.app_name);
                    }
                    // Else displays the back arrow
                    else {
                        getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_arrow_back_24);
                    }
                }
            });

这里是处理主页按钮动作的代码:

@Override
public boolean onOptionsItemSelected(MenuItem item){
    // Close the soft keyboard right away
    Tools.setSoftKeyboardVisible(mViewPager, false);

    switch (item.getItemId()) {
        case android.R.id.home:

            // When no more fragments to remove, open the navigation drawer
            if (getSupportFragmentManager().getBackStackEntryCount() <= 0) {
                mDrawerLayout.openDrawer(GravityCompat.START);
            }
            // Removes the latest fragment
            else {
                getSupportFragmentManager().popBackStack();
            }

            return true;
    }
    return super.onOptionsItemSelected(item);
}

最后是处理返回按钮操作的代码:

@Override
public void onBackPressed() {
    // When no more fragments to remove, closes the activity
    if (getSupportFragmentManager().getBackStackEntryCount() <= 0) {
        super.onBackPressed();
    }
    // Else removes the latest fragment
    else {
        getSupportFragmentManager().popBackStack();
    }
}

注意:我正在使用AppCompatActivityNavigationView和主题Theme.AppCompat.Light.NoActionBar


我在AppCompatActivity中没有onActivityCreated方法可以重写,无法实现你的解决方案。 - vinicius gati

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