iOS上的深度页面转换

16 uitableview ios swift swift2

我正在尝试创建一个类似于POP菜单的Facebook菜单滑动动画或类似于InShorts App的动画.Android文档涵盖了这个..但是在iOS中找不到任何提示.

我试过的是将细胞转化为

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {

    UIView.animateWithDuration(0.1, animations: { () -> Void in
        cell.transform = CGAffineTransformMakeScale(0.8, 0.8)
    })

}
Run Code Online (Sandbox Code Playgroud)

但这并没有像预期的那样工作.谷歌搜索我发现正是我想要达到的目标.但它上UIView.我认为如果我们制作动画最适合UITableViewCell?我真的很感激有人愿意帮助解决这个问题.是我正在开发的入门项目

LC *_*C 웃 7

这种效果可以使用UICollectionView.Here是UICollectionViewFlowLayout类.

class UltravisualLayout: UICollectionViewLayout {

    private var contentWidth:CGFloat!
    private var contentHeight:CGFloat!
    private var yOffset:CGFloat = 0

    var maxAlpha:CGFloat = 1
    var minAlpha:CGFloat = 0

    var widthOffset:CGFloat = 35
    var heightOffset:CGFloat = 35

    private var cache = [UICollectionViewLayoutAttributes]()

    private var itemWidth:CGFloat{
        return (collectionView?.bounds.width)!
    }
    private var itemHeight:CGFloat{
        return (collectionView?.bounds.height)!
    }
    private var collectionViewHeight:CGFloat{
        return (collectionView?.bounds.height)!
    }


    private var numberOfItems:Int{
        return (collectionView?.numberOfItemsInSection(0))!
    }


    private var dragOffset:CGFloat{
        return (collectionView?.bounds.height)!
    }
    private var currentItemIndex:Int{
        return max(0, Int(collectionView!.contentOffset.y / collectionViewHeight))
    }

    var nextItemBecomeCurrentPercentage:CGFloat{
        return (collectionView!.contentOffset.y / (collectionViewHeight)) - CGFloat(currentItemIndex)
    }

    override func prepareLayout() {
        cache.removeAll(keepCapacity: false)
        yOffset = 0

        for item in 0 ..< numberOfItems{

            let indexPath = NSIndexPath(forItem: item, inSection: 0)
            let attribute = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
            attribute.zIndex = -indexPath.row

            if (indexPath.item == currentItemIndex+1) && (indexPath.item < numberOfItems){

                attribute.alpha = minAlpha + max((maxAlpha-minAlpha) * nextItemBecomeCurrentPercentage, 0)
                let width = itemWidth - widthOffset + (widthOffset * nextItemBecomeCurrentPercentage)
                let height = itemHeight - heightOffset + (heightOffset * nextItemBecomeCurrentPercentage)

                let deltaWidth =  width/itemWidth
                let deltaHeight = height/itemHeight

                attribute.frame = CGRectMake(0, yOffset, itemWidth, itemHeight)
                attribute.transform = CGAffineTransformMakeScale(deltaWidth, deltaHeight)

                attribute.center.y = (collectionView?.center.y)! +  (collectionView?.contentOffset.y)!
                attribute.center.x = (collectionView?.center.x)! + (collectionView?.contentOffset.x)!
                yOffset += collectionViewHeight

            }else{
                attribute.frame = CGRectMake(0, yOffset, itemWidth, itemHeight)
                attribute.center.y = (collectionView?.center.y)! + yOffset
                attribute.center.x = (collectionView?.center.x)!
                yOffset += collectionViewHeight
            }
            cache.append(attribute)
        }
    }

    //Return the size of ContentView
    override func collectionViewContentSize() -> CGSize {
        contentWidth = (collectionView?.bounds.width)!
        contentHeight = CGFloat(numberOfItems) * (collectionView?.bounds.height)!
        return CGSizeMake(contentWidth, contentHeight)
    }

