Animuj zmiany układu za pomocą przejścia

Wypróbuj sposób tworzenia wiadomości
Jetpack Compose to zalecany zestaw narzędzi UI na Androida. Dowiedz się, jak korzystać z animacji w Compose

Struktura przejścia Androida umożliwia animowanie wszystkich rodzajów ruchu w interfejsie, zapewniając układ początkowy i końcowy. Możesz wybrać typ animacji, np. rozjaśnić lub pomniejszyć widok lub zmienić jego rozmiar, a struktura przejścia określa sposób animacji od układu początkowego do końcowego.

Proces przejścia obejmuje te funkcje:

  • Animacje na poziomie grupy: stosuj efekty animacji do wszystkich widoków w hierarchii widoków.
  • Wbudowane animacje: używaj wstępnie zdefiniowanych animacji w przypadku typowych efektów, takich jak rozjaśnianie czy ruch.
  • Obsługa plików zasobów: wczytuj hierarchie widoków i wbudowane animacje z plików zasobów układu.
  • Wywołania zwrotne cyklu życia: odbierają wywołania zwrotne, które zapewniają kontrolę nad animacją i procesem zmiany hierarchii.

Przykładowy kod animowany między kolejnymi zmianami układu znajdziesz na stronie Basicprzejść.

Podstawowy proces animacji między 2 układami wygląda tak:

  1. Utwórz obiekt Scene dla układu początkowego i końcowego. Scena układu początkowego jest jednak często określana automatycznie na podstawie bieżącego układu.
  2. Utwórz obiekt Transition, aby określić odpowiedni typ animacji.
  3. Wywołaj metodę TransitionManager.go(), a system uruchomi animację, aby zamienić układy.

Diagram na ilustracji 1 pokazuje zależności między układami, scenami, przejściem i końcową animacją.

Rysunek 1. Podstawowa ilustracja przedstawiająca sposób tworzenia animacji przez platformę przejścia.

Tworzenie sceny

Sceny przechowują stan hierarchii widoków, w tym wszystkie widoki i wartości ich właściwości. Platforma przejść może uruchamiać animacje między sceną początkową a końcową.

Możesz tworzyć sceny z pliku zasobów układu lub grupy widoków w kodzie. Jednak scena początkowa przejścia często jest określana automatycznie na podstawie bieżącego interfejsu.

Scena może też określać własne działania, które mają być wykonywane po wprowadzeniu jej zmiany. Ta funkcja jest przydatna przy czyszczeniu ustawień widoku po przejściu do sceny.

Tworzenie sceny z zasobu układu

Możesz utworzyć instancję Scene bezpośrednio z pliku zasobów szablonu. Ta metoda jest przydatna, gdy hierarchia widoków w pliku jest w większości statyczna. Powstała scena przedstawia stan hierarchii widoków w chwili utworzenia instancji Scene. Jeśli zmienisz hierarchię widoku, odtwórz scenę. Platforma tworzy scenę na podstawie całej hierarchii widoków w pliku. Nie można utworzyć sceny z części pliku układu.

Aby utworzyć instancję Scene z pliku zasobów układu, pobierz główny element sceny z układu jako ViewGroup. Następnie wywołaj funkcję Scene.getSceneForLayout(), podając jej poziom główny i identyfikator zasobu w pliku układu, który zawiera hierarchię widoków sceny.

Definiowanie układów scen

Fragmenty kodu w pozostałej części tej sekcji pokazują, jak utworzyć 2 różne sceny z tym samym elementem głównym sceny. Fragmenty kodu pokazują też, że można wczytywać wiele niepowiązanych obiektów Scene, nie sugerując, że są one ze sobą powiązane.

Przykład składa się z tych definicji układu:

  • Główny układ aktywności z etykietą tekstową i elementem podrzędnym FrameLayout.
  • ConstraintLayout pierwszej sceny z 2 polami tekstowymi.
  • ConstraintLayout dla drugiej sceny z tymi samymi polami tekstowymi w różnej kolejności.

