MotionLayout: animasi lebih baik, lebih sedikit kode


Google terus meningkatkan kehidupan kami dengan merilis perpustakaan dan API baru yang praktis. Di antaranya adalah MotionLayout baru. Mengingat banyaknya animasi dalam aplikasi kami, kolega saya Cedric Holtz segera mengimplementasikan animasi paling penting dari aplikasi kami - voting in dating - menggunakan API baru, sambil menyimpan sejumlah besar kode. Saya membagikan terjemahan artikelnya.

Konferensi Google I / O 2019 baru-baru ini berakhir, di mana pembaruan dan peningkatan terbaru untuk SDK tercinta kami diumumkan. Secara pribadi, saya sangat tertarik pada presentasi oleh Nicholas Road dan John Hoford tentang fungsi masa depan ConstraintLayout. Lebih khusus, tentang ekspansi dalam bentuk MotionLayout.

Setelah rilis beta, saya ingin menerapkan animasi kencan berdasarkan perpustakaan ini.

Pertama, mari kita tentukan persyaratannya:

"MotionLayout adalah ConstraintLayout yang memungkinkan Anda untuk menghidupkan tata letak antara berbagai kondisi." - Dokumentasi


Jika Anda belum membaca serangkaian artikel oleh Nicholas Road yang menjelaskan ide-ide kunci dari MotionLayout, saya sangat merekomendasikan untuk membacanya.

Jadi, dengan pengantar selesai, sekarang mari kita lihat apa yang ingin kita dapatkan:



Tumpukan kartu


Kami menunjukkan peta yang digeser


Untuk memulainya, tambahkan MotionLayout ke direktori tata letak, yang sejauh ini hanya berisi satu kartu teratas:

<androidx.constraintlayout.motion.widget.MotionLayout android:id="@+id/motionLayout"     xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:app="http://schemas.android.com/apk/res-auto"     android:layout_width="match_parent"     android:layout_height="match_parent"     app:layoutDescription="@xml/scene_swipe"     app:motionDebug="SHOW_ALL">     <FrameLayout         android:id="@+id/topCard"         android:layout_width="0dp"         android:layout_height="0dp" /> </androidx.constraintlayout.motion.widget.MotionLayout> 

Perhatikan baris ini: app: motionDebug = "SHOW_ALL". Hal ini memungkinkan kita untuk menampilkan informasi debug, lintasan objek, keadaan dengan awal dan akhir animasi, serta kemajuan saat ini. Baris sangat membantu ketika melakukan debug, tetapi jangan lupa untuk menghapusnya sebelum mengirimnya ke prod: tidak ada pengingat untuk ini.

Seperti yang Anda lihat, kami tidak menetapkan batasan untuk tampilan di sini. Mereka akan diambil dari adegan (MotionScene), yang sekarang kita definisikan.

Mari kita mulai dengan mendefinisikan keadaan awal: satu kartu terletak di tengah layar, dengan lekukan sekitar.

 <MotionScene xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto">    <ConstraintSet android:id="@+id/rest">        <Constraint            android:id="@id/topCard"            android:layout_width="match_parent"            android:layout_height="match_parent"            android:layout_marginBottom="50dp"            android:layout_marginEnd="50dp"            android:layout_marginStart="50dp"            android:layout_marginTop="50dp"            app:layout_constraintEnd_toEndOf="parent"            app:layout_constraintStart_toStartOf="parent">    </ConstraintSet> </MotionScene> 

Tambahkan set kendala (ConstraintSet) lulus dan suka. Mereka akan mencerminkan keadaan kartu teratas ketika benar-benar bergeser ke kiri atau kanan. Kami ingin peta berhenti sebelum menghilang dari layar untuk menampilkan animasi yang indah yang menegaskan keputusan kami.

 <ConstraintSet    android:id="@+id/pass"    app:deriveConstraintsFrom="@+id/rest">    <Constraint        android:id="@id/topCard"        android:layout_width="0dp"        android:layout_height="match_parent"        android:layout_marginBottom="80dp"        android:layout_marginEnd="200dp"        android:layout_marginStart="50dp"        android:layout_marginTop="20dp"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintWidth_percent="0.7" /> </ConstraintSet> <ConstraintSet    android:id="@+id/like"    app:deriveConstraintsFrom="@id/rest">    <Constraint        android:id="@id/topCard"        android:layout_width="0dp"        android:layout_height="match_parent"        android:layout_marginBottom="80dp"        android:layout_marginEnd="50dp"        android:layout_marginStart="200dp"        android:layout_marginTop="20dp"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintWidth_percent="0.7" /> </ConstraintSet> 

