+2

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

Bắt đầu thực hiện

Đầ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 :

  1. func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?)-> TimeInterval : Dùng để trả về thời gian cho animation
  2. 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.width / finalFrame.width :
      finalFrame.width / initialFrame.width
    
    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 UICollectionViewDataSourceUICollectionViewDelegate

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:

  1. Zoom out và zoom in photo ( Sử dụng UIScrollView )
  2. 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 width =   view.bounds.width
        let height = (image.size.height * width) / image.size.width
        panImageView.frame = CGRect(x: 0, y: 0, width: width, 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 😄

    func actionPanImageView(pan: UIPanGestureRecognizer) {
        switch pan.state {
        case .began:
            isAction = true
            startPoint = pan.location(in: self.view) // Gán vị trí bắt đầu khi chạm vào màn hình
            scrollView.zoomScale = 1
            scrollView.isHidden = true
            panImageView.center = view.center
            view.addSubview(panImageView) // Add pan image view vào view chính và dùng nó để di chuyển theo direction của ngón tay
            break
        case .changed:
            let updatePoint = pan.location(in: view)
            let dy = abs(startPoint.y - updatePoint.y) // Tính khoảng cách theo chiều dọc giữa vị trí ngón tay hiện tay và vị trí bắt đầu
            let dx = abs(startPoint.x - updatePoint.x) // Tính khoảng cách theo chiều ngang giữa vị trí ngón tay hiện tay và vị trí bắt đầu
            var scale = updatePoint.y > startPoint.y ? startPoint.y/updatePoint.y : updatePoint.y/startPoint.y // Tính scale theo y
            scale = scale > 0.6 ? scale : 0.6
            //Update lại vị trí của pan image view và transform scale
            let y = updatePoint.y > startPoint.y ? center.y + dy : center.y - dy
            let x = updatePoint.x > startPoint.x ? center.x + dx : center.x - dx
            panImageView.transform = CGAffineTransform(scaleX: scale, y: scale)
            panImageView.center = CGPoint(x: x, y: y)
            bgView.alpha = scale
            break
        case .ended:
            let updatePoint = pan.location(in: self.view)
            let distanceY = updatePoint.y > startPoint.y ? updatePoint.y - startPoint.y : startPoint.y - updatePoint.y
            if distanceY > 70 {
                bgView.alpha = 0
                panImageView.contentMode = .scaleAspectFill
                dismiss(animated: true, completion: nil)
            } else {
                UIView.animate(withDuration: 0.5, animations: {
                    self.panImageView.transform = CGAffineTransform(scaleX: 1, y: 1)
                    self.panImageView.center = self.center
                }, completion: { (completed) in
                     self.scrollView.isHidden = false
                     self.panImageView.removeFromSuperview()
                     self.isAction = false
                })
                bgView.alpha = 1
            }
            break
        default:
            break
        }
    }

Kết luận

Mình đã hướng dẫn các bạn làm 1 hiệu ứng xem photo đơn giản sử dụng UIViewControllerAnimatedTransitioning. Các bạn có thể download example tại đây. Cảm ơn các bạn đã đọc bài viết của mình 😄


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí