Pull to refresh

Как легко сделать Navigation Drawer и вкладки, используемые в популярных приложениях от Google

Reading time15 min
Views125K
При использовании популярного приложения Play Маркет многие обратили внимание на вкладки для переключения контента. Такое применение вкладок можно найти и в других приложениях от Google, таких как Play Музыка, Play Пресса.



На этой почве возникает интерес, а иногда и необходимость (если заказчик просит) реализовать увиденное. Я не стал исключением и при проектировании нового приложения, дизайн которого был набросан на черновике, присутствовал очень схожий дизайн, хотя и имел всего несколько вкладок. Казалось бы, что сложного? Сейчас откроем официальную документацию, просмотрим необходимые разделы и приступим к делу. Но, изучив документацию, не смог обнаружить соответствующих примеров — и тут же возник новый вопрос. Почему Android разработчики из компании Google по умолчанию не предоставляют примеров с необходимой функциональностью, чтобы сделать это довольно просто, ведь это реализовано в каждом их приложении? Также, погуглив, нашлись аналогичные вопросы на Stack Overflow. Исходя из этого, оказалось, что существует проблема или, по крайней мере, нераскрытый вопрос, в котором следует разобраться.

Ниже хочу рассказать о том, как всё же можно реализовать паттерн Navigation Drawer вместе с вкладками, как в популярных приложениях от Google.

В примере будет использоваться интегрированная среда разработки Eclipse, но все действия можно будет воспроизвести, используя и другие среды, к примеру, недавно вышедшую и набирающую популярность Android Studio от компании Google, основанную на IntelliJ IDEA.

Создание проекта


Создадим новый проект. Для этого перейдем в File > New > Android Application Project. Заполним поля, такие как имя приложения, пакета и версии SDK. Далее проследуем по экранам, нажимая клавишу Next и оставляя всё по умолчанию.



Среда разработки для нас создаст новый проект со стандартной структурой.

Пример будет работать, начиная с API версии 8. Это обосновано тем, что пользователи ещё пользуются девайсами со старой версией Android. Ниже приведены данные о количестве устройств, работающих под управлением различных версий платформы на состояние 12.08.2014.



Action Bar для API 8


Но ActionBar, сочетающий в себе заголовок и меню, появился начиная с Android 3.0 (API 11). Для того, чтобы его использовать, необходимо подключить к проекту библиотеку Android-Support-v7-Appcompat, любезно предоставленную компанией Google. Детальную инструкцию по добавлению библиотеки к проекту можно найти по адресу: developer.android.com/tools/support-library/setup.html

Есть две возможности добавить библиотеку к проекту — без использования ресурсов и с использованием. В реализации этого проекта будет использоваться библиотека с использованием ресурсов. После того, как библиотека будет добавлена в дерево проектов, необходимо перейти в Properties, нажав по проекту правой клавишей мыши и выбрать в категориях Android, затем нажать клавишу Add. В появившемся списке выбрать android-support-v7-appcompat и нажать OK > Apply > OK. Библиотека добавлена в проект. Но если попытаться запустить приложение, то ActionBar будет ещё не виден. Необходимо в res/values/styles.xml изменить строчку:

<style name="AppBaseTheme" parent="android:Theme.Light">

в res/values-v11/styles.xml изменить строчку:
<style name="AppBaseTheme" parent="android:Theme.Holo.Light ">

в res/values-v14/styles.xml изменить строчку:
<style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar">

на
<style name="AppBaseTheme" parent="@style/Theme.AppCompat.Light">


Также в главной активности необходимо наследоваться не от Activity, а от ActionBarActivity (android.support.v7.app.ActionBarActivity). После проделанных действий и запуска приложения можно увидеть ActionBar, включая и на ранних версиях API.



Зайдем в папку Menu и отредактируем файл main.xml, чтобы выглядел следующим образом:


<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:sabd="http://schemas.android.com/apk/res-auto" >

    <item
        android:id="@+id/action_search"
        android:icon="@android:drawable/ic_menu_search"
        android:orderInCategory="100"
        android:title="@string/action_search"
        sabd:showAsAction="ifRoom"/>

</menu>


Необходимо обратить внимание на следующую строчку: xmlns:sabd=http://schemas.android.com/apk/res-auto. Теперь необходимо атрибут showAsAction задавать следующим образом: sabd:showAsAction=" ". Также следует зайти в strings.xml и изменить строчку:


<string name="action_settings">Settings</string>

на

<string name="action_search">Search</string>


Теперь меню будет иметь привычный вид.



