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 đàn hồi là một trong những lực như vậy, giúp hướng dẫn tính tương tác và chuyển động. Lực đàn hồi có các thuộc tính sau: độ giảm chấn và độ cứng. Trong ảnh động dựa trên lò xo, giá trị và vận tốc được tính dựa trên lực lò xo được áp dụng trên mỗi khung hình.

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

Vòng đời của ả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 của lò xo sẽ cập nhật giá trị ảnh động và vận tốc trên mỗi khung hình. Ảnh động tiếp tục cho đến khi lực đàn hồi đạ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, thì biểu tượng đó sẽ bật về vị trí ban đầu bằng một lực vô hình nhưng quen thuộc.

Hình 1 minh hoạ một 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 cử chỉ chạm.

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

Tạo ảnh động lò xo

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

Các phần sau đây sẽ thảo luận chi tiết về các bước chung để tạo một ảnh độ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 của mình như sau:

  1. Mở tệp build.gradle cho mô-đun ứng dụng của bạn.
  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ề Dynamicanimation trên trang phiên bản.

Tạo ảnh độ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 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, 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) mà bạn muốn ảnh động dừng lại.

Lưu ý: Tại thời điểm tạo một ảnh động lò xo, bạn không bắt buộc phải chỉ định vị trí cuối cùng của lò xo. Mặc dù vậy, bạn phải xác định 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ò xo có thể tạo ảnh động cho các thành phần hiển thị trên màn hình bằng cách thay đổi các thuộc tính thực tế trong các đối tượng thành phần hiển thị. Hệ thống có các khung hiển thị sau:

  • ALPHA: Biểu thị độ trong suốt alpha trên khung hiển thị. Theo mặc định, giá trị là 1 (không trong suốt), với giá trị 0 thể hiện độ trong suốt hoàn toàn (không nhìn thấy).
  • 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 một delta so với toạ độ bên trái, toạ độ trên cùng và độ cao của khung hiển thị. Các thuộc tính này do vùng chứa bố cục của khung hiển thị đặt.
    • TRANSLATION_X mô tả toạ độ bên trái.
    • TRANSLATION_Y mô tả toạ độ trên cùng.
    • TRANSLATION_Z mô tả chiều sâu của khung hiển thị so với độ cao của khung 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 xoay.
  • 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 theo pixel. Tham số này cũng cho biết vị trí theo mức độ 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 xoay của khung hiển thị đó.
  • 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 khung 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 lắng nghe các bản 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 sẽ thông báo cho khung hiển thị khác cập nhật vị trí lò xo dựa trên thay đổi xảy ra trong thuộc tính của khung 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 bắt đầu hiệu ứng chuyển động. Mặc dù vậy, bạn chỉ nên đăng ký trình nghe cập nhật nếu cần cập nhật từng khung hình khi giá trị hoạt ảnh thay đổi. Trình nghe cập nhật ngăn ảnh động chạy trên một luồng riêng biệt.

  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. Đoạn mã mẫu sau đây minh hoạ cách sử dụng chung 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 khi một ảnh động kết thúc. 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ỷ.

Xoá 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 các phương thức removeUpdateListener()removeEndListener() tương ứng.

Đặt giá trị bắt đầu của ả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 đối tượng làm giá trị bắt đầu.

Đặt dải giá trị cho ảnh động

Bạn có thể đặt giá trị tối thiểu và tối đa cho ảnh động 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 hiệu ứng cho các thuộc tính có phạm vi vốn có, 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à truyề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à truyề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 một 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 vận tốc ban đầu

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

Để đặt vận tốc, hãy gọi phương thức setStartVelocity() và truyền vận tốc theo pixel mỗi giây. Phương thức này trả về đối tượng lực đàn hồi mà vận tốc được đặt.

Lưu ý: Sử dụng GestureDetector.OnGestureListener hoặc các phương thức lớp VelocityTracker để truy xuất và tính toán vận tốc của 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);

Chuyển đổi dp trên giây thành pixel trên giây