    //Return Attributes  whose frame lies in the Visible Rect
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var layoutAttributes = [UICollectionViewLayoutAttributes]()
        for attribute in cache{
            if CGRectIntersectsRect(attribute.frame, rect){
                layoutAttributes.append(attribute)
            }
        }
        return layoutAttributes
    }


    override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
        return true
    }

    override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        let itemIndex = round(proposedContentOffset.y / (dragOffset))
        let yOffset = itemIndex * (collectionView?.bounds.height)!
        return CGPoint(x: 0, y: yOffset)
    }
    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {

        // Logic that calculates the UICollectionViewLayoutAttributes of the item
        // and returns the UICollectionViewLayoutAttributes
        return UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
    }

}
Run Code Online (Sandbox Code Playgroud)

这是演示项目链接 ......

非常感谢本教程.


Cas*_*sey 5

如果您想要InShorts视频正在做什么,我建议使用视图控制器转换.一篇有用的文章就在这里.

您要实现的完整示例代码如下所示.为了简洁起见,我在app委托中实现了所有这些,但你绝对应该将它分解为单独的类.

视频:这里

更新的代码

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDelegate, UIViewControllerAnimatedTransitioning {

    var window: UIWindow?

    var percentManager: UIPercentDrivenInteractiveTransition?

    /**
     Keep track of which way the animation is going
     */
    var isPopping = false

    /**
     The index of the view controller that is currently presented.
     */
    var index: Int {
        get {
            guard let lastVC = navController.viewControllers.last as? ViewController else { return NSNotFound }
            return lastVC.index
        }
    }

    /**
     Acting as a very basic data source. 
     - Note: When this value is set the navigation stack is reset and the first VC is created
     */
    var count: Int = 0 {
        didSet {
            guard count > 0 else { return }

            // load the first view controller
            navController.setViewControllers([ViewController(index: 0)], animated: true)
        }
    }

    lazy var navController: UINavigationController = {

        // create a navigation controller and hide it's nav bar
        let navController = UINavigationController()
        navController.navigationBar.hidden = true

        return navController
    }()

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // add the nav controller (and it's first view controller) to the window
        window = UIWindow(frame: UIScreen.mainScreen().bounds)
        window?.rootViewController = navController
        window?.makeKeyAndVisible()

        // add a pan recognizer to the nav controller
        let panRecognizer = UIPanGestureRecognizer(target: self, action: Selector("didPan:"))
        navController.view.addGestureRecognizer(panRecognizer)
        navController.delegate = self

        // update the 'data source'
        count = 5

        return true
    }

    internal func didPan(recognizer: UIPanGestureRecognizer) {
        guard let view = recognizer.view else { assertionFailure("no view"); return }

        switch (recognizer.state) {
        case .Began:
            // detect if it is an upward swipe
            if recognizer.velocityInView(view).y < 0 {
                // don't go out of bounds
                guard index + 1 < count else { recognizer.enabled = false; recognizer.enabled = true; return }

                isPopping = false

                // create the percentManager and start pushing the next view controller
                percentManager = UIPercentDrivenInteractiveTransition()
                navController.pushViewController(ViewController(index: index+1), animated: true)
            }
                // detect if it is a downward swipe
            else if recognizer.velocityInView(view).y > 0 {
                // don't go out of bounds
                guard index - 1 >= 0 else { recognizer.enabled = false; recognizer.enabled = true; return }

                isPopping = true

                // create the percentManager and start popping the current view controller
                percentManager = UIPercentDrivenInteractiveTransition()
                navController.popViewControllerAnimated(true)
            }
        case .Changed:
            // update the percent manager
            let translation = recognizer.translationInView(view)
            let percentOffset = translation.y/CGRectGetHeight(view.bounds) * (isPopping ? 1 : -1)
            percentManager?.updateInteractiveTransition(percentOffset)
        case .Ended:

            // give the percent manager it's final instruction before niling it
            if isPopping {
                if recognizer.velocityInView(view).y > 0 {
                    percentManager?.finishInteractiveTransition()
                } else {
                    percentManager?.cancelInteractiveTransition()
                }
            } else {
                if recognizer.velocityInView(view).y < 0 {
                    percentManager?.finishInteractiveTransition()
                } else {
                    percentManager?.cancelInteractiveTransition()
                }
            }
            percentManager = nil
        default:
            break
        }
    }

    // MARK: UINavigationControllerDelegate

    func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return percentManager
    }

    func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return self
    }


    // MARK: UIViewControllerAnimatedTransitioning

    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 0.4
    }

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        guard let newVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as? ViewController else { assertionFailure("no new view controller"); return }
        guard let oldVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as? ViewController  else { assertionFailure("no existing view controller"); return }

        let scaleTransform = CGAffineTransformMakeScale(0.9, 0.9)
        let yOffsetTransform = CGAffineTransformMakeTranslation(0, -CGRectGetHeight(oldVC.view.frame))

        let isPopping = newVC.index < oldVC.index

        let animationBlock: Void -> Void
        if isPopping == true {
            // place the previous vc at the top of the screen
            newVC.view.transform = yOffsetTransform

            animationBlock = {
                oldVC.view.transform = scaleTransform
                newVC.view.transform = CGAffineTransformIdentity
            }

            // add the views onto the transition view controller
            transitionContext.containerView()?.addSubview(oldVC.view)
            transitionContext.containerView()?.addSubview(newVC.view)
        } else {

            // scale the new view a bit smaller
            newVC.view.transform = scaleTransform

            // slide the old view controller up and scale the new view controller back to 100%
            animationBlock = {
                oldVC.view.transform = yOffsetTransform
                newVC.view.transform = CGAffineTransformIdentity
            }

            // add the views onto the transition view controller
            transitionContext.containerView()?.addSubview(newVC.view)
            transitionContext.containerView()?.addSubview(oldVC.view)
        }

        // perform the animation
        UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: animationBlock, completion: { finished in
            // cleanup
            oldVC.view.transform = CGAffineTransformIdentity
            newVC.view.transform = CGAffineTransformIdentity
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
        })
    }
}


