Анимация перехода между Activity в Android – 5+ с использованием RecyclerView (Material Design)

Введение

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

В этом уроке я покажу вам как добиться этого результата, делая пример приложения, в соответствии с принципами материального дизайна Google.

В этом учебнике я буду считать, что вы уже знакомы с Android разработкой и используете Android Studio. Необходимы базовые знания об интентах, жизненном цикле активности и новом элементе RecyclerView, появившемся с API 21, в июне 2014 года. Я не собираюсь погружаться в детали этого класса, но, если вы заинтересованы, вы можете найти большое объяснение в учебнике Tuts +.

Базовая структура приложения проста. Существует два вида 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"),
};

Макет основной 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
        }
    }));

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

first_activity-600-wide

 

Вторая 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 готова.

second_activity-600-wide

 

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 файл ресурсов следующие строки:

Затем, для каждой из трех пар, в коде макета добавьте атрибут а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.

С точки зрения программирования нужно прикрепить определенный ActivityOptions bundle в  intent. Нам нужен метод makeSceneTransitionAnimation, который принимает в качестве параметра контекст приложения и нужные нам общие элементы. В RecyclerView в методе onItemClick , изменим ранее определенный intent:

Для каждого общего элемента, который будет анимирован, необходимо добавить к методу makeSceneTransitionAnimation новый объект Pair,   имеющий два значения, первое это ссылка на View, второе является значением атрибута transitionName.

Будьте осторожны при импорте класса Pair. Вам нужно будет включить в android.support.v4.util пакет, а не android.util пакет. Также, не забывайте использовать метод ActivityCompat.startActivity вместо startActivity, потому что иначе не сможете запустить приложение на средах с API ниже 16.

Вот и все. Мы закончили. Это так просто.

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

Анимация перехода в этом приложении не будет работать на устройствах с версией Android ниже 5.0. К сожалению, из соображений быстродействия приложений библиотека совместимости  не обеспечивает полную обратную совместимость с этими эффектами перехода.

Чтобы приложение выглядело в лучших традициях Material Design, как показано на предыдущих скриншотах, вам также нужно будет изменить цвета вашей темы. Измените основную тему из папки values-v21, чтобы получить хороший результат:

Додати коментар