Как я с лагом Navigation Drawer боролся

Привет, Хабр!

Меня зовут Алексей. Я разрабатываю под Android. Отладка в эмуляторе подобна смерти, поэтому я пользуюсь своим HTC Desire HD. Зверёк уже очень древний, за что я не могу его не любить, потому что любые шероховатости и неровности в приложении на нём отдаются славными лагами. Кстати, очень рекомендую запускать свои проекты на аппаратах средней мощности, ведь не у всех пользователей флагманы. Так вот, работая над своим новым приложением, я обнаружил, что при переключении между фрагментами через Navigation Drawer шторка навигации заметно пролагивает. При создании фрагмента делались запросы к базе и подгружались SharedPreferences. Мне было просто противно наблюдать этот лаг, и я придумал как избавиться от него. Всех, кому интересно, прошу под кат.

Я создал простенький проект с NavigationDrawer и тремя фрагментами (ссылка на Github в конце статьи). Код не претендует на звание идеального, я старался написать максимально просто и понятно. Код шторки взят прямо из примеров Google. Решил проблему я очень просто и в лоб: загрузка фрагмента запускается в отдельном потоке и задерживается на 0,3 секунды (магическое число, подобранное экспериментально).

Сначала вешаем слушателя на элементы списка так:

mDrawerList.setOnItemClickListener(new DrawerItemClickListener());


И метод selectItem был таким:

private void selectItem(int position) {
	Fragment fragment = new ContentFragment();
	Bundle args = new Bundle();
	args.putInt("positions", position);
	fragment.setArguments(args);

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

	mDrawerList.setItemChecked(position, true);
	setTitle(leftDrawerTitles[position]);
	mDrawerLayout.closeDrawer(mDrawerList);
}


А чтобы убрать лаг, изменим эти два места:

1) Из selectItem перенесём в onCreate весь код по выделению активного элемента и закрытию Drawer. selectItem станет таким:

private void selectItem(int position) {
	Fragment fragment = new ContentFragment();
	Bundle args = new Bundle();
	args.putInt("positions", position);
	fragment.setArguments(args);

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


2) А в onCreate вешаем слушателя на элементы Drawer и запускаем поток:

mDrawerList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
	public void onItemClick(AdapterView<?> adapterView, View view, final int position, long l) {
		mDrawerList.setItemChecked(position, true);
		setTitle(leftDrawerTitles[position]);
		mDrawerLayout.closeDrawer(mDrawerList);

		// Смена фрагмента запускается в отдельном потоке и задерживается на 0.3 секунды,
		// чтобы избежать пролагивания при переключении
		new Thread(new Runnable() {
			public void run() {
				try {
					TimeUnit.MILLISECONDS.sleep(300);
					selectItem(position);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}).start();
	}
});


Вот и всё. Может быть это и нелепый костыль, но я всё равно был безумно рад, открыв его для себя.

P.S. Хотелось бы добавить про отладку на телефоне. Очень рекомендую пользоваться ADB Over Wi-Fi (требуются root права). Wi-Fi не ограничивает вас в передвижениях и в поворотах аппарата.
P.P.S. Очень хочу получить консруктивную критику и советы знающих людей.

Ссылка на репозиторий: https://github.com/Rozag/Lags-free-navigation-drawer
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 14

    +2
    [offtop]
    Отладка в эмуляторе подобна смерти

    Не знаю, что у вас на кoмпе с железом, но я на свой не cамый топовый ноут поставил Genymotion — скорость на уровне среднего на рынке устройства (реально, работает почти не хуже моего xiaomi mi2a). И приложение на эмулятор деплоится намного быстрее, чем по юсб на смарт, не говоря уже о Wi-Fi
    [/offtop]
      +1
      Я, конечно же, имел в виду стандартный эмулятор, который идёт вместе с SDK. Про Genymotion я знаю, но щупать своё творение в руке, по-моему, гораздо приятнее :)
        +2
        Кстати, про adb over wifi — это не требует рут доступа, просто достаточно при usb подключении рестартануть adb на устройстве в режиме tcp/ip, делается так adb tcpip <port, например 5555> и всё, можно цеплятся по сети: adb connect ip:port, причем работает до перезагрузки девайса, то есть если вы перешли в другую сеть — просто надо переподключится по новому ip
          0
          Ивиняюсь за «ться» в комментарии :(
      +1
      Как-то уж очень костыльно выходит. Пробовали вынести получение данных из БД в CursorLoader? По идеи должно помочь. При первом запуске делаете cursorLoader.initLoader() а при обновлении cursorLoader.restartLoader().
        0
        Я согласен. Как мне кажется, вы не то место оптимизировали.
        +1
        new Handler().postDelayed(new Runnable() {...}, 300);
          0
          Согласен, я для решения этой проблемы просто с задержкой закрываю Navigation Drawer, давая полностью пройти FragmentTransaction
          +1
          У Navigation Drawer есть другая проблема — он не всегда корректно открывается/закрывается, и если у вас есть UI тесты, которые с ним взаимодействуют — ждите рандомных фейлов этих тестов из-за NavigationDrawer.

          Я задал такой вопрос на stackoverflow, но пока нормального решения нет: http://stackoverflow.com/questions/21848875/robotium-ui-testing-for-app-with-navigation-drawer
            +2
            Решение — действительно костыль. Подбор какого-либо числа исходя из девайса — признак неверного решения. Скажем, на LG Optimus Link — 300 мс мало. Нужно хотя бы 500.
            Оптимальным считаю следующий вариант:
            по клику на пункт меню — записать в переменную (скажем pendingFragment) фрагмент, который нужно показать. И выполнить транзакцию по событию OnDrawerClosed() в DrawerListener.
            Решение не мое, но оно мне нравится:
            1. Идеально сработает на любом девайсе
            2. Не увеличит количество костылей в Вашем коде
              0
              Действительно красивое решение! Спасибо вам.
              0
              И много у вас данных из БД подгружается?

              В любом случае для лучше делать это во фрагментах асинхронно через CursorLoader как уже написали выше.

              developer.android.com/guide/components/loaders.html
              developer.android.com/reference/android/content/AsyncTaskLoader.htm
              developer.android.com/reference/android/content/CursorLoader.html
                0
                На самом деле очень-очень мало.
                0
                Между тем, цитата из первого абзаца документации к DrawerLayout:

                DrawerLayout
                DrawerLayout.DrawerListener can be used to monitor the state and motion of drawer views. Avoid performing expensive operations such as layout during animation as it can cause stuttering; try to perform expensive operations during the STATE_IDLE state.

                Only users with full accounts can post comments. Log in, please.