class ViewController: UIViewController {
    let index: Int

    init(index: Int) {
        self.index = index
        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor(red: CGFloat((arc4random()%255))/255.0, green: CGFloat((arc4random()%255))/255.0, blue: CGFloat((arc4random()%255))/255.0, alpha: 1)
        view.layer.cornerRadius = 12
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
Run Code Online (Sandbox Code Playgroud)


E-R*_*die 5

更新

根据你的视频,我整理出你想做什么样的动画。所以这次我的方法

  1. 使用 3 个视图引用,分别是previouscurrentnext
  2. 将平移手势添加到持有这些视图的视图(当前 self.view),并实现平移触摸的 3 种状态
  3. 根据手势记录应用过渡,并排列视图
  4. 一些功劳归于 GitHub 上的这个库,这对我帮助很大。

代码

这段代码有简单的视图,它们根据我们在颜色数组上的颜色数量改变它们的背景。因此,在您的情况下,您可以创建自定义 XIB 视图,并且您还可以添加自己的数据源而不是颜色。:)

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

输出

在此处输入图片说明

原答案

您可以仅在需要单元格的情况下执行此操作,而无需进行自定义转换 UIViewController

解决方案

解决方案包括:

  1. 跟踪 2 个单元格,一个是演示者 ( presenterCell ),另一个是将要呈现的 ( isBeingPresentedCell )
  2. 实现scrollViewDidScroll:哪个协调整个操作,因为一切都依赖于scrollingOffset
  3. 处理滚动方向来选择哪个单元格是presenter,哪个单元格正在被呈现
  4. 每次单元格出列时,都会设置身份转换,因为在正常屏幕上我们只显示一个单元格
  5. 保持覆盖整个帧的矩形的比例因子,以便在变换时应用正确的因子
  6. ****你有一些问题与自动布局ViewController。请清除现有约束,用 tableView 填充视图,然后让它离顶部0px (不是顶部布局指南)。

代码

废话不多说,代码自言自语(也有人评论)

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
    }
}
Run Code Online (Sandbox Code Playgroud)

用户界面修复

在此处输入图片说明

输出动画

在此处输入图片说明

希望它能满足您的要求:)

  • 刚刚尝试了更新的代码,如果向左滑动它会崩溃并且很容易卡在页面之间 (2认同)