Przykład został zaprojektowany w taki sposób, aby cała animacja odbywała się w układzie podrzędnym głównego układu aktywności. W układzie głównym pozostaje statyczna.

Główny układ aktywności jest zdefiniowany tak:

res/layout/activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/master_layout">
    <TextView
        android:id="@+id/title"
        ...
        android:text="Title"/>
    <FrameLayout
        android:id="@+id/scene_root">
        <include layout="@layout/a_scene" />
    </FrameLayout>
</LinearLayout>

Ta definicja układu zawiera pole tekstowe i podrzędny FrameLayout dla poziomu głównego sceny. Układ pierwszej sceny znajduje się w głównym pliku układu. Dzięki temu aplikacja może wyświetlić ten element jako część początkowego interfejsu użytkownika i przesłać go do sceny, ponieważ platforma może wczytać do sceny tylko cały plik układu.

Układ pierwszej sceny jest zdefiniowany tak:

res/layout/a_scene.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    
    
</androidx.constraintlayout.widget.ConstraintLayout>

Układ drugiej sceny zawiera te same 2 pola tekstowe – z tymi samymi identyfikatorami – rozmieszczone w innej kolejności. Jest on zdefiniowany w ten sposób:

res/layout/inna_scene.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    
    
</androidx.constraintlayout.widget.ConstraintLayout>

Generuj sceny na podstawie układów

Po utworzeniu definicji dla 2 układów ograniczeń możesz uzyskać scenę dla każdego z nich. Dzięki temu możesz przełączać się między 2 konfiguracjami interfejsu. Aby uzyskać scenę, musisz mieć odniesienie do jej poziomu głównego oraz identyfikatora zasobu układu.

Ten fragment kodu pokazuje, jak uzyskać odniesienie do poziomu głównego sceny i utworzyć 2 obiekty Scene z plików układu:

Kotlin

val sceneRoot: ViewGroup = findViewById(R.id.scene_root)
val aScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this)
val anotherScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this)

Java

Scene aScene;
Scene anotherScene;

// Create the scene root for the scenes in this app.
sceneRoot = (ViewGroup) findViewById(R.id.scene_root);

// Create the scenes.
aScene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this);
anotherScene =
    Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this);

W aplikacji są teraz 2 obiekty Scene oparte na hierarchiach widoków. Obie sceny używają rdzenia sceny zdefiniowanego przez element FrameLayout w res/layout/activity_main.xml.

Tworzenie sceny w kodzie

Możesz też utworzyć w kodzie instancję Scene z obiektu ViewGroup. Ta metoda jest przydatna, gdy modyfikujesz hierarchie widoków bezpośrednio w kodzie lub gdy generujesz je dynamicznie.

Aby utworzyć scenę na podstawie hierarchii widoków w kodzie, użyj konstruktora Scene(sceneRoot, viewHierarchy). Wywołanie tego konstruktora jest równoważne wywołaniu funkcji Scene.getSceneForLayout(), gdy plik szablonu został już powiększony.

Ten fragment kodu pokazuje, jak utworzyć instancję Scene z poziomu głównego elementu sceny i hierarchii widoków sceny w kodzie:

Kotlin

val sceneRoot = someLayoutElement as ViewGroup
val viewHierarchy = someOtherLayoutElement as ViewGroup
val scene: Scene = Scene(sceneRoot, viewHierarchy)

Java

Scene mScene;

// Obtain the scene root element.
sceneRoot = (ViewGroup) someLayoutElement;

// Obtain the view hierarchy to add as a child of
// the scene root when this scene is entered.
viewHierarchy = (ViewGroup) someOtherLayoutElement;

// Create a scene.
mScene = new Scene(sceneRoot, mViewHierarchy);

Tworzenie działań dotyczących sceny

