Статьи

Введение в новые переходы активности Lollipop

Что вы будете создавать

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

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

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

Основная структура приложения проста. Существует два действия: основное, MainActivity.java , задача которого состоит в отображении списка элементов, и второе, DetailActivity.java , в котором будут показаны сведения об элементе, выбранном в предыдущем списке.

Чтобы показать список элементов, основное действие будет использовать RecyclerView Виджет представлен в Android Lollipop. Первое, что вам нужно сделать, это добавить следующую строку в раздел зависимостей в файле build.grade вашего проекта, чтобы включить обратную совместимость:

1
compile ‘com.android.support:recyclerview-v7:+’

Для краткости мы не будем определять фактическую базу данных или аналогичный источник данных для приложения. Вместо этого мы будем использовать пользовательский класс 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», «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»),
};

Структура основного действия проста, потому что список заполнит весь экран. Макет содержит 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>

Мы почти подошли к концу первой части урока. Вам все еще нужно написать 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
        }
    }));

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

Первое задание после завершения

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

С точки зрения дизайна, макет этого действия очень важен, так как это самая важная часть приложения. Но что касается 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=»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>

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

Одним из вариантов может быть использование переменной позиции в качестве ссылки. Позиция элемента в списке соответствует позиции элемента в массиве, поэтому не должно быть ничего плохого в использовании этого целого числа в качестве уникальной ссылки.

Это будет работать, но если вы выберете этот подход и по какой-либо причине набор данных будет изменен во время выполнения, ссылка не будет соответствовать интересующему вас контакту. Именно поэтому лучше использовать идентификатор для этого случая. Эта информация — метод 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));

Процесс аналогичен для других полей. Второе занятие наконец готово.

Второе задание по завершении

Мы наконец-то добрались до сути учебника, анимируя два действия, используя новый метод Lollipop для перехода с использованием общего элемента.

Первое, что вам нужно сделать, это отредактировать вашу тему в файле 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 не обеспечивает полной обратной совместимости для этих анимаций.

После того, как вы отредактировали свой файл 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 будет знать, какие виды разделены между двумя действиями, и правильно анимирует переход. Повторите тот же процесс для двух других представлений.

С точки зрения кодирования вам нужно будет прикрепить конкретный комплект 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>