Делаем вкладки с помощью TabLayout

Автор: | 01.02.2018

Сейчас вкладки лучше всего реализовывать за счёт использования ViewPager с пользовательским «индикатором вкладок» сверху. В этой статье мы будем использовать TabLayout от Google, включенный в библиотеку Android Support Design в Android 6.0 Marshmallow (API 23).

До Android Marshmallow самым простым способом создания вкладок с помощью фрагментов было использование вкладок ActionBar. Однако, все методы, связанные с режимами навигации в классе ActionBar (такие как setNavigationMode(), addTab(), selectTab() и т.д.) на данный момент являются устаревшими.

Библиотека Android Support Design

Перед началом работы с вкладками, нужно добавить необходимую библиотеку. Чтобы добавить библиотеку в свой проект, нужно в файле build.gradle модуля приложения добавить следующую зависимость в блок dependencies.

ext {
  supportLibrary = "27.0.2"
}

dependencies {
  ...
  implementation "com.android.support:design:${supportLibrary}"
}

Создание TabLayout

Просто добавьте android.support.design.widget.TabLayout, который будет использоваться для отображения различных параметров вкладок, в код разметки. Компонент android.support.v4.view.ViewPager будет использоваться для создания страницы, отображающей фрагменты, которые мы создадим далее.

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

  <android.support.design.widget.TabLayout
      android:id="@+id/sliding_tabs"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      app:tabMode="fixed"
      />

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

Создание фрагментов

Теперь, когда у нас есть ViewPager и вкладки в разметке, мы можем перейти к определению содержимого каждой из вкладок. Поскольку каждая вкладка представляет собой только фрагмент, нам необходимо создать и определить Fragment, который нужно показать. В зависимости от ваших требований, в вашем приложении может быть один или несколько фрагментов.

В папке res/layout создадим файл fragment_page.xml и определим в нём код разметки, который будет отображаться на экране при выборе определённой вкладки.

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

Теперь создадим класс PageFragment и определим в нём логику подключения для фрагмента.

public class PageFragment extends Fragment {
  public static final String ARG_PAGE = "ARG_PAGE";

  private int mPage;

  public static PageFragment newInstance(int page) {
    Bundle args = new Bundle();
    args.putInt(ARG_PAGE, page);
    PageFragment fragment = new PageFragment();
    fragment.setArguments(args);
    return fragment;
  }

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (getArguments() != null) {
      mPage = getArguments().getInt(ARG_PAGE);
    }
  }

  @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
      Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_page, container, false);
    TextView textView = (TextView) view;
    textView.setText("Fragment #" + mPage);
    return view;
  }
}

Подключение FragmentPagerAdapter

Следующее, что нужно сделать, это реализовать адаптер для вашего ViewPager, который контролирует порядок вкладок, заголовков и связанного с ними контента. Наиболее важными методами для реализации здесь являются getPageTitle(int position), который используется для получения заголовка нужно вкладки, и getItem(int position), который определяет фрагмент для каждой вкладки.

public class SampleFragmentPagerAdapter extends FragmentPagerAdapter {
  final int PAGE_COUNT = 3;
  private String tabTitles[] = new String[] { "Tab1", "Tab2", "Tab3" };
  private Context context;

  public SampleFragmentPagerAdapter(FragmentManager fm, Context context) {
    super(fm);
    this.context = context;
  }

  @Override public int getCount() {
    return PAGE_COUNT;
  }

  @Override public Fragment getItem(int position) {
    return PageFragment.newInstance(position + 1);
  }

  @Override public CharSequence getPageTitle(int position) {
    // генерируем заголовок в зависимости от позиции
    return tabTitles[position];
  }
}

Настройка вкладки

Наконец, нам нужно прикрепить наш ViewPager к SampleFragmentPagerAdapter, а затем настроить вкладки с помощью двух шагов:

  • В методе onCreate() активности определим ViewPager и подключим адаптер.
  • Установим ViewPager в TabLayout, чтобы подключить пейджер с вкладками.
