Анимация переходов между двумя фрагментами. Анимационные переходы между представлениями

03.04.2019

Paul is a Design and Perf Advocate

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

TL;DR

  • Используйте свойство transition для перемещения между представлениями; не следует применять left , top или любое другое свойство, которое вызывает перерасчет макета.
  • Любая анимация должна быть мгновенной, ее продолжительность не следует делать большой.
  • Продумайте, как будут меняться анимационные эффекты и макеты при увеличении размера экрана; то, что подойдет для небольшого экрана может выглядеть неуклюже на экране большого размера.

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

Note: Нужно стремиться к тому, чтобы частота кадров любой анимации составляла 60 кадров в секунду. Это позволит избежать запинок при воспроизведении анимации, которые вряд ли понравятся вашим пользователям. Любому анимируемому элементу следует задать свойство will-change для всех компонентов, которые планируется изменять, задолго до того, как будет реализована сама анимация. Весьма вероятно, что для переходов между представлениями будет использоваться свойство will-change: transform

Использование атрибутов translation для перемещения между представлениями

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

Чтобы достичь такого эффекта, вам потребуется контейнер для обоих представлений, которому будет задано свойство overflow: hidden . Таким образом, оба представления смогут находиться в контейнере одновременно без отображения горизонтальных полос прокрутки, при этом каждое представление будет при необходимости скользить из стороны в сторону внутри контейнера.

Код CSS для контейнера:

Container { width: 100%; height: 100%; overflow: hidden; position: relative; }

Положение контейнера задается как relative . Это означает, что каждое представление, находящееся внутри контейнера, можно разместить точно в левом верхнем углу, а затем перемещать с помощью свойства transform. Такой способ дает выигрыш с точки зрения производительности по сравнению с использованием свойства left (поскольку оно вызывает перерасчет макета и фактическую перерисовку страницы), кроме того, как правило, его проще рационализировать.

View { width: 100%; height: 100%; position: absolute; left: 0; top: 0; /* let the browser know we plan to animate each view in and out */ will-change: transform; }

Добавление transition к свойству transform обеспечивает привлекательный эффект скольжения. Чтобы скольжение вызывало приятные ощущения, необходимо использовать нестандартную кривую cubic-bezier , которая рассматривалась в документе [Руководство по изменению скорости при нестандартной анимации] (custom-easing.html).

View { /* Prefixes are needed for Safari and other WebKit-based browsers */ transition: -webkit-transform 0.3s cubic-bezier(0.465, 0.183, 0.153, 0.946); transition: transform 0.3s cubic-bezier(0.465, 0.183, 0.153, 0.946); }

Представление, которое исчезает с экрана, следует перемещать вправо, поэтому в данном случае представление с подобными сведениями необходимо двигать:

Details-view { -webkit-transform: translateX(100%); transform: translateX(100%); }

Var container = document.querySelector(".container"); var backButton = document.querySelector(".back-button"); var listItems = document.querySelectorAll(".list-item"); /** * Toggles the class on the container so that * we choose the correct view. */ function onViewChange(evt) { container.classList.toggle("view-change"); } // When you click on a list item bring on the details view. for (var i = 0; i < listItems.length; i++) { listItems[i].addEventListener("click", onViewChange, false); } // And switch it back again when you click on the back button backButton.addEventListener("click", onViewChange);

Наконец, добавляем декларации CSS для этих классов.

View-change .list-view { -webkit-transform: translateX(-100%); transform: translateX(-100%); } .view-change .details-view { -webkit-transform: translateX(0); transform: translateX(0); }

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

Note: Создание такой иерархии, которая будет работать в разных браузерах, может быть очень сложной задачей. Например, в iOS для повторного включения fling-прокрутки требуется дополнительное свойство CSS, -webkit-overflow-scrolling: touch , однако при этом невозможно контролировать, для какой оси эта прокрутка будет работать (тогда как стандартное свойство overflow позволяет это делать). Обязательно тестируйте работу своего кода на различных устройствах!

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

Обеспечение работы анимации на больших экранах

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

У многих начинающих разработчиков на Angular камнями преткновения становятся лень, страх оказаться в незнакомой ситуации и предстать в глупом виде перед своими сверстниками или анимация. А у тех, кто поборол лень и страх, познакомился с анимацией и начал углубляться в адскую кухню более сложных проектов, часто возникают вопросы для решении более специфических задач. Рассмотрим рецепт приготовления решения для одной распространенной задачи, а именно - анимации постраничного перехода.

Немного об ингредиентах нашего рецепта демо-проекта

В основном компоненте у нас есть небольшая навигация и routing-outlet для организации вывода страниц.

И плюс два компонента с контентом которые будут служить нам страницами.

Для начала нам потребуется модуль BrowserAnimationsModule -- 1шт.
Подключим его в файле app.module.ts.

Не стоит забывать, что для корректной работы в браузерах, которые не поддерживают Web Animations API, нужен полифил web-animations .

Для этого, в консоле проекта пропишем: npm install --save web-animations-js и в файле polyfills.ts раскомментируем строку:

И после этого подключаем его в основном модуле:


Добавляем заранее приготовленную анимацию по вкусу:)

Для начала немного подкорректируем наш роутинг чтоб при переходе между страницами мы еще и передавали состояние для анимации (state):

После чего в основном компоненте app.component.ts добавим функцию для получения этого состояния getState(), а также подключим и задекларируем нашу анимацию:

Теперь забиндим анимацию и подвяжем ее под результат функции getState() в основном темплейте:

На данном этапе может возникнуть вопрос “зачем?” Ведь у нас, в анимации stateChangeExpr прописан как: "* <=> *", а это по сути любая смена состояний. Но как показывает практика, в проектах редко бывает только одна анимация. Множество разных анимации переходов по страницах как раз будут основаны на состояниях (state). Вам нужно будет прописывать только stateChangeExpr для нужного перехода.

Рассмотрим один из примеров. Допустим вам нужна уникальная анимация только при переходе с страницы “контакты” на страницу “новостей”. В роутинге вы будете передавать state со значениями “contacts” и “news” соответственно. Тогда значение stateChangeExpr в анимации для этого конкретного случая будет "contacts => news".

На данном этапе у нас уже есть почти готовое блюдо:

Но идеальное блюдо требует не только идеального приготовления, но и внимания к деталям.

По-этому, на определенные объекты пропишем дополнительную анимацию. Пусть это будут абзацы на странице Page1.

Добавляем к ним класс:

После этого дописываем анимацию.

В 6 строке мы описали статическое поведение наших абзацев при переходе на страницу с ними. А в строках 16-20 поведение абзацев после перехода на страницу.

В нашем рецепте мы использовали stagger() , который с заданным интервалом переберет наши абзацы и применит к ним анимацию.

Хочу обратить внимание на то, что при переходе на другие страницы, элементов с классом .anim может не быть. А при компиляции query(":enter .anim" выдаст ошибку, именно поэтому нужно использовать надстройку { optional: true } которая будет игнорировать отсутствующую выборку.

Теперь можем любоваться супер-блюдом, которое у нас получилось:)

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

В общем на этом все, надеюсь материал был полезен.

Bon appétit!

** На момент написания статьи использовался @angular/cli: 1.7.3 с последней версией Angular 5.2.9. Код также будет поддерживаться и на версиях Angular 2 и выше.
Отдельная благодарность Gerard Sans за содействие при написании статьи.

Эволюцией CSS3 стала возможность задавать поведение для переходов и анимации. Фронтенд-разработчики много лет просили реализовать эти взаимодействия внутри HTML и CSS, без использования JavaScript или Flash. Теперь их мечта сбылась.

С помощью переходов CSS3 у вас есть потенциал менять внешний вид и поведение элемента всякий раз, когда происходит изменение его состояния, к примеру, когда на элемент наводится курсор, он получает фокус, становится активным или к элементу происходит переход по ссылке.

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

Переходы

Ключевые кадры анимации

Чтобы установить несколько точек, в которых элемент должен совершать переход, используется правило @keyframes . Это правило включает имя анимации, любое число контрольных точек, а также свойства, предназначенные для анимации.

@keyframes slide { 0% { left: 0; top: 0; } 50% { left: 244px; top: 100px; } 100% { left: 488px; top: 0; } }

Вендорные префиксы в правиле @keyframes

Правило @keyframes должно быть с вендорными префиксами, так же, как и все другие свойства transition и animation . Префиксы для @keyframes выглядят следующим образом:

  • @-moz-keyframes
  • @-o-keyframes
  • @-webkit-keyframes

Анимация выше называется slide , состояния начинаются сразу же после открытия правила @keyframes . Разные контрольные точки ключевых кадров задаются помощью процентов, начиная с 0% и работая до 100%, с промежуточной точкой на 50%. При желании вместо 0% и 100% могут быть использованы ключевые слова from и to . Кроме 50% также могут быть указаны дополнительные контрольные точки. Свойства элементов для анимации перечислены внутри каждой контрольной точки, left и top в приведённом выше примере.

Важно отметить, как и с переходами, могут быть анимированы только отдельные свойства. Подумайте, как вы могли бы переместить элемент сверху вниз, например. Попытка анимировать от top: 0 до bottom: 0 не будет работать, потому что анимация может применять только переход в пределах одного свойства, а не от одного свойства к другому. В этом случае элемент необходимо анимировать от top: 0 до top: 100% .

animation-name

После того, как объявлены ключевые кадры для анимации, они должны быть назначены для элемента. Для этого используется свойство animation-name с именем анимации из правила @keyframes , как значение свойства. Декларация animation-name применяется к элементу, для которого должна быть задана анимация.

Stage:hover .ball { animation-name: slide; }

Использования одного свойства animation-name при этом недостаточно. Кроме того, необходимо объявить свойство animation-duration и значение, чтобы браузер знал, как долго должна длиться анимация до завершения.

animation-duration, функция времени и animation-delay

После того, как вы объявили свойство animation-name для элемента, анимация ведёт себя подобно переходам. Она включают в себя длительность, функцию времени и задержку при желании. Сперва анимации требуется длительность, объявленная с помощью свойства animation-duration . Как и в случае с переходами, длительность может быть задана в секундах или миллисекундах.

Stage:hover .ball { animation-name: slide; animation-duration: 2s; }

Функция времени и задержка могут быть объявлены с помощью свойств animation-timing-function и animation-delay , соответственно. Значения этих свойств подражают и ведут себя так же, как это делается с переходами.

Stage:hover .ball { animation-name: slide; animation-duration: 2s; animation-timing-function: ease-in-out; animation-delay: .5s; }

Анимация ниже должна вызвать отскок ball один раз при перемещении вправо, но только при наведении курсора на stage.

@keyframes slide { 0% { left: 0; top: 0; } 50% { left: 244px; top: 100px; } 100% { left: 488px; top: 0; } } .stage { height: 150px; position: relative; } .ball { height: 50px; position: absolute; width: 50px; } .stage:hover .ball { animation-name: slide; animation-duration: 2s; animation-timing-function: ease-in-out; animation-delay: .5s; }

Настройка анимации

Анимация также предлагает возможность дальнейшей настройки поведения элемента, в том числе можно задать, сколько раз анимация выполняется, а также направление, в котором анимация завершится.

animation-iteration-count

По умолчанию, анимация запускает свой цикл один раз от начала до конца, а затем останавливается. Чтобы анимация повторялась много раз может быть использовано свойство animation-iteration-count . Значения для него включают целое число или ключевое слово infinite . С помощью целого числа анимация будет повторяться столько раз, сколько указано, в то время как ключевое слово infinite будет повторять анимацию бесконечно и никогда не остановится.

Stage:hover .ball { animation-name: slide; animation-duration: 2s; animation-timing-function: ease-in-out; animation-delay: .5s; animation-iteration-count: infinite; }

animation-direction

Кроме возможности установить, сколько раз анимация повторяется, вы можете также объявить направление, в котором анимация завершается, с помощью свойства animation-direction . Значения этого свойства включают в себя normal , reverse , alternate и alternate-reverse .

Значение normal воспроизводит анимацию, как и намеревалось, от начала до конца. Значение reverse воспроизводит анимацию точно наоборот, чем это определено в правиле @keyframes , таким образом, начиная со 100% и работая в обратном направлении до 0%.

Значение alternate воспроизведёт анимацию вперёд, а затем назад. В ключевых кадрах это включает выполнение вперёд от 0% до 100%, а затем в обратном направлении от 100% до 0%. Используя свойство animation-iteration-count можно ограничить количество раз, когда анимация работает вперёд и назад. Счёт начинается с 1, когда анимация проходит вперёд от 0% до 100%, а затем добавляется 1, когда анимация проходит в обратном порядке от 100% до 0%. Объединение в общей сложности двух итераций. Значение alternate также инвертирует любые функции времени при воспроизведении в обратном направлении. Если анимация использует значение ease-in идущее от 0% до 100%, то затем использует значение ease-out идущее от 100% до 0%.

