Android nâng cao trong Kotlin 03.2: Ảnh động với MotionLayout

1. Trước khi bắt đầu

Lớp học lập trình này nằm trong khoá học Kotlin nâng cao cho Android. Bạn sẽ nhận được nhiều giá trị nhất qua khoá học này nếu thực hiện các lớp học lập trình theo trình tự, nhưng đó không phải là yêu cầu bắt buộc. Tất cả các lớp học lập trình của khoá học đều được liệt kê trên trang đích của lớp học lập trình Kiến thức nâng cao về cách tạo ứng dụng Android bằng Kotlin.

MotionLayout là một thư viện cho phép bạn thêm chuyển động phong phú vào ứng dụng Android. Thư viện này dựa trên ConstraintLayout, và cho phép bạn tạo hiệu ứng cho mọi thứ mà bạn có thể tạo bằng ConstraintLayout.

Bạn có thể sử dụng MotionLayout để tạo hiệu ứng cho vị trí, kích thước, chế độ hiển thị, độ đậm nhạt, màu sắc, độ cao, xoay và các thuộc tính khác của nhiều khung hiển thị cùng một lúc. Bằng cách sử dụng XML khai báo, bạn có thể tạo ảnh động phối hợp cho nhiều khung hiển thị (khó làm được việc này trong mã).

Ảnh động là một cách hiệu quả để nâng cao trải nghiệm dùng ứng dụng. Bạn có thể sử dụng ảnh động để:

  • Hiển thị các thay đổi – việc tạo ảnh động giữa các trạng thái giúp người dùng theo dõi các thay đổi trong giao diện người dùng một cách tự nhiên.
  • Thu hút sự chú ý – sử dụng ảnh động để thu hút sự chú ý đến các phần tử quan trọng trên giao diện người dùng.
  • Xây dựng các thiết kế đẹp mắt – chuyển động hiệu quả trong thiết kế giúp ứng dụng trông tinh tế hơn.

Điều kiện tiên quyết

Lớp học lập trình này dành cho những nhà phát triển có kinh nghiệm phát triển Android. Trước khi cố gắng hoàn thành lớp học lập trình này, bạn nên:

  • Biết cách tạo một ứng dụng có hoạt động, bố cục cơ bản và chạy ứng dụng đó trên thiết bị hoặc trình mô phỏng bằng Android Studio. Làm quen với ConstraintLayout. Hãy đọc Lớp học lập trình về bố cục ràng buộc để tìm hiểu thêm về ConstraintLayout.

Bạn sẽ thực hiện

  • Xác định một ảnh động bằng ConstraintSetsMotionLayout
  • Tạo hiệu ứng dựa trên sự kiện kéo
  • Thay đổi ảnh động bằng KeyPosition
  • Thay đổi thuộc tính bằng KeyAttribute
  • Chạy ảnh động bằng mã
  • Tạo ảnh động cho tiêu đề có thể thu gọn bằng MotionLayout

Bạn cần có

  • Android Studio 4.0 (Trình chỉnh sửa MotionLayout chỉ hoạt động với phiên bản Android Studio này.)

2. Bắt đầu

Để tải ứng dụng mẫu xuống, bạn có thể làm theo một trong hai cách sau:

... hoặc sao chép kho lưu trữ GitHub từ dòng lệnh bằng cách sử dụng lệnh sau:

$ git clone https://github.com/googlecodelabs/motionlayout.git

3. Tạo ảnh động bằng MotionLayout

Trước tiên, bạn sẽ tạo một ảnh động di chuyển một khung hiển thị từ đầu màn hình xuống cuối màn hình để phản hồi các lượt nhấp của người dùng.

Để tạo một ảnh động từ mã khởi đầu, bạn sẽ cần những phần chính sau:

  • Một MotionLayout, là lớp con của ConstraintLayout. Bạn chỉ định tất cả các khung hiển thị cần được tạo hiệu ứng động trong thẻ MotionLayout.
  • Một MotionScene, là tệp XML mô tả một ảnh động cho MotionLayout.
  • Một Transition, là một phần của MotionScene, chỉ định thời lượng, điều kiện kích hoạt của ảnh động và cách di chuyển các khung hiển thị.
  • Một ConstraintSet chỉ định cả các ràng buộc startend của quá trình chuyển đổi.

Hãy cùng xem xét từng loại, bắt đầu từ MotionLayout.

Bước 1: Khám phá mã hiện có

MotionLayout là một lớp con của ConstraintLayout, vì vậy, lớp này hỗ trợ tất cả các tính năng tương tự trong khi thêm ảnh động. Để sử dụng MotionLayout, hãy thêm khung hiển thị MotionLayout vào vị trí mà bạn sẽ sử dụng ConstraintLayout.

  1. Trong res/layout, hãy mở activity_step1.xml. Tại đây, bạn có một ConstraintLayout với một ImageView duy nhất là ngôi sao, có màu sắc được áp dụng bên trong.

activity_step1.xml

<!-- initial code -->
<androidx.constraintlayout.widget.ConstraintLayout
       ...
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       >

   <ImageView
           android:id="@+id/red_star"
           ...
   />

</androidx.constraintlayout.motion.widget.MotionLayout>

ConstraintLayout này không có bất kỳ ràng buộc nào, vì vậy, nếu chạy ứng dụng ngay bây giờ, bạn sẽ thấy màn hình hiển thị các ngôi sao không bị ràng buộc, tức là các ngôi sao sẽ được đặt ở một vị trí không xác định. Android Studio sẽ đưa ra cảnh báo về việc thiếu các điều kiện ràng buộc.

Bước 2: Chuyển đổi sang Bố cục chuyển động

Để tạo ảnh động bằng MotionLayout,, bạn phải chuyển đổi ConstraintLayout thành MotionLayout.

Để bố cục của bạn sử dụng một cảnh chuyển động, bố cục đó phải trỏ đến cảnh chuyển động.

  1. Để làm việc này, hãy mở vùng thiết kế. Trong Android Studio 4.0, bạn mở vùng thiết kế bằng cách sử dụng biểu tượng chia tách hoặc thiết kế ở trên cùng bên phải khi xem tệp XML bố cục.

a2beea710c2decb7.png

  1. Sau khi bạn mở giao diện thiết kế, hãy nhấp chuột phải vào bản xem trước rồi chọn Convert to MotionLayout (Chuyển đổi sang MotionLayout).

4fa936a98a8393b9.png

Thao tác này sẽ thay thế thẻ ConstraintLayout bằng thẻ MotionLayout và thêm motion:layoutDescription vào thẻ MotionLayout trỏ đến @xml/activity_step1_scene.

activity_step1**.xml**

<!-- explore motion:layoutDescription="@xml/activity_step1_scene" -->
<androidx.constraintlayout.motion.widget.MotionLayout
       ...
       motion:layoutDescription="@xml/activity_step1_scene">

Cảnh chuyển động là một tệp XML duy nhất mô tả một ảnh động trong MotionLayout.

Ngay khi bạn chuyển đổi sang MotionLayout, bề mặt thiết kế sẽ hiển thị Trình chỉnh sửa chuyển động

66d0e80d5ab4daf8.png

Có 3 phần tử giao diện người dùng mới trong Trình chỉnh sửa chuyển động:

  1. Tổng quan – Đây là lựa chọn theo phương thức xuất hiện cho phép bạn chọn các phần khác nhau của ảnh động. Trong hình ảnh này, start ConstraintSet được chọn. Bạn cũng có thể chọn hiệu ứng chuyển đổi giữa startend bằng cách nhấp vào mũi tên giữa hai hiệu ứng này.
  2. Phần – Bên dưới phần tổng quan là một cửa sổ phần thay đổi dựa trên mục tổng quan hiện được chọn. Trong hình ảnh này, thông tin start ConstraintSet xuất hiện trong cửa sổ lựa chọn.
  3. Attribute (Thuộc tính) – Bảng thuộc tính cho phép bạn xem và chỉnh sửa các thuộc tính của mục hiện được chọn trong cửa sổ tổng quan hoặc cửa sổ lựa chọn. Trong hình ảnh này, các thuộc tính cho start ConstraintSet đang xuất hiện.

Bước 3: Xác định các ràng buộc về thời gian bắt đầu và kết thúc

