+4

Triển khai UX hiệu quả hơn với Android Animations

Mở đầu

Khi quá chú trọng vào chức năng của ứng dụng, ta có thể sẽ đánh giá thấp sự ảnh hưởng của trải nghiệm người dùng. Đa phần người dùng sẽ không thích những ứng dụng không có sự hấp dẫn về mặt trực quan. Để tạo ra sự khác biệt cho ứng dụng, chúng ta cần cung cấp cho các chức năng của ứng dụng một bộ Animations.

Lấy một ví dụ: Ta cần một ứng dụng đặt bánh pizza trong đó người dùng có thể đặt bánh pizza với một số lượng tùy chỉnh. Bây giờ bạn sẽ giới thiệu ca sử dụng này như thế nào?

Dưới đây là một ví dụ mà bạn có thể tham khảo:

Điều cần thiết bây giờ là ta cần chuyển đổi ảnh GIF trên thành code để triển khai trong ứng dụng. Công việc nghe có vẻ không hề dễ dàng. Đầu tiên ta cần quan sát kĩ, thậm chí có thể cần phải làm chậm tốc độ của ảnh GIF lại để có thể theo dõi chính xác những gì đang diễn ra. Hãy để ý kĩ và kiểm tra từng màn hình một:

  • Màn hình danh sách pizza
  • Màn hình tùy biến
  • Theo dõi màn hình giao hàng của bạn

Bây giờ ta có một sự hiểu biết tốt hơn về thiết kế. Giờ là thời điểm để chuyển đổi nó thành mã code.

Màn hình Pizza List

Màn hình này đang hiển thị một danh sách các loại pizza và bằng cách nhấp vào nút CUSTOMIZE, ứng dụng điều hướng đến một màn hình khác với hình ảnh bánh pizza của màn hình hiện tại và đế bánh pizza của màn hình khác. Bằng cách thêm một vài dòng code chúng ta có thể đạt được điều này:

customiseButton.setOnClickListener {
    val intent = Intent(this, CustomiseActivity::class.java)
    val options = ActivityOptionsCompat
        .makeSceneTransitionAnimation(
            this, 
            pizzaImageView as View, 
            "pizza")
    startActivity(intent, options.toBundle())
}

Đừng quên thêm đoạn code dưới đây vào file styles.xml của bạn:

<item name="android:windowContentTransitions">true</item>

Màn hình Customization

Ở đây chúng ta cần rất nhiều animations. Trước khi bắt đầu về màn hình này, hãy tìm hiểu một chút về ObjectAnimator, ValueAnimatorAnimationSet.

  • ObjectAnimator: Lớp con này của ValueAnimator cung cấp hỗ trợ cho hoạt ảnh các thuộc tính trên các đối tượng đích.
  • ValueAnimator: Lớp này cung cấp một công cụ thời gian đơn giản để chạy các animation, tính toán các giá trị animated và đặt chúng trên các đối tượng đích.
  • AnimationSet: Đại diện cho một nhóm Animations được biểu diễn cùng nhau. Ta cần hiển thị nhiều animation đồng thời hoặc theo thứ tự liên tục. AnimationSet là phù hợp nhất cho việc này.

AnimateCheeseHeight

Khi bắt đầu màn hình này, ta nhận thấy có thể điều khiển chiều cao của lớp phô mai tăng lên. Vì vậy, chúng ta cần sử dụng ValueAnimator. Ta cần tăng chiều cao từ 10dp đến 30dp, và cần cập nhật giá trị với AnimatorUpdateListener. Từ đó ta nhận được giá trị chiều cao được cập nhật và cập nhật các thông số bố cục của PizzaCheeseImageView.

val anim = ValueAnimator.ofInt(pizzaCheeseImageView.measuredHeight, height)
    anim.addUpdateListener { valueAnimator ->
        val value = valueAnimator.animatedValue as Int
        val layoutParams = pizzaCheeseImageView.layoutParams
        layoutParams.height = value
        pizzaCheeseImageView.layoutParams = layoutParams
    }
    anim.duration = 300
    anim.start()

AnimateCrustHeight

Với việc chiều cao lớp phô mai tăng lên, ta cũng cần tăng chiều cao của lớp vỏ bánh. Ta cũng sử dụng ValueAnimator. Ngoài ra khi chiều cao tăng, ta cần thay đổi cả bán kính góc để cung cấp hình dạng đồng nhất cho hình ảnh của đế bánh hoặc lớp phô mai. Vì vậy ta sẽ cập nhật đồng thời cả 2 thông số chiều cao và bán kính góc của PizzaCrustImageView