И, наконец, значение alternate-reverse сочетает оба значения alternate и reverse , запуская анимацию назад, а затем вперёд. Значение alternate-reverse начинается со 100% и выполняется до 0%, а затем снова обратно до 100%.

Stage:hover .ball { animation-name: slide; animation-duration: 2s; animation-timing-function: ease-in-out; animation-delay: .5s; animation-iteration-count: infinite; animation-direction: alternate; }

animation-play-state

Свойство animation-play-state позволяет анимации воспроизводиться или остановиться на паузу, с помощью ключевых слов running и paused , соответственно. Когда анимация снимается с паузы, она возобновляет работу из текущего состояния, а не начинается снова с самого начала.

В приведённом ниже примере свойство animation-play-state устанавливается на паузу, когда stage активен при щелчке по нему. Обратите внимание на то, как анимация временно приостанавливается, пока вы не отпустите кнопку мыши.

Stage:hover .ball { animation-name: slide; animation-duration: 2s; animation-timing-function: ease-in-out; animation-delay: .5s; animation-iteration-count: infinite; animation-direction: alternate; } .stage:active .ball { animation-play-state: paused; }

animation-fill-mode

Свойство animation-fill-mode определяет, как элемент должен быть стилизован - до, после или до и после запуска анимации. Свойство animation-fill-mode принимает четыре значения ключевых слов, включая none , forwards , backwards и both .

Значение none не будет применять любые стили к элементу до или после того, как анимация запущена.

Значение forwards сохранит стили, объявленные в последнем указанном ключевом кадре. На эти стили, однако, могут повлиять значения свойств animation-direction и animation-iteration-count , меняя, где анимация заканчивается.

Значение backwards будет применять стили первого заданного ключевого кадра, до того, как анимация будет работать. Включает применение этих стилей в течение всего времени, что может быть задано в animation-delay . Значение backwards также может зависеть от значения свойства animation-direction .

Наконец, значение both применит поведение сразу от обоих значений forwards и backwards .

Stage:hover .ball { animation-name: slide; animation-duration: 2s; animation-timing-function: ease-in-out; animation-delay: .5s; animation-fill-mode: forwards; } .stage:active .ball { animation-play-state: paused; }

Сокращённое свойство animation

К счастью анимация , подобно переходам, может быть записана в сокращённом формате. Это получается с помощью одного свойства animation вместо нескольких объявлений. Порядок значений в свойстве animation должен быть следующий: animation-name , animation-duration , animation-timing-function , animation-delay , animation-iteration-count , animation-direction , animation-fill-mode и, наконец, animation-play-state .

Stage:hover .ball { animation: slide 2s ease-in-out .5s infinite alternate; } .stage:active .ball { animation-play-state: paused; }

Ресурсы и ссылки

  • Understanding CSS3 Transitions на A List Apart
  • CSS Cubic-Bezier Builder на Rob LaPlaca
  • The Guide To CSS Animation: Principles and Examples на Smashing Magazine
  • Using CSS Animations на Mozilla Developer Network

Post Views: 3 215

Переходы в приложениях Material Design обеспечивают визуальную целостность. Когда пользователь перемещается по приложению, различные его элементы меняют своё состояние. Движение и трансформация укрепляют идею о том, что интерфейс осязаемый и соединяет общие элементы представлений.

В этой статье мы продемонстрируем, как реализовать переход из изображения в RecyclerView на одной активности в изображение в ViewPager в другой активности, используя Shared Elements для того, чтобы определить, какие представления участвуют в процессе перехода и как. Мы также рассмотрим сложный случай обратного перехода в список к элементу, которого ранее не было на экране.

Вот результат, которого мы собираемся достичь:

Что такое общие элементы?

Переход общего элемента определяет, как происходит анимация элемента, который присутствует в двух активностях, между ними. Например, изображение, которое отображается в ImageView на активностях А и Б , переходит от активности А к активности Б когда Б становится видимой.

Реализация перехода от RecyclerView к ViewPager

Ниже приведена анимация перехода общего элемента из RecyclerView в ViewPager.

Для начала откроем файл res/values-v21/styles.xml и в свойствах темы зададим windowsContentTransitions а анимацию.

true @transition/change_image_transform

Теперь определим анимацию, создав файл change_image_transform.xml в папке res/transition .

