Tạo ảnh động cho chuyển động bằng vật lý lò xo

Thử cách Compose
Jetpack Compose là bộ công cụ giao diện người dùng được đề xuất cho Android. Tìm hiểu cách sử dụng Ảnh động trong Compose.

Chuyển động dựa trên vật lý là do lực tác động. Lực lò xo là một lực dẫn hướng tương tác và chuyển động. Lực của lò xo có các tính chất sau: giảm chấn và độ cứng. Trong ảnh động dựa trên lò xo, giá trị và tốc độ được tính toán dựa trên lực của lò xo áp dụng trên mỗi khung hình.

Nếu bạn muốn ảnh động của ứng dụng chậm lại theo một hướng duy nhất, hãy cân nhắc sử dụng ảnh động vuốt dựa trên lực ma sát.

Vòng đời của một ảnh động lò xo

Trong ảnh động dựa trên lò xo, lớp SpringForce cho phép bạn tuỳ chỉnh độ cứng của lò xo, tỷ lệ giảm chấn và vị trí cuối cùng của lò xo. Ngay khi ảnh động bắt đầu, lực lò xo sẽ cập nhật giá trị ảnh động và tốc độ trên mỗi khung hình. Ảnh động sẽ tiếp tục cho đến khi lực lò xo đạt đến trạng thái cân bằng.

Ví dụ: nếu bạn kéo một biểu tượng ứng dụng xung quanh màn hình rồi thả biểu tượng đó ra bằng cách nhấc ngón tay khỏi biểu tượng đó, biểu tượng đó sẽ đẩy về vị trí ban đầu bằng một lực vô hình nhưng quen thuộc.

Hình 1 minh hoạ hiệu ứng lò xo tương tự. Dấu cộng (+) ở giữa vòng tròn cho biết lực được áp dụng thông qua một cử chỉ chạm.

Bản phát hành vào mùa xuân
Hình 1. Hiệu ứng thả lò xo

Tạo ảnh động có hiệu ứng lò xo

Sau đây là các bước chung để tạo ảnh động có hiệu ứng lò xo cho ứng dụng:

Các phần sau đây thảo luận chi tiết các bước chung để tạo ảnh động có hiệu ứng lò xo.

Thêm thư viện hỗ trợ

Để sử dụng thư viện hỗ trợ dựa trên vật lý, bạn phải thêm thư viện hỗ trợ vào dự án như sau:

  1. Mở tệp build.gradle cho mô-đun ứng dụng.
  2. Thêm thư viện hỗ trợ vào phần dependencies.

    Groovy

            dependencies {
                def dynamicanimation_version = '1.0.0'
                implementation "androidx.dynamicanimation:dynamicanimation:$dynamicanimation_version"
            }
            

    Kotlin

            dependencies {
                val dynamicanimation_version = "1.0.0"
                implementation("androidx.dynamicanimation:dynamicanimation:$dynamicanimation_version")
            }
            

    Để xem các phiên bản hiện tại của thư viện này, hãy xem thông tin về Ảnh động động trên trang phiên bản.

Tạo ảnh động có hiệu ứng lò xo

Lớp SpringAnimation cho phép bạn tạo ảnh động lò xo cho một đối tượng. Để tạo ảnh động có hiệu ứng lò xo, bạn cần tạo một thực thể của lớp SpringAnimation và cung cấp một đối tượng cũng như thuộc tính của đối tượng mà bạn muốn tạo ảnh động và vị trí lò xo cuối cùng (không bắt buộc) nơi bạn muốn ảnh động nằm ở đó.

Lưu ý: Tại thời điểm tạo ảnh động có hiệu ứng lò xo, bạn không bắt buộc phải đặt vị trí cuối cùng của lò xo. Tuy nhiên, bạn phải xác định đối số này trước khi bắt đầu ảnh động.

Kotlin

val springAnim = findViewById<View>(R.id.imageView).let { img ->
    // Setting up a spring animation to animate the view’s translationY property with the final
    // spring position at 0.
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y, 0f)
}

