优化抽屉和活动启动速度

Posted

技术标签:

【中文标题】优化抽屉和活动启动速度【英文标题】:Optimizing drawer and activity launching speed 【发布时间】:2013-08-22 23:23:20 【问题描述】:

我正在使用谷歌DrawerLayout

当一个项目被点击时,抽屉会顺利关闭,Activity 将被启动。将这些活动转化为Fragments 是不是一种选择。因此,启动活动然后关闭抽屉也不是一种选择。关闭抽屉并同时启动 Activity 会导致关闭动画卡顿。

鉴于我想先顺利关闭它,然后启动 Activity,我遇到了用户单击抽屉项目和他们看到他们想要去的 Activity 之间的延迟问题。

这就是每个项目的点击监听器的样子。

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

我的activity也是DrawerListener,它的onDrawerClosed方法看起来像:

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

onDrawerItemSelection 只是启动了五项活动之一。

我对@9​​87654330@ 的DrawerActivity 什么都不做。

我正在对此进行检测,从调用 onClick 到 onDrawerClosed 结束的那一刻平均需要 500-650 毫秒。

有一个明显的延迟,一旦抽屉关闭,在相应的活动启动之前。

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

结束动画发生,这是几毫秒的时间(假设是 300)。

然后,抽屉在视觉上关闭和其侦听器被触发之间可能存在一些延迟。我正试图弄清楚到底发生了多少这种情况by looking at DrawerLayout source,但还没有弄清楚。

然后是启动的 Activity 执行其启动生命周期方法所需的时间,直至 onResume。我还没有对此进行检测,但我估计大约需要 200-300 毫秒。

这似乎是一个问题,走错路会付出相当高的代价,所以我想确保我完全理解它。

一种解决方案是跳过结束动画,但我希望保留它。

如何尽可能缩短过渡时间?

【问题讨论】:

我怎样才能尽可能地减少我的过渡时间? - 你可以像这样使用onDrawerSlide() gist.github.com/luksprog/6316295 ,我不知道那会是多少拯救你。另外,你在抽屉活动的scheduleLaunchAndCloseDrawer(v);onPause() 中做什么? inScheduleLaunchAndCloseDrawer 我只是存储对视图的引用。我稍后会匹配它的 id 以确定要启动哪个 Activity。我什么都不做。我试过在 onDrawerSlide 中这样做,但它也会结结巴巴。我尝试过超过 80% 的某个阈值。 我有一些我想明天尝试的东西,它正在发布一个 runnable 以在某个预定延迟(例如 350-400 毫秒)启动活动。这可能仍然卡顿,本质上目标是减少抽屉关闭和听众被解雇之间的延迟为零。我会在尝试时更新问题。 这可能是一个解决方案,但以任意时间间隔发布可运行文件听起来不是一个好主意。您不妨尝试使用Handler.postAtFrontOfQueue(Runnable)onDrawerClosed() 回调中发布Runnable 启动活动。 我同意这听起来很老套。我会尝试两个。我怀疑延迟的很大一部分是在我视觉上感知抽屉关闭和 android 调用 onDrawerClosed() 之间,我相信 postAtFrontOfQueue 无助于解决。但我会试一试并报告。 【参考方案1】:

根据docs,

避免在动画期间执行昂贵的操作,例如布局,因为它会导致卡顿;尝试在 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;
        
    

【讨论】:

我相信这是更好的答案。它引用文档并根据文档中的建议实现一种方法。 +1 但是当使用这个时,你应该立即运行第一个片段以在第一个活动启动时工作 哪个函数调用 selectItem() ?它被超类覆盖了吗? @libathos mDrawerToggle.runWhenIdle(...) 和 mDrawerLayout.closeDrawers() 是重点,不管在哪里使用。 @Zhang NS,你调用runnable.run(),使用Runnable作为一个简单的函数接口,也许使用其他东西,或者在Handler.post中运行runnable。跨度> 【参考方案2】:

我在使用 DrawerLayout 时遇到了同样的问题。

我对此进行了研究,然后找到了一个不错的解决方案。

我正在做的是.....

如果您为 DrawerLayout 引用 Android 示例应用程序,请检查 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 毫秒。然后我的处理程序将调用片段。根据要求运行顺利。

希望对你也有帮助。

享受编码... :)

【讨论】:

感谢您的回复,这基本上是我所做的,除了我重用了相同的处理程序,因此我可以在必要时删除已发布的 Runnable。看我的回答。 希望它也能帮助@yarian。无论如何感谢您的评论。 很好,这太有帮助了【参考方案3】:

所以我似乎已经用一个合理的方案解决了这个问题。

可感知延迟的最大来源是抽屉在视觉上关闭到调用onDrawerClosed 之间的延迟。我通过将Runnable 发布到私人Handler 来解决这个问题,该Handler 在特定的延迟时间启动预期的活动。选择此延迟以对应抽屉关闭。

我尝试在 80% 的进度后启动 onDrawerSlide,但这有两个问题。首先是它结结巴巴。第二个是,如果你将百分比增加到 90% 或 95%,由于动画的性质,它根本不会被调用的可能性会增加——然后你不得不回退到 onDrawerClosed,这会失败目的。

此解决方案可能会卡顿,特别是在旧手机上,但只需将延迟提高到足够高,就可以将可能性降低到 0。我认为 250 毫秒是卡顿和延迟之间的合理平衡。

代码的相关部分如下所示:

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();
    

【讨论】:

你的问题能解决吗?根据您的要求,我的回答效果很好。 什么意思?我在你之前发布了这个答案。它与您的非常相似,但不会每次都创建一个 Handler,这样您就可以取消之前的计划启动。 您已经创建了单独的类来处理它,而我只有通过创建处理程序在自己的类中。你以你的方式正确,我也以我的方式。享受编码...... 在 onDrawerItemSelection(v) 中提交片段事务时要小心,因为当应用程序在用户选择抽屉项目之后、抽屉关闭之前停止时,它会导致 IllegalStateException . @CiskeBoekelo 你说的很对。此解决方案专门用于启动活动。如果我使用抽屉在 Fragment 之间切换,我可能会立即执行 Fragment 事务,然后隐藏抽屉。【参考方案4】:

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。

【讨论】:

【参考方案5】:

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

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);

【讨论】:

我不喜欢后期延迟方法,我认为你的解决方案更好;-)【参考方案6】:

更好的方法是使用 onDrawerSlide(View, float) 方法并在 slideOffset 为 0 时启动 Activity。见下文

public void onDrawerSlide(View drawerView, float slideOffset) 
    if (slideOffset <= 0 && mPendingDrawerIntent != null) 
        startActivity(mPendingDrawerIntent);
        mPendingDrawerIntent = null;
    

只需在 Drawer 的 ListView.OnItemClickListener onItemClick 方法中设置 mPendingDrawerIntent 即可。

【讨论】:

【参考方案7】:

此答案适用于使用RxJava 和RxBinding 的人。想法是防止活动启动,直到抽屉关闭。 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 

          
        );
  

【讨论】:

以上是关于优化抽屉和活动启动速度的主要内容,如果未能解决你的问题,请参考以下文章

iOS启动速度优化

无盘 怎么优化Marvall千兆网卡的启动速度,。

优化启动速度

Android内核开发:系统启动速度优化-Android OS启动优化(转)

探究 Java 应用的启动速度优化

探究 Java 应用的启动速度优化