RecyclerView и ViewPager используют для своей работы адаптер, поэтому они не могут установить transitionName в разметке XML. Нужно использовать View.setTransitionName() и View.setTag() , чтобы динамически установить transitionName и tag соответственно во время привязки представления к адаптеру. Поэтому в метода адаптера onBindViewHolder() добавим следующий код:

@Override public void onBindViewHolder(@NonNull final MyViewHolder holder, int position) { holder.image.setTag(position); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { holder.image.setTransitionName(holder.image.getContext().getString(R.string.transition_name, position)); // в качестве примера ресурсу R.string.transition_name задано значение name%1$d } }

Здесь поле image — это ImageView, анимацию перехода которого мы собираемся реализовать. В результате каждому элементу будет задан transitionName, равный nameX , где X — позиция элемента в списке.

Примечание: в нашем примере будут использоваться следующие имена активностей: SourceActivity для активности с RecycleView и DestinationActivity для активности с ViewPager.

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

  • Во-первых, каждое представление должно иметь уникальный transitionName .
  • Во-вторых, нужно подождать, пока ViewPager начнёт показывать изображения, прежде чем включать анимацию.

Теперь нужно реализовать обработку нажатия на элемент. При нажатии будет открываться вторая активность, в которую будут передавать созданные имя и тег. Для этого пробросим в адаптер слушатель из активности.

Interface ClickListener { void onItemClick(ImageView image); } private ClickListener listener; ... RecyclerAdapter(List photoList, ClickListener listener) { this.photoList = new ArrayList<>(photoList); this.listener = listener; } ... @Override public void onBindViewHolder(@NonNull final MyViewHolder holder, int position) { final int photo = photoList.get(position); holder.image.setImageResource(photo); holder.image.setTag(position); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { holder.image.setTransitionName(holder.image.getContext().getString(R.string.transition_name, position)); } holder.image.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { listener.onItemClick(holder.image); } }); }

В активности же создадим экземпляр этого слушателя и переопределим метод onItemClick() .

@Override protected void onCreate(Bundle savedInstanceState) { ... recyclerView.setAdapter(new RecyclerAdapter(photoList, new RecyclerAdapter.ClickListener() { @Override public void onItemClick(ImageView image) { Intent intent = new Intent(SourceActivity.this, DestinationActivity.class); intent.putExtra("current", (int) image.getTag()); intent.putIntegerArrayListExtra("list", photoList); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(SourceActivity.this, image, image.getTransitionName()); startActivity(intent, options.toBundle()); } else { startActivity(intent); } } })); }

Здесь основным моментом является использование объекта ActivityOptions , в который с помощью метода makeSceneTransitionAnimation() передаются контекст, представление, которое нужно анимировать, и его transitionName.

После этого стартует интент и мы переходим на активность с ViewPager . Как уже говорилось выше, нельзя сразу при старте активности запускать анимацию, поскольку ViewPager нужно время, чтобы сгенерировать элементы. Поэтому перед вызовом setContentView() в методе onCreate() добавим вызов метода postponeEnterTransition() , который запрещает переход общего элемента.

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { postponeEnterTransition(); } setContentView(R.layout.activity_destination); ... }

После этого начинается инициализация ViewPager и передача данных в адаптер. В адаптере нам нужно задать те же transitionName и tag, что был у элемента в RecyclerView, после чего вызвать ме

@Override @NonNull public Object instantiateItem(@NonNull ViewGroup collection, int position) { int photo = photoList.get(position); LayoutInflater inflater = LayoutInflater.from(mContext); View v = inflater.inflate(R.layout.item, collection, false); ImageView img = v.findViewById(R.id.image); img.setImageResource(photo); collection.addView(v); String name = mContext.getString(R.string.transition_name, position); img.setTag(position); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { img.setTransitionName(name); if (position == current) { listener.setStartPostTransition(img); } } return v; }

Теперь, когда всё подготовлено, нам нужно запустить анимацию. Пробросим интерфейс в адаптер из активности, в котором с помощью метода setStartPostTransition() запустим анимацию, которая была прервана при старте активности.

@TargetApi(21) @Override public void setStartPostTransition(final View view) { view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { view.getViewTreeObserver().removeOnPreDrawListener(this); startPostponedEnterTransition(); return false; } }); }

В результате мы получим плавную анимацию перехода из RecyclerView в ViewPager.

Реализация перехода от ViewPager к RecyclerView

