优化抽屉和活动启动速度

53

我正在使用 Google 的 DrawerLayout

当一个项目被点击时,抽屉会平滑地关闭,然后启动一个 Activity。将这些活动转换为 Fragment 不是一个选项。因此,启动一个活动,然后关闭抽屉也不是一个选项。同时关闭抽屉和启动活动会使关闭动画卡顿。

考虑到我想先平滑地关闭它,然后再启动活动,我遇到了一个问题,即用户单击抽屉项目时,看到他们想要去的活动之间存在延迟。

每个项目的点击监听器如下所示。

final View.OnClickListener mainItemClickListener = new View.OnClickListener() {
    @Override
    public void onClick(final View v) {
        mViewToLaunch = v;
        mDrawerLayout.closeDrawers();
    }
};

我的活动也是DrawerListener,它的onDrawerClosed方法如下:

@Override
public synchronized void onDrawerClosed(final View view) {
    if (mViewToLaunch != null) {
        onDrawerItemSelection(mViewToLaunch);
        mViewToLaunch = null;
    }
}

onDrawerItemSelection只是启动五个活动中的一个。

DrawerActivityonPause上我什么也不做。

我正在进行仪器测试,从onClick被调用到onDrawerClosed结束平均需要500-650毫秒。

抽屉关闭后,在对应的活动启动之前会有明显的延迟。

我意识到有几件事情正在发生:

  • 关闭动画会发生,这就是几毫秒的时间(假设300)。

  • 然后可能存在一些延迟,即抽屉视觉上关闭和其监听器被触发之间的延迟。我正试图通过查看DrawerLayout源代码来确定这些时间的确切差异,但我还没有想清楚。

  • 然后是启动的活动执行其启动生命周期方法直至(包括)onResume所需的时间。我还没有对此进行仪器测试,但我估计需要大约200-300毫秒。

这似乎是一个走错路线会非常昂贵的问题,所以我想确保我完全理解它。

一个解决方案是跳过关闭动画,但我希望保留它。

如何尽可能缩短我的转换时间?


inScheduleLaunchAndCloseDrawer 我只是存储了一个视图的引用,稍后通过其ID进行匹配以确定要启动哪个Activity。在onPause中我没有做任何事情。我尝试在onDrawerSlide中执行此操作,但它也会出现卡顿。我尝试将其设置在80%的某个阈值之后。 - yarian
明天我有一个想尝试的东西,就是发布一个可运行的程序,在一些预定的延迟时间后启动活动,比如350-400毫秒。这可能仍然会出现卡顿,基本上目标是将抽屉关闭和监听器触发之间的延迟减少到零。我会在尝试后更新问题。 - yarian
这可能是一个解决方案,但在任意时间间隔发布可运行项并不是一个好主意。你可以尝试使用Handler.postAtFrontOfQueue(Runnable)onDrawerClosed()回调中发布一个启动活动的Runnable - user
我同意这听起来有点糊弄人。我会尝试两种方法。我怀疑延迟的很大一部分是因为我在视觉上感知抽屉已关闭,然后 Android 调用了 onDrawerClosed(),我认为 postAtFrontOfQueue 无法解决这个问题。但我会尝试两种方法并回报结果。 - yarian
我应该提到,如果我按间隔发布,它将在DrawerActivity拥有的Handler上完成,这样双击就不会导致两个事件被触发等等。 - yarian
显示剩余3条评论
7个回答

42

根据文档,在动画期间避免执行诸如布局之类的昂贵操作,因为这可能会导致卡顿。尝试在STATE_IDLE状态下执行这些操作。

可以重写ActionBarDrawerToggleonDrawerStateChanged方法(它实现了DrawerLayout.DrawerListener),而不是使用Handler并硬编码时间延迟,这样可以在抽屉完全关闭时执行昂贵的操作。

在MainActivity中:

private class SmoothActionBarDrawerToggle extends ActionBarDrawerToggle {

    private Runnable runnable;

    public SmoothActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, Toolbar toolbar, int openDrawerContentDescRes, int closeDrawerContentDescRes) {
        super(activity, drawerLayout, toolbar, openDrawerContentDescRes, closeDrawerContentDescRes);
    }

    @Override
    public void onDrawerOpened(View drawerView) {
        super.onDrawerOpened(drawerView);
        invalidateOptionsMenu();
    }
    @Override
    public void onDrawerClosed(View view) {
        super.onDrawerClosed(view);
        invalidateOptionsMenu();
    }
    @Override
    public void onDrawerStateChanged(int newState) {
        super.onDrawerStateChanged(newState);
        if (runnable != null && newState == DrawerLayout.STATE_IDLE) {
            runnable.run();
            runnable = null;
        }
    }

    public void runWhenIdle(Runnable runnable) {
        this.runnable = runnable;
    }
}

onCreate中设置DrawerListener:

mDrawerToggle = new SmoothActionBarDrawerToggle(this, mDrawerLayout, mToolbar, R.string.open, R.string.close);
mDrawerLayout.setDrawerListener(mDrawerToggle);
最终,
private void selectItem(int position) {
    switch (position) {
        case DRAWER_ITEM_SETTINGS: {
            mDrawerToggle.runWhenIdle(new Runnable() {
                @Override
                public void run() {
                    Intent intent = new Intent(MainActivity.this, SettingsActivity.class);
                    startActivity(intent);
                }
            });
            mDrawerLayout.closeDrawers();
            break;
        }
        case DRAWER_ITEM_HELP: {
            mDrawerToggle.runWhenIdle(new Runnable() {
                @Override
                public void run() {
                    Intent intent = new Intent(MainActivity.this, HelpActivity.class);
                    startActivity(intent);
                }
            });
            mDrawerLayout.closeDrawers();
            break;
        }
    }
}

5
我认为这是更好的答案。它参考了文档并基于文档中的建议实现了一种方法。+1 - dsrees
2
但是在使用时,您应立即运行第一个片段以便在启动第一个活动时进行工作。 - Sheychan
1
哪个函数调用了selectItem()?它被超类覆盖了吗? - Libathos
@libathos mDrawerToggle.runWhenIdle(...) 和 mDrawerLayout.closeDrawers() 是关键点,无论在哪里使用都是如此。 - Chan Chun Him
@张NS,您调用runnable.run()时,将Runnable作为一个简单的函数接口,可能会使用其他东西,或者将runnable放在Handler.post中运行,因为它应该这样做。 - alekshandru

27

我也遇到了DrawerLayout的同样问题。

我进行了研究,然后找到了一个好的解决方法。

我所做的是......

如果您参考Android示例应用程序中的DrawerLayout,则请检查selectItem(position)的代码。

在此功能中,根据位置选择调用片段。根据我的需求,我使用下面的代码对其进行了修改,而且没有动画关闭卡顿问题,这很好用。

private void selectItem(final int position) {
    //Toast.makeText(getApplicationContext(), "Clicked", Toast.LENGTH_SHORT).show();
    mDrawerLayout.closeDrawer(drawerMain);
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            Fragment fragment = new TimelineFragment(UserTimeLineActivity.this);
            Bundle args = new Bundle();
            args.putInt(TimelineFragment.ARG_PLANET_NUMBER, position);
            fragment.setArguments(args);

            FragmentManager fragmentManager = getSupportFragmentManager();
            fragmentManager.beginTransaction().replace(R.id.content_frame, fragment).commit();

            // update selected item and title, then close the drawer
            mCategoryDrawerList.setItemChecked(position, true);

            setTitle("TimeLine: " + mCategolyTitles[position]);
        }
    }, 200);


    // update the main content by replacing fragments


}

在这里,我首先关闭了DrawerLayout,大约需要250毫秒。然后我的处理程序将调用片段,这样会很顺畅,并符合要求。

希望对你也有所帮助。

享受编码吧... :)


1
谢谢您的回复,这基本上就是我所做的,只不过我重用了相同的处理程序,以便在必要时可以删除已发布的Runnable。请参见我的答案。 - yarian
希望这也能帮到@yarian。无论如何,感谢您的评论。 - Shreyash Mahajan
1
很好,这将会有所帮助。 - kevingoos

20

看起来我已经找到了一个合理的解决方案。

可感知延迟最大的来源是抽屉视觉关闭和调用onDrawerClosed之间的延迟。我通过将一个Runnable发布到一个私有的Handler中来解决这个问题,该Handler在一些指定的延迟后启动预期的活动。选择此延迟与抽屉关闭相对应。

我曾尝试在80%的进度上进行启动 onDrawerSlide,但这有两个问题。第一个问题是出现卡顿。第二个问题是,如果将百分比增加到90%或95%,则由于动画的本质,可能性在增加,它根本不会被调用- 然后您必须退回到onDrawerClosed,这就失去了目的。

此解决方案可能会在旧手机上出现卡顿,但可以通过增加足够高的延迟将可能性减少至0。我认为250ms是卡顿和延迟之间合理的平衡。

代码的相关部分如下:

public class DrawerActivity extends SherlockFragmentActivity {
    private final Handler mDrawerHandler = new Handler();

    private void scheduleLaunchAndCloseDrawer(final View v) {
        // Clears any previously posted runnables, for double clicks
        mDrawerHandler.removeCallbacksAndMessages(null); 

        mDrawerHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                onDrawerItemSelection(v);
            }
        }, 250);
        // The millisecond delay is arbitrary and was arrived at through trial and error

        mDrawerLayout.closeDrawer();
    }
}

