Depth Page transform on iOS

前端 未结 6 1046
[愿得一人]
[愿得一人] 2020-12-15 07:03

I am trying to create a animation like of Facebook Menu Slide Down Animation of POP framework or exactly like of InShorts App. Android Documentation covers this..But cannot

6条回答
  •  生来不讨喜
    2020-12-15 07:36

    Update

    Based on your video I sorted out what kind of animation you want to do. So my approach this time

    1. Work with 3 views references, which are previous, current and next.
    2. Add pan gesture to the view which is holding these views (currently self.view), and implement the 3 states of pan touch
    3. Apply transition based on gesture records, and arrange the views
    4. Some credits go to this library on GitHub, which really helped me a lot.

    Code

    This code has simple views, which change their background corresponding to the number of colors that we have on color array. So in your case you can create your custom XIB View, and also instead of colors you can add your own datasource. :)

    import UIKit
    
    class StackedViewController: UIViewController {
        var previousView: UIView! = nil
        var currentView: UIView! = nil
        var nextView: UIView! = nil
        var currentIndex = 0
    
        let colors: [UIColor] = [.redColor(), .greenColor(), .yellowColor(), .grayColor()]
        var offset: CGFloat = CGFloat()
    
        // MARK: - View lifecycle
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // Set the offset
            offset = 64.0
    
            setupViews()
        }
    
        // MARK: - Setups
    
        func setupViews() {
            self.view.backgroundColor = .blackColor()
    
            self.currentView = getCurrentView()
            self.view.addSubview(currentView)
    
            let pan = UIPanGestureRecognizer(target: self, action: "panAction:")
            self.view.addGestureRecognizer(pan)
        }
    
        // MARK: - Actions
    
        func panAction(gesture:UIPanGestureRecognizer){
            let p = gesture.translationInView(self.view)
    
            // Edge cases to disable panning when the view stack is finished
            if p.y < 0 && getPreviousView() == nil || p.y > 0 && getNextView() == nil {
                return
            }
    
            if gesture.state == .Began {
                if let prev = getPreviousView() {
                    self.previousView = prev
                    self.view.addSubview(prev)
                    prev.frame = self.view.frame
                    prev.center = CGPointMake(self.view.bounds.width/2, self.view.bounds.height/2)
                    self.view.sendSubviewToBack(previousView)
                }
    
                if let next = getNextView() {
                    self.nextView = next
                    self.view.addSubview(next)
                    next.frame = CGRect(origin: CGPoint(x: 0, y: -self.view.bounds.height), size: self.view.frame.size)
                }
            } else if gesture.state == .Changed {
                UIView.animateWithDuration(0.1, animations: { () -> Void in
                    if p.y < 0 {
                        self.previousView.hidden = false
                        self.currentView.center.y = self.view.bounds.height/2 + p.y
                        self.previousView.center.y = self.view.bounds.height/2
    
                        // Transforming ratio from 0-1 to 0.9-1
                        let ratio = (-p.y/CGRectGetHeight(self.view.bounds))
                        let lightRatio = 0.9 + (ratio/10)
    
                        // Apply transformation
                        self.apply3DDepthTransform(self.previousView, ratio: lightRatio)
                    } else if p.y > 0 {
                        self.currentView.center.y = self.view.bounds.height/2
                        let prevPosY = -self.view.bounds.height/2 + p.y
                        if prevPosY < self.view.bounds.height/2 {
                            self.nextView?.center.y = prevPosY
                        } else {
                            self.nextView?.center.y = self.view.bounds.height/2
                        }
    
                        // Transforming ratio from 0-1 to 0.9-1
                        let ratio = p.y/CGRectGetHeight(self.view.bounds)
                        let lightRatio = 1 - (ratio/10)
    
                        // Apply transformation
                        self.apply3DDepthTransform(self.currentView, ratio: lightRatio)
    
                        // Hide the background view when showing another because the edges of this will be shown due to transformation
                        if self.previousView != nil {
                            self.previousView.hidden = true
                        }
                    }
                })
    
            } else if gesture.state == .Ended {
                UIView.animateWithDuration(0.5, delay: 0,
                    options: [.CurveEaseOut], animations: { () -> Void in
    
                        if p.y < -self.offset && self.previousView != nil {
                            // Showing previous item
                            self.currentView.center.y = -self.view.bounds.height/2
    
                            // Finish the whole transition
                            self.apply3DDepthTransform(self.previousView, ratio: 1)
                        } else if p.y > self.offset && self.nextView != nil {
                            // Showing next item
                            self.nextView?.center.y = self.view.bounds.height/2
    
                            // Finish the whole transition
                            self.apply3DDepthTransform(self.currentView, ratio: 0.9)
                        } else {
                            // The pan has not passed offset so just return to the main coordinates
                            self.previousView?.center.y = -self.view.bounds.height/2
                            self.currentView.center.y = self.view.bounds.height/2
                        }
                    }, completion: { (_) -> Void in
                        if p.y < -self.offset && self.previousView != nil {
                            self.currentView = self.getPreviousView()
                            self.currentIndex = (self.currentIndex > 0) ? self.currentIndex - 1 : self.currentIndex;
                        } else if p.y > self.offset && self.nextView != nil {
                            self.currentView = self.getNextView()
                            self.currentIndex = (self.currentIndex == self.colors.count - 1) ? self.currentIndex : self.currentIndex + 1;
                        }
    
                        // Remove all views and show the currentView
                        for view in self.view.subviews {
                            view.removeFromSuperview()
                        }
    
                        self.previousView = nil
                        self.nextView = nil
    
                        self.view.addSubview(self.currentView)
                        self.currentView.center = CGPointMake(self.view.bounds.width/2, self.view.bounds.height/2)
                })
            }
        }
    
        // MARK: - Helpers
    
        func getCurrentView() -> UIView? {
            let current = UIView(frame: self.view.frame)
            current.backgroundColor = colors[currentIndex]
    
            return current
        }
    
        func getNextView() -> UIView? {
            if currentIndex >= colors.count - 1 {
                return nil
            }
            let next = UIView(frame: self.view.frame)
            next.backgroundColor = colors[currentIndex + 1]
    
            return next
        }
    
        func getPreviousView() -> UIView? {
            if currentIndex <= 0 {
                return nil
            }
            let prev = UIView(frame: self.view.frame)
            prev.backgroundColor = colors[currentIndex - 1]
    
            return prev
        }
    
        // MARK: Animation
    
        func apply3DDepthTransform(view: UIView, ratio: CGFloat) {
            view.layer.transform = CATransform3DMakeScale(ratio, ratio, ratio)
            view.alpha = 1 - ((1 - ratio)*10)
        }
    }
    

    Output

    Original Answer

    You can do it only with need of the cells, without having to make custom transition with UIViewController

    Solution

    Solution consists on:

    1. Keeping track of 2 cells, the one which is the presenter (presenterCell), and the one which is going to be presented (isBeingPresentedCell)
    2. Implement scrollViewDidScroll: which coordinates the whole operation, because everything is dependent on scrollingOffset
    3. Handle the direction of the scroll to choose which cell is the presenter and which one is being presented
    4. Everytime that a cell will be dequeued it will be set an identity transformation because on normal screen we have just one cell shown
    5. Keep ratio factor of the rect that is covering the whole frame, in order to apply the correct factor at the transformation
    6. **** You have some problems with the Autolayout on the ViewController. Please clear existing constraints, fill the view with the tableView and then let it be 0px from the Top (Not Top Layout Guide).

    Code

    No more talking, code talks itself (it is also some commented)

    import UIKit
    
    enum ViewControllerScrollDirection: Int {
        case Up
        case Down
        case None
    }
    
    class ViewController: UIViewController {
        @IBOutlet weak var menuTableView: UITableView!
        var colors :[UIColor] = [UIColor.greenColor(),UIColor.grayColor(),UIColor.purpleColor(),UIColor.redColor()]
        var presenterCell: UITableViewCell! = nil
        var isBeingPresentedCell: UITableViewCell! = nil
        var lastContentOffset: CGFloat = CGFloat()
        var scrollDirection: ViewControllerScrollDirection = .None
    
        // MARK: - View Lifecycle
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
            menuTableView.dataSource = self
            menuTableView.delegate = self
            menuTableView.pagingEnabled = true
        }
    }
    
    extension ViewController:UITableViewDataSource,UITableViewDelegate {
    
        // MARK: - Delegation
    
        // MARK: TableView Datasource
    
        func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return 4
        }
    
        func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCellWithIdentifier("cell")! as UITableViewCell
            cell.contentView.backgroundColor = colors[indexPath.row]
            cell.backgroundColor = UIColor.blackColor()
            cell.contentView.layer.transform = CATransform3DIdentity
            cell.selectionStyle = .None
    
            return cell
        }
    
        // MARK: TableView Delegate
    
        func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
            return self.view.frame.size.height
        }
    
        // MARK: ScrollView Delegate
    
        func scrollViewDidScroll(scrollView: UIScrollView) {
            self.scrollDirection = getScrollDirection(scrollView)
    
            // The cells in visible cells are ordered, so depending on scroll we set the one that we want to present
            if self.scrollDirection == .Up {
                self.presenterCell = menuTableView.visibleCells.last
                self.isBeingPresentedCell = menuTableView.visibleCells.first
            } else {
                self.presenterCell = menuTableView.visibleCells.first
                self.isBeingPresentedCell = menuTableView.visibleCells.last
            }
    
            // If we have the same cells or either of them is nil don't do anything
            if (self.isBeingPresentedCell == nil || self.presenterCell == nil) {
                return;
            } else if (self.isBeingPresentedCell == self.presenterCell) {
                return;
            }
    
            // Always animate the presenter cell to the identity (fixes the problem when changing direction on pan gesture)
            UIView.animateWithDuration(0.1, animations: { () -> Void in
                self.presenterCell.contentView.layer.transform = CATransform3DIdentity;
            })
    
            // Get the indexPath
            guard let indexPath = menuTableView.indexPathForCell(presenterCell) else {
                return;
            }
    
            // Get the frame for that indexPath
            let frame = menuTableView.rectForRowAtIndexPath(indexPath)
    
            // Find how much vertical space is the isBeingPresented cell using on the frame and return always the positive value
            var diffY = frame.origin.y - self.lastContentOffset
            diffY = (diffY > 0) ? diffY : -diffY
    
            // Find the ratio from 0-1 which corresponds on transformation from 0.8-1
            var ratio = CGFloat(diffY/CGRectGetHeight(self.menuTableView.frame))
            ratio = 0.8 + (ratio/5)
    
            // Make the animation
            UIView.animateWithDuration(0.1, animations: { () -> Void in
                self.isBeingPresentedCell.contentView.layer.transform = CATransform3DMakeScale(ratio, ratio, ratio)
            })
        }
    
        // MARK: - Helpers
    
        func getScrollDirection(scrollView: UIScrollView) -> ViewControllerScrollDirection {
            let scrollDirection = (self.lastContentOffset > scrollView.contentOffset.y) ? ViewControllerScrollDirection.Down : ViewControllerScrollDirection.Up
            self.lastContentOffset = scrollView.contentOffset.y;
    
            return scrollDirection
        }
    }
    

    UI Fix

    Output animation

    Hope it will fill your requirements :)

提交回复
热议问题