Теперь нужно осуществить обратную анимацию при возврате из DestinationActivity в SourceActivity. Основной проблемой здесь может являться то, что при возврате в первую активность не обязательно вернётся тот же элемент, что был при переходе. В таком случае нужно определить позицию нового элемента, сделать скролл, если необходимо, и воспроизвести анимацию.

В DestinationActivity нам нужно переопределить метод finishAfterTransition() , внутри которого нужно поместить в интент текущую позицию элемента.

@Override public void finishAfterTransition() { int pos = viewPager.getCurrentItem(); Intent intent = new Intent(); intent.putExtra("exit_position", pos); setResult(RESULT_OK, intent); if (current != pos) { View view = viewPager.findViewWithTag(pos); setSharedElementCallback(view); } super.finishAfterTransition(); } @TargetApi(21) private void setSharedElementCallback(final View view) { setEnterSharedElementCallback(new SharedElementCallback() { @Override public void onMapSharedElements(List names, Map sharedElements) { names.clear(); sharedElements.clear(); names.add(view.getTransitionName()); sharedElements.put(view.getTransitionName(), view); } }); }

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

После этого активность закрывается и мы возвращаемся в SourceActivity . Здесь мы вызываем метод активности onActivityReenter() , чтобы поймать интент с позицией.

Public void onActivityReenter(int resultCode, Intent data) { super.onActivityReenter(resultCode, data); if (resultCode == RESULT_OK && data != null) { exitPosition = data.getIntExtra("exit_position", 0); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); View viewAtPosition = layoutManager.findViewByPosition(exitPosition); // Прокрутка к позиции, если вьюха для текущей позиции не null (т.е. // не является частью дочерних элементов layout-менеджера) или если // она видна не полностью. if (viewAtPosition == null || layoutManager.isViewPartiallyVisible(viewAtPosition, false, true)) { layoutManager.scrollToPosition(exitPosition); setTransitionOnView(); } // карточка видна, нужно поставить колбек else { setTransitionOnView(); } } } }

Если прокручивать не нужно или прокрутка завершена, начинаем воспроизведение анимации. Для этого вызывается метод setTransitionOnView() , в котором ожидаем окончания скролла и запускаем анимацию.

@TargetApi(Build.VERSION_CODES.LOLLIPOP) private static class CustomSharedElementCallback extends SharedElementCallback { private View mView; public void setView(View view) { mView = view; } @Override public void onMapSharedElements(List names, Map sharedElements) { names.clear(); sharedElements.clear(); if (mView != null) { String transitionName = ViewCompat.getTransitionName(mView); names.add(transitionName); sharedElements.put(transitionName, mView); } } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void setTransitionOnView() { final CustomSharedElementCallback callback = new CustomSharedElementCallback(); setExitSharedElementCallback(callback); getWindow().getSharedElementExitTransition().addListener(new Transition.TransitionListener() { @Override public void onTransitionStart(Transition transition) { } @Override public void onTransitionPause(Transition transition) { } @Override public void onTransitionResume(Transition transition) { } @Override public void onTransitionEnd(Transition transition) { removeCallback(); } @Override public void onTransitionCancel(Transition transition) { removeCallback(); } private void removeCallback() { getWindow().getSharedElementExitTransition().removeListener(this); setExitSharedElementCallback(null); } }); postponeEnterTransition(); recyclerView.getViewTreeObserver() .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { recyclerView.getViewTreeObserver().removeOnPreDrawListener(this); RecyclerView.ViewHolder holder = recyclerView.findViewHolderForAdapterPosition(exitPosition); if (holder instanceof RecyclerAdapter.MyViewHolder) { callback.setView(((RecyclerAdapter.MyViewHolder) holder).image); } return true; } }); }

На этом всё. Теперь анимация обратного перехода будет работать даже в том случае, если элемент находился до этого вне поля видимости.

Исходный код примера можно посмотреть на GitHub, перейдя по

Одним из краеугольных камней в Material design являются осмысленные движения между экранами. Lollipop предоставляет поддержку этих анимаций в форме фреймворка переходов между Activity и Fragment. Поскольку статей по данной теме не так много, я решил написать свою собственную!

Наш конечный продукт будет достаточно прост. Мы будем делать приложение-галерею с котиками. При нажатии на изображение будет открываться экран с подробностями. Благодаря фреймворку переход из сетки изображений в окно с подробностями будет сопровождаться анимацией.