public class MainActivity extends AppCompatActivity {

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Получаем ViewPager и устанавливаем в него адаптер
    ViewPager viewPager = findViewById(R.id.viewpager);
    viewPager.setAdapter(
        new SampleFragmentPagerAdapter(getSupportFragmentManager(), MainActivity.this));

    // Передаём ViewPager в TabLayout
    TabLayout tabLayout = findViewById(R.id.sliding_tabs);
    tabLayout.setupWithViewPager(viewPager);
  }
}

Посмотрим, что получилось:

Настройка TabLayout

Существует множество атрибутов, которые вы можете использовать для настройки поведения TabLayout, например:

Название Параметры Описание
tabBackground @drawable/image Фон, применяемый к вкладкам
tabGravity center, fill Гравитация вкладок
tabIndicatorColor @color/blue Цвет линии индикатора
tabIndicatorHeight @dimen/tabh Высота линии индикатора
tabMaxWidth @dimen/tabmaxw Максимальная ширина вкладки
tabMode fixed, scrollable Выбор режима — фиксированные вкладки или прокручиваемый список
tabTextColor @color/blue Цвет текста на вкладке

Здесь вы можете посмотреть все атрибуты для TabLayout.

Создание стиля для TabLayout

Как правило, цвет индикатора вкладки устанавливается как accent, определённый в вашей теме Material Design. Вы можете переопределить этот цвет, создав свой собственный стиль в файле res/values/styles.xml и затем применить этот стиль к TabLayout.

<style name="MyCustomTabLayout" parent="Widget.Design.TabLayout">
  <item name="tabIndicatorColor">#0000FF</item>
</style>

Вы можете переопределить этот стиль для TabLayout в коде разметки:

<android.support.design.widget.TabLayout
    android:id="@+id/sliding_tabs"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:tabMode="fixed"
    style="@style/MyCustomTabLayout"
    />

Ниже вы можете увидеть пример ещё одного стиля, который можно задать для TabLayout:

<style name="MyCustomTabLayout" parent="Widget.Design.TabLayout">
  <item name="tabMaxWidth">200dp</item>
  <item name="tabIndicatorColor">?attr/colorAccent</item>
  <item name="tabIndicatorHeight">2dp</item>
  <item name="tabPaddingStart">12dp</item>
  <item name="tabPaddingEnd">12dp</item>
  <item name="tabBackground">?attr/selectableItemBackground</item>
  <item name="tabTextAppearance">@style/MyCustomTabTextAppearance</item>
  <item name="tabSelectedTextColor">?android:textColorPrimary</item>
</style>

<style name="MyCustomTabTextAppearance" parent="TextAppearance.Design.Tab">
  <item name="android:textSize">14sp</item>
  <item name="android:textColor">?android:textColorSecondary</item>
  <item name="textAllCaps">true</item>
</style>

Добавление иконок в TabLayout

В настоящее время класс TabLayout не предоставляет модель, которая позволяет добавлять иконки для вкладок. Однако вы можете вручную добавить иконки после настройки TabLayout.

Внутри класса FragmentPagerAdapter вы можете удалить строку getPageTitle() или просто вернуть null.

@Override public CharSequence getPageTitle(int position) {
  return null;
}

После настройки TabLayout в классе активности, вы можете использовать функцию getTabAt() для установки иконки:

int[] imageResId = {
    R.drawable.number_one, R.drawable.number_two, R.drawable.number_three
};

for (int i = 0; i < imageResId.length; i++) {
  tabLayout.getTabAt(i).setIcon(imageResId[i]);
}

Посмотрим результат:

Добавление иконок и текста в TabLayout

Другой подход — использовать SpannableString для добавление иконок и текста в TabLayout. Снова перепишем метод getPageTitle().

