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

Thử cách sử dụng 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ý được thúc đẩy bởi lực. Lực lò xo là một lực như vậy dẫn hướng tương tác và chuyển động. Lực 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ực lò xo, giá trị và vận tốc được tính dựa trên lực của lò xo áp dụng trên từng khung hình.

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

Vòng đời của ảnh động mùa xuân

Trong ảnh động dựa trên lực 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 lên khỏi biểu tượng, thì biểu tượng đó sẽ kéo 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 tác 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 bản phát hành mùa xuân

Tạo ảnh động mùa xuân

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 trình bày 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 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ề Ảnh động động trên trang phiên bản.

Tạo ảnh động mùa xuân

Lớp SpringAnimation cho phép bạn tạo ảnh động mùa xuân 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, 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 ả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 giá trị 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 các thuộc tính thực tế trong các đối tượng khung hiển thị. Có các khung hiển thị sau đây 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 thành phần hiển thị (view) dưới dạng một delta tính từ toạ độ bên trái, toạ độ trên cùng và độ cao. Những thuộc tính này được thiết lập bởi vùng chứa bố cục.
    • TRANSLATION_X mô tả toạ độ bên trái.
    • TRANSLATION_Y mô tả toạ độ cao nhất.
    • 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 việc xoay trong chế độ 2D (thuộc tính rotation) và 3D xung quanh điểm trung tâm.
  • SCROLL_XSCROLL_Y: Những 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. Cột này cũng cho biết vị trí người dùng cuộn trê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 khung hiển thị xung quanh điểm trung tâm.
  • 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. Những trình nghe này theo dõi thông tin 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 khung hiển thị khác để cập nhật vị trí mùa xuân dựa trên thay đổi phát sinh 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 ảnh động bắt đầu. Tuy nhiên, bạn chỉ nên đăng ký trình nghe cập nhật nếu cần cập nhật trên mỗi khung hình khi có thay đổi về giá trị ảnh động. Trình nghe cập nhật 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ề sự 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 sẽ thông báo 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 mỗi khi ảnh động đạt đến trạng thái cân bằng hoặc bị huỷ.

Xoá người nghe

Để ngừng nhận 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 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 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 giới hạn giá trị thuộc tính ở một phạm vi nhất định. Việc này cũng giúp bạn 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ị nhỏ nhất, 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ị lớn nhất, 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 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 ở thời điểm 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 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 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ị theo dp/giây rồi chuyển đổi giá trị đó thành pixel/giây. Việc xác định giá trị tính bằng dp/giây cho phép tốc độ không phụ thuộc vào mật độ và 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/giây sang pixel/giây.

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

Lưu ý: Hãy sử dụng các phương thức GestureDetector.OnGestureListener hoặc VelocityTracker để truy xuất và tính 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 sang pixel/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 đặt một giá trị cố định làm điểm bắt đầu của tốc độ, hãy cung cấp giá trị tính bằng dp/giây rồi chuyển đổi giá trị đó thành pixel/giây. Để chuyển đổi, hãy sử dụng phương thức applyDimension() của 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 mùa xuân

Lớp SpringForce xác định phương thức getter và phương thức setter cho từng thuộc tính lò xo, chẳng hạn như tỷ lệ giảm chấn và độ cứng. Để đặt thuộc tính lò xo, bạn cần phải truy xuất đối tượng lò xo hoặc tạo lực lò xo 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 lò xo tuỳ chỉnh, hãy tham khảo mục 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 vì 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ỷ số tắt dần mô tả sự giảm dần của dao động 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ừ một tỷ lệ nảy lên tiếp theo. Bạn có thể giảm độ ẩm của lò xo theo 4 cách khác nhau:

  • Hiện tượng giảm chấn thương xảy ra khi tỷ số 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 tới hạn xảy ra khi tỷ số giảm chấn bằng 1. Phương pháp này cho phép đối tượng trở về vị trí nghỉ trong khoảng thời gian ngắn nhất.
  • Giảm thiểu xảy ra khi tỷ số 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í còn lại, sau đó dần dần đi đến vị trí còn lại.
  • Không bị giảm chấn xảy ra khi tỷ số 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 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 sẽ trả về đối tượng lực lò xo đã đặ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 bằng 0, thì lò xo sẽ không bao giờ đạt đến vị trí còn lại. Nói cách khác, nó dao động vĩnh viễn.

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

Hình 2: Độ nảy cao

Hình 3: Tỷ lệ thoát trung bình

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

Hình 5: Không nảy

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. Một lò xo cứng sẽ tác dụng nhiều lực hơn vào vật thể được gắn khi lò xo không nằm ở 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 đã đặt độ cứng.

    Lưu ý: Độ cứng phải là một 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 lực lò xo tuỳ chỉnh thay cho việc sử dụng lực lò xo mặc định. Lực lò xo tuỳ chỉnh cho phép bạn chia sẻ cùng một thực thể của lực lò xo trên nhiều ảnh động lò xo. Sau khi tạo lực của 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 thành ảnh động.

    setSpring(force);

Bắt đầu ảnh động

Có 2 cách bạn có thể bắt đầu tạo ảnh động mùa xuân: Bằng cách gọi start() hoặc gọi phương thức animateToFinalPosition(). Bạn cần gọi cả hai phương thức này 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 ảnh động 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ị phụ thuộc vào một khung hiển thị khác. Đối với ảnh động như vậy, sẽ thuận tiện hơn nếu bạn sử dụng phương thức animateToFinalPosition(). Khi sử dụng phương thức này trong ảnh động mùa xuân 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ạ một ả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ạ mùa xuân dạng chuỗi
Hình 10. Bản minh hoạ mùa xuân theo chuỗi

Để 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 tại mỗi xung ảnh động, xảy ra trước khi truyền bản vẽ. Do đó, các thay đổi đượ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. Tình huống lý tưởng mà bạn cần huỷ hoặc chuyển đến cuối quá trình liên kết là khi một lượt tương tác của người dùng yêu cầu chấm dứt ảnh động ngay lập tức. Điều này chủ yếu xảy ra khi người dùng đột ngột thoát khỏi một ứng dụng hoặc khung hiển thị bị ẩn.

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

Trước khi 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 mùa xuân. Nếu trạng thái chưa được tắt, ảnh động sẽ không bao giờ đạt đến vị trí còn lại. Để kiểm tra trạng thái của mùa xuân, hãy gọi phương thức canSkipToEnd(). Nếu lò xo bị giảm âm, phương thức sẽ trả về true, nếu không thì false.

Sau khi biết trạng thái của mùa xuân, 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 trên luồng chính.

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