Внедрение бокового меню


Следующим нашим шагом будет внедрение бокового меню (Navigation Drawer). Это панель, которая отображает основные параметры навигации приложения в левом краю экрана. Раскрывается жестом от левого края экрана или по нажатию на значок приложения в панели действий.



Изменим основной ресурс activity_main.xml:


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


Боковое меню будет заполняться в ListView, для этого добавим в string.xml строковый массив названий:


<string-array name="screen_array">
        <item>Screen 1</item>
        <item>Screen 2</item>
        <item>Screen 3</item>
    </string-array>


Необходимо определить, как будет выглядеть позиция в ListView. Для этого создадим в папке layout новый ресурс с названием drawer_list_item.xml:


<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/text1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/activated_background"
    android:gravity="center_vertical"
    android:minHeight="?attr/listPreferredItemHeightSmall"
    android:paddingLeft="16dp"
    android:paddingRight="16dp"
    android:textAppearance="?android:attr/textAppearanceMedium"
    android:textColor="#fff" />


Для функционирования работы созданного ресурса далее дополнительно создадим в папке res новую папку drawable и в ней создадим селектор activated_background.xml, поместив в него:


<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@color/blue" android:state_activated="true"/>
    <item android:drawable="@color/blue" android:state_selected="true"/>
    <item android:drawable="@color/blue" android:state_pressed="true"/>
    <item android:drawable="@color/blue" android:state_checked="true"/>
    <item android:drawable="@android:color/transparent"/>

</selector>


В папке values создадим ресурс для цветов color.xml и поместим туда цвет, который будет отвечать за выделение пункта списка в боковом меню:


<?xml version="1.0" encoding="utf-8"?>
<resources>

    <item name="blue" type="color">#FF33B5E5</item>

</resources>


Следующим шагом будет добавление иконок в приложение, а именно значка бокового меню. Скачать архив иконок можно по прямой ссылке с официального сайта Google по адресу: developer.android.com/downloads/design/Android_Design_Icons_20130926.zip. В архиве будет многочисленное число иконок для разных событий, но нужны иконки из папки Navigation_Drawer_Indicator. Следует каждый графический объект с названием ic_drawer.png поместить в проект с правильной плотностью вида drawable-???..

Для оповещения о том, что меню открыто или закрыто, добавим в string.xml ещё записи:


<string name="drawer_open">Open navigation drawer</string>
<string name="drawer_close">Close navigation drawer</string>


Заодно удалим из ресурсов следующую строчку, так как она нам уже не понадобится:


<string name="hello_world">Hello world!</string>


Главное Activity


Теперь необходимо переписать класс MainActivity. Для поддержки старых устройств мы используем библиотеку поддержки Android Support Library (v4), при создании проекта она автоматически добавляется в папку libs. В листинге присутствуют комментарии, которые смогут дать возможность понять, как работает код. Для дополнительной информации можно воспользоваться официальной документацией: developer.android.com/training/implementing-navigation/nav-drawer.html. Ниже листинг.

Листинг
package com.nikosamples.develop.smp0001navigationdrawertabs;

import com.nikosamples.develop.smp0001navigationdrawertabs.fragments.ScreenOne;
import com.nikosamples.develop.smp0001navigationdrawertabs.fragments.ScreenThree;
import com.nikosamples.develop.smp0001navigationdrawertabs.fragments.ScreenTwo;

import android.content.res.Configuration;
import android.os.Bundle;
import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;

public class MainActivity extends ActionBarActivity {

	private String[] mScreenTitles;
	private DrawerLayout mDrawerLayout;
	private ListView mDrawerList;
	