Bạn có thể xác định tất cả ảnh động theo thời gian bắt đầu và kết thúc. Điểm bắt đầu mô tả giao diện màn hình trước khi có ảnh động và điểm kết thúc mô tả giao diện màn hình sau khi ảnh động hoàn tất. MotionLayout chịu trách nhiệm tìm ra cách tạo ảnh động giữa trạng thái bắt đầu và trạng thái kết thúc (theo thời gian).

MotionScene sử dụng thẻ ConstraintSet để xác định trạng thái bắt đầu và kết thúc. ConstraintSet là một tập hợp các quy tắc ràng buộc có thể áp dụng cho các thành phần hiển thị. Trong đó có các ràng buộc về chiều rộng, chiều cao và ConstraintLayout. Nó cũng bao gồm một số thuộc tính như alpha. Tệp này không chứa các khung hiển thị mà chỉ chứa các quy tắc ràng buộc đối với những khung hiển thị đó.

Mọi ràng buộc được chỉ định trong ConstraintSet sẽ ghi đè các ràng buộc được chỉ định trong tệp bố cục. Nếu bạn xác định các giới hạn trong cả bố cục và MotionScene, thì chỉ các giới hạn trong MotionScene mới được áp dụng.

Trong bước này, bạn sẽ ràng buộc khung hiển thị ngôi sao để bắt đầu ở đầu màn hình và kết thúc ở cuối màn hình.

Bạn có thể hoàn tất bước này bằng Trình chỉnh sửa chuyển động hoặc bằng cách chỉnh sửa trực tiếp văn bản của activity_step1_scene.xml.

  1. Chọn start ConstraintSet trong bảng tổng quan

6e57661ed358b860.png

  1. Trong bảng lựa chọn, hãy chọn red_star. Hiện tại, nó cho thấy Nguồn của layout – điều này có nghĩa là nó không bị hạn chế trong ConstraintSet này. Sử dụng biểu tượng bút chì ở phía trên bên phải để Tạo ràng buộc

f9564c574b86ea8.gif

  1. Xác nhận rằng red_star cho thấy Nguồn là start khi start ConstraintSet được chọn trong bảng điều khiển tổng quan.
  2. Trong bảng điều khiển Attributes (Thuộc tính), khi chọn red_star trong start ConstraintSet, hãy thêm một Constraint (Quy tắc ràng buộc) ở trên cùng và bắt đầu bằng cách nhấp vào nút + màu xanh dương.

2fce076cd7b04bd.png

  1. Mở xml/activity_step1_scene.xml để xem mã mà Trình chỉnh sửa chuyển động đã tạo cho ràng buộc này.

activity_step1_scene.xml

<!-- Constraints to apply at the start of the animation -->
<ConstraintSet android:id="@+id/start">
   <Constraint
           android:id="@+id/red_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

ConstraintSetid@id/start và chỉ định tất cả các điều kiện hạn chế cần áp dụng cho tất cả các khung hiển thị trong MotionLayout. Vì MotionLayout này chỉ có một chế độ xem, nên chỉ cần một Constraint.

Constraint bên trong ConstraintSet chỉ định mã nhận dạng của khung hiển thị mà nó đang ràng buộc, @id/red_star được xác định trong activity_step1.xml. Bạn cần lưu ý rằng thẻ Constraint chỉ chỉ định các ràng buộc và thông tin bố cục. Thẻ Constraint không biết rằng thẻ này đang được áp dụng cho một ImageView.

Điều kiện ràng buộc này chỉ định chiều cao, chiều rộng và 2 điều kiện ràng buộc khác cần thiết để ràng buộc khung hiển thị red_star với phần đầu trên cùng của thành phần mẹ.

  1. Chọn end ConstraintSet trong bảng điều khiển tổng quan.

346e1248639b6f1e.png

  1. Làm theo các bước tương tự như trước đây để thêm một Constraint cho red_star trong end ConstraintSet.
  2. Để sử dụng Trình chỉnh sửa chuyển động để hoàn tất bước này, hãy thêm một ràng buộc vào bottomend bằng cách nhấp vào nút + màu xanh dương.

fd33c779ff83c80a.png

  1. Mã trong XML sẽ có dạng như sau:

activitiy_step1_scene.xml

<!-- Constraints to apply at the end of the animation -->
<ConstraintSet android:id="@+id/end">
   <Constraint
           android:id="@+id/red_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintBottom_toBottomOf="parent" />
</ConstraintSet>

Giống như @id/start, ConstraintSet này có một Constraint duy nhất trên @id/red_star. Lần này, bạn sẽ đặt cố định bố cục đó ở cuối màn hình.

Bạn không bắt buộc phải đặt tên cho chúng là @id/start@id/end, nhưng việc này sẽ thuận tiện hơn.

Bước 4: Xác định hiệu ứng chuyển cảnh

Mỗi MotionScene cũng phải có ít nhất một hiệu ứng chuyển cảnh. Quá trình chuyển đổi xác định mọi phần của một ảnh động, từ đầu đến cuối.

Quá trình chuyển đổi phải chỉ định một ConstraintSet bắt đầu và kết thúc cho quá trình chuyển đổi. Hiệu ứng chuyển đổi cũng có thể chỉ định cách sửa đổi ảnh động theo những cách khác, chẳng hạn như thời gian chạy ảnh động hoặc cách tạo ảnh động bằng cách kéo các khung hiển thị.

  1. Trình chỉnh sửa chuyển động đã tạo một hiệu ứng chuyển đổi cho chúng ta theo mặc định khi tạo tệp MotionScene. Mở activity_step1_scene.xml để xem hiệu ứng chuyển cảnh được tạo.

activity_step1_scene.xml

<!-- A transition describes an animation via start and end state -->
<Transition
   motion:constraintSetEnd="@+id/end"
   motion:constraintSetStart="@id/start"
   motion:duration="1000">
  <KeyFrameSet>
  </KeyFrameSet>
</Transition>

Đây là tất cả những gì MotionLayout cần để tạo ảnh động. Xem xét từng thuộc tính:

  • constraintSetStart sẽ được áp dụng cho các khung hiển thị khi ảnh động bắt đầu.
  • constraintSetEnd sẽ được áp dụng cho các khung hiển thị khi ảnh động kết thúc.
  • duration chỉ định thời lượng của ảnh động tính bằng mili giây.

Sau đó, MotionLayout sẽ tìm ra một đường dẫn giữa các điều kiện ràng buộc bắt đầu và kết thúc, rồi tạo ảnh động cho đường dẫn đó trong khoảng thời gian đã chỉ định.

Bước 5: Xem trước ảnh động trong Trình chỉnh sửa chuyển động

dff9ecdc1f4a0740.gif

Ảnh động: Video về cách phát bản xem trước hiệu ứng chuyển cảnh trong Trình chỉnh sửa chuyển động

  1. Mở Motion Editor (Trình chỉnh sửa chuyển động) rồi chọn hiệu ứng chuyển cảnh bằng cách nhấp vào mũi tên giữa startend trong bảng tổng quan.

1dc541ae8c43b250.png

  1. Bảng lựa chọn cho thấy các nút điều khiển phát và thanh tua khi bạn chọn một hiệu ứng chuyển cảnh. Nhấp vào nút phát hoặc kéo vị trí hiện tại để xem trước ảnh động.

a0fd2593384dfb36.png

Bước 6: Thêm trình xử lý khi nhấp

Bạn cần có cách để bắt đầu ảnh động. Một cách để thực hiện việc này là làm cho MotionLayout phản hồi các sự kiện nhấp chuột trên @id/red_star.

  1. Mở trình chỉnh sửa chuyển động và chọn hiệu ứng chuyển cảnh bằng cách nhấp vào mũi tên giữa điểm bắt đầu và điểm kết thúc trong bảng điều khiển tổng quan.

b6f94b344ce65290.png

  1. Nhấp vào 699f7ae04024ccf6.png Create click or swipe handler (Tạo trình xử lý thao tác nhấp hoặc vuốt) trong thanh công cụ của bảng tổng quan . Thao tác này sẽ thêm một trình xử lý để bắt đầu một hiệu ứng chuyển đổi.
  2. Chọn Trình xử lý lượt nhấp trong cửa sổ bật lên

ccf92d06335105fe.png

  1. Thay đổi Lượt xem thành lượt nhấp thành red_star.

b0d3f0c970604f01.png

  1. Nhấp vào Add (Thêm), trình xử lý lượt nhấp được biểu thị bằng một dấu chấm nhỏ trên Chuyển động trong Trình chỉnh sửa chuyển động.