Java

final View img = findViewById(R.id.imageView);
// Setting up a spring animation to animate the view’s translationY property with the final
// spring position at 0.
final SpringAnimation springAnim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y, 0);

Ảnh động dựa trên lực lò xo có thể tạo ảnh động cho các khung hiển thị trên màn hình bằng cách thay đổi thuộc tính thực tế trong các đối tượng khung hiển thị. Các chế độ xem sau đây hiện có trong hệ thống:

  • ALPHA: Biểu thị độ trong suốt alpha trên khung hiển thị. Theo mặc định, giá trị là 1 (không rõ ràng), với giá trị 0 thể hiện độ trong suốt hoàn toàn (không hiển thị).
  • TRANSLATION_X, TRANSLATION_YTRANSLATION_Z: Các thuộc tính này kiểm soát vị trí của khung hiển thị dưới dạng delta từ toạ độ bên trái, toạ độ trên cùng và độ cao do vùng chứa bố cục thiết lập.
    • TRANSLATION_X mô tả toạ độ bên trái.
    • TRANSLATION_Y mô tả toạ độ trên cùng.
    • TRANSLATION_Z mô tả độ sâu của thành phần hiển thị so với độ cao của thành phần hiển thị đó.
  • ROTATION, ROTATION_XROTATION_Y: Các thuộc tính này kiểm soát chế độ xoay ở chế độ 2D (thuộc tính rotation) và 3D xung quanh điểm tổng hợp.
  • SCROLL_XSCROLL_Y: Các thuộc tính này cho biết độ lệch cuộn của nguồn bên trái và cạnh trên cùng tính bằng pixel. Chỉ số này cũng cho biết vị trí dựa trên mức độ người dùng cuộn trang.
  • SCALE_XSCALE_Y: Các thuộc tính này kiểm soát việc điều chỉnh tỷ lệ 2D của một khung hiển thị xung quanh điểm tổng hợp.
  • X, YZ: Đây là các thuộc tính tiện ích cơ bản để mô tả vị trí cuối cùng của thành phần hiển thị trong vùng chứa.

Đăng ký trình nghe

Lớp DynamicAnimation cung cấp 2 trình nghe: OnAnimationUpdateListenerOnAnimationEndListener. Các trình nghe này theo dõi nội dung cập nhật trong ảnh động, chẳng hạn như khi có thay đổi về giá trị ảnh động và khi ảnh động kết thúc.

OnAnimationUpdateListener

Khi muốn tạo ảnh động cho nhiều khung hiển thị để tạo ảnh động theo chuỗi, bạn có thể thiết lập OnAnimationUpdateListener để nhận lệnh gọi lại mỗi khi có thay đổi về thuộc tính của khung hiển thị hiện tại. Lệnh gọi lại thông báo cho thành phần hiển thị khác cập nhật vị trí lò xo của nó dựa trên thay đổi phát sinh trong thuộc tính của thành phần hiển thị hiện tại. Để đăng ký trình nghe, hãy thực hiện các bước sau:

  1. Gọi phương thức addUpdateListener() và đính kèm trình nghe vào ảnh động.

    Lưu ý: Bạn cần đăng ký trình nghe cập nhật trước khi ảnh động bắt đầu. Tuy nhiên, trình nghe cập nhật chỉ nên được đăng ký nếu bạn cần cập nhật trên mỗi khung hình khi giá trị ảnh động thay đổi. Trình nghe cập nhật sẽ ngăn ảnh động có thể chạy trên một luồng riêng.

  2. Ghi đè phương thức onAnimationUpdate() để thông báo cho phương thức gọi về thay đổi trong đối tượng hiện tại. Mã mẫu sau đây minh hoạ cách sử dụng tổng thể của OnAnimationUpdateListener.

Kotlin