    private ActionBarDrawerToggle mDrawerToggle;
    private CharSequence mDrawerTitle;
    private CharSequence mTitle;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	
		mTitle = mDrawerTitle = getTitle();
		mScreenTitles = getResources().getStringArray(R.array.screen_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, mScreenTitles));
		// Set the list's click listener
		mDrawerList.setOnItemClickListener(new DrawerItemClickListener());
		
		getSupportActionBar().setDisplayHomeAsUpEnabled(true);
		getSupportActionBar().setHomeButtonEnabled(true);
		
		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) {
				getSupportActionBar().setTitle(mTitle);
				supportInvalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
			}

			/** Called when a drawer has settled in a completely open state. */
			public void onDrawerOpened(View drawerView) {
				getSupportActionBar().setTitle(mDrawerTitle);
            	supportInvalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
			}
		};

		// Set the drawer toggle as the DrawerListener
		mDrawerLayout.setDrawerListener(mDrawerToggle);
		
		// Initialize the first fragment when the application first loads.
		if (savedInstanceState == null) {
			selectItem(0);
		}

	}
	
	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu;
		MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.main, menu);
		return super.onCreateOptionsMenu(menu);
	}

	/* 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_search).setVisible(!drawerOpen);
        return super.onPrepareOptionsMenu(menu);
    }
    
    @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 action buttons
        switch(item.getItemId()) {
        case R.id.action_search:
            // Show toast about click.
        	Toast.makeText(this, R.string.action_search, Toast.LENGTH_SHORT).show();
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
	}
	
	 /* The click listener for ListView in the navigation drawer */
	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) {
        // Update the main content by replacing fragments
        Fragment fragment = null;
        switch (position) {
        case 0:
            fragment = new ScreenOne();
            break;
        case 1:
            fragment = new ScreenTwo();
            break;
        case 2:
        	fragment = new ScreenThree();
            break;
        default:
            break;
        }
 
        // Insert the fragment by replacing any existing fragment
        if (fragment != null) {
        	FragmentManager fragmentManager = getSupportFragmentManager();
            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(mScreenTitles[position]);
    	    mDrawerLayout.closeDrawer(mDrawerList);
        } else {
            // Error
            Log.e(this.getClass().getName(), "Error. Fragment is not created");
        }
    }
	
	@Override
	public void setTitle(CharSequence title) {
	    mTitle = title;
	    getSupportActionBar().setTitle(mTitle);
	}
	
	/**
     * When using the ActionBarDrawerToggle, you must call it during
     * onPostCreate() and onConfigurationChanged()...
     */
	
	@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);
		// Pass any configuration change to the drawer toggles
		mDrawerToggle.onConfigurationChanged(newConfig);
	}
	
}



Добавим фрагменты


Добавим новый пакет fragments, который будет содержать фрагменты. И поместим в него три класса-фрагмента. Для примера я выложу код одного фрагмента, остальные необходимо сделать по аналогии, изменив название класса, конструктора и разметки.

package com.nikosamples.develop.smp0001navigationdrawertabs.fragments;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.nikosamples.develop.smp0001navigationdrawertabs.R;

public class ScreenOne extends Fragment {

	public ScreenOne() {
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {

		View rootView = inflater.inflate(R.layout.screen_first, container,
				false);

		return rootView;
	}

}


Также добавим в ресурсы разметку для этих классов. Для примера, выложу разметку одного фрагмента screen_one.xml. В остальных необходимо изменить только текст атрибута android:text:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:text="@string/screen_one"
        android:textAppearance="?android:attr/textAppearanceMedium" />

</RelativeLayout>


Добавим в string.xml ещё несколько строчек, которые будут информировать о том, какой экран открыт из меню:


<string name="screen_one">Screen one</string>
<string name="screen_two">Screen two</string>
<string name="screen_three">Screen three</string>


На этом этапе реализация Navigation Drawer завершена. Если запустить приложение, мы увидим работу бокового меню. Как на старых устройствах:



Так и на новых:



Внедрение вкладок


Теперь приступаем к добавлению вкладок. Первым делом, чтобы нам правильно добавить вкладки, необходимо понять принцип расположения элементов навигации. Существует строгая иерархия, в которой должны располагаться вкладки:

0 — Navigation Drawer занимает самый верхний уровень навигации.
1 — Action Bar второй уровень.
2 — Вкладки нижний уровень.



Для реализации необходимо использовать два дополнительных класса, которые Google снова любезно предоставляет. Это классы SlidingTabLayout и SlidingTabStrip. Добавим их в проект. Для этого создадим новый пакет view, там создадим новые классы с соответствующим названием и переместим в них код. При возникновении ошибок в методе createDefaultTabView(Context context) класса SlidingTabLayout следует подавить предупреждение, дописав над методом @SuppressLint(«NewApi»)

Внесем все новые изменения для фрагмента ScreenOne. Первым делом изменим разметку screen_one.xml:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical">

    <com.nikosamples.develop.smp0001navigationdrawertabs.view.SlidingTabLayout
          android:id="@+id/sliding_tabs"
          android:layout_width="match_parent"
          android:layout_height="wrap_content" />