cec3913e67fb4105.png

  1. Khi bạn chọn hiệu ứng chuyển cảnh trong bảng tổng quan, hãy thêm thuộc tính clickAction của toggle vào trình xử lý OnClick mà bạn vừa thêm trong bảng thuộc tính.

9af6fc60673d093d.png

  1. Mở activity_step1_scene.xml để xem mã mà Trình chỉnh sửa chuyển động đã tạo

activity_step1_scene.xml

<!-- A transition describes an animation via start and end state -->
<Transition
    motion:constraintSetStart="@+id/start"
    motion:constraintSetEnd="@+id/end"
    motion:duration="1000">
    <!-- MotionLayout will handle clicks on @id/red_star to "toggle" the animation between the start and end -->
    <OnClick
        motion:targetId="@id/red_star"
        motion:clickAction="toggle" />
</Transition>

Transition yêu cầu MotionLayout chạy ảnh động để phản hồi các sự kiện nhấp chuột bằng thẻ <OnClick>. Xem xét từng thuộc tính:

  • targetId là khung hiển thị để theo dõi các lượt nhấp.
  • clickAction của toggle sẽ chuyển đổi giữa trạng thái bắt đầu và trạng thái kết thúc khi nhấp vào. Bạn có thể xem các lựa chọn khác cho clickAction trong tài liệu.
  1. Chạy mã, nhấp vào Bước 1, sau đó nhấp vào ngôi sao màu đỏ và xem ảnh động!

Bước 5: Ảnh động đang hoạt động

Chạy ứng dụng! Bạn sẽ thấy ảnh động chạy khi nhấp vào ngôi sao.

7ba88af963fdfe10.gif

Tệp cảnh chuyển động đã hoàn tất xác định một Transition trỏ đến ConstraintSet bắt đầu và kết thúc.

Khi bắt đầu ảnh động (@id/start), biểu tượng ngôi sao sẽ bị ràng buộc với phần trên cùng bên trái của màn hình. Ở cuối ảnh động (@id/end), biểu tượng ngôi sao bị ràng buộc vào cuối màn hình.

<?xml version="1.0" encoding="utf-8"?>

<!-- Describe the animation for activity_step1.xml -->
<MotionScene xmlns:app="http://schemas.android.com/apk/res-auto"
            xmlns:android="http://schemas.android.com/apk/res/android">
   <!-- A transition describes an animation via start and end state -->
   <Transition
           motion:constraintSetStart="@+id/start"
           motion:constraintSetEnd="@+id/end"
           motion:duration="1000">
       <!-- MotionLayout will handle clicks on @id/star to "toggle" the animation between the start and end -->
       <OnClick
               motion:targetId="@id/red_star"
               motion:clickAction="toggle" />
   </Transition>

   <!-- Constraints to apply at the end of the animation -->
   <ConstraintSet android:id="@+id/start">
       <Constraint
               android:id="@+id/red_star"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               motion:layout_constraintStart_toStartOf="parent"
               motion:layout_constraintTop_toTopOf="parent" />
   </ConstraintSet>

   <!-- Constraints to apply at the end of the animation -->
   <ConstraintSet android:id="@+id/end">
       <Constraint
               android:id="@+id/red_star"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               motion:layout_constraintEnd_toEndOf="parent"
               motion:layout_constraintBottom_toBottomOf="parent" />
   </ConstraintSet>
</MotionScene>

4. Tạo hiệu ứng dựa trên các sự kiện kéo

Trong bước này, bạn sẽ tạo một ảnh động phản hồi sự kiện kéo của người dùng (khi người dùng vuốt màn hình) để chạy ảnh động. MotionLayout hỗ trợ theo dõi các sự kiện chạm để di chuyển khung hiển thị, cũng như các cử chỉ hất dựa trên vật lý để tạo chuyển động mượt mà.

Bước 1: Kiểm tra mã ban đầu

  1. Để bắt đầu, hãy mở tệp bố cục activity_step2.xmlMotionLayout hiện có. Hãy xem mã này.

activity_step2.xml

<!-- initial code -->

<androidx.constraintlayout.motion.widget.MotionLayout
       ...
       motion:layoutDescription="@xml/step2" >

   <ImageView
           android:id="@+id/left_star"
           ...
   />

   <ImageView
           android:id="@+id/right_star"
           ...
   />

   <ImageView
           android:id="@+id/red_star"
           ...
   />

   <TextView
           android:id="@+id/credits"
           ...
           motion:layout_constraintTop_toTopOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.motion.widget.MotionLayout>

Bố cục này xác định tất cả các khung hiển thị cho ảnh động. Ba biểu tượng ngôi sao không bị hạn chế trong bố cục vì chúng sẽ được tạo hiệu ứng trong cảnh chuyển động.

Phần thông tin ghi công TextView có các ràng buộc được áp dụng, vì phần này nằm ở cùng một vị trí trong toàn bộ ảnh động và không sửa đổi bất kỳ thuộc tính nào.

Bước 2: Tạo hiệu ứng cho cảnh

Giống như ảnh động cuối cùng, ảnh động này sẽ được xác định bằng ConstraintSet, bắt đầu và kết thúc cũng như Transition.

Xác định ConstraintSet bắt đầu

  1. Mở cảnh chuyển động xml/step2.xml để xác định ảnh động.
  2. Thêm các điều kiện ràng buộc cho điều kiện ràng buộc bắt đầu start. Lúc bắt đầu, cả 3 ngôi sao đều được căn giữa ở cuối màn hình. Các ngôi sao bên phải và bên trái có giá trị alpha0.0, tức là chúng hoàn toàn trong suốt và bị ẩn.

step2.xml

<!-- TODO apply starting constraints -->

<!-- Constraints to apply at the start of the animation -->
<ConstraintSet android:id="@+id/start">
   <Constraint
           android:id="@+id/red_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintBottom_toBottomOf="parent" />

   <Constraint
           android:id="@+id/left_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:alpha="0.0"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintBottom_toBottomOf="parent" />

   <Constraint
           android:id="@+id/right_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:alpha="0.0"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintBottom_toBottomOf="parent" />
</ConstraintSet>

Trong ConstraintSet này, bạn chỉ định một Constraint cho mỗi ngôi sao. Mỗi ràng buộc sẽ được MotionLayout áp dụng khi bắt đầu ảnh động.

Mỗi khung hiển thị ngôi sao đều được căn giữa ở cuối màn hình bằng cách sử dụng các ràng buộc bắt đầu, kết thúc và dưới cùng. Cả hai ngôi sao @id/left_star@id/right_star đều có thêm giá trị alpha khiến chúng không xuất hiện và giá trị này sẽ được áp dụng khi bắt đầu ảnh động.

Các bộ điều kiện ràng buộc startend xác định điểm bắt đầu và điểm kết thúc của ảnh động. Một điều kiện ràng buộc ở đầu, chẳng hạn như motion:layout_constraintStart_toStartOf, sẽ ràng buộc đầu của một khung hiển thị với đầu của một khung hiển thị khác. Điều này có thể gây nhầm lẫn lúc đầu, vì tên start được dùng cho cả . Cả hai đều được dùng trong bối cảnh của các ràng buộc. Để giúp phân biệt, start trong layout_constraintStart đề cập đến "start" (bắt đầu) của khung hiển thị, tức là bên trái trong ngôn ngữ từ trái sang phải và bên phải trong ngôn ngữ từ phải sang trái. Nhóm điều kiện ràng buộc start đề cập đến điểm bắt đầu của ảnh động.

Xác định ConstraintSet cuối

  1. Xác định ràng buộc cuối để dùng một chuỗi nhằm đặt cả 3 ngôi sao cùng nhau bên dưới @id/credits. Ngoài ra, thao tác này sẽ đặt giá trị cuối của alpha của các ngôi sao bên trái và bên phải thành 1.0.

step2.xml

<!-- TODO apply ending constraints -->

<!-- Constraints to apply at the end of the animation -->
<ConstraintSet android:id="@+id/end">

   <Constraint
           android:id="@+id/left_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:alpha="1.0"
           motion:layout_constraintHorizontal_chainStyle="packed"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintEnd_toStartOf="@id/red_star"
           motion:layout_constraintTop_toBottomOf="@id/credits" />

   <Constraint
           android:id="@+id/red_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           motion:layout_constraintStart_toEndOf="@id/left_star"
           motion:layout_constraintEnd_toStartOf="@id/right_star"
           motion:layout_constraintTop_toBottomOf="@id/credits" />

   <Constraint
           android:id="@+id/right_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:alpha="1.0"
           motion:layout_constraintStart_toEndOf="@id/red_star"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/credits" />