Если вы желаете увидеть, что получилось - готовое приложение находится на GitHub .

Поддержка предыдущих версий Android?

У меня есть для вас две новости: хорошая и плохая. Плохая новость заключается в том, что до Lollipop данный фреймворк не работает. Не смотря на это, проблема решается методами библиотеки поддержки с помощью которой вы можете реализовать анимированые переходы доступные в API 21+.

В статье будут использоваться функции из библиотеки поддержки для обеспечения перемещения контента.

Имена переходов

Для ассоциации View на первом экране и его двойника на втором нужна связь. Lollipop предлагает использовать свойство “transition name ” для связи View между собой.

Существует два способа добавления имени перехода (transition name ) для ваших View:

  • В коде можно использовать ViewCompat.setTransitionName() . Конечно, вы так же можете просто вызвать setTransitionName() , если поддержка начинается с Lollipop.
  • Для добавления в XML, используйте атрибут android:transitionName .
Важно отметить, что внутри одного макета (layout ), имена переходов должны быть уникальны. Держите это в уме при организации переходов. Указывая transition name для ListView или RecyclerView задаст это же имя и для всех остальных элементов.

Настройка FragmentTransaction

Настройка FragmentTransactions должна быть вам очень знакомой:

GetSupportFragmentManager() .beginTransaction() .addSharedElement(sharedElement, transitionName) .replace(R.id.container, newFragment) .addToBackStack(null) .commit();
Чтобы указать какую View будем передавать между фрагментами - используем метод addSharedElement() .

Переданная View в addSharedElement() это View из первого фрагмента, которую вы хотите разделить (share ) со вторым фрагментом. Имя перехода тут является именем перехода в разделенной (shared ) View во втором фрагменте.

Настройка анимации перехода

Наконец-то пришел момент, когда мы зададим анимацию перехода между фрагментами.

Для shared элементов:

  • Для перехода с первого фрагмента во второй используем метод setSharedElementEnterTransition() .
  • Для возврата назад используем метод setSharedElementReturnTransition() . Анимация произойдет при нажатии кнопки назад.
Заметьте, что вам необходимо вызвать эти методы во втором фрагменте, поскольку, если вы сделаете это в первом - ничего не произойдет.

Вы так же можете анимировать переходы для всех non-shared View. Для этих View, используйте setEnterTransition() , setExitTransition() , setReturnTransition() , и setReenterTransition() в соответствующих фрагментах.

Каждый из этих методов принимает один параметр Transition предназначенный для выполнения анимации.

Создавать анимацию мы будем очень просто. Мы используем наш кастомный transition для передвижения изображения (об этом чуть позже), и исчезание ( Fade) при выходе.

Классы анимации перехода

Android предоставляет некоторые готовые анимации переходов, что подходят для большинства случаев. Fade выполняет анимацию исчезновения. Slide анимирует переход появления/исчезновения скольжением из угла экрана. Explode анимация подобная взрыву, изображение движется от краев экрана. И наконец, AutoTransition заставит изображение исчезать, двигаться и изменять размер. Это лишь некоторые примеры из пакета перемещений, их на самом деле намного больше!

Я упоминал, что нам понадобится кастомный переход для нашего изображения. Вот он:

Public class DetailsTransition extends TransitionSet { public DetailsTransition() { setOrdering(ORDERING_TOGETHER); addTransition(new ChangeBounds()). addTransition(new ChangeTransform()). addTransition(new ChangeImageTransform())); } }
Наш кастомный переход есть ни что иное как набор из трех готовых переходов собранных вместе:

Все вместе

Код который в итоге у нас получился оказался достаточно простым:

DetailsFragment details = DetailsFragment.newInstance(); // Note that we need the API version check here because the actual transition classes (e.g. Fade) // are not in the support library and are only available in API 21+. The methods we are calling on the Fragment // ARE available in the support library (though they don"t do anything on API < 21) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { details.setSharedElementEnterTransition(new DetailsTransition()); details.setEnterTransition(new Fade()); setExitTransition(new Fade()); details.setSharedElementReturnTransition(new DetailsTransition()); } getActivity().getSupportFragmentManager() .beginTransaction() .addSharedElement(holder.image, "sharedImage") .replace(R.id.container, details) .addToBackStack(null) .commit();
Вот и все! Простой способ реализации анимации переходов между двумя фрагментами готов!