Вступление
Одним из наиболее интересных аспектов спецификаций Material Design является визуальная преемственность между действиями. С помощью всего лишь нескольких строк кода новые API-интерфейсы Lollipop позволяют осуществлять значимый переход между двумя действиями благодаря бесшовной и непрерывной анимации. Это нарушает классические границы активности предыдущих версий Android и позволяет пользователю понять, как элементы переходят из одной точки в другую.
В этом руководстве я покажу вам, как этого добиться, и приведите пример приложения в соответствие с рекомендациями Google по дизайну материалов.
Предпосылки
В этом руководстве я предполагаю, что вы уже знакомы с разработкой для Android и используете Android Studio в качестве IDE. Я буду активно использовать Android, исходя из базовых знаний о жизненном цикле активности, и о новом виджете RecyclerView
представленном в API 21 в июне прошлого года. Я не собираюсь углубляться в детали этого класса, но, если вам интересно, вы можете найти отличное объяснение в этом уроке Tuts + .
1. Создайте первое действие
Основная структура приложения проста. Существует два действия: основное, MainActivity.java , задача которого состоит в отображении списка элементов, и второе, DetailActivity.java , в котором будут показаны сведения об элементе, выбранном в предыдущем списке.
Шаг 1: виджет RecyclerView
Чтобы показать список элементов, основное действие будет использовать RecyclerView
Виджет представлен в Android Lollipop. Первое, что вам нужно сделать, это добавить следующую строку в раздел зависимостей в файле build.grade вашего проекта, чтобы включить обратную совместимость:
1
|
compile ‘com.android.support:recyclerview-v7:+’
|
Шаг 2: Определение данных
Для краткости мы не будем определять фактическую базу данных или аналогичный источник данных для приложения. Вместо этого мы будем использовать пользовательский класс Contact
. Каждый элемент будет иметь имя, цвет и основную контактную информацию, связанную с ним. Вот как выглядит реализация класса Contact
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
public class Contact {
// The fields associated to the person
private final String mName, mPhone, mEmail, mCity, mColor;
Contact(String name, String color, String phone, String email, String city) {
mName = name;
}
// This method allows to get the item associated to a particular id,
// uniquely generated by the method getId defined below
public static Contact getItem(int id) {
for (Contact item : CONTACTS) {
if (item.getId() == id) {
return item;
}
}
return null;
}
// since mName and mPhone combined are surely unique,
// we don’t need to add another id field
public int getId() {
return mName.hashCode() + mPhone.hashCode();
}
public static enum Field {
NAME, COLOR, PHONE, EMAIL, CITY
}
public String get(Field f) {
switch (f) {
case COLOR: return mColor;
case PHONE: return mPhone;
case EMAIL: return mEmail;
case CITY: return mCity;
case NAME: default: return mName;
}
}
}
|
В итоге вы получите хороший контейнер для информации, которая вас волнует. Но нам нужно заполнить его некоторыми данными. Вверху Contact
добавьте следующий фрагмент кода для заполнения набора данных.
Определяя данные как public
и static
, каждый класс в проекте может их прочитать. В некотором смысле мы имитируем поведение базы данных, за исключением того, что мы жестко кодируем ее в классе.
01
02
03
04
05
06
07
08
09
10
|
public static final Contact[] CONTACTS = new Contact[] {
new Contact(«John», «#33b5e5», «+01 123456789», «[email protected]», «Venice»),
new Contact(«Valter», «#ffbb33», «+01 987654321», «[email protected]», «Bologna»),
new Contact(«Eadwine», «#ff4444», «+01 123456789», «[email protected]», «Verona»),
new Contact(«Teddy», «#99cc00», «+01 987654321», «[email protected]», «Rome»),
new Contact(«Ives», «#33b5e5», «+01 11235813», «[email protected]», «Milan»),
new Contact(«Alajos», «#ffbb33», «+01 123456789», «[email protected]», «Bologna»),
new Contact(«Gianluca», «#ff4444», «+01 11235813», «[email protected]», «Padova»),
new Contact(«Fane», «#99cc00», «+01 987654321», «[email protected]», «Venice»),
};
|
Шаг 3: Определение основных макетов
Структура основного действия проста, потому что список заполнит весь экран. Макет содержит RelativeLayout
в качестве корня — но он также может быть и LinearLayout
— и RecyclerView
как его единственный дочерний элемент.
01
02
03
04
05
06
07
08
09
10
11
|
<RelativeLayout xmlns:android=»https://schemas.android.com/apk/res/android»
android:layout_width=»match_parent»
android:layout_height=»match_parent»
android:background=»#f5f5f5″>
<android.support.v7.widget.RecyclerView
android:layout_width=»match_parent»
android:layout_height=»match_parent»
android:id=»@+id/rv» />
</RelativeLayout>
|
Поскольку виджет RecyclerView
упорядочивает подэлементы и ничего более, вам также необходимо разработать макет отдельного элемента списка. Мы хотим иметь цветной кружок слева от каждого элемента списка контактов, поэтому сначала нужно определить drawable circle.xml .
1
2
3
4
5
6
7
8
9
|
<shape
xmlns:android=»http://schemas.android.com/apk/res/android»
android:shape=»oval»>
<solid
android:color=»#000″/>
<size
android:width=»32dp»
android:height=»32dp»/>
</shape>
|
Теперь у вас есть все элементы, необходимые для определения макета элемента списка.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
<RelativeLayout xmlns:android=»http://schemas.android.com/apk/res/android»
android:layout_width=»match_parent»
android:layout_height=»82dp»
android:padding=»@dimen/activity_horizontal_margin»
android:background=»?android:selectableItemBackground»
android:clickable=»true»
android:focusable=»true»
android:orientation=»vertical» >
<View
android:id=»@+id/CONTACT_circle»
android:layout_width=»40dp»
android:layout_height=»40dp»
android:background=»@drawable/circle»
android:layout_centerVertical=»true»
android:layout_alignParentLeft=»true»/>
<LinearLayout
android:layout_width=»wrap_content»
android:layout_height=»wrap_content»
android:layout_centerVertical=»true»
android:layout_toRightOf=»@+id/CONTACT_circle»
android:layout_marginLeft=»@dimen/activity_horizontal_margin»
android:orientation=»vertical»>
<TextView
android:id=»@+id/CONTACT_name»
android:layout_width=»wrap_content»
android:layout_height=»wrap_content»
android:text=»Jonh Doe»
android:textColor=»#000″
android:textSize=»18sp»/>
<TextView
android:id=»@+id/CONTACT_phone»
android:layout_width=»wrap_content»
android:layout_height=»wrap_content»
android:text=»+01 123456789″
android:textColor=»#9f9f9f»
android:textSize=»15sp»/>
</LinearLayout>
</RelativeLayout>
|
Шаг 4: Показать данные с помощью RecyclerView
Мы почти подошли к концу первой части урока. Вам все еще нужно написать RecyclerView.ViewHolder
и RecyclerView.Adapter
и назначить все для связанного представления в методе onCreate
основного действия. В этом случае RecyclerView.ViewHolder
также должен уметь обрабатывать щелчки, поэтому вам нужно будет добавить определенный класс, способный на это. Давайте начнем с определения класса, отвечающего за обработку кликов.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public class RecyclerClickListener implements RecyclerView.OnItemTouchListener {
private OnItemClickListener mListener;
GestureDetector mGestureDetector;
public interface OnItemClickListener {
public void onItemClick(View view, int position);
}
public RecyclerClickListener(Context context, OnItemClickListener listener) {
mListener = listener;
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override public boolean onSingleTapUp(MotionEvent e) {
return true;
}
});
}
@Override 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.getChildPosition(childView));
return true;
}
return false;
}
@Override public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) { }
}
|
Необходимо указать RecyclerView.Adapter
, который я назову его DataManager
. Он отвечает за загрузку данных и вставку их в представления списка. Этот класс менеджера данных также будет содержать определение RecyclerView.ViewHolder
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
public class DataManager extends RecyclerView.Adapter<DataManager.RecyclerViewHolder> {
public static class RecyclerViewHolder extends RecyclerView.ViewHolder {
TextView mName, mPhone;
View mCircle;
RecyclerViewHolder(View itemView) {
super(itemView);
mName = (TextView) itemView.findViewById(R.id.CONTACT_name);
mPhone = (TextView) itemView.findViewById(R.id.CONTACT_phone);
mCircle = itemView.findViewById(R.id.CONTACT_circle);
}
}
@Override
public RecyclerViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.contact_item, viewGroup, false);
return new RecyclerViewHolder(v);
}
@Override
public void onBindViewHolder(RecyclerViewHolder viewHolder, int i) {
// get the single element from the main array
final Contact contact = Contact.CONTACTS[i];
// Set the values
viewHolder.mName.setText(contact.get(Contact.Field.NAME));
viewHolder.mPhone.setText(contact.get(Contact.Field.PHONE));
// Set the color of the shape
GradientDrawable bgShape = (GradientDrawable) viewHolder.mCircle.getBackground();
bgShape.setColor(Color.parseColor(contact.get(Contact.Field.COLOR)));
}
@Override
public int getItemCount() {
return Contact.CONTACTS.length;
}
}
|
Наконец, добавьте следующий код в onCreate
метод, ниже setContentView.
Основное направление деятельности готово.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
RecyclerView rv = (RecyclerView) findViewById(R.id.rv);
LinearLayoutManager llm = new LinearLayoutManager(this);
rv.setLayoutManager(llm);
rv.setHasFixedSize(true);
rv.setAdapter(new DataManager());
rv.addOnItemTouchListener( // and the click is handled
new RecyclerClickListener(this, new RecyclerClickListener.OnItemClickListener() {
@Override public void onItemClick(View view, int position) {
// STUB:
// The click on the item must be handled
}
}));
|
Вот как выглядит приложение, если вы его создаете и запускаете.
2. Создайте действие Подробности
Шаг 1: макет
Второе занятие намного проще. Он берет идентификатор выбранного контакта и получает дополнительную информацию, которую не показывает первое действие.
С точки зрения дизайна, макет этого действия очень важен, так как это самая важная часть приложения. Но что касается XML, это тривиально. Макет представляет собой серию экземпляров TextView
размещенных в приятной форме с использованием RelativeLayout
и LinearLayout
. Вот как выглядит макет:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
<LinearLayout xmlns:android=»http://schemas.android.com/apk/res/android»
xmlns:tools=»http://schemas.android.com/tools» android:layout_width=»match_parent»
android:layout_height=»match_parent»
android:orientation=»vertical»>
<ImageView
android:layout_width=»match_parent»
android:layout_height=»200dp»
android:scaleType=»centerCrop»
android:src=»@mipmap/material_wallpaper»/>
<RelativeLayout
android:layout_width=»match_parent»
android:layout_height=»82dp»
android:padding=»@dimen/activity_vertical_margin»>
<View
android:id=»@+id/DETAILS_circle»
android:layout_width=»48dp»
android:layout_height=»48dp»
android:background=»@drawable/circle»
android:layout_centerVertical=»true»
android:layout_alignParentLeft=»true»/>
<TextView
android:id=»@+id/DETAILS_name»
android:layout_width=»wrap_content»
android:layout_height=»wrap_content»
android:text=»Jonh Doe»
android:layout_toRightOf=»@+id/DETAILS_circle»
android:layout_marginLeft=»@dimen/activity_horizontal_margin»
android:layout_centerVertical=»true»
android:textColor=»#000″
android:textSize=»25sp»/>
</RelativeLayout>
<LinearLayout
android:layout_width=»wrap_content»
android:layout_height=»wrap_content»
android:layout_centerVertical=»true»
android:padding=»@dimen/activity_horizontal_margin»
android:orientation=»vertical»>
<RelativeLayout
android:layout_width=»match_parent»
android:layout_height=»wrap_content»>
<TextView
android:id=»@+id/DETAILS_phone_label»
android:layout_width=»wrap_content»
android:layout_height=»wrap_content»
android:text=»Phone:»
android:textColor=»#000″
android:textSize=»20sp»/>
<TextView
android:id=»@+id/DETAILS_phone»
android:layout_width=»wrap_content»
android:layout_height=»wrap_content»
android:layout_toRightOf=»@+id/DETAILS_phone_label»
android:layout_marginLeft=»@dimen/activity_horizontal_margin»
android:text=»+01 123456789″
android:textColor=»#9f9f9f»
android:textSize=»20sp»/>
</RelativeLayout>
<RelativeLayout
android:layout_width=»match_parent»
android:layout_height=»wrap_content»
android:layout_marginTop=»@dimen/activity_vertical_margin»>
<TextView
android:id=»@+id/DETAILS_email_label»
android:layout_width=»wrap_content»
android:layout_height=»wrap_content»
android:text=»Email:»
android:textColor=»#000″
android:textSize=»20sp»/>
<TextView
android:id=»@+id/DETAILS_email»
android:layout_width=»wrap_content»
android:layout_height=»wrap_content»
android:layout_toRightOf=»@+id/DETAILS_email_label»
android:layout_marginLeft=»@dimen/activity_horizontal_margin»
android:text=»[email protected]»
android:textColor=»#9f9f9f»
android:textSize=»20sp»/>
</RelativeLayout>
<RelativeLayout
android:layout_width=»match_parent»
android:layout_height=»wrap_content»
android:layout_marginTop=»@dimen/activity_vertical_margin»>
<TextView
android:id=»@+id/DETAILS_city_label»
android:layout_width=»wrap_content»
android:layout_height=»wrap_content»
android:text=»City:»
android:textColor=»#000″
android:textSize=»20sp»/>
<TextView
android:id=»@+id/DETAILS_city»
android:layout_width=»wrap_content»
android:layout_height=»wrap_content»
android:layout_toRightOf=»@+id/DETAILS_city_label»
android:layout_marginLeft=»@dimen/activity_horizontal_margin»
android:text=»Rome»
android:textColor=»#9f9f9f»
android:textSize=»20sp»/>
</RelativeLayout>
</LinearLayout>
</LinearLayout>
|
Шаг 2. Отправка и получение идентификатора с помощью Intent Extras
Поскольку эти два действия связаны намерением, вам необходимо отправить некоторую информацию, которая позволит второму действию понять, какие контакт вы запросили детали.
Одним из вариантов может быть использование переменной позиции в качестве ссылки. Позиция элемента в списке соответствует позиции элемента в массиве, поэтому не должно быть ничего плохого в использовании этого целого числа в качестве уникальной ссылки.
Это будет работать, но если вы выберете этот подход и по какой-либо причине набор данных будет изменен во время выполнения, ссылка не будет соответствовать интересующему вас контакту. Именно поэтому лучше использовать идентификатор для этого случая. Эта информация — метод getId
определенный в классе Contact
.
Отредактируйте обработчик onItemClick
для списка элементов, как показано ниже.
1
2
3
4
5
|
@Override public void onItemClick(View view, int position) {
Intent intent = new Intent(MainActivity.this, DetailsActivity.class);
intent.putExtra(DetailsActivity.ID, Contact.CONTACTS[position].getId());
startActivity(intent);
}
|
DetailsActivity
получит информацию от дополнительных DetailsActivity
создаст правильный объект, используя идентификатор в качестве ссылки. Это показано в следующем блоке кода.
1
2
3
|
// Before the onCreate
public final static String ID = «ID»;
public Contact mContact;
|
1
2
|
// In the onCreate, after the setContentView method
mContact = Contact.getItem(getIntent().getIntExtra(ID, 0));
|
Как и прежде в методе onCreateViewHolder
объекта RecylerView
, представления инициализируются с findViewById
метода findViewById
и заполняются с помощью setText
. Например, чтобы настроить поле имени, мы делаем следующее:
1
2
|
mName = (TextView) findViewById(R.id.DETAILS_name);
mName.setText(mContact.get(Contact.Field.NAME));
|
Процесс аналогичен для других полей. Второе занятие наконец готово.
3. Значимые переходы
Мы наконец-то добрались до сути учебника, анимируя два действия, используя новый метод Lollipop для перехода с использованием общего элемента.
Шаг 1. Настройте свой проект
Первое, что вам нужно сделать, это отредактировать вашу тему в файле style.xml в папке values-v21 . Таким образом, вы разрешаете переходы содержимого и задаете вход и выход из представлений, которые не являются общими для двух действий.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
<style name=»AppTheme» parent=»AppTheme.Base»></style>
<style name=»AppTheme.Base» parent=»android:Theme.Material.Light»>
<item name=»android:windowContentTransitions»>true</item>
<item name=»android:windowEnterTransition»>@android:transition/slide_bottom</item>
<item name=»android:windowExitTransition»>@android:transition/slide_bottom</item>
<item name=»android:windowAllowEnterTransitionOverlap»>true</item>
<item name=»android:windowAllowReturnTransitionOverlap»>true</item>
<item name=»android:windowSharedElementEnterTransition»>@android:transition/move</item>
<item name=»android:windowSharedElementExitTransition»>@android:transition/move</item>
</style>
|
Обратите внимание, что ваш проект должен быть нацелен (и поэтому должен быть скомпилирован) как минимум на Android API 21 .
Анимации будут игнорироваться в системах, в которых не установлен Lollipop. К сожалению, из-за соображений производительности библиотека AppCompat не обеспечивает полной обратной совместимости для этих анимаций.
Шаг 2. Назначьте имя перехода в файлах макета
После того, как вы отредактировали свой файл style.xml , вы должны указать на связь между двумя общими элементами взглядов.
В нашем примере общими видами являются поле, содержащее имя контакта, номер телефона и цветной круг. Для каждого из них вы должны указать общее имя перехода . По этой причине начните добавлять в файл ресурсов strings.xml следующие элементы:
1
2
3
|
<string name=»transition_name_name»>transition:NAME</string>
<string name=»transition_name_circle»>transition:CIRCLE</string>
<string name=“transition_name_phone”>transition:PHONE</string>
|
Затем для каждой из трех пар в файлах макета добавьте атрибут android:transitionName
с соответствующим значением. Для цветного круга код выглядит так:
1
2
3
4
5
6
7
8
9
|
<!— In the single item layout: the item we are transitioning *from* —>
<View
android:id=“@+id/CONTACT_circle”
android:transitionName=“@string/transition_name_circle”
android:layout_width=“40dp”
android:layout_height=“40dp”
android:background=“@drawable/circle”
android:layout_centerVertical=“true”
android:layout_alignParentLeft=“true”/>
|
1
2
3
4
5
6
7
8
9
|
<!— In the details activity: the item we are transitioning *to* —>
<View
android:id=“@+id/DETAILS_circle”
android:transitionName=“@string/transition_name_circle”
android:layout_width=“48dp”
android:layout_height=“48dp”
android:background=“@drawable/circle”
android:layout_centerVertical=“true”
android:layout_alignParentLeft=“true”/>
|
Благодаря этому атрибуту Android будет знать, какие виды разделены между двумя действиями, и правильно анимирует переход. Повторите тот же процесс для двух других представлений.
Шаг 3: Настройте намерение
С точки зрения кодирования вам нужно будет прикрепить конкретный комплект ActivityOptions
к цели. Вам нужен метод makeSceneTransitionAnimation
, который принимает в качестве параметров контекст приложения и столько общих элементов, сколько нам нужно. В методе onItemClick
RecyclerView
отредактируйте ранее определенное Intent
следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
@Override public void onItemClick(View view, int position) {
Intent intent = new Intent(MainActivity.this, DetailsActivity.class);
intent.putExtra(DetailsActivity.ID, Contact.CONTACTS[position].getId());
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
// the context of the activity
MainActivity.this,
// For each shared element, add to this method a new Pair item,
// which contains the reference of the view we are transitioning *from*,
// and the value of the transitionName attribute
new Pair<View, String>(view.findViewById(R.id.CONTACT_circle),
getString(R.string.transition_name_circle)),
new Pair<View, String>(view.findViewById(R.id.CONTACT_name),
getString(R.string.transition_name_name)),
new Pair<View, String>(view.findViewById(R.id.CONTACT_phone),
getString(R.string.transition_name_phone))
);
ActivityCompat.startActivity(MainActivity.this, intent, options.toBundle());
}
|
Для того чтобы каждый общий элемент был анимирован, вам нужно добавить в метод makeSceneTransitionAnimation
новый элемент Pair
. Каждая Pair
имеет два значения: первое — это ссылка на представление, из которого вы переходите, второе — это значение атрибута transitionName
.
Будьте осторожны при импорте класса Pair
. Вам нужно будет включить пакет android.support.v4.util
, а не пакет android.util
. Кроме того, не забудьте использовать метод ActivityCompat.startActivity
вместо метода startActivity
, потому что в противном случае вы не сможете запустить свое приложение в средах с API ниже 16.
Вот и все. Вы сделали. Это так просто.
Вывод
В этом уроке вы узнали, как красиво и плавно переходить между двумя действиями, которые имеют один или несколько общих элементов, обеспечивая визуально приятную и значимую непрерывность.
Вы начали с выполнения первого из двух действий, роль которого заключается в отображении списка контактов. Затем вы выполнили второе действие, разработав его макет и реализовав способ передачи уникальной ссылки между этими двумя действиями. Наконец, вы посмотрели, как работает makeSceneTransitionAnimation
, благодаря атрибуту XML transitionName
.
Бонусный совет: стилистические детали
Чтобы создать настоящее приложение с дизайном материалов, как показано на предыдущих скриншотах, вам также нужно изменить цвета вашей темы. Отредактируйте вашу базовую тему в папке values-v21, чтобы добиться хорошего результата.
01
02
03
04
05
06
07
08
09
10
11
|
<style name=“AppTheme” parent=“AppTheme.Base”>
<item name=“android:windowTitleSize”>0dp</item>
<item name=“android:colorPrimary”>@color/colorPrimary</item>
<item name=“android:colorPrimaryDark”>@color/colorPrimaryDark</item>
<item name=“android:colorAccent”>@color/colorAccent</item>
<item name=“android:textColorPrimary”>#fff</item>
<item name=“android:textColor”>#727272</item>
<item name=“android:navigationBarColor”>#303F9F</item>
</style>
|