- Введение
- Необходимые условия
- 1. Создаем Activity
- Шаг 1: RecyclerView
- Шаг 2: Определение данных
- Шаг 3: Определение основных макетов
- Шаг 4: отображение данных с помощью RecyclerView
- 2. Создание Details Activity
- Шаг 1: Макет
- Шаг 2: отправка и получение ID через Intent Extras
- 3. Анимация переходов
- Шаг 1: Настройка Вашего Проекта
- Шаг 2: Присвойте имена переходам в Layout файлах
- Шаг 3: настроить intent
- Вывод
- Бонус: Стилистические Детали
Введение
Одним из наиболее интересных аспектов материального дизайна является визуальная преемственность окон приложения. С помощью нескольких строк кода новые интерфейсы API Lollipop позволяют реализовать интересный эффект перехода между двумя activity, благодаря бесшовной и непрерывной анимации . Это ломает границы классического поведения предыдущих версий Android и позволяет пользователю понять, как элементы переходят от одной точки к другой.
В этом уроке я покажу вам как добиться этого результата, делая пример приложения, в соответствии с принципами материального дизайна Google.
Необходимые условия
В этом учебнике я буду считать, что вы уже знакомы с Android разработкой и используете Android Studio. Необходимы базовые знания об интентах, жизненном цикле активности и новом элементе RecyclerView, появившемся с API 21, в июне 2014 года. Я не собираюсь погружаться в детали этого класса, но, если вы заинтересованы, вы можете найти большое объяснение в учебнике Tuts +.
1. Создаем Activity
Базовая структура приложения проста. Существует два вида activity, один основной, MainActivity.java, задача которого – отображение списка элементов, и второй, DetailActivity.java, который будет показать детали элемента, выбранного в списке MainActivity.java.
Шаг 1: RecyclerView
Чтобы отобразить список элементов, основной вид activity будет использовать RecyclerView. Первое, что вам нужно сделать: добавьте следующую строку в разделе зависимостей в build.grade файл проекта для включения обратной совместимости:
compile 'com.android.support:recyclerview-v7:+'
Шаг 2: Определение данных
Для краткости мы не создаем базу данных или аналогичный источник данных для приложения. Вместо этого, мы будем использовать пользовательский класс, Contact
. Каждый элемент будет иметь имя, цвет и основную контактную информацию, связанную с ней. Вот как выглядит реализация класса Contact
:
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; mColor = color; mPhone = phone; mEmail = email; mCity = city; } // 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; } } }
Нам нужно заполнить его некоторыми данными. В верхней части класса контакта добавьте следующий фрагмент кода для заполнения набора данных.
public static final Contact[] CONTACTS = new Contact[] { new Contact("John", "#33b5e5", "+01 123456789", "john@example.com", "Venice"), new Contact("Valter", "#ffbb33", "+01 987654321", "valter@example.com", "Bologna"), new Contact("Eadwine", "#ff4444", "+01 123456789", "eadwin@example.com", "Verona"), new Contact("Teddy", "#99cc00", "+01 987654321", "teddy@example.com", "Rome"), new Contact("Ives", "#33b5e5", "+01 11235813", "ives@example.com", "Milan"), new Contact("Alajos", "#ffbb33", "+01 123456789", "alajos@example.com", "Bologna"), new Contact("Gianluca", "#ff4444", "+01 11235813", "me@gian.lu", "Padova"), new Contact("Fane", "#99cc00", "+01 987654321", "fane@example.com", "Venice"), };
Шаг 3: Определение основных макетов
Макет основной activity является простым, потому что список заполнит весь экран. Макет включает в себя корневой RelativeLayout — но это так же может быть LinearLayout тоже — и RecyclerView как ее единственный потомок.
<RelativeLayout xmlns:android="http://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.
<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>
Теперь у вас есть все элементы, необходимые для определения макета элемента списка.
<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 основной activity.
RecyclerView.ViewHolder должен обрабатывать клики, поэтому необходимо добавить обработчик.
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) { } }
Создадим класс DataManager, унаследованный от RecyclerView.Adapter. Он отвечает за загрузку данных и заполнение ними списка. Здесь мы также инициализируем RecyclerView.ViewHolder.
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. Основной вид activity готов.
RecyclerView rv = (RecyclerView) findViewById(R.id.rv); // layout reference LinearLayoutManager llm = new LinearLayoutManager(this); rv.setLayoutManager(llm); rv.setHasFixedSize(true); // to improve performance rv.setAdapter(new DataManager()); // the data manager is assigner to the RV 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. Создание Details Activity
Шаг 1: Макет
Вторая activity гораздо проще. Она принимает идентификатор контакта и отображает дополнительную информацию.
С точки зрения дизайна, макет этой activity является критическим, поскольку это наиболее важная часть приложения. Но что касается XML, то это тривиально. Макет – это несколько экземпляров TextView расположенных в определенном порядке, с использованием LinearLayout и RelativeLayout. Код макета выглядит так:
<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="jonh.doe@example.com" 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: отправка и получение ID через Intent Extras
Поскольку эти два вида деятельности связаны с intent, необходимо отправить второй activity данные о контакте, для которого мы хотим получить подробную информацию.
Одним из вариантов может быть использование позиции переменной в качестве ссылки. Положение элемента в списке соответствует позиции элемента в массиве, поэтому не должно быть ничего плохого в использовании ее в виде уникальной ссылки.
Это будет работать, но если по каким-то причинам набор данных будет изменен во время выполнения, ссылка не будет соответствовать интересующему вас элементу списка. Именно поэтому лучше использовать идентификатор. Его можно получить с помощью метода getId в классе Contact.
Изменим обработчик нажатия элементов списка onItemClick, как показано ниже.
@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 будем получать информацию от Intent Extras и строить нужный объект используя ID в качестве ссылки. Это показано в следующем блоке кода.
// Before the onCreate public final static String ID = "ID"; public Contact mContact; // In the onCreate, after the setContentView method mContact = Contact.getItem(getIntent().getIntExtra(ID, 0));
Как и прежде в методе RecyclerViewHolder onCreateViewHolder, инициализируем View, используя метод findViewById, и заполняем его используя метод setText. Например, чтобы настроить поле “имя”, мы поступаем следующим образом:
mName = (TextView) findViewById(R.id.DETAILS_name); mName.setText(mContact.get(Contact.Field.NAME));
Процесс аналогичен и для других полей. Вторая activity готова.
3. Анимация переходов
Мы наконец добрались до основной части урока, где мы будем оживлять два вида activity, используя новый способ анимации перехода между ними.
Шаг 1: Настройка Вашего Проекта
Первое, что вам нужно сделать, это изменить вашу тему в style.xml файле в values-v21 папке. Таким образом, вы подключите контент переходов для View, которые не являются общими между двумя activity.
<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>
Обратите внимание, что в настройках вашего проекта targetSdkVersion и compileSdkVersion должны быть установлены не ниже 21.
Шаг 2: Присвойте имена переходам в Layout файлах
После того, как вы отредактировали ваш style.xml файл, вы должны указать на отношение общих элементов между двумя activity.
В нашем примере activity имеют общие View, содержащие имя контакта, один из номеров телефона, и цветной круг. Для каждого из них, вы должны указать общее название перехода. Для этого добавим в strings.xml файл ресурсов следующие строки:
<string name="transition_name_name">transition:NAME</string>
<string name="transition_name_circle">transition:CIRCLE</string>
<string name=“transition_name_phone”>transition:PHONE</string>
Затем, для каждой из трех пар, в коде макета добавьте атрибут аndroid:transitionName с соответствующим значением. Для цветного круга, например, код выглядит так:
<!— 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”/> <!— 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 будет знать, какие View общие между двумя видами activity и будет правильно анимировать переход. Повторите тот же процесс для двух других View.
Шаг 3: настроить intent
С точки зрения программирования нужно прикрепить определенный ActivityOptions bundle в intent. Нам нужен метод makeSceneTransitionAnimation, который принимает в качестве параметра контекст приложения и нужные нам общие элементы. В RecyclerView в методе onItemClick , изменим ранее определенный intent:
@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, имеющий два значения, первое это ссылка на View, второе является значением атрибута transitionName.
Будьте осторожны при импорте класса Pair. Вам нужно будет включить в android.support.v4.util пакет, а не android.util пакет. Также, не забывайте использовать метод ActivityCompat.startActivity вместо startActivity, потому что иначе не сможете запустить приложение на средах с API ниже 16.
Вот и все. Мы закончили. Это так просто.
Вывод
В этом уроке мы узнали, как сделать красивый и плавный переход между двумя видами activity, которые разделяют одно или несколько общих элементов, который обеспечивает визуальную преемственность.
Анимация перехода в этом приложении не будет работать на устройствах с версией Android ниже 5.0. К сожалению, из соображений быстродействия приложений библиотека совместимости не обеспечивает полную обратную совместимость с этими эффектами перехода.
Бонус: Стилистические Детали
Чтобы приложение выглядело в лучших традициях Material Design, как показано на предыдущих скриншотах, вам также нужно будет изменить цвета вашей темы. Измените основную тему из папки values-v21, чтобы получить хороший результат:
<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>
Перевод источника. Автор урока Gianluca Segato. Скачать исходный код проекта