优化抽屉和活动启动速度
Posted
技术标签:
【中文标题】优化抽屉和活动启动速度【英文标题】:Optimizing drawer and activity launching speed 【发布时间】:2013-08-22 23:23:20 【问题描述】:我正在使用谷歌DrawerLayout
。
当一个项目被点击时,抽屉会顺利关闭,Activity
将被启动。将这些活动转化为Fragment
s 是不是一种选择。因此,启动活动然后关闭抽屉也不是一种选择。关闭抽屉并同时启动 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
只是启动了五项活动之一。
我对@987654330@ 的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 状态期间执行昂贵的操作。
您可以覆盖ActionBarDrawerToggle
的onDrawerStateChanged
方法(实现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
);
【讨论】:
以上是关于优化抽屉和活动启动速度的主要内容,如果未能解决你的问题,请参考以下文章