// Setting up a spring animation to animate the view1 and view2 translationX and translationY properties
val (anim1X, anim1Y) = findViewById<View>(R.id.view1).let { view1 ->
    SpringAnimation(view1, DynamicAnimation.TRANSLATION_X) to
            SpringAnimation(view1, DynamicAnimation.TRANSLATION_Y)
}
val (anim2X, anim2Y) = findViewById<View>(R.id.view2).let { view2 ->
    SpringAnimation(view2, DynamicAnimation.TRANSLATION_X) to
            SpringAnimation(view2, DynamicAnimation.TRANSLATION_Y)
}

// Registering the update listener
anim1X.addUpdateListener { _, value, _ ->
    // Overriding the method to notify view2 about the change in the view1’s property.
    anim2X.animateToFinalPosition(value)
}

anim1Y.addUpdateListener { _, value, _ -> anim2Y.animateToFinalPosition(value) }

Java

// Creating two views to demonstrate the registration of the update listener.
final View view1 = findViewById(R.id.view1);
final View view2 = findViewById(R.id.view2);

// Setting up a spring animation to animate the view1 and view2 translationX and translationY properties
final SpringAnimation anim1X = new SpringAnimation(view1,
        DynamicAnimation.TRANSLATION_X);
final SpringAnimation anim1Y = new SpringAnimation(view1,
    DynamicAnimation.TRANSLATION_Y);
final SpringAnimation anim2X = new SpringAnimation(view2,
        DynamicAnimation.TRANSLATION_X);
final SpringAnimation anim2Y = new SpringAnimation(view2,
        DynamicAnimation.TRANSLATION_Y);

// Registering the update listener
anim1X.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {

// Overriding the method to notify view2 about the change in the view1’s property.
    @Override
    public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float value,
                                  float velocity) {
        anim2X.animateToFinalPosition(value);
    }
});

anim1Y.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {

  @Override
    public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float value,
                                  float velocity) {
        anim2Y.animateToFinalPosition(value);
    }
});

OnAnimationEndListener

OnAnimationEndListener thông báo cho kết thúc ảnh động. Bạn có thể thiết lập trình nghe để nhận lệnh gọi lại bất cứ khi nào ảnh động đạt đến trạng thái cân bằng hoặc bị huỷ. Để đăng ký trình nghe, hãy thực hiện các bước sau:

  1. Gọi phương thức addEndListener() và đính kèm trình nghe vào ảnh động.
  2. Ghi đè phương thức onAnimationEnd() để nhận thông báo bất cứ khi nào một ảnh động đạt đến trạng thái cân bằng hoặc bị huỷ.

Loại bỏ người nghe

Để ngừng nhận các lệnh gọi lại cập nhật ảnh động và lệnh gọi lại kết thúc ảnh động, hãy gọi phương thức removeUpdateListener()removeEndListener() tương ứng.

Đặt giá trị bắt đầu ảnh động

Để đặt giá trị bắt đầu của ảnh động, hãy gọi phương thức setStartValue() và truyền giá trị bắt đầu của ảnh động. Nếu bạn không đặt giá trị bắt đầu, ảnh động sẽ sử dụng giá trị hiện tại của thuộc tính của đối tượng làm giá trị bắt đầu.

Đặt phạm vi giá trị ảnh động

Bạn có thể đặt giá trị ảnh động tối thiểu và tối đa khi muốn hạn chế giá trị thuộc tính trong một phạm vi nhất định. Điều này cũng giúp kiểm soát phạm vi trong trường hợp bạn tạo ảnh động cho các thuộc tính có phạm vi nội tại, chẳng hạn như alpha (từ 0 đến 1).

  • Để đặt giá trị tối thiểu, hãy gọi phương thức setMinValue() và chuyển giá trị tối thiểu của thuộc tính.
  • Để đặt giá trị tối đa, hãy gọi phương thức setMaxValue() và chuyển giá trị tối đa của thuộc tính.

Cả hai phương thức đều trả về ảnh động mà giá trị đang được đặt.