Vận tốc của lò xo phải tính bằng pixel/giây. Nếu bạn chọn cung cấp một giá trị cố định làm điểm bắt đầu của vận tốc, hãy cung cấp giá trị theo dp mỗi giây, sau đó chuyển đổi 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 các thuộc tính mùa xuân

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

Lưu ý: 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 vì tất cả các phương thức setter đều trả về đối tượng lực đàn hồi.

Tỷ lệ giảm chấn

Tỷ lệ giảm chấn mô tả sự giảm dần trong 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 các dao động từ lần bật này sang lần bật tiếp theo. Có 4 cách để bạn có thể giảm chấn lò xo:

  • Hiện tượng giảm chấn quá mức xảy ra khi tỷ lệ giảm chấn lớn hơn 1. Nó giúp đối tượng từ từ trở về vị trí ban đầu.
  • Hiện tượng giảm chấn tới hạn xảy ra khi tỷ lệ giảm chấn bằng 1. Thao tác này cho phép đối tượng trở về vị trí nghỉ trong thời gian ngắn nhất.
  • Hiện tượng giảm chấn xảy ra khi tỷ lệ giảm chấn nhỏ hơn 1. Thao tác này cho phép đối tượng vượt quá nhiều lần bằng cách truyền vị trí còn lại, sau đó dần đạt đến vị trí còn lại.
  • Không giảm chấn xảy ra khi tỷ lệ giảm chấn bằng 0. Thao tác này cho phép đối tượng dao động mãi mãi.

Để thêm tỷ lệ giảm chấn vào 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 đàn hồi 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, nó sẽ dao động mãi mãi.

Hệ thống có các hằng số tỷ lệ giảm chấn sau:

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, đo lường độ bền của lò xo. Lò xo cứng sẽ tác dụng nhiều lực hơn lên đối tượng đượ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 tăng độ 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 đàn hồi mà độ cứng được đặt.

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

Hệ thống có các hằng số độ cứng sau:

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 đàn hồi tuỳ chỉnh

Bạn có thể tạo lực đàn hồi tuỳ chỉnh thay vì sử dụng lực đàn hồi mặc định. Lực 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 đàn hồi, 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 các 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 cho ảnh động.

    setSpring(force);

Bắt đầu ảnh động

Bạn có thể bắt đầu một ảnh động lò xo theo 2 cách: Bằng cách gọi start() hoặc bằng cách gọi phương thức animateToFinalPosition(). Cả hai phương thức này đều cần được gọi trên luồng chính.

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

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

Vì phương thức này cập nhật vị trí cuối cùng của 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 hướng của ảnh động. Ví dụ: trong một ảnh độ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 sử dụng phương thức animateToFinalPosition(). Bằng cách sử dụng phương thức này trong một ảnh động lò xo được liên kết, bạn không cần lo lắng nếu ảnh động mà bạn muốn cập nhật tiếp theo hiện đang chạy.

Hình 10 minh hoạ một ảnh động lò xo được liên kết, 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 xích
Hình 10. Bản minh hoạ lò xo liên kết

Để sử dụng phương thức animateToFinalPosition(), hãy gọi phương thức animateToFinalPosition() rồi truyền vị trí còn lại của lò xo. Bạn cũng có thể đặt vị trí nghỉ 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ịp ảnh động, xảy ra trước lượt truyền vẽ. Do đó, các thay đổi sẽ được phản ánh trong khung hình 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. Tình huống lý tưởng mà bạn cần huỷ hoặc bỏ qua đến cuối ảnh động là khi một lượt tương tác của người dùng yêu cầu ảnh động phải kết thúc ngay lập tức. Điều này thường xảy ra khi người dùng thoát ứng dụng đột ngột hoặc chế độ xem trở nên không nhìn thấy được.

Bạn có thể sử dụng 2 phương thức để kết thúc ảnh động. Phương thức cancel() sẽ kết thúc ảnh động ở giá trị hiện tại. Phương thức skipToEnd() sẽ bỏ qua ảnh động đến giá trị cuối cùng rồi kết thúc ảnh động.

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

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

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