Tambahkan kedua set kendala ke adegan sebelumnya. Mereka hampir identik, hanya dicerminkan di kedua sisi layar.

Sekarang kita memiliki tiga set batasan - mulai, suka dan lulus. Mari kita mendefinisikan Transisi antara negara-negara ini.

Untuk melakukan ini, tambahkan satu transisi ke kiri untuk swipe, yang lain ke kanan untuk swipe.

 <Transition    app:constraintSetEnd="@+id/pass"    app:constraintSetStart="@+id/rest"    app:duration="300">    <OnSwipe        app:dragDirection="dragLeft"        app:onTouchUp="autoComplete"        app:touchAnchorId="@id/topCard"        app:touchAnchorSide="left"        app:touchRegionId="@id/topCard" /> </Transition> <Transition    app:constraintSetEnd="@+id/like"    app:constraintSetStart="@+id/rest"    app:duration="300">    <OnSwipe        app:dragDirection="dragRight"        app:onTouchUp="autoComplete"        app:touchAnchorId="@+id/topCard"        app:touchAnchorSide="right"        app:touchRegionId="@id/topCard" /> </Transition> 

Jadi, untuk kartu teratas, kami mengatur animasi swipe ke kiri dan cermin yang sama - untuk swipe ke kanan.

Properti ini akan membantu meningkatkan interaksi dengan adegan kami:

  • touchRegionId: karena kita menambahkan padding di sekitar peta, kita perlu memastikan bahwa sentuhan hanya dikenali di area peta itu sendiri, dan bukan seluruh MotionLayout. Ini dapat dilakukan dengan menggunakan touchRegionId.
  • onTouchUp: apa yang akan terjadi pada animasi setelah kami melepaskan kartu? Itu harus pindah atau kembali ke keadaan awal, sehingga AutoComplete berlaku.

Mari kita lihat apa yang terjadi:



Peta secara otomatis mati layar


Sekarang kita akan bekerja pada animasi, yang akan dimulai ketika peta mati layar.

Kami menambahkan dua set ConstraintSet lainnya untuk setiap keadaan akhir animasi kami: peta meninggalkan layar ke kiri dan kanan.

Dalam contoh berikut, saya akan menunjukkan cara membuat negara seperti, dan negara bagian akan mengulanginya mencerminkan. Contoh yang berfungsi dapat sepenuhnya dilihat di repositori .

 <ConstraintSet android:id="@+id/offScreenLike">    <Constraint        android:id="@id/topCard"        android:layout_width="0dp"        android:layout_height="match_parent"        android:layout_marginBottom="80dp"        android:layout_marginEnd="50dp"        android:layout_marginTop="20dp"        app:layout_constraintStart_toEndOf="parent"        app:layout_constraintWidth_percent="0.7" />   </ConstraintSet> 

Sekarang, seperti pada contoh sebelumnya, Anda perlu menentukan transisi dari negara swipe ke negara final. Transisi akan otomatis bekerja segera setelah animasi dari swipe. Ini dapat dilakukan dengan menggunakan autoTransition:

 <Transition app:autoTransition="animateToEnd" app:constraintSetEnd="@+id/offScreenLike" app:constraintSetStart="@+id/like" app:duration="150" /> 

Sekarang kami memiliki kartu gesek yang dapat digesek dari layar!



Animasi peta bawah



Sekarang mari kita membuat kartu terbawah untuk membuat ilusi infinity deck.

Tambahkan satu peta lagi ke tata letak, mirip dengan yang pertama:

 <FrameLayout    android:id="@+id/bottomCard"    android:layout_width="0dp"    android:layout_height="0dp"    android:background="@color/colorAccent" /> 

Ubah XML untuk menetapkan batasan yang berlaku untuk peta ini di setiap tahap animasi:

 <ConstraintSet android:id="@id/rest">    <!-- ... -->    <Constraint android:id="@id/bottomCard">        <Layout            android:layout_width="match_parent"            android:layout_height="match_parent"            android:layout_marginBottom="50dp"            android:layout_marginEnd="50dp"            android:layout_marginStart="50dp"            android:layout_marginTop="50dp" />        <Transform            android:scaleX="0.90"            android:scaleY="0.90" />    </Constraint> </ConstraintSet> <ConstraintSet    android:id="@+id/offScreenLike"    app:deriveConstraintsFrom="@id/like">    <!-- ... -->    <Constraint android:id="@id/bottomCard">        <Transform            android:scaleX="1"            android:scaleY="1" />    </Constraint> </ConstraintSet> 