@Override public CharSequence getPageTitle(int position) {
  int[] imageResId = {
      R.drawable.number_one, R.drawable.number_two, R.drawable.number_three
  };

  // генерируем название в зависимости от позиции
  Drawable image = context.getResources().getDrawable(imageResId[position]);
  image.setBounds(0, 0, image.getIntrinsicWidth(), image.getIntrinsicHeight());
  // заменяем пробел иконкой
  SpannableString sb = new SpannableString("   " + tabTitles[position]);
  ImageSpan imageSpan = new ImageSpan(image, ImageSpan.ALIGN_BOTTOM);
  sb.setSpan(imageSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
  return sb;
}

По умолчанию, вкладка, созданная TabLayout, устанавливает для свойства textAllCaps значение true, что предотвращает визуализацию ImageSpans. Вы можете переопределить это поведение. изменив в styles.xml свойство tabTextAppearance.

<style name="MyCustomTabLayout" parent="Widget.Design.TabLayout">
  ...
  <item name="tabTextAppearance">@style/MyCustomTabTextAppearance</item>
</style>

<style name="MyCustomTabTextAppearance" parent="TextAppearance.Design.Tab">
  ...
  <item name="textAllCaps">false</item>
</style>

Обратите внимание на дополнительные пробелы, которые добавляются перед заголовком вкладки при создании класса SpannableString. Пустое пространство используется для размещения иконки, чтобы название отображалось полностью. В зависимости от того, где вы хотите разместить иконку, вы можете указать начало диапазона и его конец в методе setSpan().

Добавление пользовательской разметки в TabLayout

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

@Override protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  ViewPager viewPager = findViewById(R.id.viewpager);
  SampleFragmentPagerAdapter pagerAdapter =
      new SampleFragmentPagerAdapter(getSupportFragmentManager(), MainActivity.this);
  viewPager.setAdapter(pagerAdapter);

  TabLayout tabLayout = findViewById(R.id.sliding_tabs);
  tabLayout.setupWithViewPager(viewPager);

  for (int i = 0; i < tabLayout.getTabCount(); i++) {
    TabLayout.Tab tab = tabLayout.getTabAt(i);
    tab.setCustomView(pagerAdapter.getTabView(i));
  }
}

Теперь добавим метод getTabView() в класс SampleFragmentPagerAdapter.

public View getTabView(int position) {
  int[] imageResId = {
      R.drawable.number_one, R.drawable.number_two, R.drawable.number_three
  };
  
  // Подключаем свою разметку `res/layout/custom_tab.xml` с компонентами TextView и ImageView
  View v = LayoutInflater.from(context).inflate(R.layout.custom_tab, null);
  TextView tv = (TextView) v.findViewById(R.id.textView);
  tv.setText(tabTitles[position]);
  ImageView img = (ImageView) v.findViewById(R.id.imgView);
  img.setImageResource(imageResId[position]);
  return v;
}

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

Получение или выбор текущей страницы

С последними обновлениями библиотеки Android Suppoty Design вы также можете получить выбранную позицию вкладки, вызвав метод getSelectedTabPosition(). Если вам нужно сохранить или восстановить выбранную позицию вкладки во время поворота экрана или других изменений конфигурации, этот метод полезен для восстановления исходной позиции.

Во-первых, переместите экземпляры tabLayout и viewPager в глобальные переменные класса активности.

public class MainActivity extends AppCompatActivity {
  ViewPager viewPager;
  TabLayout tabLayout;

Во-вторых, мы можем сохранить и восстановить позицию вкладки, используя методы onSaveInstanceState() и onRestoreInstanceState().

public static String POSITION = "POSITION";

@Override public void onSaveInstanceState(Bundle outState) {
  super.onSaveInstanceState(outState);
  outState.putInt(POSITION, tabLayout.getSelectedTabPosition());
}

@Override protected void onRestoreInstanceState(Bundle savedInstanceState) {
  super.onRestoreInstanceState(savedInstanceState);
  viewPager.setCurrentItem(savedInstanceState.getInt(POSITION));
}

Использование вкладок без фрагментов

В случае, если вы не хотите использовать в своём приложении фрагменты, вы можете воспользоваться классом android.support.v4.view.PagerAdapter, как, например, в нашем приложении «Карточки для детей«.

Здесь вместо фрагментов в ViewPager передаётся адаптер, наследующий от PagerAdapter. Его код можно увидеть ниже.

public class TabAdapter extends android.support.v4.view.PagerAdapter {
  private Context context;
  private Typeface montserratBlack, montserratMedium;

