抽屉式布局中的项目点击 - 何时更换碎片才是正确的时间?

46

我正在开发一个应用程序,使用导航抽屉模式(带有DrawerLayout)。

每次点击抽屉的项目,都会替换主容器中的片段。

然而,我不确定何时是正确的时间进行片段事务? 当抽屉开始关闭时?还是在关闭后?

在谷歌的 documentaion example 中,您可以看到他们在项目点击后立即进行事务处理,然后关闭抽屉。
结果,抽屉看起来很卡顿,不流畅,效果很差(我的应用程序也出现了这种情况)。

GmailGoogle Drive应用程序中,似乎它们是在抽屉关闭后进行交易的(我是对的吗?)。 因此,抽屉不会出现延迟和非常平滑,但至少需要1秒钟(抽屉关闭所需的时间),才能看到下一个片段。 似乎在立即执行片段事务时,没有办法使抽屉平滑。 你对此有何看法? 提前致谢!

1
我同意!当我从抽屉的onItemClick()中启动新活动时,我看到了卡顿的抽屉关闭动画。这可能是因为许多活动都在UI线程上进行。所以现在我必须从DrawerLayout.DrawerListener中启动一个新的活动...就像你提到的那样,这会减慢用户体验。 - Someone Somewhere
2
能否更改抽屉关闭动画的速度?速度似乎是一个重要的属性需要暴露出来...然而我在文档中找不到它!(http://developer.android.com/reference/android/support/v4/widget/DrawerLayout.html) - Someone Somewhere
@SomeoneSomewhere 你可以改变动画的长度,但这并不是一个优雅的解决方案:https://dev59.com/-WIk5IYBdhLWcg3wPL7R - Makario
@SomeoneSomewhere 我确实不喜欢突然关闭的动画,但是我更不喜欢让用户等待更长时间。 - theblang
你可以查看这个解决方案:https://dev59.com/zl8e5IYBdhLWcg3wo7mE#32455989 - Quentin G.
如果在您的情况下动画关闭并不是很重要,DrawerLayout也可以在没有动画的情况下关闭:closeDrawer(View drawerView, boolean animate)。该方法会立即关闭抽屉。 - Ahmed
7个回答

25

没错,完全同意。执行带有视图的片段(fragment)事务会导致布局传递,从而导致动画卡顿,参考DrawerLayout文档:

可以使用DrawerLayout.DrawerListener来监视抽屉视图的状态和运动。在动画期间避免执行昂贵的操作,比如布局,因为它可能会导致卡顿; 尝试在STATE_IDLE状态下执行昂贵的操作。

所以,请在抽屉关闭后执行您的片段事务,或者有人修补支持库以某种方式修复它 :)


4
这个问题只存在于支持库中吗? - theblang
3
如何等待状态变为 STATE_IDLE? - nabil london

24
另一种解决方法是创建一个Handler,在关闭抽屉后延迟发布Runnable,如此所示:https://dev59.com/3mMl5IYBdhLWcg3wgnOl#18483633。这种方法的好处是,如果等待DrawerListener#onDrawerClosed(),你的片段将被更快地替换,但当然任意的延迟并不能保证抽屉动画会及时完成。
话虽如此,我使用了200毫秒的延迟,效果非常好。
private class DrawerItemClickListener implements OnItemClickListener {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
        drawerLayout.closeDrawer(drawerList);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                switchFragments(position); // your fragment transactions go here
            }
        }, 200);
    }
}

这种方法会产生另一个问题,详见此处描述。 - MadDeveloper
当提交正在进行时,点击任务菜单会导致崩溃。 - Sheychan
您先生,您真是个救命恩人:D - Kushan

15

以下是我在实现类似 Gmail 应用中平滑交易动画时所采取的方法:

activity_drawer.xml

<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <!-- The main content view -->
    <FrameLayout
        android:id="@+id/content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- The navigation drawer -->
    <ListView 
    android:id="@+id/left_drawer"
        android:layout_width="280dp"
        android:layout_height="match_parent"
        android:layout_gravity="left"
        android:choiceMode="singleChoice" />

</android.support.v4.widget.DrawerLayout>

DrawerActivity.java

private Fragment mContentFragment;
private Fragment mNextContentFragment;
private boolean mChangeContentFragment = false;

private Handler mHandler = new Handler();

...

@Override
public void onCreate(Bundle savedInstanceState) {
    ...

    mDrawerLayout.setDrawerListener(new DrawerListener());

    mDrawerList.setOnItemClickListener(new DrawerItemClickListener());

    ...
}

....

private class DrawerItemClickListener implements ListView.OnItemClickListener {