Untuk melakukan ini, kita dapat menggunakan properti ConstraintSet yang nyaman.

Secara default, setiap set baru mengambil atribut dari induk MotionLayout. Tetapi menggunakan flag deriveConstraintsFrom, Anda dapat mengatur orangtua lain untuk set kami. Harus diingat bahwa jika kita menetapkan batasan menggunakan tag kendala, maka kita mendefinisikan kembali semua batasan dari set induk. Untuk menghindari hal ini, Anda dapat mengatur atribut spesifik dalam tag sehingga hanya mereka yang diganti.



Dalam kasus kami, ini berarti bahwa dalam set pass kami tidak mendefinisikan tag Layout, tetapi menyalinnya dari induknya. Namun, kami menimpa Transform, oleh karena itu, kami mengganti semua atribut yang ditentukan dalam tag Transform dengan milik kami, dalam hal ini, perubahan skala.

Ini adalah betapa mudahnya menggunakan MotionLayout untuk menambahkan elemen baru dan mengintegrasikannya dengan animasi adegan kami.



Membuat animasi tidak ada habisnya



Setelah animasi selesai, kartu teratas tidak dapat dihapus, karena sekarang telah menjadi kartu terbawah. Untuk mendapatkan animasi tanpa akhir, Anda perlu menukar kartu.

Awalnya saya ingin melakukan ini dengan transisi baru:

 <Transition    app:autoTransition="jumpToEnd"    app:constraintSetEnd="@+id/rest"    app:constraintSetStart="@+id/offScreenLike"    app:duration="0" /> 



Seluruh animasi dimainkan sebagaimana mestinya. Sekarang kami memiliki setumpuk kartu yang dapat Anda gesek tanpa henti!

Setelah sedikit menggesek, saya perhatikan sesuatu. Transisi ke akhir animasi dek berhenti ketika Anda menyentuh kartu. Meskipun durasi animasi nol, masih berhenti, yang buruk.



Saya berhasil menang hanya dengan satu cara - dengan secara terprogram mengubah transisi aktif di MotionLayout.

Untuk melakukan ini, kami akan menetapkan panggilan balik setelah animasi selesai. Segera setelah offScreenLike dan offScreenPass selesai, kami cukup mereset transisi kembali ke keadaan diam dan tidak menunjukkan kemajuan.

 motionLayout.setTransitionListener(object : TransitionAdapter() {    override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {        when (currentId) {            R.id.offScreenPass,            R.id.offScreenLike -> {                motionLayout.progress = 0f                motionLayout.setTransition(R.id.rest, R.id.like)            }        }    }   }) 

Tidak masalah transisi mana yang kami atur, lewati atau sukai, ketika kami menggeser kami beralih ke yang diinginkan.



Itu terlihat sama, tetapi animasi tidak berhenti! Mari kita lanjutkan!

Pengikatan data


Buat data uji untuk ditampilkan di peta. Untuk saat ini, kami akan membatasi diri untuk mengubah warna latar belakang setiap kartu.

Kami membuat ViewModel dengan metode svayp yang hanya menggantikan data baru. Ikat ke Aktivitas dengan cara ini:

 val viewModel = ViewModelProviders .of(this) .get(SwipeRightViewModel::class.java) viewModel .modelStream .observe(this, Observer { bindCard(it) }) motionLayout.setTransitionListener(object : TransitionAdapter() { override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) { when (currentId) { R.id.offScreenPass, R.id.offScreenLike -> { motionLayout.progress = 0f motionLayout.setTransition(R.id.rest, R.id.like) viewModel.swipe() } } } }) 

Tetap menginformasikan kepada ViewModel tentang penyelesaian animasi usap, dan itu akan memperbarui data yang saat ini ditampilkan.



Ikon sembulan


Tambahkan dua tampilan, yang ketika menggesek muncul di salah satu sisi layar (hanya satu yang ditampilkan di bawah, yang kedua dicerminkan).

 <ImageView android:id="@+id/likeIndicator" android:layout_width="0dp" android:layout_height="0dp" /> 

