Navigation Drawer это панель, которая отображает основные опции навигации приложения по левому краю экрана. Эта панель спрятана большую часть времени, но раскрывается, когда пользователь проводит пальцем от левого края экрана, или на верхнем уровне приложения пользователь прикасается к иконке приложения в панели действий.
Реализовать Navigation Drawer можно с помощью API DrawerLayout доступного в библиотеке поддержки.
Создание макета Navigation Drawer
Чтобы добавить Navigation Drawer, нужно объявить пользовательский интерфейс с объектом DrawerLayout в качестве корневого вида макета. Внутри DrawerLayout, добавьте один вид, который содержит главный контент для экрана (ваш основной макет, когда навигационный ящик скрыт) и другой вид, который содержит контент навигационного ящика.
Например, следующая схема использует DrawerLayout с двумя дочерними видами: FrameLayout содержит основной контент (населенный фрагментом во время выполнения), и ListView для навигационного ящика.
<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="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:background="#111"/>
</android.support.v4.widget.DrawerLayout>
Этот макет демонстрирует некоторые важные характеристики макета:
Основной вид контента (FrameLayout) должен быть первым ребенком в DrawerLayout поскольку порядок XML предполагает z-порядок и навигационный ящик должен быть в верхней части контента.
Основной вид контента имеет соответствующие родительскому виду ширину и высоту, потому что он представляет весь пользовательский интерфейс, когда навигационный ящик скрыт.
Вид навигационного ящика (ListView) должен указать свой горизонтальный вес с помощью атрибута android:layout_gravity.
Вид навигационного ящика определяет свою ширину в DP единицах, а высота соответствует родительскому виду. Ширина навигационного ящика не должна быть больше, чем 320dp, так что пользователь всегда может увидеть часть основного контента.
Инициализация списка навигационного ящика
В вашей активности, одна из первых необходимых вещей это сделать инициализацию списка элементов навигационного ящика. Как вы это сделаете, зависит от содержания вашего приложения, но навигационный ящик часто состоит из ListView, поэтому список должен быть населен адаптером (таким, как ArrayAdapter или SimpleCursorAdapter).
Например, вот как вы можете инициализировать список навигации массивом строк:
public class MainActivity extends Activity {
private String[] mPlanetTitles;
private DrawerLayout mDrawerLayout;
private ListView mDrawerList;
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPlanetTitles = getResources().getStringArray(R.array.planets_array);
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerList = (ListView) findViewById(R.id.left_drawer);
// Set the adapter for the list view
mDrawerList.setAdapter(new ArrayAdapter<String>(this,
R.layout.drawer_list_item, mPlanetTitles));
// Set the list's click listener
mDrawerList.setOnItemClickListener(new DrawerItemClickListener());
...
}
}
Обработка событий кликов Navigation Drawer
Когда пользователь выбирает элемент в списке навигационного ящика, система вызывает onItemClick () слушателя OnItemClickListener определенного в setOnItemClickListener ().
Что вы делаете в методе onItemClick () зависит от того, как вы реализовали структуру приложения. В следующем примере, каждый пункт в списке вставляет другой фрагмент в главное окно контента (FrameLayout определенный с помощью R.id.content_frame ID):
private class DrawerItemClickListener implements ListView.OnItemClickListener {
@Override
public void onItemClick(AdapterView parent, View view, int position, long id) {
selectItem(position);
}
}
/** Swaps fragments in the main content view */
private void selectItem(int position) {
// Create a new fragment and specify the planet to show based on position
Fragment fragment = new PlanetFragment();
Bundle args = new Bundle();
args.putInt(PlanetFragment.ARG_PLANET_NUMBER, position);
fragment.setArguments(args);
// Insert the fragment by replacing any existing fragment
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.content_frame, fragment)
.commit();
// Highlight the selected item, update the title, and close the drawer
mDrawerList.setItemChecked(position, true);
setTitle(mPlanetTitles[position]);
mDrawerLayout.closeDrawer(mDrawerList);
}
@Override
public void setTitle(CharSequence title) {
mTitle = title;
getActionBar().setTitle(mTitle);
}
Слушание событий открытия и закрытия навигационного ящика
Для прослушивания событий открытия и закрытия навигационного ящика, вызовите метод setDrawerListener () для DrawerLayout и передайте ему реализацию DrawerLayout.DrawerListener. Этот интерфейс обеспечивает методы обратного вызова, такие как onDrawerOpened () и onDrawerClosed ().
Однако, вместо того, чтобы реализовать DrawerLayout.DrawerListener, если ваша деятельность включает в себя панель действий action bar, вы можете вместо этого расширить класс ActionBarDrawerToggle. ActionBarDrawerToggle реализует DrawerLayout.DrawerListener так что вы можете переопределить эти функции обратного вызова, также он облегчает надлежащее поведение взаимодействия между иконкой панели действий и навигационным ящиком.
Вы должны изменять содержимое панели действий action bar, когда навигационный ящик виден, например, изменить название и удалить элементы действий, которые контекстны к основному содержанию. Следующий код показывает, как можно переопределить методы обратного вызова DrawerLayout.DrawerListener с экземпляром класса ActionBarDrawerToggle:
public class MainActivity extends Activity {
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
private CharSequence mDrawerTitle;
private CharSequence mTitle;
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
mTitle = mDrawerTitle = getTitle();
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close) {
/** Called when a drawer has settled in a completely closed state. */
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
getActionBar().setTitle(mTitle);
invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
}
/** Called when a drawer has settled in a completely open state. */
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
getActionBar().setTitle(mDrawerTitle);
invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
}
};
// Set the drawer toggle as the DrawerListener
mDrawerLayout.setDrawerListener(mDrawerToggle);
}
/* Called whenever we call invalidateOptionsMenu() */
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// If the nav drawer is open, hide action items related to the content view
boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList);
menu.findItem(R.id.action_websearch).setVisible(!drawerOpen);
return super.onPrepareOptionsMenu(menu);
}
}
Открытие и закрытие с иконкой приложения
Пользователи могут открывать и закрывать навигационный ящик жестом, но если вы используете панель действий, вы должны также позволить пользователям открывать и закрывать навигационный ящик нажатием на иконку приложения. И значок приложения также должен указывать на наличие навигационного ящика специальным значком. Вы можете реализовать все это поведение с помощью ActionBarDrawerToggle.
Для того, чтобы ActionBarDrawerToggle сделал эту работу, создать его экземпляр с конструктором, который потребует следующие аргументы:
Активность, которая хостит навигационный ящик.
DrawerLayout.
drawable ресурс для использования в качестве индикатора ящика. Это стандартный значок навигационного ящика.
Строка ресурса для описания действия «открытого ящика".
Строка ресурса для описания действия "закрытого" ящика.
Затем, вам нужно вызвать ActionBarDrawerToggle в нескольких местах жизненного цикла активности:
public class MainActivity extends Activity {
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
...
public void onCreate(Bundle savedInstanceState) {
...
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerToggle = new ActionBarDrawerToggle(
this, /* host Activity */
mDrawerLayout, /* DrawerLayout object */
R.drawable.ic_drawer, /* nav drawer icon to replace 'Up' caret */
R.string.drawer_open, /* "open drawer" description */
R.string.drawer_close /* "close drawer" description */
) {
/** Called when a drawer has settled in a completely closed state. */
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
getActionBar().setTitle(mTitle);
}
/** Called when a drawer has settled in a completely open state. */
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
getActionBar().setTitle(mDrawerTitle);
}
};
// Set the drawer toggle as the DrawerListener
mDrawerLayout.setDrawerListener(mDrawerToggle);
getActionBar().setDisplayHomeAsUpEnabled(true);
getActionBar().setHomeButtonEnabled(true);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Sync the toggle state after onRestoreInstanceState has occurred.
mDrawerToggle.syncState();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mDrawerToggle.onConfigurationChanged(newConfig);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Pass the event to ActionBarDrawerToggle, if it returns
// true, then it has handled the app icon touch event
if (mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
// Handle your other action bar items...
return super.onOptionsItemSelected(item);
}
...
}
DrawerLayout действует как контейнер верхнего уровня для контента окна, который позволяет интерактивному виду "ящика" вырваться из края окна.
Ящик позиционируется и компонуется с помощью атрибута android:layout_gravity для дочерних видов соответствующих с какой стороны вы хотите чтобы вид ящика вышел - влево или вправо.
Чтобы использовать DrawerLayout расположите основной вид контента как первый дочерний элемент с шириной и высотой match_parent.
Добавте вид ящика как дочерний элемент после вида основного контента и установите layout_gravity соответствующим образом. Ящики обычно используют match_parent по высоте с фиксированной шириной.
DrawerLayout.DrawerListener может быть использован для мониторинга состояния и движения вида ящика. Избегайте выполнения дорогостоящих операций, таких как компоновка во время анимации, так как это может вызвать зависание; попробуйте выполнить дорогостоящие операции во время состояния STATE_IDLE.
DrawerLayout.SimpleDrawerListener предлагает по умолчанию реализации каждого метода обратного вызова.
Любые ящики, расположенные с левой стороны всегда должны содержать контент для навигации по приложению, в то время как любые ящики, расположенные с правой стороны всегда должны содержать действия для текущего содержания.
Обеспечивание навигации вверх
Все экраны в вашем приложении, которые не являются главным входом в приложение (экран "Home"), должны предложить пользователю способ для перехода к экрану логического родителя в иерархии приложения, нажав кнопку Up в панели действий.
Укажите родительскую активность
Для реализации Up навигации, первый шаг заключается в объявлении, какая активность является подходящей родительской для каждой активности. Это облегчает системе использовать навигационные шаблоны, потому что система может определить логическую родительскую активность из файла манифеста.
Начиная с Android 4.1 (API уровня 16), вы можете объявлять логического родителя для каждой активности, указав android:parentActivityName атрибут в элементе <activity> .
Если ваше приложение поддерживает Android 4.0 и ниже, включите библиотеку поддержки в ваше приложение и добавьте <meta-data> элемент внутри <activity>. Затем укажите родительскую активность в качестве значения android.support.PARENT_ACTIVITY, соответствующее android:parentActivityName атрибуту.
Например:
<application ... >
...
<!-- The main/home activity (it has no parent activity) -->
<activity
android:name="com.example.myfirstapp.MainActivity" ...>
...
</activity>
<!-- A child of the main activity -->
<activity
android:name="com.example.myfirstapp.DisplayMessageActivity"
android:label="@string/title_activity_display_message"
android:parentActivityName="com.example.myfirstapp.MainActivity" >
<!-- Parent activity meta-data to support 4.0 and lower -->
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.example.myfirstapp.MainActivity" />
</activity>
</application>
С родительской активностью, объявленной этим способом, вы можете перемещаться до соответствующего родителя, используя API NavUtils.
Добавить Up действие
Чтобы обеспечить Up навигацию со значком приложения в панели действий, вызовите setDisplayHomeAsUpEnabled ():
@Override
public void onCreate(Bundle savedInstanceState) {
...
getActionBar().setDisplayHomeAsUpEnabled(true);
}
Это добавляет курсор ВЛЕВО рядом с иконкой приложения и включает его как кнопку действия такую, что, когда пользователь нажимает ее, ваша активность получает вызов onOptionsItemSelected(). Идентификатор для этого действия android.R.id.home.
Навигация Up к родительской активности
Для навигации Up, когда пользователь нажимает значок приложения, вы можете использовать статический метод класса NavUtils navigateUpFromSameTask (). Когда вы вызываете этот метод, он заканчивает текущую активность и начинает (или продолжает) соответствующую родительскую активность. Если целевая родительская активность в заднем стеке задач, она перенесется вперед. Как это будет сделано зависит от того, как родительская активность имеет возможность обрабатывать вызов onNewIntent():
Если родительская активность имеет режим запуска <singleTop>, или Up намерение содержит FLAG_ACTIVITY_CLEAR_TOP, родительская активность доводится до верхней части стека и получает намерение через метод onNewIntent ().
Если родительская активность имеет режим запуска <standard>, и Up намерение не содержит FLAG_ACTIVITY_CLEAR_TOP, родительская активность выталкивается из стека, и новый экземпляр этой активности создается на вершине стека, чтобы получить намерение.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
// Respond to the action bar's Up/Home button
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
Тем не менее, использование navigateUpFromSameTask () подходит только тогда, когда ваше приложение является владельцем текущей задачи (то есть, когда пользователь начал эту задачу из вашего приложения). Если это не так, и ваша активность была начата в задаче, которая принадлежит к другому приложения, тогда навигация Up должна создать новую задачу, которая принадлежит вашему приложению, которое требует, чтобы вы создали новый стек задач.
Навигация Up с новым стеком
Если ваша активность обеспечивает фильтры намерений, которые позволяют другим приложениям начать активность, вы должны реализовать метод onOptionsItemSelected () обратного вызова, так что если пользователь нажимает кнопку Up после ввода вашей активности из задачи другого приложения, ваше приложение начинает новую задачу с подходящим стеком прежде чем перейти Up.
Вы можете сделать это, сначала вызвав shouldUpRecreateTask (), чтобы проверить, существует ли экземпляр текущей активности в другой задаче приложения. Если метод возвращает истину, тогда затем построить новую задачу с TaskStackBuilder. В противном случае, вы можете использовать метод navigateUpFromSameTask(), как показано выше.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
// Respond to the action bar's Up/Home button
case android.R.id.home:
Intent upIntent = NavUtils.getParentActivityIntent(this);
if (NavUtils.shouldUpRecreateTask(this, upIntent)) {
// This activity is NOT part of this app's task, so create a new task
// when navigating up, with a synthesized back stack.
TaskStackBuilder.create(this)
// Add all of this activity's parents to the back stack
.addNextIntentWithParentStack(upIntent)
// Navigate up to the closest parent
.startActivities();
} else {
// This activity is part of this app's task, so simply
// navigate up to the logical parent activity.
NavUtils.navigateUpTo(this, upIntent);
}
return true;
}
return super.onOptionsItemSelected(item);
}
Для того, чтобы метод addNextIntentWithParentStack () работал, вы должны объявить логического родителя каждой активности в файле манифеста с помощью атрибута android:parentActivityName (и соответствующего <meta-data> элемента), как описано выше.
Обеспечение надлежащей навигации назад
Навигация назад это как пользователи перемещаются назад через историю экранов, которые они ранее посещали. Все Android устройства обеспечивают кнопку назад для этого типа навигации, так что ваше приложение не должно добавлять кнопку назад в UI.
Почти во всех ситуациях система поддерживает стек активностей в то время как пользователь осуществляет навигацию приложения. Это позволяет системе правильно перемещаться назад, когда пользователь нажимает кнопку Назад. Тем не менее, есть несколько случаев, в которых ваше приложение должно вручную указать поведение Назад для того, чтобы обеспечить лучший пользовательский опыт.
Шаблоны навигации, которые требуют вручную указать поведение Назад, включают в себя:
Когда пользователь входит в активность глубокого уровня непосредственно из уведомления, приложения, виджета или навигационного ящика.
Некоторые случаи, в которых пользователь переходит между фрагментами.
Когда пользователь переходит по страницам в WebView.
Синтезировать новый стек для глубоких ссылок
Обычно, система последовательно строит стек, когда пользователь переходит от одной активности к другой. Тем не менее, когда пользователь входит в ваше приложение с помощью глубокой ссылки, которая запускает активность в своей задаче, необходимо синтезировать новый задний стек, потому что активность работает в новой задаче без заднего стека вообще.
Например, когда уведомление переводит пользователя к активности глубоко в иерархии вашего приложения, вы должны добавить активности в стек вашей задачи, так что нажатие Назад переведет вверх иерархии приложения вместо выхода из приложения.
Укажите родительские активности в манифесте
Начиная с Android 4.1 (API уровня 16), вы можете объявлять логического родителя каждой активности, указав android:parentActivityName атрибут в <activity> элементе. Это облегчает системе навигацию, поскольку она может определить логический Назад или Вверх путь навигации с этой информацией.
Если ваше приложение поддерживает Android 4.0 и ниже, включите библиотеку поддержки в ваше приложение и добавте <meta-data> элемент внутри <activity>. Затем укажите родительскую активность в качестве значения android.support.PARENT_ACTIVITY, соответствующую android:parentActivityName атрибуту.
<application ... >
...
<!-- The main/home activity (it has no parent activity) -->
<activity
android:name="com.example.myfirstapp.MainActivity" ...>
...
</activity>
<!-- A child of the main activity -->
<activity
android:name="com.example.myfirstapp.DisplayMessageActivity"
android:label="@string/title_activity_display_message"
android:parentActivityName="com.example.myfirstapp.MainActivity" >
<!-- The meta-data element is needed for versions lower than 4.1 -->
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.example.myfirstapp.MainActivity" />
</activity>
</application>
С объявленной родтельской активностью, вы можете использовать API NavUtils, чтобы синтезировать новый задний стек.
Создать стек при запуске активности
Добавление активностей в задний стек начинается с события, которое генерируется пользователем в вашем приложении. То есть, вместо вызова startActivity(), используйте TaskStackBuilder API, чтобы определить каждую активность, которая должна быть помещена в новый задний стек. Затем запускайте целевую активность, вызывая startActivities (), или создайте соответствующее PendingIntent вызывая getPendingIntent ().
Например, когда уведомление напрявляет пользователя к активности глубоко в иерархии вашего приложения, вы можете использовать этот код, чтобы создать PendingIntent, который запускает активность и вставляет новый задний стек в целевую задачу:
// Intent for the activity to open when user selects the notification
Intent detailsIntent = new Intent(this, DetailsActivity.class);
// Use TaskStackBuilder to build the back stack and get the PendingIntent
PendingIntent pendingIntent =
TaskStackBuilder.create(this)
// add all of DetailsActivity's parents to the stack,
// followed by DetailsActivity itself
.addNextIntentWithParentStack(upIntent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setContentIntent(pendingIntent);
...
В результате PendingIntent определяет не только запуск активности (как определено detailsIntent), но также задний стек, который должен быть вставлен в задачу (все родители DetailsActivity определенные detailsIntent). Поэтому, когда запускается DetailsActivity, нажатие Назад проходит назад через каждую из родительских активностей класса DetailsActivity.
Примечание: Для того, чтобы метод addNextIntentWithParentStack () работал, вы должны объявить логического родителя каждой активности в файле манифеста, с помощью атрибута android:parentActivityName (и соответствующего <meta-data> элемента), как описано выше.
Реализация навигации Назад для фрагментов
При использовании фрагментов в вашем приложении, отдельные объекты FragmentTransaction могут представлять контекстные изменения, которые должны быть добавлены к заднему стеку. Например, если вы реализуете поток master/detail на телефоне путем замены фрагментов, вы должны убедиться, что нажатие кнопки Назад на detail экране возвращает пользователя к главному экрану. Чтобы сделать это, вызовете addToBackStack (), прежде чем совершить транзакцию:
// Works with either the framework FragmentManager or the
// support package FragmentManager (getSupportFragmentManager).
getSupportFragmentManager().beginTransaction()
.add(detailFragment, "detail")
// Add this transaction to the back stack
.addToBackStack()
.commit();
Когда есть объекты FragmentTransaction в заднем стеке, и пользователь нажимает кнопку Назад, FragmentManager выталкивает самую последнюю операцию из заднего стека и выполняет обратное действие (например, удаление фрагмента, если транзакция добавила его).
Примечание: Вы не должны добавлять транзакции в задний стек, когда транзакция относится к горизонтальной навигации (например, при переключении вкладок) или при изменении внешнего вида контента (например, при настройке фильтров).
Если ваше приложение обновляет другие элементы пользовательского интерфейса, чтобы отразить текущее состояние ваших фрагментов, например, на панели действий, не забудьте обновить пользовательский интерфейс, когда вы совершаете транзакци. Вы должны обновить пользовательский интерфейс после изменений стека в дополнение к фиксации транзакции. Вы можете слушать, когда FragmentTransaction вернется путем создания FragmentManager.OnBackStackChangedListener:
getSupportFragmentManager().addOnBackStackChangedListener(
new FragmentManager.OnBackStackChangedListener() {
public void onBackStackChanged() {
// Update your UI here.
}
});
Реализация навигации Назад для WebViews
Если часть вашего приложения содержится в WebView, может быть целесообразным для навигации Назад, чтобы пройти историю браузера. Чтобы сделать это, вы можете изменить onBackPressed () и прокси в WebView, если он имеет состояние истории:
@Override
public void onBackPressed() {
if (mWebView.canGoBack()) {
mWebView.goBack();
return;
}
// Otherwise defer to system default behavior.
super.onBackPressed();
}
Будьте осторожны при использовании этого механизма для динамических веб-страниц, которые могут давать большую историю. Страницы, которые генерируют обширную историю, например, те, которые делают частые изменения в документе хэш, могут сделать утомительным для пользователей выход из вашей активности.
Реализация навигации потомков
Descendant navigation это навигации вниз по иерархии приложения.
Descendant навигации, как правило, реализуется с помощью Intent объектов и startActivity (), либо путем добавления фрагментов активности с использованием объектов FragmentTransaction.
Реализация потока Мастер / Деталь для телефонов и планшетов
В навигационнои потоке мастер / деталь, мастер экран содержит список элементов в коллекции, и экран деталей показывает подробную информацию о конкретном элементе в этой коллекции. Реализация навигации с мастер-экрана на экран деталей является одной из форм навигации потомков.
Сенсорные телефоны являются наиболее подходящими для отображения одного экрана единовременно (мастер экран или экран деталей). Навигация потомков в этом случае часто реализуется с помощью Intent, которое запускает активность, представляющую экран деталей. С другой стороны, планшетные дисплеи, особенно если смотреть в альбомной ориентации, лучше всего подходят для отображения нескольких панелей контента: мастер слева, и детали справа). Здесь навигация потомков, как правило, реализуется с помощью FragmentTransaction, которая добавляет, удаляет или заменяет панель деталей с новым содержанием.
Навигация к внешней активности
Есть случаи, когда навигация вниз в иерархии вашего приложения приводит к активности из других приложений. Например, при просмотре экрана контактов для записи в адресной книге телефона, дочерний экран, показывающий недавние сообщения от контакта в социальной сети, может принадлежать к приложению социальной сети.
При запуске активности другого приложения, чтобы позволить пользователю говорить, писать электронную почту или выбирать фотографий, как правило, не требуется, чтобы пользователь вернулся к этой активности, если пользователь возобновит приложение с помощью Launcher (домашний экран устройства). Было бы запутанным, если прикосновение к значку приложения приведет пользователя на экран "писать электронную почту".
Для предотвращения этого, просто добавьте флаг FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET флаг, используемый для запуска внешней активности, например, так:
Intent externalActivityIntent = new Intent(Intent.ACTION_PICK);
externalActivityIntent.setType("image/*");
externalActivityIntent.addFlags(
Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
startActivity(externalActivityIntent);
Реализация потока адаптивного интерфейса
В зависимости от компоновки, которую ваше приложение в настоящее время показывает, поток пользовательского интерфейса может быть разным. Например, если ваше приложение находится в режиме двойной панели, нажатие на пункт на левой панели будет отображать содержимое на правой панели; если приложение находится в режиме одной панели, содержание должно отображаться собственной активностью.
Определение текущей компоновки
Так как ваша реализация каждого макета будет немного отличаться, одна из первых вещей, котору вам, вероятно, придется сделать, это определить, какой макет в настоящее время отображается. Например, вы бы хотели знать, находится ли пользователь в режиме «одной панели» или режиме "двойного панели". Вы можете сделать это с помощью запроса:
public class NewsReaderActivity extends FragmentActivity {
boolean mIsDualPane;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout);
View articleView = findViewById(R.id.article);
mIsDualPane = articleView != null &&
articleView.getVisibility() == View.VISIBLE;
}
}
Обратите внимание, что этот код запрашивает доступна панель или нет, что является гораздо более гибким, чем жесткое кодирование запроса для конкретного макета.
Другой пример того, как можно приспособиться к существованию различных компонентов, чтобы проверить, являются ли они доступными, прежде чем выполнять операции на них. Например, в приложении чтения новостей есть кнопка, которая открывает меню, но кнопка существует только при работе от версии старше Android 3.0 (потому что это функция перешла к ActionBar на уровне API 11+). Таким образом, чтобы добавить слушателя события для этой кнопки, вы можете сделать так:
Button catButton = (Button) findViewById(R.id.categorybutton);
OnClickListener listener = /* create your listener here */;
if (catButton != null) {
catButton.setOnClickListener(listener);
}
Реагировать в соответствии с текущей компоновкой
Некоторые действия могут иметь различный результат в зависимости от текущей компоновки. Например, в приложении чтения новостей, нажатие на заголовок из списка заголовков открывает статью справа в панели, если интерфейс находится в режиме двойной панели, но запустит отдельну активность, если интерфейс находится в режиме одной панели:
@Override
public void onHeadlineSelected(int index) {
mArtIndex = index;
if (mIsDualPane) {
/* display article on the right pane */
mArticleFragment.displayArticle(mCurrentCat.getArticle(index));
} else {
/* start a separate activity */
Intent intent = new Intent(this, ArticleActivity.class);
intent.putExtra("catIndex", mCatIndex);
intent.putExtra("artIndex", index);
startActivity(intent);
}
}
Точно так же, если приложение находится в режиме двойной панели, следует настроить панель действий с вкладками для навигации, в то время как, если приложение находится в режиме одной панели, оно должно установить навигацию с помощью виджета.
final String CATEGORIES[] = { "Top Stories", "Politics", "Economy", "Technology" };
public void onCreate(Bundle savedInstanceState) {
....
if (mIsDualPane) {
/* use tabs for navigation */
actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_TABS);
int i;
for (i = 0; i < CATEGORIES.length; i++) {
actionBar.addTab(actionBar.newTab().setText(
CATEGORIES[i]).setTabListener(handler));
}
actionBar.setSelectedNavigationItem(selTab);
}
else {
/* use list navigation (spinner) */
actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_LIST);
SpinnerAdapter adap = new ArrayAdapter(this,
R.layout.headline_item, CATEGORIES);
actionBar.setListNavigationCallbacks(adap, handler);
}
}
Повторное использование фрагментов в других активностях
Повторяющийся шаблон предназначен для нескольких экранов, имеющий часть своего интерфейса, который реализован как панель на некоторых конфигурациях экрана и как отдельная активность на других конфигурациях. Например, в приложении чтения новостей, текст новости представлен в правой части окна на больших экранах, но это отдельная активность на небольших экранах.
В подобных случаях, как правило, можно избежать дублирования кода за счет повторного использования одного и того же подкласса фрагмента в ряде активностей. Например, ArticleFragment используется в макете двойной панели:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal">
<fragment android:id="@+id/headlines"
android:layout_height="fill_parent"
android:name="com.example.android.newsreader.HeadlinesFragment"
android:layout_width="400dp"
android:layout_marginRight="10dp"/>
<fragment android:id="@+id/article"
android:layout_height="fill_parent"
android:name="com.example.android.newsreader.ArticleFragment"
android:layout_width="fill_parent" />
</LinearLayout>
И повторно (без макета) в макете активности для небольших экранов (ArticleActivity):
ArticleFragment frag = new ArticleFragment();
getSupportFragmentManager().beginTransaction().add(android.R.id.content, frag).commit();
Естественно, это имеет тот же эффект, что и обявление фрагмента в макете XML, но в этом случае макет XML является ненужной работой, потому что фрагмент является единственным компонентом этой активности.
Один очень важный момент, который нужно иметь в виду при проектировании ваших фрагментов, это не создавать сильную связь с конкретной активностью. Обычно вы можете сделать это, определив интерфейс, который абстрагирует все способы, которыми фрагмент должен взаимодействовать с содержащей активностью, а затем хост активность реализует этот интерфейс:
public class HeadlinesFragment extends ListFragment {
...
OnHeadlineSelectedListener mHeadlineSelectedListener = null;
/* Must be implemented by host activity */
public interface OnHeadlineSelectedListener {
public void onHeadlineSelected(int index);
}
...
public void setOnHeadlineSelectedListener(OnHeadlineSelectedListener listener) {
mHeadlineSelectedListener = listener;
}
}
Затем, когда пользователь выбирает заголовок, фрагмент уведомляет слушателя определенного хост активностью (в противоположность уведомления конкретной жестко закодированной активности):
public class HeadlinesFragment extends ListFragment {
...
@Override
public void onItemClick(AdapterView<?> parent,
View view, int position, long id) {
if (null != mHeadlineSelectedListener) {
mHeadlineSelectedListener.onHeadlineSelected(position);
}
}
...
}
Обработка изменения конфигурации экрана
Если вы используете отдельные активности для реализации отдельных частей своего интерфейса, вы должны иметь в виду, что может быть необходимо реагировать на определенные изменения конфигурации (например, изменение вращения) для того, чтобы сохранить ваш интерфейс последовательным.
Например, на типичном 7 "планшете под управлением Android 3.0 или выше, приложение чтения новостей использует отдельную активность, чтобы отобразить новость, когда работает в портретном режиме, но использует двухпанельный макет, когда находится в ландшафтном режиме.
Это означает, что, когда пользователь находится в портретном режиме и активность для просмотра статьи появляется на экране, вы должны обнаружить, что ориентация изменяется на ландшафтную и реагировать соответствующим образом, заканчивая свою активность и вернуться к основной активности, так что содержимое может отображаться в двухпанельной компоновке:
public class ArticleActivity extends FragmentActivity {
int mCatIndex, mArtIndex;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mCatIndex = getIntent().getExtras().getInt("catIndex", 0);
mArtIndex = getIntent().getExtras().getInt("artIndex", 0);
// If should be in two-pane mode, finish to return to main activity
if (getResources().getBoolean(R.bool.has_two_panes)) {
finish();
return;
}
...
}
Fragment Navigation Drawer
С выпуском Android 5.0 Lollipop, новый материальный стиль дизайна навигационного ящика охватывает всю высоту экрана и отображается на ActionBar и перекрывает полупрозрачный StatusBar.
Создайте файл menu/drawer_view.xml:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_first_fragment"
android:icon="@drawable/ic_one"
android:title="First" />
<item
android:id="@+id/nav_second_fragment"
android:icon="@drawable/ic_two"
android:title="Second" />
<item
android:id="@+id/nav_third_fragment"
android:icon="@drawable/ic_three"
android:title="Third" />
</group>
</menu>
Обратите внимание, что вы можете установить один из этих элементов, по умолчанию выбраным с помощью android:checked="true".
Вы также можете создать подзаголовки и групповые элементы вместе:
<item android:title="Sub items">
<menu>
<item
android:icon="@drawable/ic_dashboard"
android:title="Sub item 1" />
<item
android:icon="@drawable/ic_forum"
android:title="Sub item 2" />
</menu>
</item>
Далее, вы должны определить свои фрагменты, которые будут отображаться в ящике.Убедитесь, что все фрагменты происходят от android.support.v4.app.Fragment.
Для того, чтобы перемещать навигационный ящик на ActionBar, мы должны использовать новый виджет панели инструментов Toolbar, как определено в библиотеке V21 AppCompat. Панель инструментов может быть встроена в иерархию видов, которая гарантирует, что ящик скользит по ActionBar.
Создать новый файл макета res/layout/toolbar.xml со следующим кодом:
<android.support.v7.widget.Toolbar
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:fitsSystemWindows="true"
android:minHeight="?attr/actionBarSize"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:background="?attr/colorPrimaryDark">
</android.support.v7.widget.Toolbar>
Обратите внимание, что, когда android:fitsSystemWindows атрибут установлен в true для вида, вид будет выложен, как если бы StatusBar и ActionBar присутствовали т.е. интерфейс на вершине получает отступ достаточный, чтобы не быть скрытым навигационной панелью. Без этого атрибута, не будет хватать отступа для панели инструментов.
Мы хотим, чтобы наш главный вид контента имел панель навигации и, следовательно, android:fitsSystemWindows устанавливается true для панели инструментов.
Чтобы использовать панель инструментов в качестве ActionBar, необходимо отключить ActionBar по умолчанию. Это может быть сделано путем установки темы приложения в styles.xml файле.
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">#673AB7</item>
<item name="colorPrimaryDark">#512DA8</item>
<item name="colorAccent">#FF4081</item>
</style>
</resources>
Также обратите внимание, что, как правило, вы должны определить цветовую гамму, выбрав начальный и основной темный цвет. Для этого, например, мы подберем фиолетовые основный цвета.
Примечание: Если вы забыли отключить ActionBar в styles.xml, вы, вероятно, увидите java.lang.IllegalStateException с сообщением об ошибке,
Настройка ящика в активности
Далее, давайте установим простой навигационный ящик, основанный на следующем файле макета, который имеет всю настройку ящика в res/layout/activity_main.xml. Обратите внимание, что панель инструментов добавляется в качестве первого дочернего элемента главного представления, добавив тег include.
<!-- This DrawerLayout has two children at the root -->
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- This LinearLayout represents the contents of the screen -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- The ActionBar displayed at the top -->
<include
layout="@layout/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!-- The main content view where fragments are loaded -->
<FrameLayout
android:id="@+id/flContent"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<!-- The navigation drawer that comes from the left -->
<!-- Note that `android:layout_gravity` needs to be set to 'start' -->
<android.support.design.widget.NavigationView
android:id="@+id/nvView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@android:color/white"
app:menu="@menu/drawer_view" />
</android.support.v4.widget.DrawerLayout>
Добавте поддержку зависимостей Gradle, и синхронизацию с Gradle:
compile 'com.android.support:design:22.2.0'
Теперь, давайте настроим ящик в нашей активности. Мы также можем настроить значок меню.
Примечание: Убедитесь, что вы реализуете правильный метод onPostCreate(Bundle savedInstanceState). Есть 2 сигнатуры и только одна показывает значок гамбургера.
public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawer;
private Toolbar toolbar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Set a Toolbar to replace the ActionBar.
toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Find our drawer view
mDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// The action bar home/up action should open or close the drawer.
switch (item.getItemId()) {
case android.R.id.home:
mDrawer.openDrawer(GravityCompat.START);
return true;
}
return super.onOptionsItemSelected(item);
}
// Make sure this is the method with just `Bundle` as the signature
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
}
}
Навигация по пунктам меню
Настройте обработчик, чтобы отвечать на события нажатия на навигационных элементах и менять фрагмент.
@Override
protected void onCreate(Bundle savedInstanceState) {
// Find our drawer view
nvDrawer = (NavigationView) findViewById(R.id.nvView);
// Setup drawer view
setupDrawerContent(nvDrawer);
}
private void setupDrawerContent(NavigationView navigationView) {
navigationView.setNavigationItemSelectedListener(
new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
selectDrawerItem(menuItem);
return true;
}
});
}
public void selectDrawerItem(MenuItem menuItem) {
// Create a new fragment and specify the planet to show based on
// position
Fragment fragment = null;
Class fragmentClass;
switch(menuItem.getItemId()) {
case R.id.nav_first_fragment:
fragmentClass = FirstFragment.class;
break;
case R.id.nav_second_fragment:
fragmentClass = SecondFragment.class;
break;
case R.id.nav_third_fragment:
fragmentClass = ThirdFragment.class;
break;
default:
fragmentClass = FirstFragment.class;
}
try {
fragment = (Fragment) fragmentClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
// Insert the fragment by replacing any existing fragment
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.flContent, fragment).commit();
// Highlight the selected item, update the title, and close the drawer
menuItem.setChecked(true);
setTitle(menuItem.getTitle());
mDrawer.closeDrawers();
}
Добавление заголовка
NavigationView также принимает пользовательский атрибут, который может ссылаться на макет, который обеспечивает заголовок нашего макета. Например, вы можете создать layout/nav_header.xml похожий на следующий:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="192dp"
android:background="?attr/colorPrimaryDark"
android:padding="16dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark"
android:orientation="vertical"
android:gravity="bottom">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Header"
android:textColor="@android:color/white"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"/>
</LinearLayout>
Вы затем можете ссылаться на это в макете res/layout/activity_main.xml в NavigationView с помощью атрибута app:headerLayout:
<!-- res/layout/activity_main.xml -->
<!-- The navigation drawer -->
<android.support.design.widget.NavigationView
...
app:headerLayout="@layout/nav_header">
</android.support.design.widget.NavigationView>
Анимация значка гамбургера
Для того, чтобы значок гамбургер оживить, чтобы указать что ящик открывается и закрывается, мы должны использовать класс ActionBarDrawerToggle.
В res/values/strings.xml добавте следующее:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="drawer_open">Open navigation drawer</string>
<string name="drawer_close">Close navigation drawer</string>
</resources>
Мы должны связать DrawerLayout и панель инструментов вместе:
protected void onCreate(Bundle savedInstanceState) {
// Set a Toolbar to replace the ActionBar.
toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Find our drawer view
dlDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
drawerToggle = setupDrawerToggle();
// Tie DrawerLayout events to the ActionBarToggle
dlDrawer.setDrawerListener(drawerToggle);
}
private ActionBarDrawerToggle setupDrawerToggle() {
return new ActionBarDrawerToggle(this, dlDrawer, toolbar, R.string.drawer_open, R.string.drawer_close);
}
Далее, мы должны убедиться, что мы синхронизировали состояние, когда экран восстанавливается или есть изменения конфигурации (т.е. поворот экрана):
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Sync the toggle state after onRestoreInstanceState has occurred.
drawerToggle.syncState();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Pass any configuration change to the drawer toggles
drawerToggle.onConfigurationChanged(newConfig);
}
Мы также должны изменить метод onOptionsItemSelected () и позволить ActionBarToggle обрабатывать события.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (drawerToggle.onOptionsItemSelected(item)) {
return true;
}
return super.onOptionsItemSelected(item);
}
ActionBarToggle будет выполнять ту же функцию, что и ранее, но добавляет немного больше проверок и позволяет клику мыши на значке открыть и закрыть ящик.
ActionBarDrawerToggle показывает пользовательский DrawerArrowDrawable для значка гамбургера.
Создание Status Bar прозрачным
Чтобы иметь статус бар полупрозрачным и позволить ящику скользить над ним, нам нужно установить android:windowTranslucentStatus как true.
Так как этот стиль не доступен для устройств до KITKAT, мы добавим res/values-v19/styles.xml для API версии 19 и далее.
В res/values-v19/styles.xml мы можем добавить следующее:
<resources><!
-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:windowTranslucentStatus">true</item>
</style>
</resources>
Ограничения
Текущая версия библиотеки поддержки имеет свои ограничения. Основная проблема связана с системой, которая выделяет текущий элемент в меню навигации.
Атрибут itemBackground для NavigationView не обрабатывает выбранное состояние элемента правильно: как-то либо все элементы будут выделены, или ни один из них. Это делает этот атрибут в основном непригодным для большинства приложений.
При работе с подменю в навигационных элементах, снова подсветка отказывается работать, как ожидается: обновление выбранного элемента в подменю вызывает исчезновение подсветки.
В конце концов, кажется, что управление выбранным элементом это по-прежнему тяжелая работа, которая должна быть решена вручную, в самом приложении.
Альтернатива фрагментам
Хотя многие примеры ящика навигации показывают, как фрагменты могут быть использованы с навигационным ящиком, вы также можете использовать RelativeLayout / LinearLayout, если вы хотите использовать ящик в виде наложения на вашу текущую отображаемоую активность.
Вместо <FrameLayout> вы можете использовать <LinearLayout>
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/drawer_layout">
<LinearLayout
android:id="@+id/content_frame"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!-- The navigation drawer -->
<ListView android:id="@+id/left_drawer"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:background="#111"/>
</android.support.v4.widget.DrawerLayout>
Вместо:
// Insert the fragment by replacing any existing fragment
getFragmentManager().beginTransaction()
.replace(R.id.content_frame, fragment)
.commit();
Вместо этого, вы можете использовать LinearLayout контейнер для надувания активности напрямую:
LayoutInflater inflater = getLayoutInflater();
LinearLayout container = (LinearLayout) findViewById(R.id.content_frame);
inflater.inflate(R.layout.activity_main, container);