</ConstraintSet>

Kết quả cuối cùng là các thành phần hiển thị sẽ trải rộng ra và lên trên từ tâm khi chúng chuyển động.

Ngoài ra, vì thuộc tính alpha được đặt trên @id/right_start@id/left_star trong cả ConstraintSets, nên cả hai khung hiển thị sẽ mờ dần khi ảnh động diễn ra.

Tạo hiệu ứng dựa trên thao tác vuốt của người dùng

MotionLayout có thể theo dõi các sự kiện kéo của người dùng hoặc một thao tác vuốt để tạo hiệu ứng động "hất" dựa trên vật lý. Điều này có nghĩa là các khung hiển thị sẽ tiếp tục chuyển động nếu người dùng hất chúng và sẽ chậm dần như một đối tượng thực khi lăn trên bề mặt. Bạn có thể thêm loại ảnh động này bằng thẻ OnSwipe trong Transition.

  1. Thay thế TODO để thêm thẻ OnSwipe bằng <OnSwipe motion:touchAnchorId="@id/red_star" />.

step2.xml

<!-- TODO add OnSwipe tag -->

<!-- A transition describes an animation via start and end state -->
<Transition
       motion:constraintSetStart="@+id/start"
       motion:constraintSetEnd="@+id/end">
   <!-- MotionLayout will track swipes relative to this view -->
   <OnSwipe motion:touchAnchorId="@id/red_star" />
</Transition>

OnSwipe chứa một số thuộc tính, trong đó quan trọng nhất là touchAnchorId.

  • touchAnchorId là khung hiển thị được theo dõi và di chuyển khi có thao tác chạm. MotionLayout sẽ giữ chế độ xem này ở cùng khoảng cách với ngón tay đang vuốt.
  • touchAnchorSide xác định phía nào của khung hiển thị cần được theo dõi. Điều này rất quan trọng đối với những khung hiển thị có kích thước thay đổi, đi theo các đường dẫn phức tạp hoặc có một cạnh di chuyển nhanh hơn cạnh còn lại.
  • dragDirection xác định hướng quan trọng cho ảnh động này (lên, xuống, trái hoặc phải).

Khi MotionLayout theo dõi các sự kiện kéo, trình nghe sẽ được đăng ký trên khung hiển thị MotionLayout chứ không phải khung hiển thị do touchAnchorId chỉ định. Khi người dùng bắt đầu một cử chỉ ở bất kỳ vị trí nào trên màn hình, MotionLayout sẽ giữ khoảng cách giữa ngón tay của họ và touchAnchorSide của khung hiển thị touchAnchorId không đổi. Ví dụ: nếu họ chạm vào vị trí cách phía cố định 100 dp, thì MotionLayout sẽ giữ khoảng cách 100 dp giữa phía đó và ngón tay của họ trong toàn bộ ảnh động.

Dùng thử

  1. Chạy lại ứng dụng rồi mở màn hình Bước 2. Bạn sẽ thấy ảnh động.
  2. Hãy thử "hất" hoặc thả ngón tay khi hoạt ảnh đang chạy để khám phá cách MotionLayout hiển thị ảnh động dựa trên vật lý một cách mượt mà!

fefcdd690a0dcaec.gif

MotionLayout có thể tạo ảnh động giữa các thiết kế rất khác nhau bằng cách sử dụng các tính năng từ ConstraintLayout để tạo hiệu ứng phong phú.

Trong ảnh động này, cả 3 khung hiển thị đều được đặt tương đối so với khung hiển thị gốc ở cuối màn hình để bắt đầu. Cuối cùng, 3 khung hiển thị được đặt tương ứng với @id/credits trong một chuỗi.

Mặc dù có những bố cục rất khác nhau, nhưng MotionLayout sẽ tạo ra một ảnh động mượt mà giữa điểm bắt đầu và điểm kết thúc.

5. Sửa đổi đường dẫn

Ở bước này, bạn sẽ tạo một ảnh động đi theo một đường dẫn phức tạp trong ảnh động và tạo ảnh động cho phần ghi nhận trong chuyển động. MotionLayout có thể sửa đổi đường dẫn mà một khung hiển thị sẽ đi từ điểm bắt đầu đến điểm kết thúc bằng cách sử dụng KeyPosition.

Bước 1: Khám phá mã hiện có

  1. Mở layout/activity_step3.xmlxml/step3.xml để xem bố cục và cảnh chuyển động hiện có. ImageViewTextView hiển thị mặt trăng và văn bản ghi công.
  2. Mở tệp cảnh chuyển động (xml/step3.xml). Bạn sẽ thấy một Transition từ @id/start đến @id/end được xác định. Ảnh động này di chuyển hình ảnh mặt trăng từ phía dưới bên trái màn hình đến phía dưới cùng bên phải màn hình bằng cách dùng 2 ConstraintSets. Văn bản ghi công mờ dần từ alpha="0.0" sang alpha="1.0" khi mặt trăng di chuyển.
  3. Chạy ứng dụng ngay rồi chọn Bước 3. Bạn sẽ thấy mặt trăng đi theo một đường thẳng từ đầu đến cuối khi bạn nhấp vào mặt trăng.

Bước 2: Bật tính năng gỡ lỗi đường dẫn

Trước khi thêm một đường cong vào chuyển động của mặt trăng, bạn nên bật tính năng gỡ lỗi đường dẫn trong MotionLayout.

Để phát triển các ảnh động phức tạp bằng MotionLayout, bạn có thể vẽ đường dẫn ảnh động của mọi khung hiển thị. Điều này rất hữu ích khi bạn muốn hình dung ảnh động và tinh chỉnh các chi tiết nhỏ của chuyển động.

  1. Để bật đường dẫn gỡ lỗi, hãy mở layout/activity_step3.xml rồi thêm motion:motionDebug="SHOW_PATH" vào thẻ MotionLayout.

activity_step3.xml

<!-- Add motion:motionDebug="SHOW_PATH" -->

<androidx.constraintlayout.motion.widget.MotionLayout
       ...
       motion:motionDebug="SHOW_PATH" >

Sau khi bật tính năng gỡ lỗi đường dẫn, khi chạy lại ứng dụng, bạn sẽ thấy đường dẫn của tất cả các khung hiển thị được trực quan hoá bằng một đường nét đứt.

23bbb604f456f65c.png

  • Vòng tròn biểu thị vị trí bắt đầu hoặc kết thúc của một khung hiển thị.
  • Đường biểu thị đường đi của một chế độ xem.
  • Hình kim cương đại diện cho một KeyPosition sửa đổi đường dẫn.

Ví dụ: trong ảnh động này, vòng tròn ở giữa là vị trí của văn bản thông tin ghi công.

Bước 3: Sửa đổi đường dẫn

Tất cả ảnh động trong MotionLayout đều được xác định bằng một ConstraintSet bắt đầu và một ConstraintSet kết thúc. ConstraintSet này xác định giao diện của màn hình trước khi ảnh động bắt đầu và sau khi ảnh động kết thúc. Theo mặc định, MotionLayout vẽ một đường thẳng (đường thẳng) giữa vị trí bắt đầu và vị trí kết thúc của mỗi khung hiển thị thay đổi vị trí.

Để tạo các đường dẫn phức tạp như đường vòng cung của mặt trăng trong ví dụ này, MotionLayout sử dụng KeyPosition để sửa đổi đường dẫn mà một khung hiển thị thực hiện giữa điểm bắt đầu và điểm kết thúc.

  1. Mở xml/step3.xml rồi thêm KeyPosition vào cảnh. Thẻ KeyPosition được đặt bên trong thẻ Transition.

eae4dae9a12d0410.png

step3.xml

<!-- TODO: Add KeyFrameSet and KeyPosition -->
<KeyFrameSet>
   <KeyPosition
           motion:framePosition="50"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.5"
   />
</KeyFrameSet>

KeyFrameSet là phần tử con của Transition và là một tập hợp của tất cả KeyFrames, chẳng hạn như KeyPosition, cần được áp dụng trong quá trình chuyển đổi.

Khi MotionLayout tính toán đường đi cho mặt trăng giữa điểm bắt đầu và điểm kết thúc, nó sẽ sửa đổi đường đi dựa trên KeyPosition được chỉ định trong KeyFrameSet. Bạn có thể thấy cách thay đổi đường dẫn này bằng cách chạy lại ứng dụng.