  public TabAdapter(Context context, Typeface montserratBlack, Typeface montserratMedium) {
    this.context = context;
    this.montserratBlack = montserratBlack;
    this.montserratMedium = montserratMedium;
  }

  @NonNull @Override public Object instantiateItem(@NonNull ViewGroup collection, int position) {
    LayoutInflater inflater = LayoutInflater.from(context);
    View v = null;
    if (position == 0) {
      v = inflater.inflate(R.layout.themes, collection, false);
      setFontsThemes(v);
    }
    if (position == 1) {
      v = inflater.inflate(R.layout.settings, collection, false);
      setVectorDrawable(v, R.id.tv_donate, R.drawable.ic_local_atm);
      setVectorDrawable(v, R.id.tv_about, R.drawable.ic_info_outline_white_32dp);
      setVectorDrawable(v, R.id.tv_apps, R.drawable.ic_google_play);
      setVectorDrawable(v, R.id.tv_share, R.drawable.ic_share_black_24px);
      setVectorDrawable(v, R.id.tv_site, R.drawable.ic_public_black_24dp);
      setFontsSettings(v);
    }
    collection.addView(v);
    return v;
  }

  private void setFontsThemes(View v) {
    setTypefaceAndPaddingCheck(v, R.id.checkboxAlphabet);
    setTypefaceAndPaddingCheck(v, R.id.checkboxAnimals);
    setTypefaceAndPaddingCheck(v, R.id.checkboxAroundTheHouse);
    setTypefaceAndPaddingCheck(v, R.id.checkboxColors);
    setTypefaceAndPaddingCheck(v, R.id.checkboxFood);
    setTypefaceAndPaddingCheck(v, R.id.checkboxInstruments);
    setTypefaceAndPaddingCheck(v, R.id.checkboxNumbers);
    setTypefaceAndPaddingCheck(v, R.id.checkboxOnTheBody);
    setTypefaceAndPaddingCheck(v, R.id.checkboxOnTheGo);
    setTypefaceAndPaddingCheck(v, R.id.checkboxOutside);
    setTypefaceAndPaddingCheck(v, R.id.checkboxShapes);
  }

  private void setFontsSettings(View v) {
    setTypefaceTextView(v, R.id.tv_transform_type, montserratBlack);
    setTypefaceTextView(v, R.id.tv_disable_ads, montserratMedium);
    setTypefaceTextView(v, R.id.tv_donate, montserratMedium);
    setTypefaceTextView(v, R.id.tv_about, montserratMedium);
    setTypefaceAndPaddingRadio(v, R.id.flow);
    setTypefaceAndPaddingRadio(v, R.id.depth);
    setTypefaceAndPaddingRadio(v, R.id.zoom);
    setTypefaceAndPaddingRadio(v, R.id.slide_over);
    setTypefaceAndPaddingRadio(v, R.id.fade);
    setTypefaceAndPaddingCheck(v, R.id.checkboxBackgroundColors);
    setTypefaceAndPaddingCheck(v, R.id.checkboxPlaySounds);
    setTypefaceAndPaddingCheck(v, R.id.checkboxShuffle);
    setTypefaceTextView(v, R.id.tv_apps, montserratMedium);
    setTypefaceTextView(v, R.id.tv_share, montserratMedium);
    setTypefaceTextView(v, R.id.tv_site, montserratMedium);
  }

  @Override public int getCount() {
    return 2;
  }

  @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
    return view == object;
  }

  public View getTabView(int position) {
    TextView textView = new TextView(context);
    textView.setTypeface(montserratBlack);
    textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 22);
    textView.setTextColor(context.getResources().getColor(R.color.white));