    @Override
    public void onItemClick(AdapterView parent, View view, int position, long id) {
        getSupportFragmentManager().beginTransaction().remove(mContentFragment).commit();

        switch (position) {
            case 0:
                mNextContentFragment = new Fragment1();
                break;

            case 1:
                mNextContentFragment = new Fragment2();
                break;

            case 2:
                mNextContentFragment = new Fragment3();
                break;
        }

        mChangeContentFragment = true;

        mDrawerList.setItemChecked(position, true);

        mHandler.postDelayed(new Runnable() {

            @Override
            public void run() {
                mDrawerLayout.closeDrawer(mDrawerList);
            }           
        }, 150);
    }
}

private class DrawerListener implements android.support.v4.widget.DrawerLayout.DrawerListener {

    @Override
    public void onDrawerClosed(View view) {
        if (mChangeContentFragment) {
             getSupportFragmentManager().beginTransaction().setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN).replace(R.id.content_frame, mNextContentFragment).commit();

             mContentFragment = mNextContentFragment;           
             mNextContentFragment = null;

             mChangeContentFragment = false;
         }
     }
 }

希望这对你有所帮助!:-)


我喜欢这个解决方案,但我不太清楚如何让后退栈行为正常工作。我可以在替换事务上添加“addToBackStack”,但由于之前的内容被删除了,所以这并没有按预期工作。我尝试过隐藏而不是删除,但那也不起作用。 - pqvst
1
@pbergqvist,为了实现后退栈的行为,您可以采取以下措施:
  1. 在删除“旧内容片段”之前使用另一个变量来保存它。
  2. 从您的活动中覆盖onBackPressed方法,并在那里用您的“旧内容片段”替换内容片段。
- saguinav
@pbergqvist,更好的方法是,如果你把“删除片段”事务、mChangeContentFragment=true和处理程序post全部放在一个名为setContentFragment(Fragment fragment)的方法中,你就可以在onBackPressed中使用这个方法,以避免重复编写相同的代码。这对你有用吗? - saguinav
我猜那应该可以行得通,但只能退回一步。否则我就得自己维护一个堆栈。 - pqvst
2
返回堆栈不应需要更改 - 谷歌的应用程序切换片段都不通过后退按钮。这很令人困惑。 - Matthew Reilly

5

我知道这个问题已经很久了,但是我遇到了同样的问题,并且想要发布我的解决方案,因为我认为它比添加硬编码延迟时间更好。我的做法是使用onDrawerClosed函数来验证抽屉确实关闭了,然后再执行我的任务。

//on button click...
private void displayView(int position) {
    switch (position) {
    //if item 1 is selected, update a global variable `"int itemPosition"` to be 1
    case 1:
        itemPosition = 1;
        //();
        break;
    default:
        break;
    }

    // update selected item and title, then close the drawer
    mDrawerList.setItemChecked(position, true);
    mDrawerList.setSelection(position);
    mDrawerLayout.closeDrawer(mDrawerList); //close drawer
}

onDrawerClosed 中打开对应的活动。
public void onDrawerClosed(View view) {
    getSupportActionBar().setTitle(mTitle);
    // calling onPrepareOptionsMenu() to show action bar icons
    supportInvalidateOptionsMenu();
    if (itemPosition == 1) {
        Intent intent = new Intent(BaseActivity.this, SecondActivity.class);
        startActivity(intent);
    }
}

2
抽屉关闭和启动新活动之间仍然存在延迟。 - IgorGanapolsky

2

只需在处理程序中编写代码并延迟200毫秒即可。

 new Handler().postDelayed(new Runnable() {
  @Override
   public void run() {
       openSelectionDrawerItem(position);          
   }
 }, 200);

1

不要延迟你的项目点击,这可能会使你的应用程序感觉很慢。我只会延迟mDrawerLayout的关闭。我也不会使用DrawerLayout.OnDrawerListener onClose(...),因为这些回调被调用得太慢了。

new Handler().postDelayed(new Runnable() {
    @Override
    public void run() {
        mDrawerLayout.closeDrawer(GravityCompat.START);
    }
}, 200);

0

如果你希望它平稳无延迟,那么在返回时将抽屉打开并在 onRestart() 方法中关闭它。

@Override
protected void onRestart() {
    // TODO Auto-generated method stub
    super.onRestart();
    mDrawerLayout.closeDrawer(mDrawerList);     
}

副作用是返回时会有一个(快速的)动画,但这可能是可以接受的。


返回什么?我只想让抽屉关闭并将我的屏幕转到另一个活动。 - IgorGanapolsky
@Igor,使用返回按钮(或任何其他允许您返回活动而不重新创建它的方法)从另一个活动返回后,抽屉仍将保持打开状态。 - Lorne Laliberte

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