Lưu ý: Nếu bạn đã đặt giá trị bắt đầu và xác định phạm vi giá trị ảnh động, hãy đảm bảo giá trị bắt đầu nằm trong phạm vi giá trị tối thiểu và tối đa.

Đặt tốc độ bắt đầu

Tốc độ bắt đầu xác định tốc độ thay đổi thuộc tính ảnh động ở đầu ảnh động. Tốc độ bắt đầu mặc định được đặt thành 0 pixel/giây. Bạn có thể đặt tốc độ bằng tốc độ của các cử chỉ chạm hoặc bằng cách sử dụng một giá trị cố định làm tốc độ bắt đầu. Nếu chọn cung cấp một giá trị cố định, bạn nên xác định giá trị bằng dp mỗi giây, sau đó chuyển đổi giá trị đó thành pixel/giây. Việc xác định giá trị tính bằng dp mỗi giây cho phép tốc độ độc lập với mật độ và các kiểu dáng. Để biết thêm thông tin về cách chuyển đổi giá trị sang pixel/giây, hãy tham khảo phần Chuyển đổi dp mỗi giây sang pixel/giây.

Để đặt tốc độ, hãy gọi phương thức setStartVelocity() và truyền vận tốc tính bằng pixel/giây. Phương thức này trả về một đối tượng lực lò xo mà trên đó vận tốc được đặt.

Lưu ý: Hãy dùng các phương thức của lớp GestureDetector.OnGestureListener hoặc VelocityTracker để truy xuất và tính toán tốc độ của các cử chỉ chạm.

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Compute velocity in the unit pixel/second
        vt.computeCurrentVelocity(1000)
        val velocity = vt.yVelocity
        setStartVelocity(velocity)
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Compute velocity in the unit pixel/second
vt.computeCurrentVelocity(1000);
float velocity = vt.getYVelocity();
anim.setStartVelocity(velocity);

Đang chuyển đổi dp/giây thành pixel/giây

Vận tốc của lò xo phải tính bằng pixel trên giây. Nếu bạn chọn cung cấp một giá trị cố định làm điểm bắt đầu tốc độ, hãy cung cấp giá trị tính bằng dp mỗi giây, sau đó chuyển đổi giá trị đó thành pixel mỗi giây. Để chuyển đổi, hãy sử dụng phương thức applyDimension() từ lớp TypedValue. Hãy tham khảo mã mẫu sau:

Kotlin

val pixelPerSecond: Float =
    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond, resources.displayMetrics)

Java

float pixelPerSecond = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond, getResources().getDisplayMetrics());

Đặt thuộc tính lò xo

Lớp SpringForce xác định phương thức getter và setter cho mỗi thuộc tính lò xo, chẳng hạn như tỷ lệ giảm chấn và độ cứng. Để đặt các thuộc tính lò xo, bạn cần truy xuất đối tượng lực lò xo hoặc tạo một lực lò xo tuỳ chỉnh mà bạn có thể đặt thuộc tính. Để biết thêm thông tin về cách tạo lực lò xo tuỳ chỉnh, hãy tham khảo phần Tạo lực lò xo tuỳ chỉnh.

Mẹo: Trong khi sử dụng các phương thức setter, bạn có thể tạo một chuỗi phương thức khi tất cả các phương thức setter đều trả về đối tượng lực lò xo.

Tỷ lệ giảm chấn

Tỷ lệ tắt dần mô tả sự giảm dần dao động của lò xo. Bằng cách sử dụng tỷ lệ giảm chấn, bạn có thể xác định tốc độ giảm dần của dao động từ lần nảy này sang lần khác. Có 4 cách để bạn giảm độ ẩm cho lò xo:

  • Tình trạng quá mức xảy ra khi tỷ lệ giảm chấn lớn hơn 1. Phương thức này cho phép đối tượng nhẹ nhàng trở về vị trí nghỉ.
  • Giảm chấn quan trọng xảy ra khi tỷ lệ giảm chấn bằng 1. Phương thức này cho phép đối tượng trở về vị trí nghỉ ngơi trong một khoảng thời gian ngắn nhất.
  • Giảm chấn xảy ra khi tỷ lệ giảm chấn nhỏ hơn 1. Phương thức này cho phép đối tượng vượt quá nhiều lần bằng cách truyền vị trí nghỉ, sau đó dần dần đến vị trí nghỉ.
  • Không làm giảm chấn xảy ra khi tỷ lệ giảm chấn bằng 0. Phương thức này cho phép đối tượng dao động vĩnh viễn.

