Depth Page transform on iOS

2019-01-17 12: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 find any hint in iOS.

What I tried was to transform the cell as

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

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

}

But this doesn't work as expected..Googling I found this one exactly what I want to achieve. But its on UIView. I think it's best fit if we animate in UITableViewCell? I would really appreciate someone willing to help this problem. Here is the starter project I am working on

回答1:

This effect can be obtained using UICollectionView.Here is the UICollectionViewFlowLayout class.

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)
    }

}

This is the demo Project link...

Great thanks to this tutorial.



回答2:

if you want exactly what the InShorts video is doing i'd recommend using view controller transitions. a helpful article is here.

full example code of what you are trying to achieve is below. i implemented it all in the app delegate for brevity, but you should definitely break it out into separate classes.

Video: here

UPDATED CODE

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")
    }
}


回答3:

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 :)



回答4:

Use facebook POP animation For drop dragged view to resting with animation

private func resetViewPositionAndTransformations() {        

    let resetPositionAnimation = POPSpringAnimation(propertyNamed: kPOPLayerTranslationXY)
    resetPositionAnimation.fromValue = NSValue(CGPoint: CGPoint(x: dragDistance.x, y: dragDistance.y))
    resetPositionAnimation.toValue = NSValue(CGPoint: CGPointZero)
    resetPositionAnimation.springBounciness = cardResetAnimationSpringBounciness
    resetPositionAnimation.springSpeed = cardResetAnimationSpringSpeed
    resetPositionAnimation.completionBlock = {
        (_, _) in
        self.layer.transform = CATransform3DIdentity
    }

    layer.pop_addAnimation(resetPositionAnimation, forKey: "resetPositionAnimation")

    let resetRotationAnimation = POPBasicAnimation(propertyNamed: kPOPLayerRotation)
    resetRotationAnimation.fromValue = POPLayerGetRotationZ(layer)
    resetRotationAnimation.toValue = CGFloat(0.0)
    resetRotationAnimation.duration = cardResetAnimationDuration

    layer.pop_addAnimation(resetRotationAnimation, forKey: "resetRotationAnimation")

    let overlayAlphaAnimation = POPBasicAnimation(propertyNamed: kPOPViewAlpha)
    overlayAlphaAnimation.toValue = 0.0
    overlayAlphaAnimation.duration = cardResetAnimationDuration
    overlayAlphaAnimation.completionBlock = { _, _ in
    }
    overlayView?.pop_addAnimation(overlayAlphaAnimation, forKey: "resetOverlayAnimation")

    let resetScaleAnimation = POPBasicAnimation(propertyNamed: kPOPLayerScaleXY)
    resetScaleAnimation.toValue = NSValue(CGPoint: CGPoint(x: 1.0, y: 1.0))
    resetScaleAnimation.duration = cardResetAnimationDuration
    layer.pop_addAnimation(resetScaleAnimation, forKey: "resetScaleAnimation")
}


回答5:

in my opinion UICollectionview and UICollectionViewLayout is better suited for such things. UITableView don't give you per scroll transforms and you would need to do all kinds dirty hacks.

If you go with UICollectionview, what you looking for is -[UICollectionViewLayout layoutAttributesForElementsInRect:] and -[UICollectionViewLayout shouldInvalidateLayoutForBoundsChange:], everything else is a bit of math.



回答6:

Try this: You can us facebook/pop https://github.com/facebook/pop for smoother effect. This code is just to give you some idea, not tested much.

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 4
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("cell")
    if cell?.contentView.viewWithTag(100) == nil
    {
        let view = UIView(frame: (cell?.contentView.bounds)!)

        view.tag = 100
        view.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
        cell?.contentView.addSubview(view)
    }
    if cell!.contentView.viewWithTag(100) != nil
    {
        let view = cell!.contentView.viewWithTag(100)
        view!.backgroundColor = colors[indexPath.row]
    }

    return cell!
}

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return self.view.frame.size.height - 20
}

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

    if cell.contentView.viewWithTag(100) != nil
    {
        let view = cell.contentView.viewWithTag(100)
        let basicAnimation = CABasicAnimation(keyPath: "transform.scale")
        basicAnimation.toValue = NSNumber(float: 1)
        basicAnimation.fromValue = NSNumber(float: 0.8)
        basicAnimation.duration = 0.4
        basicAnimation.removedOnCompletion = false
        view!.layer.addAnimation(basicAnimation, forKey: "transform.scale")
    }

//        if tableView.cellForRowAtIndexPath(NSIndexPath(forRow: indexPath.row - 1, inSection: 0)) != nil
//        {
//            let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: indexPath.row - 1, inSection: 0))
//            if cell!.contentView.viewWithTag(100) != nil
//            {
//                let view = cell!.contentView.viewWithTag(100)
//                let basicAnimation = CABasicAnimation(keyPath: "transform.scale")
//                basicAnimation.toValue = NSNumber(float: 0.8)
//                basicAnimation.fromValue = NSNumber(float: 1)
//                basicAnimation.duration = 0.4
//                basicAnimation.removedOnCompletion = false
//                view!.layer.addAnimation(basicAnimation, forKey: "transform.scale")
//            }
//        }
//        if tableView.cellForRowAtIndexPath(NSIndexPath(forRow: indexPath.row + 1, inSection: 0)) != nil
//        {
//            let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: indexPath.row + 1, inSection: 0))
//            
//            if cell!.contentView.viewWithTag(100) != nil
//            {
//                let view = cell!.contentView.viewWithTag(100)
//                let basicAnimation = CABasicAnimation(keyPath: "transform.scale")
//                basicAnimation.toValue = NSNumber(float: 0.8)
//                basicAnimation.fromValue = NSNumber(float: 1)
//                basicAnimation.duration = 0.4
//                basicAnimation.removedOnCompletion = false
//                view!.layer.addAnimation(basicAnimation, forKey: "transform.scale")
//            }
//        }
//        let scaleAnimation = POPDecayAnimation(propertyNamed: kPOPLayerScaleXY)
//        scaleAnimation.fromValue = NSValue(CGSize: CGSize(width: 0.8, height: 0.8))
//        scaleAnimation.toValue = NSValue(CGSize: CGSize(width: 1, height: 1))

//        cell.contentView.layer.pop_addAnimation(scaleAnimation, forKey: "kPOPLayerScaleXY")

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


}