В мире Google Play нет справедливости: кто лучше выглядит — тот всех клиентов и забирает. В среде мобильной разработки лучше выглядеть означает быть органично вписанным в дизайн операционной системы.

Этим летом Google презентовала пятую версию ОС Android. Важной особенностью стало свежее видение дизайна мобильных приложений. Разработчикам предоставили новые инструменты, позволяющие создать приложения в стиле Material Design. Под этим красивым словосочетанием скрывается целая философия оформления визуального, динамического и интерактивного дизайна. Сегодня мы познакомимся с основными новшествами, которые помогут мобильному разработчику создать красивое и органичное приложение.

 

Цветовая схема

Прежде всего, появилась новая схема (theme) оформления приложения — Material Theme. Вот основные параметры этой цветовой палитры:

colorPrimaryDark — самая темная часть интерфейса, цвет панели уведомлений;
colorPrimary — основное «цветовое пятно» нашего приложения, цвет панели инструментов;
textColorPrimary — цвет текста в панели инструментов;
windowBackground — фон приложения;
navigationBarColor — цвет панели навигации внизу экрана.
Чтобы заполнить эти параметры, достаточно объявить их в файле colors.xml:

<resources>
  <color name="colorPrimary">#F50055</color>
  <color name="colorPrimaryDark">#C51160</color>
  ...
</resourses>
Рис. 1. Цветовая схема
Рис. 1. Цветовая схема
 

Списки

В Material Design уделяется большое внимание отображению однотипных данных. Нельзя давать пользователю скучать ни секунды. Даже если активное приложение показывает только унылые числа и графики, глаз пользователя должно радовать то, как эта информация преподнесена. Именно для этих целей появился класс CardView.

Сегодня мы создадим свое приложение, чтобы лучше разобраться, что же нам принес мир Material Design. Для начала в layout-файле опишем внешний вид элементов будущего списка.

<android.support.v7.widget.CardView
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
>

Внутри будут храниться все визуальные элементы. В данном случае это картинка-иллюстрация, заголовок к ней и краткое описание. Сегодня мы используем RelativeLayout, а значит, в параметрах элементов нужно указать, как именно они будут расположены относительно своих соседей. Чтобы приложение не «налезало» на края дисплея устройства, укажем отступ через параметр android:padding.

<RelativeLayout
  android:padding="5dp"
...>

Зафиксируем иллюстрацию в верхнем левом углу. Параметром android:layout_marginRight мы создаем небольшой промежуток от соседнего элемента:

<ImageView
  android:layout_alignParentLeft="true"
  android:layout_alignParentTop="true"
  android:layout_marginRight="16dp"
  android:id="@+id/itemImage"
.../>

Заголовок поместим сверху (alignParentTop) и с правой стороны (toRightOf) от изображения:

<TextView
  android:id="@+id/itemTitle"
  android:layout_toRightOf="@+id/itemImage"
  android:layout_alignParentTop="true"
.../>

Тем же способом добавим поле с кратким описанием: оно будет справа от изображения и под заголовком:

<TextView
  android:id="@+id/itemText"
  android:layout_toRightOf="@+id/itemImage"
  android:layout_below="@+id/itemTitle"
.../>

Теперь разберемся с тем, где будет храниться отображаемая информация. Для этого создадим класс ItemData, в конструкторе которого будет вся необходимая информация.

ItemData(String title, String descr, int photoId) {
  this.title = title;
  this.descr=desr;
  this.photoId=photoId
}

Созданный элемент нужно размножить с помощью RecyclerView и заполнить данными. В случае с объектами класса CardView нужно использовать класс RecyclerView. Он немножко запутаннее, чем привычные нам адаптеры, но мы с этим справимся. Для начала поместим его в layout-файле:

<android.support.v7.widget.RecyclerView
  android:layout_height="wrap_content"
  android:layout_width="wrap_content"
  android:id="@+id/rv"
/>
Рис. 2. Применение списка в стиле Material
Рис. 2. Применение списка в стиле Material

В стандартном Android SDK есть несколько вариантов отображения элементов RecyclerView: линейно (Linear), сеткой (Grid) и сеткой в шахматном порядке (StaggeredGrid).

Вспомним прошлую статью про библиотеки, она нам сегодня пригодится:

@Bind(R.id.rv)
RecyclerView rv;
...
ButterKnife.bind(this);

Требуемый адаптер можно создать, расширив класс RecyclerView.Adapter:

public class RVAdapter extends RecyclerView.Adapter<RVAdapter.PersonViewHolder>

Разработчики Android постарались минимизировать затратный по времени и ресурсам вызов метода findViewById, поэтому нам придется реализовать собственный ViewHolder:

Public class ItemVH extends RecyclerView.ViewHolder{
  TextView title;
  ...
  ItemVH(View itemView) {
    super(itemView);
    title = (TextView)itemView.findViewById(R.id.itemTitle);
    ...
  }
}

Создадим конструктор для адаптера. Главной его функцией будет передача в адаптер списка с элементами ItemData:

List<ItemData> items;
...
RVAdapter(List<ItemData> items)
  {this.items = items;}

Теперь переопределим метод onCreateViewHolder, он будет вызван ОС для инициализации визуальных элементов приложения. Поэтому тут нам следует указать, что объект RecyclerView будет состоять из множества элементов item:

public ItemVH onCreateViewHolder(ViewGroup viewGroup, int i) {
  View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item, viewGroup, false);
  ItemVH ivh = new ItemVH(v);
  return ivh;
}

В методе onBindViewHolder происходит непосредственное заполнение данными ячеек CardView:

public void onBindViewHolder(ItemVH ivh, int i)
  {ivh.title.setText(items.get(i).title);...}

Нашему RecyclerView требуется также LayoutManager для того, чтобы указать, как именно элементы будут расположены, сегодня нам будет достаточно линейного списка.

LinearLayoutManager llm = new LinearLayoutManager(context);
rv.setLayoutManager(llm);

Использование измененного адаптера вызывает дополнительные сложности — теперь не так просто обработать прикосновение пользователя к элементу списка. Более свободное обращение с отображаемыми данными создало необходимость самостоятельно реализовывать интерфейс обработки касаний. Сначала сформируем абстрактный класс, который и будет реагировать на касание экрана:

public interface OnItemClickListener
  {public void onItemClick(View view, int position);}

Теперь нужно задать обработчик любого касания вообще. Для этого создадим объект класса RecyclerView.OnItemTouchListener:

RecyclerItemClickListener implements RecyclerView.OnItemTouchListener

В нем создадим экземпляр обработчика касания:

private OnItemClickListener mListener;

Конструктору требуется указать контекст приложения и реализацию интерфейса обработчика касания элемента.

public RecyclerItemClickListener(Context context, OnItemClickListener listener) {
  mListener = listener;
  mGestureDetector = new GestureDetector(context,
    newGestureDetector.SimpleOnGestureListener() {
      @Override
      public boolean onSingleTapUp(MotionEvent e) {return true;}
    }
  );
}

Вся магия кроется в GestureDetector. Когда пользователь коснется рукой одного из элементов RecyclerView, ОС вызовет следующий метод:

public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
  View childView = view.findChildViewUnder(e.getX(), e.getY());
  if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) {
    mListener.onItemClick(childView, view.getChildAdapterPosition(childView));
  }
  return false;
}

Если в точке, которой коснулся пользователь, существует элемент RecyclerView и есть обработчик данного события (и это событие именно касание экрана), то вызываем метод onClick с указанием номера элемента RecyclerView.

Такие костыли — наглядное следствие ООП-разработки. С увеличением глубины наследования повышается связность кода и уменьшается гибкость. С одной стороны, теперь гораздо легче создать приложение с ярким дизайном, с другой — все чаще приходится использовать костыли.


И все-таки применение дизайнерских решений должно быть рациональным. В мире еще много телефонов на старых версиях ОС, на которых такое приложение работать не будет.
 

Добавим эффектов

Чтобы воздействовать на психику искушенного пользователя XXI века, современные компании стремятся плоское сделать объемным, а объемное — плоским. Поэтому мобильные телефоны теперь толщиной с кредитную карту, а приложения становятся «трехмерными». Любому объекту — наследнику класса View можно задать параметр elevation, который создает эффект, будто бы объект «парит» в воздухе и «отбрасывает тень».

Для CardView закруглим углы и зададим отступ:

card_view:cardElevation="15dp"
card_view:cardCornerRadius="7dp"

А у RecyclerView укажем, что фоном будет служить голубой прямоугольник с закругленными углами:

android:background="@drawable/back_rect"

Он задается XML-файлом в папке drawables:

<shape
  ...
  <stroke android:width="2dp" android:color="#ff207d94" />
  <padding android:left="2dp" android:top="2dp"
  ...
>
  ...
</shape>
Рис. 3. Онлайн-курсы по Material Design
Рис. 3. Онлайн-курсы по Material Design
 

FloatingActionButton

Этот элемент пропагандируется Google уже долгое время, но раньше его приходилось создавать вручную. В новой же ОС появился отдельный класс и все упростилось до объявления в layout-файле.

<android.support.design.widget.FloatingActionButton
  ...
  android:layout_margin="@dimen/fab_margin"
  android:src="@android:drawable/ic_input_add"
/>
 

Динамические цвета