KeyPosition có một số thuộc tính mô tả cách sửa đổi đường dẫn. Các mục quan trọng nhất là:

  • framePosition là một số từ 0 đến 100. Thuộc tính này xác định thời điểm áp dụng KeyPosition trong ảnh động, với 1 là 1% trong ảnh động và 99 là 99% trong ảnh động. Vì vậy, nếu giá trị là 50, bạn sẽ áp dụng giá trị đó ngay ở giữa.
  • motionTarget là khung hiển thị mà KeyPosition sửa đổi đường dẫn.
  • keyPositionType là cách KeyPosition sửa đổi đường dẫn này. Giá trị này có thể là parentRelative, pathRelative hoặc deltaRelative (như giải thích trong bước tiếp theo).
  • percentX | percentY là mức độ sửa đổi đường dẫn tại framePosition (giá trị từ 0 đến 1, cho phép giá trị âm và giá trị > 1).

Bạn có thể nghĩ theo cách này: "Tại framePosition, hãy sửa đổi đường dẫn của motionTarget bằng cách di chuyển đường dẫn đó theo percentX hoặc percentY theo toạ độ do keyPositionType xác định".

Theo mặc định, MotionLayout sẽ làm tròn mọi góc xuất hiện khi bạn sửa đổi đường dẫn. Nếu xem ảnh động mà bạn vừa tạo, bạn có thể thấy mặt trăng đi theo một đường cong tại chỗ uốn. Đối với hầu hết các ảnh động, đây là điều bạn muốn. Nếu không, bạn có thể chỉ định thuộc tính curveFit để tuỳ chỉnh.

Dùng thử

Nếu chạy lại ứng dụng, bạn sẽ thấy ảnh động cho bước này.

46b179c01801f19e.gif

Mặt trăng đi theo một đường vòng cung vì nó đi qua một KeyPosition được chỉ định trong Transition.

<KeyPosition
       motion:framePosition="50"
       motion:motionTarget="@id/moon"
       motion:keyPositionType="parentRelative"
       motion:percentY="0.5"
/>

Bạn có thể đọc KeyPosition này là: "Tại framePosition 50 (giữa chừng ảnh động), hãy sửa đổi đường dẫn của motionTarget @id/moon bằng cách di chuyển đường dẫn đó theo 50% Y (xuống nửa màn hình) theo toạ độ do parentRelative (toàn bộ MotionLayout) xác định."

Vì vậy, ở giữa ảnh động, mặt trăng phải đi qua một KeyPosition có vị trí thấp hơn 50% trên màn hình. KeyPosition này hoàn toàn không sửa đổi chuyển động X, vì vậy, mặt trăng vẫn sẽ di chuyển từ đầu đến cuối theo chiều ngang. MotionLayout sẽ tìm ra một đường dẫn mượt mà đi qua KeyPosition này trong khi di chuyển giữa điểm bắt đầu và điểm kết thúc.

Nếu quan sát kỹ, bạn sẽ thấy văn bản ghi công bị giới hạn bởi vị trí của mặt trăng. Tại sao nó không di chuyển theo chiều dọc?

1c7cf779931e45cc.gif

<Constraint
       android:id="@id/credits"
       ...
       motion:layout_constraintBottom_toBottomOf="@id/moon"
       motion:layout_constraintTop_toTopOf="@id/moon"
/>

Hoá ra, mặc dù bạn đang sửa đổi đường đi của mặt trăng, nhưng vị trí bắt đầu và kết thúc của mặt trăng hoàn toàn không di chuyển theo chiều dọc. KeyPosition không sửa đổi vị trí bắt đầu hoặc vị trí kết thúc, vì vậy, văn bản ghi công bị giới hạn ở vị trí kết thúc cuối cùng của mặt trăng.

Nếu muốn phần ghi công di chuyển cùng với mặt trăng, bạn có thể thêm KeyPosition vào phần ghi công hoặc sửa đổi các điều kiện ràng buộc bắt đầu trên @id/credits.

Trong phần tiếp theo, bạn sẽ tìm hiểu kỹ về các loại keyPositionType trong MotionLayout.

6. Tìm hiểu về keyPositionType

Trong bước cuối cùng, bạn đã sử dụng loại keyPosition của parentRelative để bù đường dẫn thêm 50% màn hình. Thuộc tính keyPositionType xác định cách MotionLayout sẽ sửa đổi đường dẫn theo percentX hoặc percentY.

<KeyFrameSet>
   <KeyPosition
           motion:framePosition="50"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.5"
   />
</KeyFrameSet>

Có 3 loại keyPosition có thể có: parentRelative, pathRelativedeltaRelative. Việc chỉ định một loại sẽ thay đổi hệ toạ độ mà percentXpercentY được tính toán.

Hệ toạ độ là gì?

Hệ toạ độ là một cách để chỉ định một điểm trong không gian. Các chỉ số này cũng hữu ích khi mô tả một vị trí trên màn hình.

Hệ toạ độ MotionLayout là một hệ toạ độ Descartes. Điều này có nghĩa là chúng có trục X và trục Y được xác định bằng hai đường vuông góc. Điểm khác biệt chính giữa hai trục này là vị trí của trục X trên màn hình (trục Y luôn vuông góc với trục X).

Tất cả hệ toạ độ trong MotionLayout đều sử dụng các giá trị từ 0.0 đến 1.0 trên cả trục X và Y. Chúng cho phép giá trị âm và giá trị lớn hơn 1.0. Ví dụ: giá trị percentX-2.0 có nghĩa là đi theo hướng ngược lại của trục X hai lần.

Nếu bạn cảm thấy những điều này hơi giống với lớp Đại số, hãy xem các hình ảnh bên dưới!

toạ độ parentRelative

a7b7568d46d9dec7.png

keyPositionType của parentRelative sử dụng cùng hệ toạ độ với màn hình. Nó xác định (0, 0) ở trên cùng bên trái của toàn bộ MotionLayout(1, 1) ở dưới cùng bên phải.

Bạn có thể sử dụng parentRelative bất cứ khi nào muốn tạo một ảnh động di chuyển qua toàn bộ MotionLayout – chẳng hạn như vòng cung mặt trăng trong ví dụ này.

Tuy nhiên, nếu bạn muốn sửa đổi một đường dẫn tương ứng với chuyển động, chẳng hạn như làm cho đường dẫn cong một chút, thì hai hệ toạ độ còn lại sẽ là lựa chọn phù hợp hơn.

toạ độ deltaRelative

5680bf553627416c.png

Delta là một thuật ngữ toán học chỉ sự thay đổi, vì vậy deltaRelative là một cách nói "thay đổi tương đối". Trong toạ độ deltaRelative, (0,0) là vị trí bắt đầu của khung hiển thị và (1,1) là vị trí kết thúc. Trục X và Y được căn chỉnh với màn hình.

Trục X luôn nằm ngang trên màn hình và trục Y luôn nằm dọc trên màn hình. So với parentRelative, điểm khác biệt chính là các toạ độ chỉ mô tả phần màn hình mà khung hiển thị sẽ di chuyển.

deltaRelative là một hệ toạ độ tuyệt vời để kiểm soát chuyển động theo chiều ngang hoặc chiều dọc một cách riêng biệt. Ví dụ: bạn có thể tạo một ảnh động chỉ hoàn thành chuyển động theo chiều dọc (Y) ở 50% và tiếp tục chuyển động theo chiều ngang (X).

pathRelative coordinates

f3aaadaac8b4a93f.png

Hệ toạ độ cuối cùng trong MotionLayoutpathRelative. Đường này khá khác biệt so với hai đường còn lại vì trục X đi theo đường chuyển động từ đầu đến cuối. Vậy (0,0) là vị trí bắt đầu và (1,0) là vị trí kết thúc.

Tại sao bạn muốn làm việc này? Thoạt nhìn, điều này khá bất ngờ, đặc biệt là vì hệ toạ độ này thậm chí không được căn chỉnh với hệ toạ độ màn hình.

