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 doesnot works as expected..Googling i found this one exactly what i want to acheive.But its on UIView
..I think Its 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
5 Answers
Answers 1
Update
Based on your video I sorted out what kind of animation you want to do. So my approach this time
- Work with 3 views references, which are previous, current and next.
- Add pan gesture to the view which is holding these views (currently
self.view
), and implement the 3 states of pan touch - Apply transition based on gesture records, and arrange the views
- 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:
- Keeping track of 2 cells, the one which is the presenter (presenterCell), and the one which is going to be presented (isBeingPresentedCell)
- Implement
scrollViewDidScroll:
which coordinates the whole operation, because everything is dependent onscrollingOffset
- Handle the direction of the scroll to choose which cell is the presenter and which one is being presented
- Everytime that a cell will be dequeued it will be set an identity transformation because on normal screen we have just one cell shown
- Keep ratio factor of the rect that is covering the whole frame, in order to apply the correct factor at the transformation
- **** 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 :)
Answers 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 { return viewControllers.indexOf(navController.viewControllers.last!)! } } /** Array of view controllers; acting as the data source */ lazy var viewControllers: [UIViewController] = { var viewControllers = [UIViewController]() for i in Range(start: 0, end: 5) { let viewController = UIViewController() viewController.view.backgroundColor = UIColor(red: CGFloat((arc4random()%255))/255.0, green: CGFloat((arc4random()%255))/255.0, blue: CGFloat((arc4random()%255))/255.0, alpha: 1) viewController.view.layer.cornerRadius = 12 viewControllers.append(viewController) } return viewControllers }() 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 the first view controller navController.viewControllers = [viewControllers[0]] // add a pan recognizer to the nav controller let panRecognizer = UIPanGestureRecognizer(target: self, action: Selector("didPan:")) navController.view.addGestureRecognizer(panRecognizer) navController.delegate = self 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 < viewControllers.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(viewControllers[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) else { assertionFailure("no new view controller"); return } guard let oldVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) else { assertionFailure("no existing view controller"); return } let scaleTransform = CGAffineTransformMakeScale(0.9, 0.9) let yOffsetTransform = CGAffineTransformMakeTranslation(0, -CGRectGetHeight(oldVC.view.frame)) let animationBlock: Void -> Void if isPopping { // 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()) }) } }
Answers 3
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") }
Answers 4
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.
Answers 5
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) // }) }
0 comments:
Post a Comment