你的问题用这个解决了吗?我的答案根据你的要求运行良好。 - Shreyash Mahajan
2
你是什么意思?我在你之前发布了这个答案。它与你的非常相似,但不会每次创建一个Handler,这使得你可以取消先前已安排的启动。 - yarian
你创建了一个单独的类来处理它,而我只是通过创建处理程序放在自己的类中。你的方法也是正确的,我的方法也是正确的。享受编码... - Shreyash Mahajan
2
在onDrawerItemSelection(v)中提交片段事务时要小心,因为当用户选择抽屉项后,但在抽屉关闭之前停止应用程序时,会导致IllegalStateException异常。 - Ciske
@CiskeBoekelo 你说得很对。这个解决方案是专门用来启动活动的。如果我使用抽屉来在片段之间切换,我可能会立即执行片段事务,然后隐藏抽屉。 - yarian

10

Google IOsched 2015的运行非常流畅(除了设置页面),原因在于他们如何实现抽屉式菜单和启动应用。

首先,他们使用处理器来延迟启动:

        // launch the target Activity after a short delay, to allow the close animation to play
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                goToNavDrawerItem(itemId);
            }
        }, NAVDRAWER_LAUNCH_DELAY);

延迟时间为:

private static final int NAVDRAWER_LAUNCH_DELAY = 250;

他们还会做的另一件事是从使用以下代码在活动 onCreate() 中启动的活动中删除动画:

overridePendingTransition(0, 0);

要查看源代码,请前往Git


4

我正在使用以下方法。运行顺畅。

public class MainActivity extends BaseActivity implements NavigationView.OnNavigationItemSelectedListener {

    private DrawerLayout drawerLayout;
    private MenuItem menuItemWaiting;

    /* other stuff here ... */

    private void setupDrawerLayout() {

        /* other stuff here ... */

        drawerLayout.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
            @Override
            public void onDrawerClosed(View drawerView) {
                super.onDrawerClosed(drawerView);
                if(menuItemWaiting != null) {
                    onNavigationItemSelected(menuItemWaiting);
                }
            }
        });

    }

    @Override
    public boolean onNavigationItemSelected(MenuItem menuItem) {

        menuItemWaiting = null;
        if(drawerLayout.isDrawerOpen(GravityCompat.START)) {
            menuItemWaiting = menuItem;
            drawerLayout.closeDrawers();
            return false;
        };

        switch(menuItem.getItemId()) {
            case R.id.drawer_action:
                startActivity(new Intent(this, SecondActivity.class));

            /* other stuff here ... */

        }
        return true;
    }
}

ActionBarDrawerToggle相同:

drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close){
    @Override
    public void onDrawerClosed(View drawerView) {
        super.onDrawerClosed(drawerView);
        if(menuItemWaiting != null) {
            onNavigationItemSelected(menuItemWaiting);
        }
    }
};
drawerLayout.setDrawerListener(drawerToggle);

我不喜欢延迟发布的方法,我认为你的解决方案更好;-) - Anton Makov

2
较好的做法是使用 onDrawerSlide(View, float) 方法,在 slideOffset 为 0 时启动 Activity。请参见下面的示例:
public void onDrawerSlide(View drawerView, float slideOffset) {
    if (slideOffset <= 0 && mPendingDrawerIntent != null) {
        startActivity(mPendingDrawerIntent);
        mPendingDrawerIntent = null;
    }
}

只需在抽屉的ListView.OnItemClickListener onItemClick方法中设置mPendingDrawerIntent。


0

这篇答案是给使用RxJavaRxBinding的人。想法是在抽屉关闭之前防止活动启动。NavigationView用于显示菜单。

public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener{

  private DrawerLayout drawer;

  private CompositeDisposable compositeDisposable;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // setup views and listeners (NavigationView.OnNavigationItemSelectedListener)

    compositeDisposable = new CompositeDisposable();
    compositeDisposable.add(observeDrawerClose());

  }

  // uncomment if second activitiy comes back to this one again
  /*
  @Override
  protected void onPause() {
      super.onPause();
      compositeDisposable.clear();
  }

  @Override
  protected void onResume() {
     super.onResume();
     compositeDisposable.add(observeDrawerClose());
  }*/

  @Override
  protected void onDestroy() {
    super.onDestroy();
    compositeDisposable.clear();
  }

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

    navSubject.onNext(id);

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

  private Disposable observeDrawerClose() {
    return RxDrawerLayout.drawerOpen(drawer, GravityCompat.START)
        .skipInitialValue() // this is important otherwise caused to zip with previous drawer event
        .filter(open -> !open)
        .zipWith(navSubject, new BiFunction<Boolean, Integer, Integer>() {
          @Override
          public Integer apply(Boolean aBoolean, Integer u) throws Exception {
            return u;
          }
        }).subscribe(id -> {
          if (id == R.id.nav_home) {
            // Handle the home action
          } else {

          }
        });
  }
}

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