Hoá ra pathRelative thực sự hữu ích cho một số việc.

  • Tăng tốc, giảm tốc hoặc dừng một khung hình trong một phần của ảnh động. Vì phương diện X sẽ luôn khớp chính xác với đường dẫn mà khung hiển thị thực hiện, nên bạn có thể dùng pathRelative KeyPosition để thay đổi framePosition mà một điểm cụ thể trong đường dẫn đó đạt được. Vì vậy, KeyPositionframePosition="50" với percentX="0.1" sẽ khiến ảnh động mất 50% thời gian để di chuyển 10% đầu tiên của chuyển động.
  • Thêm một đường cong nhỏ vào đường dẫn. Vì chiều Y luôn vuông góc với chuyển động, nên việc thay đổi Y sẽ thay đổi đường dẫn thành đường cong so với chuyển động tổng thể.
  • Thêm phương diện thứ hai khi deltaRelative không hoạt động. Đối với chuyển động hoàn toàn theo chiều ngang và chiều dọc, deltaRelative sẽ chỉ tạo ra một phương diện hữu ích. Tuy nhiên, pathRelative sẽ luôn tạo ra các toạ độ X và Y có thể sử dụng.

Trong bước tiếp theo, bạn sẽ tìm hiểu cách tạo các đường dẫn phức tạp hơn nữa bằng cách sử dụng nhiều KeyPosition.

7. Xây dựng các đường dẫn phức tạp

Nhìn vào ảnh động mà bạn đã tạo ở bước cuối cùng, ảnh động này tạo ra một đường cong mượt mà, nhưng hình dạng có thể giống "mặt trăng" hơn.

Sửa đổi đường dẫn có nhiều phần tử KeyPosition

MotionLayout có thể sửa đổi thêm một đường dẫn bằng cách xác định bao nhiêu KeyPosition tuỳ ý để có được chuyển động bất kỳ. Đối với ảnh động này, bạn sẽ tạo một đường vòng cung, nhưng bạn có thể làm cho mặt trăng nhảy lên xuống ở giữa màn hình nếu muốn.

  1. Mở xml/step4.xml. Bạn sẽ thấy hoạt động này có cùng các khung hiển thị và KeyFrame mà bạn đã thêm ở bước trước.
  2. Để làm tròn phần trên cùng của đường cong, hãy thêm 2 KeyPositions nữa vào đường dẫn của @id/moon, một ngay trước khi đường cong đạt đến đỉnh và một sau khi đạt đến đỉnh.

500b5ac2db48ef87.png

step4.xml

<!-- TODO: Add two more KeyPositions to the KeyFrameSet here -->
<KeyPosition
       motion:framePosition="25"
       motion:motionTarget="@id/moon"
       motion:keyPositionType="parentRelative"
       motion:percentY="0.6"
/>
<KeyPosition
       motion:framePosition="75"
       motion:motionTarget="@id/moon"
       motion:keyPositionType="parentRelative"
       motion:percentY="0.6"
/>

Các KeyPositions này sẽ được áp dụng 25% và 75% trong suốt quá trình diễn ra ảnh động, đồng thời khiến @id/moon di chuyển theo một đường dẫn cách 60% từ đầu màn hình. Kết hợp với KeyPosition hiện có ở mức 50%, điều này tạo ra một đường cong mượt mà để mặt trăng đi theo.

Trong MotionLayout, bạn có thể thêm bao nhiêu KeyPositions tuỳ thích để có được đường chuyển động mà bạn muốn. MotionLayout sẽ áp dụng từng KeyPosition tại framePosition đã chỉ định và tìm ra cách tạo chuyển động mượt mà xuyên suốt tất cả KeyPositions.

Dùng thử

  1. Chạy lại ứng dụng. Chuyển đến Bước 4 để xem ảnh động hoạt động. Khi bạn nhấp vào mặt trăng, mặt trăng sẽ đi theo đường dẫn từ điểm bắt đầu đến điểm kết thúc, đi qua từng KeyPosition được chỉ định trong KeyFrameSet.

Tự khám phá

Trước khi chuyển sang các loại KeyFrame khác, hãy thử thêm một số KeyPositions khác vào KeyFrameSet để xem bạn có thể tạo ra những loại hiệu ứng nào chỉ bằng cách sử dụng KeyPosition.

Sau đây là một ví dụ minh hoạ cách tạo một đường dẫn phức tạp di chuyển qua lại trong quá trình tạo ảnh động.

cd9faaffde3dfef.png

step4.xml

<!-- Complex paths example: Dancing moon -->
<KeyFrameSet>
   <KeyPosition
           motion:framePosition="25"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.6"
           motion:percentX="0.1"
   />
   <KeyPosition
           motion:framePosition="50"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.5"
           motion:percentX="0.3"
   />
   <KeyPosition
           motion:framePosition="75"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.6"
           motion:percentX="0.1"
   />
</KeyFrameSet>

Sau khi khám phá xong KeyPosition, trong bước tiếp theo, bạn sẽ chuyển sang các loại KeyFrames khác.

8. Thay đổi các thuộc tính trong khi chuyển động

Việc tạo ảnh động động thường có nghĩa là thay đổi size, rotation hoặc alpha của các khung hiển thị khi ảnh động diễn ra. MotionLayout hỗ trợ tạo ảnh động cho nhiều thuộc tính trên mọi thành phần hiển thị bằng cách dùng KeyAttribute.

Ở bước này, bạn sẽ dùng KeyAttribute để điều chỉnh tỷ lệ và xoay mặt trăng. Bạn cũng sẽ dùng KeyAttribute để trì hoãn thời điểm xuất hiện của văn bản cho đến khi mặt trăng gần hoàn thành hành trình của mình.

Bước 1: Thay đổi kích thước và xoay bằng KeyAttribute

  1. Mở xml/step5.xml chứa ảnh động mà bạn đã tạo ở bước cuối cùng. Để tạo sự đa dạng, màn hình này sử dụng một bức ảnh không gian khác làm nền.
  2. Để làm cho mặt trăng mở rộng kích thước và xoay, hãy thêm hai thẻ KeyAttribute vào KeyFrameSet tại keyFrame="50"keyFrame="100"

bbae524a2898569.png

step5.xml

<!-- TODO: Add KeyAttributes to rotate and resize @id/moon -->

<KeyAttribute
       motion:framePosition="50"
       motion:motionTarget="@id/moon"
       android:scaleY="2.0"
       android:scaleX="2.0"
       android:rotation="-360"
/>
<KeyAttribute
       motion:framePosition="100"
       motion:motionTarget="@id/moon"
       android:rotation="-720"
/>

Các KeyAttributes này được áp dụng ở 50% và 100% của ảnh động. KeyAttribute đầu tiên ở 50% sẽ xảy ra ở đầu vòng cung, khiến khung hiển thị tăng gấp đôi kích thước cũng như xoay -360 độ (hoặc một vòng tròn đầy đủ). KeyAttribute thứ hai sẽ hoàn thành vòng xoay thứ hai thành -720 độ (2 vòng tròn đầy đủ) và giảm kích thước về mức bình thường vì các giá trị scaleXscaleY mặc định là 1.0.

Giống như KeyPosition, KeyAttribute sử dụng framePositionmotionTarget để chỉ định thời điểm áp dụng KeyFrame và chế độ xem cần sửa đổi. MotionLayout sẽ nội suy giữa KeyPositions để tạo ảnh động mượt mà.

KeyAttributes hỗ trợ các thuộc tính có thể áp dụng cho tất cả các khung hiển thị. Các thuộc tính này hỗ trợ thay đổi các thuộc tính cơ bản như visibility, alpha hoặc elevation. Bạn cũng có thể thay đổi hướng xoay như bạn đang làm ở đây, xoay theo 3 chiều bằng rotateXrotateY, điều chỉnh kích thước bằng scaleXscaleY hoặc dịch vị trí của khung hiển thị theo trục X, Y hoặc Z.

Bước 2: Trì hoãn thời điểm xuất hiện thông tin ghi công

Một trong những mục tiêu của bước này là cập nhật ảnh động để văn bản ghi công không xuất hiện cho đến khi ảnh động gần hoàn tất.

  1. Để trì hoãn thời điểm xuất hiện dòng tín dụng, hãy xác định thêm một KeyAttribute để đảm bảo rằng alpha sẽ vẫn là 0 cho đến keyPosition="85". MotionLayout vẫn sẽ chuyển đổi mượt mà từ 0 sang 100 alpha, nhưng sẽ thực hiện việc này trong 15% cuối cùng của ảnh động.

step5.xml

<!-- TODO: Add KeyAttribute to delay the appearance of @id/credits -->

<KeyAttribute
       motion:framePosition="85"
       motion:motionTarget="@id/credits"
       android:alpha="0.0"
/>

KeyAttribute này giữ alpha của @id/credits ở mức 0,0 trong 85% đầu tiên của ảnh động. Vì bắt đầu ở giá trị alpha là 0, nên điều này có nghĩa là ảnh động sẽ không hiển thị trong 85% thời gian đầu.

Hiệu ứng cuối cùng của KeyAttribute này là các khoản tín dụng xuất hiện vào cuối ảnh động. Điều này tạo cảm giác như thể chúng được phối hợp với mặt trăng đang lặn ở góc bên phải màn hình.

Bằng cách trì hoãn ảnh động trên một khung hiển thị trong khi một khung hiển thị khác di chuyển như thế này, bạn có thể tạo ra những ảnh động ấn tượng và mang lại cảm giác sống động cho người dùng.

Dùng thử

  1. Chạy lại ứng dụng và chuyển đến Bước 5 để xem ảnh động hoạt động. Khi bạn nhấp vào mặt trăng, mặt trăng sẽ đi theo đường dẫn từ đầu đến cuối, đi qua từng KeyAttribute được chỉ định trong KeyFrameSet.

2f4bfdd681c1fa98.gif

Vì bạn xoay mặt trăng hai vòng tròn đầy đủ, nên giờ đây, mặt trăng sẽ lộn hai vòng ra sau và phần ghi công sẽ xuất hiện muộn hơn cho đến khi ảnh động gần hoàn tất.

Tự khám phá

Trước khi chuyển sang loại KeyFrame cuối cùng, hãy thử sửa đổi các thuộc tính chuẩn khác trong KeyAttributes. Ví dụ: hãy thử thay đổi rotation thành rotationX để xem ảnh động mà nó tạo ra.

Dưới đây là danh sách các thuộc tính chuẩn mà bạn có thể thử:

  • android:visibility
  • android:alpha
  • android:elevation
  • android:rotation
  • android:rotationX
  • android:rotationY
  • android:scaleX
  • android:scaleY
  • android:translationX
  • android:translationY
  • android:translationZ

9. Thay đổi thuộc tính tuỳ chỉnh

Ảnh động đa dạng liên quan đến việc thay đổi màu sắc hoặc các thuộc tính khác của một khung hiển thị. Mặc dù MotionLayout có thể dùng KeyAttribute để thay đổi bất kỳ thuộc tính chuẩn nào được liệt kê trong tác vụ trước, nhưng bạn dùng CustomAttribute để chỉ định bất kỳ thuộc tính nào khác.

Bạn có thể dùng CustomAttribute để đặt bất kỳ giá trị nào có phương thức setter. Ví dụ: bạn có thể đặt backgroundColor trên một Thành phần hiển thị bằng cách sử dụng CustomAttribute. MotionLayout sẽ sử dụng reflection để tìm phương thức setter, sau đó gọi phương thức này nhiều lần để tạo ảnh động cho khung hiển thị.

Ở bước này, bạn sẽ dùng CustomAttribute để đặt thuộc tính colorFilter trên mặt trăng nhằm tạo hiệu ứng động như minh hoạ bên dưới.

5fb6792126a09fda.gif

Xác định thuộc tính tuỳ chỉnh

  1. Để bắt đầu, hãy mở xml/step6.xml chứa ảnh động mà bạn đã tạo ở bước trước.
  2. Để làm cho mặt trăng thay đổi màu sắc, hãy thêm hai KeyAttributeCustomAttribute trong KeyFrameSet tại keyFrame="0", keyFrame="50"keyFrame="100".

214699d5fdd956da.png

step6.xml

<!-- TODO: Add Custom attributes here -->
<KeyAttribute
       motion:framePosition="0"
       motion:motionTarget="@id/moon">
   <CustomAttribute
           motion:attributeName="colorFilter"
           motion:customColorValue="#FFFFFF"
   />
</KeyAttribute>
<KeyAttribute
       motion:framePosition="50"
       motion:motionTarget="@id/moon">
   <CustomAttribute
           motion:attributeName="colorFilter"
           motion:customColorValue="#FFB612"
   />
</KeyAttribute>
<KeyAttribute
       motion:framePosition="100"
       motion:motionTarget="@id/moon">
   <CustomAttribute
           motion:attributeName="colorFilter"
           motion:customColorValue="#FFFFFF"
   />
</KeyAttribute>

Bạn thêm CustomAttribute vào bên trong KeyAttribute. CustomAttribute sẽ được áp dụng tại framePosition do KeyAttribute chỉ định.

Bên trong CustomAttribute, bạn phải chỉ định một attributeName và một giá trị để đặt.

  • motion:attributeName là tên của phương thức setter sẽ được gọi theo thuộc tính tùy chỉnh này. Trong ví dụ này, setColorFilter trên Drawable sẽ được gọi.
  • motion:custom*Value là một giá trị tuỳ chỉnh thuộc loại được ghi chú trong tên, trong ví dụ này, giá trị tuỳ chỉnh là một màu được chỉ định.

Giá trị tuỳ chỉnh có thể có bất kỳ loại nào sau đây:

  • Màu
  • Số nguyên
  • Số thực dấu phẩy động
  • Chuỗi
  • Phương diện
  • Boolean

Khi sử dụng API này, MotionLayout có thể tạo ảnh động cho mọi thứ cung cấp một phương thức thiết lập trên mọi khung hiển thị.

Dùng thử

  1. Chạy lại ứng dụng và chuyển đến Bước 6 để xem ảnh động hoạt động. Khi bạn nhấp vào mặt trăng, mặt trăng sẽ đi theo đường dẫn từ đầu đến cuối, đi qua từng KeyAttribute được chỉ định trong KeyFrameSet.

5fb6792126a09fda.gif

Khi bạn thêm KeyFrames, MotionLayout sẽ thay đổi đường đi của mặt trăng từ đường thẳng thành đường cong phức tạp, thêm một cú lộn ngược hai vòng, thay đổi kích thước và thay đổi màu ở giữa ảnh động.

Trong ảnh động thực tế, bạn thường sẽ tạo ảnh động cho nhiều khung hiển thị cùng lúc, kiểm soát chuyển động của các khung hiển thị đó theo nhiều đường dẫn và tốc độ khác nhau. Bằng cách chỉ định một KeyFrame khác cho mỗi khung hiển thị, bạn có thể dàn dựng các ảnh động phong phú để tạo chuyển động cho nhiều khung hiển thị bằng MotionLayout.

10. Sự kiện kéo và đường dẫn phức tạp

Trong bước này, bạn sẽ khám phá cách sử dụng OnSwipe với các đường dẫn phức tạp. Cho đến nay, ảnh động của mặt trăng đã được kích hoạt bởi một trình nghe OnClick và chạy trong một khoảng thời gian cố định.

Để kiểm soát các ảnh động có đường dẫn phức tạp bằng OnSwipe, chẳng hạn như ảnh động mặt trăng mà bạn đã tạo trong vài bước gần đây, bạn cần hiểu cách hoạt động của OnSwipe.

Bước 1: Khám phá hành vi OnSwipe

  1. Mở xml/step7.xml rồi tìm khai báo OnSwipe hiện có.

step7.xml

<!-- Fix OnSwipe by changing touchAnchorSide →

<OnSwipe
       motion:touchAnchorId="@id/moon"
       motion:touchAnchorSide="bottom"
/>
  1. Chạy ứng dụng trên thiết bị của bạn rồi chuyển đến Bước 7. Hãy thử xem bạn có thể tạo ra một ảnh động mượt mà bằng cách kéo mặt trăng dọc theo đường vòng cung hay không.

Khi bạn chạy ảnh động này, ảnh động sẽ không đẹp lắm. Sau khi lên đến đỉnh của vòng cung, mặt trăng sẽ bắt đầu nhảy xung quanh.

ed96e3674854a548.gif

Để hiểu rõ lỗi này, hãy xem xét điều gì sẽ xảy ra khi người dùng chạm ngay bên dưới đỉnh của vòng cung. Vì thẻ OnSwipemotion:touchAnchorSide="bottom" MotionLayout sẽ cố gắng giữ khoảng cách giữa ngón tay và cuối khung hiển thị không đổi trong suốt ảnh động.

Tuy nhiên, vì phần dưới của mặt trăng không phải lúc nào cũng đi theo cùng một hướng, mà sẽ đi lên rồi xuống, nên MotionLayout không biết phải làm gì khi người dùng vừa vượt qua đỉnh của vòng cung. Để xem xét điều này, vì bạn đang theo dõi phần dưới cùng của mặt trăng, nên phần này sẽ được đặt ở đâu khi người dùng chạm vào đây?

56cd575c5c77eddd.png

Bước 2: Sử dụng mặt bên phải

Để tránh những lỗi như thế này, điều quan trọng là bạn phải luôn chọn touchAnchorIdtouchAnchorSide luôn tiến theo một hướng trong suốt thời gian của toàn bộ ảnh động.

Trong ảnh động này, cả phía right và phía left của mặt trăng sẽ di chuyển theo một hướng trên màn hình.

Tuy nhiên, cả bottomtop sẽ đảo ngược hướng. Khi OnSwipe cố gắng theo dõi những đối tượng này, nó sẽ bị nhầm lẫn khi hướng của đối tượng thay đổi.

  1. Để ảnh động này tuân theo các sự kiện chạm, hãy thay đổi touchAnchorSide thành right.

step7.xml

<!-- Fix OnSwipe by changing touchAnchorSide →

<OnSwipe
       motion:touchAnchorId="@id/moon"
       motion:touchAnchorSide="right"
/>

Bước 3: Sử dụng dragDirection

Bạn cũng có thể kết hợp dragDirection với touchAnchorSide để làm cho đường nhánh đi theo hướng khác với hướng thông thường. Điều quan trọng là touchAnchorSide chỉ tiến theo một hướng, nhưng bạn có thể cho MotionLayout biết hướng cần theo dõi. Ví dụ: bạn có thể giữ lại touchAnchorSide="bottom" nhưng thêm dragDirection="dragRight". Thao tác này sẽ khiến MotionLayout theo dõi vị trí của cuối thành phần hiển thị, nhưng chỉ xem xét vị trí của thành phần đó khi di chuyển sang phải (bỏ qua chuyển động dọc). Vì vậy, mặc dù phần dưới cùng di chuyển lên xuống, nhưng phần này vẫn sẽ chuyển động chính xác với OnSwipe.

  1. Cập nhật OnSwipe để theo dõi chính xác chuyển động của mặt trăng.

step7.xml

<!-- Using dragDirection to control the direction of drag tracking →

<OnSwipe
       motion:touchAnchorId="@id/moon"
       motion:touchAnchorSide="bottom"
       motion:dragDirection="dragRight"
/>

Dùng thử

  1. Chạy lại ứng dụng và thử kéo mặt trăng đi hết đường. Mặc dù đi theo một đường cong phức tạp, nhưng MotionLayout sẽ có thể tiến hành ảnh động để phản hồi các sự kiện vuốt.

5458dff382261427.gif

11. Chạy chuyển động bằng mã

Bạn có thể dùng MotionLayout để tạo ảnh động phong phú khi sử dụng cùng với CoordinatorLayout. Ở bước này, bạn sẽ tạo một tiêu đề có thể thu gọn bằng cách sử dụng MotionLayout.

Bước 1: Khám phá mã hiện có

  1. Để bắt đầu, hãy mở layout/activity_step8.xml.
  2. Trong layout/activity_step8.xml, bạn sẽ thấy rằng CoordinatorLayoutAppBarLayout đang hoạt động đã được tạo.

activity_step8.xml

<androidx.coordinatorlayout.widget.CoordinatorLayout
       ...>
   <com.google.android.material.appbar.AppBarLayout
           android:id="@+id/appbar_layout"
           android:layout_width="match_parent"
           android:layout_height="180dp">
       <androidx.constraintlayout.motion.widget.MotionLayout
               android:id="@+id/motion_layout"
               ... >
           ...
       </androidx.constraintlayout.motion.widget.MotionLayout>
   </com.google.android.material.appbar.AppBarLayout>
  
   <androidx.core.widget.NestedScrollView
           ...
           motion:layout_behavior="@string/appbar_scrolling_view_behavior" >
           ...
   </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Bố cục này sử dụng CoordinatorLayout để chia sẻ thông tin cuộn giữa NestedScrollViewAppBarLayout. Vì vậy, khi NestedScrollView cuộn lên, nó sẽ thông báo cho AppBarLayout về thay đổi này. Đó là cách bạn triển khai một thanh công cụ thu gọn như thế này trên Android – thao tác cuộn văn bản sẽ được "phối hợp" với tiêu đề thu gọn.

Cảnh chuyển động mà @id/motion_layout trỏ đến tương tự như cảnh chuyển động trong bước cuối cùng. Tuy nhiên, nội dung khai báo OnSwipe đã bị xoá để cho phép nội dung này hoạt động với CoordinatorLayout.

  1. Chạy ứng dụng rồi chuyển đến Bước 8. Bạn sẽ thấy rằng khi bạn di chuyển văn bản, mặt trăng sẽ không di chuyển.

Bước 2: Tạo MotionLayout có thể cuộn

  1. Để chế độ xem MotionLayout cuộn ngay khi NestedScrollView cuộn, hãy thêm motion:minHeightmotion:layout_scrollFlags vào MotionLayout.

activity_step8.xml

<!-- Add minHeight and layout_scrollFlags to the MotionLayout -->

<androidx.constraintlayout.motion.widget.MotionLayout
       android:id="@+id/motion_layout"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       motion:layoutDescription="@xml/step8"
       motion:motionDebug="SHOW_PATH"
       android:minHeight="80dp"
       motion:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed"  >
  1. Chạy lại ứng dụng rồi chuyển đến Bước 8. Bạn sẽ thấy MotionLayout thu gọn khi bạn cuộn lên. Tuy nhiên, ảnh động chưa tiến triển dựa trên hành vi cuộn.

Bước 3: Di chuyển chuyển động bằng mã

  1. Mở Step8Activity.kt . Chỉnh sửa hàm coordinateMotion() để cho MotionLayout biết về những thay đổi về vị trí cuộn.

Step8Activity.kt

// TODO: set progress of MotionLayout based on an AppBarLayout.OnOffsetChangedListener

private fun coordinateMotion() {
    val appBarLayout: AppBarLayout = findViewById(R.id.appbar_layout)
    val motionLayout: MotionLayout = findViewById(R.id.motion_layout)

    val listener = AppBarLayout.OnOffsetChangedListener { unused, verticalOffset ->
        val seekPosition = -verticalOffset / appBarLayout.totalScrollRange.toFloat()
        motionLayout.progress = seekPosition
    }

    appBarLayout.addOnOffsetChangedListener(listener)
}

Mã này sẽ đăng ký một OnOffsetChangedListener sẽ được gọi mỗi khi người dùng cuộn với độ lệch cuộn hiện tại.

MotionLayout hỗ trợ tìm kiếm hiệu ứng chuyển đổi bằng cách đặt thuộc tính tiến trình. Để chuyển đổi giữa verticalOffset và tiến trình theo tỷ lệ phần trăm, hãy chia cho tổng phạm vi cuộn.

Dùng thử

  1. Triển khai lại ứng dụng và chạy ảnh động Bước 8. Bạn sẽ thấy MotionLayout sẽ tiến hành ảnh động dựa trên vị trí cuộn.

ee5ce4d9e33a59ca.gif

Bạn có thể tạo ảnh động tuỳ chỉnh cho thanh công cụ thu gọn động bằng cách sử dụng MotionLayout. Bằng cách sử dụng một chuỗi KeyFrames, bạn có thể đạt được hiệu ứng rất nổi bật.

12. Xin chúc mừng

Lớp học lập trình này đề cập đến API cơ bản của MotionLayout.

Để xem thêm ví dụ về cách sử dụng MotionLayout, hãy xem mẫu chính thức. Và đừng quên xem tài liệu!

Tìm hiểu thêm

MotionLayout hỗ trợ nhiều tính năng hơn nữa mà lớp học lập trình này không đề cập đến, chẳng hạn như KeyCycle, cho phép bạn kiểm soát các đường dẫn hoặc thuộc tính bằng các chu kỳ lặp lại và KeyTimeCycle, cho phép bạn tạo hiệu ứng dựa trên thời gian thực. Hãy xem các mẫu để biết ví dụ về từng loại.

Để biết đường liên kết đến các lớp học lập trình khác trong khoá học này, hãy xem trang đích của các lớp học lập trình trong khoá học Kiến thức nâng cao về cách tạo ứng dụng Android bằng Kotlin.