Tạo hiệu ứng xem ảnh với UIViewControllerAnimatedTransitioning
Đầu tiên mình sẽ hướng dẫn các bạn tạo hiệu ứng xem ảnh với UIViewControllerAnimatedTransitioning Đầu tiên, tạo 1 project có tên FacebookPhotoScreen và sử dụng ngôn ngữ Swift PopAnimator Tạo 1 subclass từ NSObject và conform với UIViewControllerAnimatedTransitioning có tên là ...
Đầu tiên mình sẽ hướng dẫn các bạn tạo hiệu ứng xem ảnh với UIViewControllerAnimatedTransitioning
Đầu tiên, tạo 1 project có tên FacebookPhotoScreen và sử dụng ngôn ngữ Swift
PopAnimator
Tạo 1 subclass từ NSObject và conform với UIViewControllerAnimatedTransitioning có tên là PopAnimator. Ta sẽ chỉ quan tâm đến 2 func trong UIViewControllerAnimatedTransitioning :
- func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?)-> TimeInterval : Dùng để trả về thời gian cho animation
- func animateTransition(using transitionContext: UIViewControllerContextTransitioning) : Tạo hiệu ứng khi present và dismiss cho view controller
class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning { let duration:TimeInterval = 0.5 // Thời gian cho animation var presenting = true //Check trạng thái của viewcontroller đang present hoặc dismiss var originFrame = CGRect.zero // Lưu lại vị trí frame của view khi present var presentCompletionAnimation: ((Bool) -> Void)? // Closure được gọi khi present thành công var dismissCompletionAnimation: ((Bool) -> Void)? // Closure được gọi khi dismiss thành công func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?)-> TimeInterval { return duration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let containerView = transitionContext.containerView // View sẽ chứa khi có animation // herbView sẽ kiểm tra biến presenting để lấy view sẽ dùng để animation.Nếu presenting = true thì sẽ lấy view controller present.Và ngược lại nếu presenting = false thì sẽ lấy view bị dismiss xuống let herbView = presenting ? transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!.view! : transitionContext.view(forKey: UITransitionContextViewKey.from)!.viewWithTag(100)! herbView.frame = presenting ? UIScreen.main.bounds : herbView.frame let initialFrame = presenting ? originFrame : UIScreen.main.bounds let finalFrame = presenting ? UIScreen.main.bounds : originFrame // Tính toán xScale và yScale cho hearView let xScaleFactor = presenting ? initialFrame.awidth / finalFrame.awidth : finalFrame.awidth / initialFrame.awidth let yScaleFactor = presenting ? initialFrame.height / finalFrame.height : finalFrame.height / initialFrame.height // Tạo 1 biến Transform cho xScale và yScale let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor) // Khi presenting = true thì ta set transform lại cho herbView để cho herbView bằng với kích thước mà cell ta sẽ touch vào if presenting { herbView.transform = scaleTransform herbView.center = CGPoint( x: initialFrame.midX, y: initialFrame.midY) herbView.clipsToBounds = true } if presenting { containerView.addSubview(transitionContext.view(forKey: UITransitionContextViewKey.to)!) } containerView.bringSubview(toFront: herbView) // Tạo hiệu ứng animation với spring và velocity của UIView UIView.animate(withDuration: duration, delay:0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: [], animations: { if self.presenting { herbView.transform = self.presenting ? CGAffineTransform(scaleX: 1, y: 1) : scaleTransform } else { herbView.frame = finalFrame } herbView.center = CGPoint(x: finalFrame.midX,y: finalFrame.midY) }, completion:{_ in transitionContext.completeTransition(true) if self.presenting { herbView.frame = UIScreen.main.bounds } self.presenting ? self.presentCompletionAnimation?(true) : self.dismissCompletionAnimation?(true) }) } }
Trong đoạn code trên các bạn sẽ thấy mã lệnh transitionContext.view(forKey: UITransitionContextViewKey.from)!.viewWithTag(100)!. Tại vì sao phải lấy view với tag là 100 thì mình sẽ nói ở phần phía dưới. Bây giờ , chúng ta đã xong phần animation khi present và dismiss cho view controller
ListPhotoViewController
- Ở ListPhotoViewController chỉ cần 1 collection view ở file xib hoặc storyboard. Và kéo IBOutlet vào view controller
class ListPhotoViewController: UIViewController { @IBOutlet private var collectionView: UICollectionView! let transition = PopAnimator() // Khởi tạo biến transition với PopAnimator mà đã tạo ở phần trên var selectCell: UIView! // Nhận giá trị của cell khi được select let images = [UIImage(named: "1"), UIImage(named: "2"), UIImage(named: "3"), UIImage(named: "4"), UIImage(named: "5"), UIImage(named: "6")] // Mảng các hình ảnh sẽ show lên collection view override func viewDidLoad() { super.viewDidLoad() // Gán delegate và datasource cho collection view collectionView.delegate = self collectionView.dataSource = self // Đăng ký cell cho collection view collectionView.register(ImageCell.self, forCellWithReuseIdentifier: "Cell") } }
Sử dụng extension để conform cho UICollectionViewDataSource và UICollectionViewDelegate
extension ListPhotoViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? ImageCell else { fatalError("You don't register cell") } // Gán image vào cho cell cell.image = images[index] return cell } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return images.count } } extension ListPhotoViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { // Gán cell được select vào cho selectCell selectCell = collectionView.cellForItem(at: indexPath) // Ẩn cell được select đi để tạo cho ngừoi dùng có cảm giác là cell này đã được phóng to lên khi present photo detail selectCell.isHidden = true // Khởi tạo photoDetail và gán image và transitioningDelegate let photoDetail = PhotoDetailViewController(nibName: "PhotoDetailViewController", bundle: nil) photoDetail.image = images[indexPath.row] photoDetail.transitioningDelegate = self present(photoDetail, animated: true, completion: nil) } }
Sau khi đã conform xong delegate và datasource thì tiếp tục với UIViewControllerTransitioningDelegate. Đây là phần quan trọng để khi bạn muốn custom cho việc present/dismiss view controller. Khi bạn present 1 controller thì UIKit sẽ tự động gọi đến thuộc tính transitioningDelegate trong view controller để hiện thị animation cho nó.Nếu bạn ko gán thì mặc định UIKit sẽ dùng kiểu default.Các bạn có thể đọc thêm về UIViewControllerTransitioningDelegate ở đây. Hiện tại, chỉ cần quan đến 3 func chính là:
- swift func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? Trả về cho delegate 1 transitioning khi dismiss view controller
- swift func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? Trả về cho delegate 1 transitioning khi present view controller
- swift func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController!, sourceViewController source: UIViewController) -> UIPresentationController? Thông báo cho delegate về việc phân cấp khi present view controller
extension ListPhotoViewController: UIViewControllerTransitioningDelegate { func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { transition.presenting = false transition.dismissCompletionAnimation = {(completed) in self.selectCell.isHidden = false } return transition } func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { transition.originFrame = selectCell!.superview!.convert(selectCell.frame, to: self.view) transition.presenting = true return transition } func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController!, sourceViewController source: UIViewController) -> UIPresentationController? { return UIPresentationController(presentedViewController: presented, presenting: presenting) } }
Như vậy chúng ta đã chuẩn bị xong mọi việc cho class ListPhotoViewController. Tiếp theo sẽ qua phần PhotoDetailViewController
PhotoDetailViewController
Yêu cầu của màn hình này bao gồm:
- Zoom out và zoom in photo ( Sử dụng UIScrollView )
- Bắt sự kiện di chuyển ngón tay để di chuyển photo ( Sử dụng UIPanGestureRecognizer)
Mình sẽ nói lại phần trên vì sao phải sử dụng view với tag là 100. Khi dismiss view controller, transitioning delegate sẽ gọi về func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? khi đó trong PopAnimator sẽ get lấy image view trong PhotoDetailViewController và scale lại về vị trí select cell. Lúc đó sẽ cho ngừoi dùng có cảm giác là image được thu nhỏ lại về ví trí trước.
panImageView = UIImageView(image: self.image) guard let image = image else { return } let awidth = view.bounds.awidth let height = (image.size.height * awidth) / image.size.awidth panImageView.frame = CGRect(x: 0, y: 0, awidth: awidth, height: height) panImageView.contentMode = UIViewContentMode.scaleAspectFill panImageView.tag = 100 //Add tag for image view. When controller dismissed then controller transition will get image view by tag and show animation panImageView.clipsToBounds = true
Bắt tay vào handle cho sự kiện di chuyển image nào