    if (position == 0) {
      textView.setText(R.string.themes);
    }
    if (position == 1) {
      textView.setText(R.string.settings);
    }
    return textView;
  }

  private void setTypefaceAndPaddingRadio(View v, int id) {
    final float scale = context.getResources().getDisplayMetrics().density;
    RadioButton view = v.findViewById(id);
    view.setTypeface(montserratMedium);
    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) {
      view.setPadding(view.getPaddingLeft() + (int) (30.0f * scale + 5.0f), view.getPaddingTop(),
          view.getPaddingRight(), view.getPaddingBottom());
    }
  }

  private void setTypefaceAndPaddingCheck(View v, int id) {
    final float scale = context.getResources().getDisplayMetrics().density;
    CheckBox view = v.findViewById(id);
    view.setTypeface(montserratMedium);
    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) {
      view.setPadding(view.getPaddingLeft() + (int) (30.0f * scale + 5.0f), view.getPaddingTop(),
          view.getPaddingRight(), view.getPaddingBottom());
    }
  }

  private void setTypefaceTextView(View v, int id, Typeface typeface) {
    TextView label = v.findViewById(id);
    label.setTypeface(typeface);
  }

  private void setVectorDrawable(View v, int id, int drawableId) {
    Drawable drawable =
        VectorDrawableCompat.create(context.getResources(), drawableId, context.getTheme());
    TextView textView = v.findViewById(id);
    textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
  }
}

Установка адаптера аналогична способам выше, просто вызываем метод setAdapter() и передаёт в него экземпляр класса TabAdapter.

@Override protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  ViewPager viewPager = findViewById(R.id.viewpager);
  montserratBlack = Typeface.createFromAsset(getAssets(), "Montserrat-Black.ttf");
  montserratMedium = Typeface.createFromAsset(getAssets(), "Montserrat-Medium.ttf");
  TabAdapter adapter = new TabAdapter(this, montserratBlack, montserratMedium);
  viewPager.setAdapter(adapter);
  TabLayout tabLayout = findViewById(R.id.tablayout);
  tabLayout.setupWithViewPager(viewPager);

  for (int i = 0; i < tabLayout.getTabCount(); i++) {
    TabLayout.Tab tab = tabLayout.getTabAt(i);
    tab.setCustomView(adapter.getTabView(i));
  }
  
  ...
}

Ещё вариант реализации вкладок

Также вы можете создать свои собственные вкладки с помощью шагов ниже. Такой подход используется в нашем приложении «Менеджер паролей для Wi-Fi сетей«.

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

<TabHost
    android:id="@+id/tabHost"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    app:tabIndicatorColor="@color/white"
    >
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:orientation="vertical"
      >
    <TabWidget
        android:id="@android:id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/primary"
        />
    <FrameLayout
        android:id="@android:id/tabcontent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
      <include
          layout="@layout/include_login"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          app:layout_behavior="@string/appbar_scrolling_view_behavior"
          />
      <include
          layout="@layout/include_signup"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          app:layout_behavior="@string/appbar_scrolling_view_behavior"
          />
    </FrameLayout>
  </LinearLayout>
</TabHost>

Затем в коде активности добавим следующий код для определения этих компонент и настройки вкладок.

tabHost = findViewById(R.id.tabHost);
tabHost.setup();
setupTab(getString(R.string.login), R.id.include_login);
setupTab(getString(R.string.action_sign_in_short), R.id.include_signup);
for (int i = 0; i < tabHost.getTabWidget().getChildCount(); i++) {
  TextView tv = tabHost.getTabWidget().getChildAt(i).findViewById(android.R.id.title);
  tv.setTextColor(getResources().getColor(R.color.white));
}

private void setupTab(String title, int id) {
  TabHost.TabSpec spec = tabHost.newTabSpec(title);
  spec.setContent(id);
  spec.setIndicator(title);
  tabHost.addTab(spec);
}

В этом случае переключать вкладки можно легко с помощью метода у TabHost setCurrentTab(int position).

Делаем вкладки с помощью TabLayout: 3 комментария

  1. Александр

    Я создал ретрофит запрос, получил данные и хочу установить количество вкладок соответственно количеству полученных строк. Куда в адаптере мне вставить этот код? Я создал AsyncTask, но не получается нигде изменить значение количества вкладок, которое задано по умолчанию.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *