SWIFT – Cách tạo Popover Card View Animation sử dụng UIPropertyAnimator
Người viết: Nguyen Duc Huy B Bài viết hôm nay mình sẽ giới thiệu cho các bạn cách tạo Popover Card View được sử dụng khá phổ biến trong các app hiện nay như Voice Memos, Map hay Stocks…, cho phép người dùng có thể mở rộng hoặc ẩn thông tin ở nền trước, đồng thời thông tin ở ...
Người viết: Nguyen Duc Huy B
Bài viết hôm nay mình sẽ giới thiệu cho các bạn cách tạo Popover Card View được sử dụng khá phổ biến trong các app hiện nay như Voice Memos, Map hay Stocks…, cho phép người dùng có thể mở rộng hoặc ẩn thông tin ở nền trước, đồng thời thông tin ở nền sau sẽ được làm mờ đi.
Để có thể tạo animation cho Card View thì ta cần phải sử dụng UIViewPropertyAnimator, object hỗ trợ animation được Apple cho ra mắt tại WWDC 2016.
Sau đây là định nghĩa về UIViewPropertyAnimator:
A class that animates changes to views and allows the dynamic modification of those animations.
Có thể hiểu ngắn gọn là UIViewPropertyAnimator cho phép bạn tạo ra animation cho sự thay đổi của views, mà điển hình là sự thay đổi về vị trí của chúng.
Bài viết này mình sẽ không đi sâu vào UIViewPropertyAnimator. Các bạn muốn tìm hiểu thêm thì có thể tham khảo links này:
https://medium.com/@NilStack/swift-world-uiviewpropertyanimator-exploration-part-1-d3f4ab8901a8
Bước 1: Setting up the view
Đầu tiên, mình sẽ tạo một UIViewController cùng file XIB view và đặt tên là CardViewController. Các bạn cũng có thể dùng storyboard hoặc code tùy theo từng project cụ thể:
CardViewController view size cần phải set chế độ freeform để nó có thể resized như một view độc lập mà không kề thừa từ các iOS Device properties.
Trong CardViewController.xib, ta sẽ tạo ra giao diện giống như dưới đây, với view màu trắng là handleArea – được sử dụng để resize view.
1 2 3 4 5 6 7 8 9 10 11 12 |
import UIKit final class CardViewController: UIViewController { @IBOutlet weak var handleArea: UIView! override func viewDidLoad() { super.viewDidLoad() } } |
Bước 2: Setting up Main View Controller
Trong MainViewController class, ta sẽ khai báo một số thuộc tính cần sử dụng:
- Enum CardState để biểu thị hai trạng thái chính của Card View là hiển thị (expanded) và ẩn (collapsed).
- Biến cardVisible biểu thị trạng thái hiện tại của Card View.
- Biến nextState biểu thị trạng thái tiếp theo của Card View.
- Biến startCardHeight và endCardHeight biểu thị chiều cao của Card tại thời điểm bắt đầu (collapsed) và thời điểm kết thúc (expanded).
- Biến animationProgressWhenInterrupted biểu thị phần trăm tiến trình của Animation khi bị gián đoạn.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// Enum for card states enum CardState { case collapsed case expanded } final class MainViewController: UIViewController { // Variable determines the next state of the card expressing that the card starts and collapased private var nextState: CardState { return cardVisible ? .collapsed : .expanded } // Variable for card view controller private var cardViewController: CardViewController! // Variable for effects visual effect view private var visualEffectView: UIVisualEffectView! // Starting and end card heights will be determined later private var startCardHeight: CGFloat = 0 private var endCardHeight: CGFloat = 0 // Current visible state of the card var cardVisible = false // Empty property animator array var runningAnimations = [UIViewPropertyAnimator]() var animationProgressWhenInterrupted: CGFloat = 0 |
Tiếp theo, ta sẽ add cardview và visual effect view vào view của MainViewController và set frame cho chúng. Ở đây visual effect view sẽ tạo hiệu ứng làm mờ khi cardview được expand.
Và để thực hiện được các thao tác expand hay collapse thì ta cần add tap gesture và pan gesture cho handleArea của CardViewController. Tap gesture sẽ nhận biết thao tác nhấn của người dùng để thực hiện animation cho cardview khi expand hoặc collapse.
Còn Pan gesture sẽ nhận biết khi người dùng kéo lên xuống cardview để phù hợp với kích thước expand của cardview.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
private func setupCard() { // Setup starting and ending card height endCardHeight = self.view.frame.height * 0.8 startCardHeight = self.view.frame.height * 0.3 // Add Visual Effects View visualEffectView = UIVisualEffectView() visualEffectView.frame = self.view.frame self.view.addSubview(visualEffectView) // Add CardViewController xib to the bottom of the screen, clipping bounds so that the corners can be rounded cardViewController = CardViewController(nibName:"CardViewController", bundle:nil) self.addChild(cardViewController) self.view.addSubview(cardViewController.view) cardViewController.view.frame = CGRect(x: 0, y: self.view.frame.height - startCardHeight, awidth: self.view.bounds.awidth, height: endCardHeight) cardViewController.view.clipsToBounds = true // Add tap and pan recognizers let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleCardTap(recognzier:))) let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleCardPan(recognizer:))) cardViewController.handleArea.addGestureRecognizer(tapGestureRecognizer) cardViewController.handleArea.addGestureRecognizer(panGestureRecognizer) } // Handle tap gesture recognizer @objc private func handleCardTap(_ recognzier: UITapGestureRecognizer) { switch recognzier.state { // Animate card when tap finishes case .ended: animateTransitionIfNeeded(state: nextState, duration: 0.5) default: break } } // Handle pan gesture recognizer @objc private func handleCardPan(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: // Start animation if pan begins startInteractiveTransition(state: nextState, duration: 0.5) case .changed: // Update the translation according to the percentage completed let translation = recognizer.translation(in: self.cardViewController.handleArea) var fractionComplete = translation.y / endCardHeight fractionComplete = cardVisible ? fractionComplete : -fractionComplete updateInteractiveTransition(fractionCompleted: fractionComplete) case .ended: // End animation when pan ends continueInteractiveTransition() default: break } } |
Bước 3: Setting up the animation
Popover Card View sử dụng UIViewPropertyAnimator để tạo animation, gắn liền với việc sử dụng Pan Gesture để xác định phần trăm Popover view được hiển thị để animation có thể cập nhật dựa trên số phần trăm đó.
Và với Tap Gesture thì Popover view bật lên sẽ khớp với vị trí cần hiển thị đã set trước đó.
Bên cạnh đó, UIViewPropertyAnimator còn sử dụng curved animation để bo tròn góc cho Card View khi expand.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
// Animate transistion function private func animateTransitionIfNeeded(state: CardState, duration: TimeInterval) { // Check if frame animator is empty if runningAnimations.isEmpty { // Create a UIViewPropertyAnimator depending on the state of the popover view // The damping ratio to apply to the initial acceleration and oscillation. let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) { switch state { case .expanded: // If expanding set popover y to the ending height and blur background self.cardViewController.view.frame.origin.y = self.view.frame.height - self.endCardHeight self.visualEffectView.effect = UIBlurEffect(style: .dark) case .collapsed: // If collapsed set popover y to the starting height and remove background blur self.cardViewController.view.frame.origin.y = self.view.frame.height - self.startCardHeight self.visualEffectView.effect = nil } } // Complete animation frame frameAnimator.addCompletion { _ in self.cardVisible = !self.cardVisible self.runningAnimations.removeAll() } // Start animation frameAnimator.startAnimation() // Append animation to running animations runningAnimations.append(frameAnimator) // Create UIViewPropertyAnimator to round the popover view corners depending on the state of the popover let cornerRadiusAnimator = UIViewPropertyAnimator(duration: duration, curve: .linear) { switch state { case .expanded: // If the view is expanded set the corner radius to 12 self.cardViewController.view.layer.cornerRadius = 12 case .collapsed: // If the view is collapsed set the corner radius to 0 self.cardViewController.view.layer.cornerRadius = 0 } } // Start the corner radius animation cornerRadiusAnimator.startAnimation() // Append animation to running animations runningAnimations.append(cornerRadiusAnimator) } } // Function to start interactive animations when view is dragged private func startInteractiveTransition(state: CardState, duration: TimeInterval) { // If animation is empty start new animation if runningAnimations.isEmpty { animateTransitionIfNeeded(state: state, duration: duration) } // For each animation in runningAnimations for animator in runningAnimations { // Pause animation and update the progress to the fraction complete percentage animator.pauseAnimation() animationProgressWhenInterrupted = animator.fractionComplete } } // Funtion to update transition when view is dragged private func updateInteractiveTransition(fractionCompleted: CGFloat) { // For each animation in runningAnimations for animator in runningAnimations { // Update the fraction complete value to the current progress animator.
Có thể bạn quan tâm
0
|