Platforma pozwala zdefiniować niestandardowe działania wykonywane w ramach sceny, które system wykonuje przy dochodzeniu do sceny lub jej opuszczaniu. W wielu przypadkach definiowanie niestandardowych działań sceny jest niepotrzebne, ponieważ platforma automatycznie animuje przejście między scenami.

Czynności wykonywane na ekranie przydają się w takich przypadkach:

  • Aby animować widoki, które nie należą do tej samej hierarchii Możesz animować widoki sceny początkowej i końcowej za pomocą działań wyjścia i wejścia.
  • Aby animować widoki, których struktura przejść nie może animować się automatycznie, np. obiekty ListView. Więcej informacji znajdziesz w sekcji o ograniczeniach.

Aby podać niestandardowe działania związane ze scenami, zdefiniuj je jako obiekty Runnable i przekaż je do funkcji Scene.setExitAction() lub Scene.setEnterAction(). Przed uruchomieniem animacji przejścia platforma wywołuje funkcję setExitAction() na scenie początkowej i funkcję setEnterAction() w scenie końcowej po uruchomieniu animacji przejścia.

Zastosuj przejście

Platforma przejścia reprezentuje styl animacji między scenami z obiektem Transition. Możesz utworzyć instancję Transition za pomocą wbudowanych podklas, takich jak AutoTransition i Fade, lub zdefiniować własne przejście. Później możesz uruchomić animację między scenami, przekazując końcową wartość Scene i Transition do TransitionManager.go().

Cykl życia przejścia jest podobny do cyklu życia aktywności i reprezentuje stany przejścia, które platforma monitoruje platformę między rozpoczęciem i zakończeniem animacji. W ważnych stanach cyklu życia platforma wywołuje funkcje wywołania zwrotnego, które możesz wdrożyć, aby dostosować interfejs użytkownika na różnych fazach przejścia.

Tworzenie przejścia

W poprzedniej sekcji pokazujemy, jak tworzyć sceny reprezentujące stan różnych hierarchii widoków. Gdy zdefiniujesz scenę początkową i końcową, pomiędzy którą chcesz się przełączać, utwórz obiekt Transition z definicją animacji. Platforma pozwala określić wbudowane przejście w pliku zasobów i zwiększyć je w kodzie lub utworzyć instancję wbudowanego przejścia bezpośrednio w kodzie.

Tabela 1. Wbudowane typy przejść.

Kategoria Oznacz Efekt
AutoTransition <autoTransition/> Przejście domyślne. Zanikanie, przesuwanie, zmienianie rozmiaru i zanikanie w widoku w tej kolejności.
ChangeBounds <changeBounds/> Przenosi widoki i zmienia ich rozmiar.
ChangeClipBounds <changeClipBounds/> Rejestruje parametr View.getClipBounds() przed zmianą sceny i po niej oraz animuje te zmiany podczas przejścia.
ChangeImageTransform <changeImageTransform/> Przechwytuje macierz elementu ImageView przed zmianą sceny i po niej oraz animuje ją podczas przejścia.
ChangeScroll <changeScroll/> Przechwytuje właściwości przewijania elementów docelowych przed zmianą sceny i po niej oraz animuje wszystkie zmiany.
ChangeTransform <changeTransform/> Rejestruje skalę i obrót widoków przed zmianą sceny i po niej oraz animuje te zmiany podczas przejścia.
Explode <explode/> Śledzi zmiany widoczności widoków docelowych w scenie początkowej i końcowej oraz przesuwa wyświetlenia do i z krawędzi sceny.
Fade <fade/> fade_in zmniejsza liczbę wyświetleń.
fade_out Zmniejsza liczbę wyświetleń.
fade_in_out (domyślnie) wykonuje fade_out, po którym następuje fade_in.
Slide <slide/> Śledzi zmiany widoczności widoków docelowych w scenie początkowej i końcowej oraz przenosi widoki do lub z jednej z krawędzi sceny.

Tworzenie instancji przejścia z pliku zasobów