val anim = ValueAnimator.ofInt(pizzaCrustImageView.measuredHeight, height)
    anim.addUpdateListener { valueAnimator ->
        val value = valueAnimator.animatedValue as Int
        val layoutParams = pizzaCrustImageView.layoutParams
        layoutParams.height = value
        pizzaCrustImageView.layoutParams = layoutParams
        pizzaCrustImageView.radius = value.div(2).toFloat()
    }
    anim.duration = 300
    anim.start()

AnimateToppings

Khi người dùng chọn một topping, ta cần hiển thị một hình ảnh động rơi xuống từ đỉnh màn hình đến phía trên phô mai của pizza. Ta đang sử dụng ObjectAnimator vì nó có một thuộc tính được gọi là translationY, giúp tạo các lớp phủ để animate từ vị trí intialY đến vị trí destinationY. Ngoài ra, ta đang sử dụng AccelerateInterpolator giúp tăng tốc dần animations.

ObjectAnimator.ofFloat(toppingsLayout,"translationY", 0f, toppingsLayout.height.toFloat() - 12f).apply {
        interpolator = AccelerateInterpolator()
        duration = 600
        start()
    }

AnimatePizzaLayout

Như chúng ta có thể quan sát rằng nhiều animation đang diễn ra, vì vậy ta có thể chia chúng thành nhiều phần.

FadeCheeseAnimation

val fadeCheese = ObjectAnimator.ofFloat(pizzaCheeseImageView, "alpha", 1f, 0f)

FadeCrustAnimation

val fadeCrust = ObjectAnimator.ofFloat(pizzaCrustImageView, "alpha", 1f, 0f)

FadeInPizzaAnimation

val fadeInPizza = ObjectAnimator.ofFloat(pizzaImageView, "alpha", 0f, 1f)

ScaleUpPizzaXAnimation

val scaleUpPizzaX = ObjectAnimator.ofFloat(pizzaImageView, "scaleX", 0f, 0.8f)

ScaleUpPizzaYAnimation

val scaleUpPizzaY = ObjectAnimator.ofFloat(pizzaImageView, "scaleY", 0f, 14f)

TranslatePizzaYAnimation

val translatePizzaY = ObjectAnimator.ofFloat(pizzaImageView, "translationY", 0f, -360f)

PlayAnimationsTogether

AnimatorSet().apply { 
  play(fadeCheese).with(fadeCrust).with(fadeInPizza).with(scaleUpPizzaX).with(scaleUpPizzaY).with(translatePizzaY)
}

Theo dõi màn hình Delivery

Trong màn hình này, chúng ta cần làm animation phần trước của hộp và xoay hộp với giá trị scalingtranslation.

AnimateFrontCover

Chúng tôi cũng có thể sử dụng ViewPropertyAnimator thay vì ObjectAnimator cho animation. Bằng cách sử dụng ViewPropertyAnimator, chúng ta có thể thực hiện nhiều animation cùng nhau mà không cần sử dụng AnimatioSet. Nó cung cấp một cú pháp tốt hơn và một cách tối ưu hóa để làm sinh động một khung nhìn.

frontCoverImageView
        .animate()
        .translationY(790f)
        .setDuration(300)
        .setListener(object : Animator.AnimatorListener {
            override fun onAnimationEnd(animation: Animator?) {
                backCoverImageView.visibility = VISIBLE
                backCoverImageView.alpha = 1f
                pizzaImageView.visibility = GONE
                frontCoverImageView.visibility = GONE
                rotateBoxAnimation()
            }
        })
        .startDelay = 500L

RotateBoxAnimation

Một lần nữa chúng ta có thể tách nhỏ các animation: ScaleDownXAnimation

val scaleDownX = ObjectAnimator.ofFloat(backCoverImageView, "scaleX", 1f, 0.6f)

ScaleDownYAnimation

val scaleDownY = ObjectAnimator.ofFloat(backCoverImageView, "scaleY", 1f, 0.6f)

RotateBoxAnimation

val rotateBox = ObjectAnimator.ofFloat(backCoverImageView, "rotation", 0f, -45f)

TranslateXAnimation

val translateX = ObjectAnimator.ofFloat(backCoverImageView, "translationX", 0f, 1000f)

PlayAnimationsTogether

AnimatorSet().apply {
    play(scaleDownX).with(scaleDownY).with(rotateBox).before(translateX)    
}

Showtime

Chạy code và thưởng thức thành quả thôi nào!

Nguồn tham khảo

Make UX better with Android Animations


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.