Это еще одно нововведение, которое поможет приложению выглядеть органичней. Если в нем присутствуют яркие изображения (а это тоже тренд), то оформление текстовых элементов будет разумно задать с помощью класса Palette. В таком случае появится плавный переход от иллюстрации к тексту.

Palette.generateAsync(bitmap,
  new Palette.PaletteAsyncListener() {
    @Override
    public void onGenerated(Palette palette) {
      ...
      textView.setTextColor(vibrant.getTitleTextColor());
    }
  }
);
 

Анимация

Новые элементы приложения не появляются и не исчезают мгновенно, все их передвижения должны быть плавными. Это позволяет пользователю немного сфокусироваться на изменениях и дарит ощущение целостности программы.

Сперва создадим эффект плавного открытия и закрытия приложения: заставим его плавно «выезжать» с нижнего края экрана и «уезжать» обратно.

Transition ts = new Slide();
ts.setStartDelay(2000);
ts.setDuration(5000);
getWindow().setEnterTransition(ts);
getWindow().setExitTransition(ts);

Есть возможность придумывать свои варианты анимации. Создадим папку anim, а в ней файлы slide_in_right и slide_out_left, основная магия будет заключаться в них. Анимация будет состоять в постепенном «сдвиге в сторону» (translate) и в изменении прозрачности сменяемых Activity.

<set ... android:shareInterpolator="false" >
  <translate android:duration="500" android:fromXDelta="100%"
    android:toXDelta="0%" />
  <alpha android:duration="500" android:fromAlpha="0.0"
    android:toAlpha="1.0" />
</set>

Вызываемое Activity будет выезжать справа, становясь все менее прозрачным. Похожее будет происходить и с родительским Activity.

<translate android:duration="5000" android:fromXDelta="0%"
  android:toXDelta="-100%"/>
<alpha android:duration="5000" android:fromAlpha="1.0"
  android:toAlpha="0.0" />

А теперь организуем запуск. Создадим новый Intent, ему укажем, какие Activity будут задействованы, затем укажем набор опций. В опциях как раз и будет описана созданная нами анимация.

Intent transitionIntent = new Intent(MainActivity.this,
secondActivity.class);
ActivityOptionsCompat options =
  ActivityOptionsCompat.makeCustomAnimation(MainActivity.this,
R.anim.slide_in_right, R.anim.slide_out_left);
startActivityForResult(transitionIntent, 31337, options.toBundle());

Выход из secondActivity будет реализован похожим образом. Укажем, что все методы выполнились корректно, и завершим Activity, выполнив анимацию.

Intent returnIntent = new Intent();
setResult(Activity.RESULT_OK, returnIntent);
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);
finish();

Добавим эффект «сворачивания» к элементам списка. Для этого указываются размеры элемента и то расстояние, которое анимация должна пройти.

int cx = view.getWidth() / 2;
int cy = view.getHeight() / 2;
Animator anim;
anim = ViewAnimationUtils.createCircularReveal(view,
cx, cy, Math.max(cx, cy), 0);
anim.start();
 

Прикосновения

Если пользователь взаимодействует с каким-то большим элементом, стоит добавить эффект локальности прикосновения, в таком случае каждое касание элемента смотрится эффектнее. С помощью StateListAnimator можно создать эффект ряби (ripple effect) при нажатии на элемент. Посмотрим, как это выглядит для объектов ImageView:

logo.setClickable(true);
RippleDrawable rippledImage = new
RippleDrawable(ColorStateList.valueOf(Color.BLACK),
logo.getDrawable(), null);
logo.setImageDrawable(rippledImage);
 

Векторные изображения

Если наше приложение будет запускаться на совершенно разных устройствах, больших затрат потребует его оптимизация. В таком случае есть смысл перейти на векторную графику. Это позволяет создавать хорошо масштабируемые иллюстрации без потери качества изображения. Файл с векторным описанием изображения достаточно поместить в папку drawbles.

<vector ...>
<path ... android:pathData="..." />

А затем уже можно работать с ним как с обычным изображением:

android:src="@drawable/vector_image"
 

Заключение

Сегодня мы рассмотрели новые фишки в Android SDK, которые обязательно помогут тебе при создании приложений. Зачастую удачное приложение остается незамеченным из-за полного отсутствия адаптации к дизайну среды. В рамках статьи сложно описать все нюансы, поэтому можешь посмотреть исходный код приложения, в котором реализованы все описанные методы. Если остались какие-то вопросы, обязательно пиши мне на почту. Удачи!

3 комментария

  1. Аватар

    surfer

    28.12.2015 в 16:58

    <> — да неужели? А у меня уже 6.0.1 на смарте стоит.

  2. Аватар

    surfer

    28.12.2015 в 16:59

    «Этим летом Google презентовала пятую версию ОС Android»

Оставить мнение