    <android.support.v4.view.ViewPager
          android:id="@+id/viewpager"
          android:layout_width="match_parent"
          android:layout_height="0px"
          android:layout_weight="1"
          android:background="@android:color/white"/>

</LinearLayout>


Важно использовать полное имя пакета для SlidingTabLayout, так как он включен в наш проект. Далее создадим новую разметку в папке layout для вкладок pager_item.xml:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/item_subtitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/page"/>
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <TextView
        android:id="@+id/item_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="80sp" />

</LinearLayout>


Войдем в string.xml и изменим строчку:


<string name="screen_one">Screen one</string> 


на


<string name="page">Page:</string>


Так как нам уже не понадобится строковый ресурс, вместо него мы сразу отобразим ViewPager с номером вкладки. Далее изменим класс ScreenOne соответствующим образом:

Код класса ScreenOne
package com.nikosamples.develop.smp0001navigationdrawertabs.fragments;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.nikosamples.develop.smp0001navigationdrawertabs.R;
import com.nikosamples.develop.smp0001navigationdrawertabs.view.SlidingTabLayout;

public class ScreenOne extends Fragment {

	private SlidingTabLayout mSlidingTabLayout;
	private ViewPager mViewPager;

	public ScreenOne() {
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {

		View rootView = inflater.inflate(R.layout.screen_one, container, false);

		return rootView;
	}
	
	@Override
	public void onViewCreated(View view, Bundle savedInstanceState) {
		// Get the ViewPager and set it's PagerAdapter so that it can display items
		mViewPager = (ViewPager) view.findViewById(R.id.viewpager);
		mViewPager.setAdapter(new SamplePagerAdapter());

		// Give the SlidingTabLayout the ViewPager, this must be 
		// done AFTER the ViewPager has had it's PagerAdapter set.
		mSlidingTabLayout = (SlidingTabLayout) view.findViewById(R.id.sliding_tabs);
		mSlidingTabLayout.setViewPager(mViewPager);
	}

	// Adapter
    class SamplePagerAdapter extends PagerAdapter {

        /**
         * Return the number of pages to display
         */
        @Override
        public int getCount() {
            return 10;
        }

		/**
		 * Return true if the value returned from is the same object as the View
		 * added to the ViewPager.
		 */
        @Override
        public boolean isViewFromObject(View view, Object o) {
            return o == view;
        }

		/**
		 * Return the title of the item at position. This is important as what
		 * this method returns is what is displayed in the SlidingTabLayout.
		 */
        @Override
        public CharSequence getPageTitle(int position) {
            return "Item " + (position + 1);
        }

		/**
		 * Instantiate the View which should be displayed at position. Here we
		 * inflate a layout from the apps resources and then change the text
		 * view to signify the position.
		 */
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            // Inflate a new layout from our resources
            View view = getActivity().getLayoutInflater().inflate(R.layout.pager_item,
                    container, false);
            // Add the newly created View to the ViewPager
            container.addView(view);

            // Retrieve a TextView from the inflated View, and update it's text
            TextView title = (TextView) view.findViewById(R.id.item_title);
            title.setText(String.valueOf(position + 1));

            // Return the View
            return view;
        }

		/**
		 * Destroy the item from the ViewPager. In our case this is simply
		 * removing the View.
		 */
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }
    }	
}



Теперь можно запустить приложение и увидеть, как работают вкладки как на старом устройстве:



Так и на новом:



Можно заметить, что боковое меню покрывает вкладки, как в правилах навигации и приложениях от Google:



На этом пример полностью завершён и его можно использовать.

Вкладки Action Bar


Хочется отметить ещё один последний, важный момент. Многие «путают» реализованные вкладки с вкладками ActionBar, они выглядят похоже:



Но у них реализация другая, поведение и в горизонтальной ориентации переносятся в ActionBar:



Если добавить вкладки через ActionBar, то боковое меню Navigation Drawer не перекроет вкладки, а выедет под ними:



На этом всё. Спасибо за внимание и приятного вам кодинга.

Среда разработки – Eclipse
Минимальная версия Android – >= 8

Ссылки


Dashboards
developer.android.com/about/dashboards/index.html?utm_source=ausdroid.net
Support Library
developer.android.com/tools/support-library/index.html
Creating a Navigation Drawer
developer.android.com/training/implementing-navigation/nav-drawer.html
Android Design in Action: Navigation Anti-Patterns
plus.google.com/photos/+NickButcher/albums/5981768132040708401
SlidingTabsBasic
developer.android.com/samples/SlidingTabsBasic/index.html
Action Bar
developer.android.com/guide/topics/ui/actionbar.html

Готовый пример можно скачать c GitHub.
Tags:
Hubs:
Total votes 41: ↑39 and ↓2+37
Comments26

Articles