Ta technika pozwala zmienić definicję przejścia bez zmiany kodu aktywności. Ta technika jest też przydatna do oddzielania złożonych definicji przejścia od kodu aplikacji. Pokazujemy to w sekcji o określaniu wielu przejść.

Aby określić wbudowane przejście w pliku zasobów, wykonaj te czynności:

  • Dodaj do projektu katalog res/transition/.
  • Utwórz nowy plik zasobów XML w tym katalogu.
  • Dodaj węzeł XML dla jednego z wbudowanych przejść.

Na przykład ten plik zasobów określa przejście Fade:

res/transition/fade_transition.xml

<fade xmlns:android="http://schemas.android.com/apk/res/android" />

Ten fragment kodu pokazuje, jak powiększyć instancję Transition w aktywności z pliku zasobów:

Kotlin

var fadeTransition: Transition =
    TransitionInflater.from(this)
                      .inflateTransition(R.transition.fade_transition)

Java

Transition fadeTransition =
        TransitionInflater.from(this).
        inflateTransition(R.transition.fade_transition);

Tworzenie w kodzie instancji przejściowej

Ta technika jest przydatna do dynamicznego tworzenia obiektów przejść, jeśli modyfikujesz interfejs użytkownika w kodzie i tworzysz proste, wbudowane instancje przejściowe z niewielką liczbą parametrów lub bez nich.

Aby utworzyć instancję wbudowanego przejścia, wywołaj jeden z konstruktorów publicznych w podklasach klasy Transition. Na przykład ten fragment kodu tworzy wystąpienie przejścia Fade:

Kotlin

var fadeTransition: Transition = Fade()

Java

Transition fadeTransition = new Fade();

Zastosuj przejście

Zwykle stosuje się przejście, aby przełączać się między różnymi hierarchiami widoków w odpowiedzi na zdarzenie, np. działanie użytkownika. Rozważmy np. aplikację do wyszukiwania: gdy użytkownik wpisuje wyszukiwane hasło i klika przycisk wyszukiwania, aplikacja zmienia się w scenę reprezentującą układ wyników, a jednocześnie stosuje przejście, które zanika i zanika w wynikach wyszukiwania.

Aby zmienić scenę podczas stosowania przejścia w odpowiedzi na zdarzenie w Twojej aktywności, wywołaj funkcję klasy TransitionManager.go() ze sceną końcową i instancją przejścia, która będzie używana na potrzeby animacji, jak pokazano w tym fragmencie:

Kotlin

TransitionManager.go(endingScene, fadeTransition)

Java

TransitionManager.go(endingScene, fadeTransition);

Platforma zmienia hierarchię widoków w głównej części sceny za pomocą hierarchii widoków z końcowej sceny, jednocześnie uruchamiając animację określoną przez instancję przejściową. Scena początkowa to ta końcowa z ostatniego przejścia. Jeśli nie było poprzedniego przejścia, scena początkowa jest określana automatycznie na podstawie bieżącego stanu interfejsu.

Jeśli nie określisz instancji przejścia, menedżer przenoszenia może zastosować automatyczne przeniesienie, które w większości sytuacji zostanie wykonane w rozsądny sposób. Więcej informacji znajdziesz w dokumentacji interfejsu API klasy TransitionManager.

Wybierz konkretne widoki docelowe

Platforma domyślnie stosuje przejścia do wszystkich widoków w scenie początkowej i końcowej. W niektórych przypadkach możesz zastosować animację tylko do podzbioru widoków w danej scenie. Schemat umożliwia wybranie konkretnych widoków, które chcesz animować. Na przykład platforma nie obsługuje animowania zmian w obiektach ListView, więc nie próbuj animować ich podczas przejścia.

Każdy widok, w którym jest animowany przejście, jest nazywany celem. Możesz wybierać tylko elementy docelowe należące do hierarchii widoku powiązanej ze sceną.

Aby usunąć co najmniej 1 widok danych z listy celów, przed rozpoczęciem przejścia wywołaj metodę removeTarget(). Aby dodać do listy celów tylko określone widoki, wywołaj funkcję addTarget(). Więcej informacji znajdziesz w dokumentacji interfejsu API klasy Transition.

Określ wiele przejść

Aby uzyskać jak największy wpływ animacji, dopasuj ją do typu zmian zachodzących między scenami. Jeśli na przykład usuwasz niektóre widoki i dodajesz inne między scenami, zanikanie lub zanikanie animacji jest wyraźną informacją, że niektóre widoki nie są już dostępne. Jeśli przenosisz widoki do różnych punktów na ekranie, lepiej animować ruch, aby użytkownicy widzieli nowe położenie widoku.

Nie musisz wybierać tylko jednej animacji, ponieważ platforma przejść umożliwia łączenie efektów animacji w zestawie przejść zawierającym grupę osobnych wbudowanych lub niestandardowych przejść.

Aby zdefiniować zestaw przejść na podstawie zbioru przejść w języku XML, utwórz plik zasobów w katalogu res/transitions/ i wymień przejścia w elemencie TransitionSet. Na przykład ten fragment kodu pokazuje, jak określić zestaw przejść, który działa tak samo jak klasa AutoTransition:

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="sequential">
    <fade android:fadingMode="fade_out" />
    <changeBounds />
    <fade android:fadingMode="fade_in" />
</transitionSet>

Aby zwiększyć zakres przejścia do obiektu TransitionSet w kodzie, wywołaj w aktywności funkcję TransitionInflater.from(). Klasa TransitionSet wykracza poza klasę Transition, więc możesz jej używać z menedżerem przenoszenia tak samo jak każdej innej instancji Transition.

Zastosuj przejście bez scen

Zmiana hierarchii widoków to nie jedyny sposób na zmodyfikowanie interfejsu użytkownika. Zmiany możesz też wprowadzać, dodając, modyfikując i usuwając widoki podrzędne w obecnej hierarchii.

Możesz np. zaimplementować interakcję z wyszukiwaniem z pojedynczym układem. Zacznij od układu, w którym widać pole wyszukiwania i ikonę wyszukiwania. Aby zmienić interfejs użytkownika tak, aby pokazywał wyniki, usuń przycisk wyszukiwania, gdy użytkownik go kliknie, wywołując funkcję ViewGroup.removeView(), i dodaj wyniki wyszukiwania, wywołując funkcję ViewGroup.addView().

Możesz użyć tego podejścia, jeśli alternatywą jest 2 prawie identyczne hierarchie. Zamiast tworzyć i utrzymywać 2 osobne pliki układu z niewielką różnicą w interfejsie, możesz mieć jeden plik układu zawierający hierarchię widoków, którą modyfikujesz w kodzie.

Jeśli wprowadzasz zmiany w bieżącej hierarchii widoku w taki sposób, nie musisz tworzyć sceny. Zamiast tego możesz utworzyć i zastosować przejście między 2 stanami w hierarchii widoku za pomocą opóźnionego przejścia. Ta funkcja platformy przejść zaczyna się od bieżącego stanu hierarchii widoku, rejestruje zmiany wprowadzone w widokach i stosuje przejście, które animuje te zmiany, gdy system ponownie wyświetla interfejs użytkownika.

Aby utworzyć opóźnione przejście w obrębie hierarchii pojedynczego widoku, wykonaj te czynności:

  1. Gdy wystąpi zdarzenie, które aktywuje przejście, wywołaj funkcję TransitionManager.beginDelayedTransition(), udostępniając widok nadrzędny wszystkich widoków, które chcesz zmienić, oraz przejście, którego chcesz użyć. Platforma przechowuje bieżący stan widoków podrzędnych i ich wartości właściwości.
  2. Wprowadź zmiany w widokach podrzędnych odpowiednio do swojego przypadku użycia. Platforma rejestruje zmiany, które wprowadzasz w widokach podrzędnych i ich właściwościach.
  3. Gdy system ponownie zmieni interfejs użytkownika zgodnie ze zmianami, ramka animuje zmiany między stanem pierwotnym a nowym.