Sekarang untuk peta, Anda perlu mengatur status animasi dengan tampilan ini.

 <ConstraintSet android:id="@id/rest">    <!-- ... -->    <Constraint android:id="@+id/like">        <Layout            android:layout_width="40dp"            android:layout_height="40dp"            app:layout_constraintBottom_toBottomOf="parent"            app:layout_constraintStart_toEndOf="parent"            app:layout_constraintTop_toTopOf="parent" />        <Transform            android:scaleX="0.5"            android:scaleY="0.5" />        <PropertySet android:alpha="0" />    </Constraint>   </ConstraintSet> <ConstraintSet    android:id="@+id/like"    app:deriveConstraintsFrom="@id/rest">    <!-- ... -->    <Constraint android:id="@+id/like">        <Layout            android:layout_width="100dp"            android:layout_height="100dp"            app:layout_constraintBottom_toBottomOf="@id/topCard"            app:layout_constraintEnd_toEndOf="@id/topCard"            app:layout_constraintStart_toStartOf="@id/topCard"            app:layout_constraintTop_toTopOf="@id/topCard" />        <Transform            android:scaleX="1"            android:scaleY="1" />        <PropertySet android:alpha="1" />    </Constraint> </ConstraintSet> 

Tidak perlu menetapkan batasan pada animasi yang melampaui layar, karena diwariskan dari orang tua. Dan dalam kasus kami, ini adalah kondisi swipe.

Hanya itu yang perlu kita lakukan. Sekarang Anda dapat menambahkan komponen ke rantai animasi dengan sangat mudah.



Jalankan animasi secara terprogram



Kita dapat membuat dua tombol pada kartu sehingga pengguna tidak hanya dapat menggesek, tetapi mengontrol menggunakan tombol.

Setiap tombol memulai animasi yang sama dengan sapuan.

Seperti biasa, berlangganan klik tombol dan mulai animasi langsung pada objek MotionLayout:

 likeButton.setOnClickListener {    motionLayout.transitionToState(R.id.like) } passButton.setOnClickListener {    motionLayout.transitionToState(R.id.pass) } 

Kita perlu menambahkan tombol ke kartu atas dan bawah agar animasi diputar terus menerus. Namun, untuk peta yang lebih rendah, klik berlangganan tidak diperlukan, karena tidak terlihat, atau peta bagian atas dianimasikan, dan kami tidak ingin menghentikannya.



Contoh hebat lainnya tentang bagaimana MotionLayout menangani perubahan status untuk kami. Mari kita sedikit memperlambat animasi:



Lihatlah transisi yang dilakukan MotionLayout ketika pass menggantikan. Keajaiban!

Geser peta di sepanjang kurva


Misalkan kita suka jika peta tidak bergerak dalam garis lurus, tetapi dalam kurva (jujur, saya hanya ingin mencoba melakukan itu).

Maka Anda perlu mendefinisikan KeyPosisi untuk gerakan di kedua arah, sehingga jalur gerakan melengkung oleh busur.

Tambahkan ini ke adegan gerak:

 <Transition    app:constraintSetEnd="@+id/like"    app:constraintSetStart="@+id/rest"    app:duration="300">    <!-- ... -->    <KeyFrameSet>        <KeyPosition            app:drawPath="path"            app:framePosition="50"            app:keyPositionType="pathRelative"            app:motionTarget="@id/topCard"            app:percentX="0.5"            app:percentY="-0.1" />           </KeyFrameSet> </Transition> 


Sekarang peta bergerak di sepanjang jalur lengkung non-dangkal. Ajaib!

Kesimpulan


Ketika Anda membandingkan jumlah kode yang saya dapatkan saat membuat animasi ini dengan implementasi animasi serupa kami saat ini dalam produksi, hasilnya sangat mengejutkan.

MotionLayout secara tidak sengaja menangani pembatalan transisi (misalnya, ketika disentuh), membuat rantai animasi, mengubah properti selama transisi, dan banyak lagi. Alat ini secara fundamental mengubah segalanya, sangat menyederhanakan logika UI.

Ada beberapa hal lain yang layak untuk dikerjakan (kebanyakan menonaktifkan animasi dan gulir dua arah di RecyclerView), tapi saya yakin ini bisa dipecahkan.

Ingat bahwa perpustakaan masih dalam status beta, tetapi sudah membuka banyak peluang menarik bagi kami. Kami menantikan rilis MotionLayout, yang, saya yakin, akan berguna lebih dari sekali di masa depan. Anda dapat melihat aplikasi yang berfungsi penuh dari artikel ini di repositori .

PS: dan karena mereka memberi saya dasar sebagai penerjemah, tim Android kami memiliki tempat untuk pengembang . Terima kasih atas perhatian anda

Source: https://habr.com/ru/post/id458854/


All Articles