Để thêm tỷ lệ giảm chấn cho lò xo, hãy thực hiện các bước sau:

  1. Gọi phương thức getSpring() để truy xuất lò xo nhằm thêm tỷ lệ giảm chấn.
  2. Gọi phương thức setDampingRatio() và truyền tỷ lệ giảm chấn mà bạn muốn thêm vào lò xo. Phương thức này trả về đối tượng lực lò xo mà bạn đã đặt tỷ lệ giảm chấn.

    Lưu ý: Tỷ lệ giảm chấn phải là một số không âm. Nếu bạn đặt tỷ lệ giảm chấn thành 0, lò xo sẽ không bao giờ đạt đến vị trí nghỉ. Nói cách khác, mã này sẽ dao động vĩnh viễn.

Những hằng số tỷ lệ giảm chấn sau đây có sẵn trong hệ thống:

Hình 2: Độ nảy cao

Hình 3: Độ nảy trung bình

Hình 4: Độ nảy thấp

Hình 5: Không thoát

Tỷ lệ giảm chấn mặc định được đặt thành DAMPING_RATIO_MEDIUM_BOUNCY.

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Setting the damping ratio to create a low bouncing effect.
        spring.dampingRatio = SpringForce.DAMPING_RATIO_LOW_BOUNCY
        …
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Setting the damping ratio to create a low bouncing effect.
anim.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
…

Độ cứng

Độ cứng xác định hằng số lò xo, dùng để đo độ bền của lò xo. Lò xo cứng tác dụng thêm lực lên vật được gắn khi lò xo không ở vị trí nghỉ. Để thêm độ cứng cho lò xo, hãy thực hiện các bước sau:

  1. Gọi phương thức getSpring() để truy xuất lò xo nhằm thêm độ cứng.
  2. Gọi phương thức setStiffness() và truyền giá trị độ cứng mà bạn muốn thêm vào lò xo. Phương thức này trả về đối tượng lực lò xo mà trên đó được đặt độ cứng.

    Lưu ý: Độ cứng phải là số dương.

Trong hệ thống có các hằng số độ cứng sau đây:

Hình 6: Độ cứng cao

Hình 7: Độ cứng trung bình

Hình 8: Độ cứng thấp

Hình 9: Độ cứng rất thấp

Độ cứng mặc định được đặt thành STIFFNESS_MEDIUM.

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Setting the spring with a low stiffness.
        spring.stiffness = SpringForce.STIFFNESS_LOW
        …
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Setting the spring with a low stiffness.
anim.getSpring().setStiffness(SpringForce.STIFFNESS_LOW);
…

Tạo lực lò xo tuỳ chỉnh

Bạn có thể tạo một lực lò xo tuỳ chỉnh thay vì sử dụng lực lò xo mặc định. Lực lượng lò xo tuỳ chỉnh cho phép bạn chia sẻ cùng một thực thể lực lò xo trên nhiều ảnh động lò xo. Sau khi tạo lực lò xo, bạn có thể đặt các thuộc tính như tỷ lệ giảm chấn và độ cứng.

  1. Tạo đối tượng SpringForce.

    SpringForce force = new SpringForce();

  2. Chỉ định thuộc tính bằng cách gọi các phương thức tương ứng. Bạn cũng có thể tạo một chuỗi phương thức.

    force.setDampingRatio(DAMPING_RATIO_LOW_BOUNCY).setStiffness(STIFFNESS_LOW);

  3. Gọi phương thức setSpring() để đặt lò xo vào ảnh động.

    setSpring(force);