Poniższy przykład pokazuje, jak przy użyciu opóźnionego przejścia animować dodawanie widoku tekstu do hierarchii widoków. Pierwszy fragment to plik z definicją układu:

res/layout/activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/mainLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <EditText
        android:id="@+id/inputText"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
    ...
</androidx.constraintlayout.widget.ConstraintLayout>

Następny fragment kodu pokazuje kod animujący dodanie widoku tekstu:

MainActivity

Kotlin

setContentView(R.layout.activity_main)
val labelText = TextView(this).apply {
    text = "Label"
    id = R.id.text
}
val rootView: ViewGroup = findViewById(R.id.mainLayout)
val mFade: Fade = Fade(Fade.IN)
TransitionManager.beginDelayedTransition(rootView, mFade)
rootView.addView(labelText)

Java

private TextView labelText;
private Fade mFade;
private ViewGroup rootView;
...
// Load the layout.
setContentView(R.layout.activity_main);
...
// Create a new TextView and set some View properties.
labelText = new TextView(this);
labelText.setText("Label");
labelText.setId(R.id.text);

// Get the root view and create a transition.
rootView = (ViewGroup) findViewById(R.id.mainLayout);
mFade = new Fade(Fade.IN);

// Start recording changes to the view hierarchy.
TransitionManager.beginDelayedTransition(rootView, mFade);

// Add the new TextView to the view hierarchy.
rootView.addView(labelText);

// When the system redraws the screen to show this update,
// the framework animates the addition as a fade in.

Zdefiniuj wywołania zwrotne cyklu życia przejścia

Cykl przejścia jest podobny do cyklu życia aktywności. Reprezentuje stany przejścia, które platforma monitoruje w okresie między wywołaniem funkcji TransitionManager.go() a zakończeniem animacji. W ważnych stanach cyklu życia platforma wywołuje wywołania zwrotne zdefiniowane przez interfejs TransitionListener.

Wywołania zwrotne cyklu życia przejścia przydają się np. do kopiowania wartości właściwości widoku z hierarchii widoku początkowego do hierarchii widoku końcowego podczas zmiany sceny. Nie możesz po prostu skopiować wartości z widoku początkowego do widoku w końcowej hierarchii widoku, ponieważ końcowa hierarchia widoku nie zostanie powiększona, dopóki przejście nie zostanie ukończone. Zamiast tego musisz zapisać wartość w zmiennej, a potem skopiować ją do końcowej hierarchii widoku po zakończeniu przejścia przez platformę. Aby otrzymać powiadomienie o zakończeniu przenoszenia, zaimplementuj w swojej aktywności funkcję TransitionListener.onTransitionEnd().

Więcej informacji znajdziesz w dokumentacji interfejsu API klasy TransitionListener.

Ograniczenia

W tej sekcji znajdziesz kilka znanych ograniczeń platformy przejść:

  • Animacje zastosowane do obiektu SurfaceView mogą się wyświetlać nieprawidłowo. Instancje SurfaceView są aktualizowane z wątku innego niż interfejs użytkownika, więc aktualizacje mogą nie być zsynchronizowane z animacjami w innych widokach.
  • Niektóre określone typy przejść mogą nie dawać żądanego efektu animacji po zastosowaniu do elementu TextureView.
  • Klasy, które rozszerzają zakres AdapterView, takie jak ListView, zarządzają widokami podrzędnymi w sposób niezgodny ze strukturą przejść. Jeśli spróbujesz animować widok na podstawie parametru AdapterView, ekran urządzenia może przestać reagować.
  • Jeśli spróbujesz zmienić rozmiar obiektu TextView za pomocą animacji, tekst wyskoczy w nowe miejsce, zanim rozmiar obiektu zostanie całkowicie zmieniony. Aby uniknąć tego problemu, nie animuj zmiany rozmiaru widoków, które zawierają tekst.