Bắt đầu ảnh động

Có hai cách để bắt đầu ảnh động có hiệu ứng lò xo: Gọi start() hoặc gọi phương thức animateToFinalPosition(). Cả hai phương thức cần được gọi trên luồng chính.

Phương thức animateToFinalPosition() thực hiện 2 tác vụ:

  • Đặt vị trí cuối cùng của lò xo.
  • Khởi động ảnh động nếu nó chưa bắt đầu.

Vì phương thức này sẽ cập nhật vị trí cuối cùng của hiệu ứng lò xo và bắt đầu ảnh động nếu cần, nên bạn có thể gọi phương thức này bất cứ lúc nào để thay đổi tiến trình của ảnh động. Ví dụ: trong ảnh động có hiệu ứng lò xo theo chuỗi, ảnh động của một khung hiển thị sẽ phụ thuộc vào một khung hiển thị khác. Đối với ảnh động như vậy, bạn nên dùng phương thức animateToFinalPosition(). Khi sử dụng phương thức này trong một ảnh động có hiệu ứng lò xo theo chuỗi, bạn không cần lo lắng liệu ảnh động bạn muốn cập nhật tiếp theo có đang chạy hay không.

Hình 10 minh hoạ ảnh động có hiệu ứng lò xo theo chuỗi, trong đó ảnh động của một khung hiển thị phụ thuộc vào một khung hiển thị khác.

Bản minh hoạ lò xo theo chuỗi
Hình 10. Bản minh hoạ theo chuỗi về mùa xuân

Để sử dụng phương thức animateToFinalPosition(), hãy gọi phương thức animateToFinalPosition() và truyền vị trí còn lại của lò xo. Bạn cũng có thể đặt vị trí còn lại của lò xo bằng cách gọi phương thức setFinalPosition().

Phương thức start() không đặt ngay giá trị thuộc tính thành giá trị bắt đầu. Giá trị thuộc tính thay đổi ở mỗi xung ảnh động, xảy ra trước khi truyền bản vẽ. Do đó, các thay đổi sẽ được phản ánh trong khung tiếp theo, như thể các giá trị được đặt ngay lập tức.

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Starting the animation
        start()
        …
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Starting the animation
anim.start();
…

Huỷ ảnh động

Bạn có thể huỷ hoặc chuyển đến cuối ảnh động. Trường hợp lý tưởng là bạn cần huỷ hoặc chuyển đến cuối lượt tương tác là khi một hoạt động tương tác của người dùng yêu cầu ảnh động phải chấm dứt ngay lập tức. Điều này chủ yếu xảy ra khi người dùng thoát khỏi một ứng dụng đột ngột hoặc khung hiển thị bị ẩn.

Có 2 phương pháp mà bạn có thể sử dụng để chấm dứt ảnh động. Phương thức cancel() chấm dứt ảnh động tại giá trị mong muốn. Phương thức skipToEnd() bỏ qua ảnh động đến giá trị cuối cùng rồi chấm dứt.

Để có thể chấm dứt ảnh động, trước tiên, bạn cần kiểm tra trạng thái của lò xo. Nếu trạng thái không được làm mờ, thì ảnh động sẽ không bao giờ đạt đến vị trí còn lại. Để kiểm tra trạng thái của lò xo, hãy gọi phương thức canSkipToEnd(). Nếu lò xo bị làm ẩm, phương thức này sẽ trả về true, nếu không thì sẽ trả về false.

Sau khi biết trạng thái của lò xo, bạn có thể chấm dứt ảnh động bằng cách sử dụng phương thức skipToEnd() hoặc phương thức cancel(). Phương thức cancel() phải được gọi chỉ trên luồng chính.

Lưu ý: Nhìn chung, phương thức skipToEnd() sẽ gây ra một bước nhảy hình ảnh.