Repository: adow/CardsAnimationDemo Branch: master Commit: c3e52b61d7c1 Files: 25 Total size: 50.3 KB Directory structure: gitextract_kkjhkyrp/ ├── .gitignore ├── CardsAnimationDemo/ │ ├── AppDelegate.swift │ ├── Assets.xcassets/ │ │ ├── 0.imageset/ │ │ │ └── Contents.json │ │ ├── 1.imageset/ │ │ │ └── Contents.json │ │ ├── 10.imageset/ │ │ │ └── Contents.json │ │ ├── 2.imageset/ │ │ │ └── Contents.json │ │ ├── 3.imageset/ │ │ │ └── Contents.json │ │ ├── 4.imageset/ │ │ │ └── Contents.json │ │ ├── 5.imageset/ │ │ │ └── Contents.json │ │ ├── 6.imageset/ │ │ │ └── Contents.json │ │ ├── 7.imageset/ │ │ │ └── Contents.json │ │ ├── 8.imageset/ │ │ │ └── Contents.json │ │ ├── 9.imageset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj/ │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── CardsCollectionViewCell.swift │ ├── CardsCollectionViewLayout.swift │ ├── Info.plist │ └── ViewController.swift ├── CardsAnimationDemo.xcodeproj/ │ ├── project.pbxproj │ └── project.xcworkspace/ │ └── contents.xcworkspacedata ├── LICENSE └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Xcode # build/ *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata *.xccheckout *.moved-aside DerivedData *.hmap *.ipa *.xcuserstate # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control # # Pods/ # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build ================================================ FILE: CardsAnimationDemo/AppDelegate.swift ================================================ // // AppDelegate.swift // TestCards // // Created by 秦 道平 on 15/10/29. // Copyright © 2015年 秦 道平. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // Override point for customization after application launch. self.window = UIWindow(frame: UIScreen.mainScreen().bounds) self.window?.makeKeyAndVisible() let viewController = ViewController() // let viewController = TestViewController() self.window?.rootViewController = viewController return true } func applicationWillResignActive(application: UIApplication) { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. } func applicationDidEnterBackground(application: UIApplication) { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. } func applicationWillEnterForeground(application: UIApplication) { // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. } func applicationDidBecomeActive(application: UIApplication) { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } func applicationWillTerminate(application: UIApplication) { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } } ================================================ FILE: CardsAnimationDemo/Assets.xcassets/0.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "wonder_woman_logo_by_kalangozilla.jpg", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CardsAnimationDemo/Assets.xcassets/1.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "aquaman_young_justice_logo_by_kalangozilla.jpg", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CardsAnimationDemo/Assets.xcassets/10.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "superman_kingdom_come_logo_by_kalangozilla.jpg", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CardsAnimationDemo/Assets.xcassets/2.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "batman_begins_poster_style_logo_by_kalangozilla.jpg", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CardsAnimationDemo/Assets.xcassets/3.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "batman_tim_burton_style_logo_by_kalangozilla.jpg", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CardsAnimationDemo/Assets.xcassets/4.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "classic_captain_marvel_jr_logo_by_kalangozilla.jpg", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CardsAnimationDemo/Assets.xcassets/5.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "food-beans-coffee-drink.jpg", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CardsAnimationDemo/Assets.xcassets/6.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "flash_logo_by_kalangozilla.jpg", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CardsAnimationDemo/Assets.xcassets/7.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "green_lantern_corps_logo_by_kalangozilla.jpg", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CardsAnimationDemo/Assets.xcassets/8.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "JLA.jpeg", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CardsAnimationDemo/Assets.xcassets/9.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "milky-way-923801_640.jpg", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CardsAnimationDemo/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CardsAnimationDemo/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CardsAnimationDemo/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: CardsAnimationDemo/Base.lproj/Main.storyboard ================================================ ================================================ FILE: CardsAnimationDemo/CardsCollectionViewCell.swift ================================================ // // CardsCollectionViewCell.swift // TestCards // // Created by 秦 道平 on 15/10/29. // Copyright © 2015年 秦 道平. All rights reserved. // import UIKit class CardsCollectionViewCell: UICollectionViewCell { var label:UILabel! var contentImageView:UIImageView! override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = UIColor.whiteColor() self.clipsToBounds = true /// imageView contentImageView = UIImageView(image:UIImage(named: "coffee")) contentImageView.translatesAutoresizingMaskIntoConstraints = false self.contentView.addSubview(contentImageView) let layout_imageView = ["imageView":contentImageView] let imageView_constraintsH = NSLayoutConstraint.constraintsWithVisualFormat("H:|-(-16.0)-[imageView]-(-16.0)-|", options: NSLayoutFormatOptions.AlignAllCenterX, metrics: nil, views: layout_imageView) let imageView_constraintsV = NSLayoutConstraint.constraintsWithVisualFormat("V:|-(-16.0)-[imageView]-(-16.0)-|", options: NSLayoutFormatOptions.AlignAllCenterY, metrics: nil, views: layout_imageView) self.contentView.addConstraints(imageView_constraintsH) self.contentView.addConstraints(imageView_constraintsV) /// label // label = UILabel() // label.translatesAutoresizingMaskIntoConstraints = false // self.contentView.addSubview(label) // label.textAlignment = NSTextAlignment.Center // label.text = "label" // label.backgroundColor = UIColor.grayColor() // label.textColor = UIColor.darkGrayColor() // let layout_label = ["label":label,"superView":self] // let label_constraintsH = NSLayoutConstraint.constraintsWithVisualFormat("H:|-(0.0)-[label]-(0.0)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: layout_label) // self.contentView.addConstraints(label_constraintsH) // let label_constraintsY = NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.CenterY, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.CenterY, multiplier: 1.0, constant: 0.0) // self.contentView.addConstraint(label_constraintsY) /// self.layer.shadowColor = UIColor.darkGrayColor().CGColor self.layer.shadowOffset = CGSizeMake(0.0, -1.0) self.layer.shadowOpacity = 0.3 } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes) { super.applyLayoutAttributes(layoutAttributes) self.layer.anchorPoint = CGPointMake(0.5, 1.0) } } ================================================ FILE: CardsAnimationDemo/CardsCollectionViewLayout.swift ================================================ // // CardsCollectionViewLayout.swift // TestCards // // Created by 秦 道平 on 15/10/29. // Copyright © 2015年 秦 道平. All rights reserved. // import UIKit func divmod(a:CGFloat,b:CGFloat) -> (quotient:CGFloat, remainder:CGFloat){ return (a / b, a % b) } class CardsCollectionViewLayout: UICollectionViewFlowLayout { private let cardWidth : CGFloat = 300.0 private let cardHeight : CGFloat = 200.0 private var numberOfItems : Int = 0 private var attributesList : [UICollectionViewLayoutAttributes] = [] /// 每个 cell 在 y 之间的距离 private let y_distance_in_cells : CGFloat = 30.0 var start_offset_y : CGFloat = 0.0 override init() { super.init() self.collectionView?.decelerationRate = UIScrollViewDecelerationRateFast self.collectionView?.pagingEnabled = true } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func collectionViewContentSize() -> CGSize { let height = max(y_distance_in_cells * CGFloat(self.numberOfItems),self.collectionView!.bounds.height + 1.0) // let height = self.collectionView!.bounds.height / 2.0 * CGFloat(self.numberOfItems) // let height = self.collectionView!.bounds.height * CGFloat(self.numberOfItems) return CGSizeMake(self.collectionView!.bounds.width, height * 2.0) } override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool { return true } func makePerspectiveTransform() -> CATransform3D { var transform = CATransform3DIdentity; transform.m34 = 1.0 / -2000; return transform; } override func prepareLayout() { super.prepareLayout() var array : [UICollectionViewLayoutAttributes] = [] let offset_y : CGFloat = self.collectionView!.contentOffset.y let max_offset_y = self.collectionView!.contentSize.height - self.collectionView!.bounds.size.height // let (times,_) = divmod(max_offset_y / 2.0, b:30.0) // start_offset_y = times * 30.0 start_offset_y = floor(max_offset_y / 2.0 / 30.0) * 30.0 let reverse_offset_y : CGFloat = start_offset_y - offset_y self.numberOfItems = self.collectionView!.numberOfItemsInSection(0) for a in 0.. 1.0 { /// alpha, 从 1.0 到 0.0 var alpha = (1.1 - ratio) * 10.0 alpha = min(alpha, 1.0) alpha = max(alpha, 0.0) attributes.alpha = alpha /// rotate, 翻转角度从 0 到 -180.0 之间, angle_ratio 从 0.0 到 1.0 var angle_ratio = 1.0 - (1.1 - ratio) * 10.0 angle_ratio = min(angle_ratio, 1.0) angle_ratio = max(angle_ratio , 0.0) /// 不使用 180°,因为这样会从反面翻转过来 let angle : CGFloat = -179.999 * angle_ratio /// 转换成弧度 let radians : CGFloat = angle * CGFloat(M_PI) / 180.0 /// 实现 3D 翻转 let transform_perctive = self.makePerspectiveTransform() let transform_3d = CATransform3DRotate(transform_perctive, radians, 1.0, 0.0, 0.0) attributes.transform3D = transform_3d // print("a:\(a),ratio:\(ratio),alpha:\(alpha),angle:\(angle)") } /// 小于 0 的会反过来,就不用显示了 if ratio > 0 { array.append(attributes) } } self.attributesList = array } override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { return self.attributesList[indexPath.row] } override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return self.attributesList } /// 吸附到固定的位置 override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { // print("offset:\(proposedContentOffset)") /// 每个 cell 之间的 y 距离是30.0,所以要保证最后停在 30.0 的整数倍上面 var targetContentOffset = proposedContentOffset let (total,more) = divmod(targetContentOffset.y, b: y_distance_in_cells) if more > 0.0 { if more >= y_distance_in_cells / 2.0 { targetContentOffset.y = ceil(total) * y_distance_in_cells } else { targetContentOffset.y = floor(total) * y_distance_in_cells } } return targetContentOffset } } ================================================ FILE: CardsAnimationDemo/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: CardsAnimationDemo/ViewController.swift ================================================ // // ViewController.swift // TestCards // // Created by 秦 道平 on 15/10/29. // Copyright © 2015年 秦 道平. All rights reserved. // import UIKit class ViewController: UIViewController { var collectionView : UICollectionView! private let cellIdentifier = "cell" var start_offset_y : CGFloat = 0.0 /// 轮转中的照片列表 var carouselImages:[UIImage] = [] /// 真正的照片列表 var images : [UIImage] = [] { didSet{ carouselImages.removeAll() carouselImages = images + images + images } } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. self.view.backgroundColor = UIColor.whiteColor() /// self.prepageImages() /// collectionView self.collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: CardsCollectionViewLayout()) self.collectionView.showsVerticalScrollIndicator = true self.collectionView.translatesAutoresizingMaskIntoConstraints = false self.view.addSubview(collectionView) self.collectionView.registerClass(CardsCollectionViewCell.self, forCellWithReuseIdentifier: cellIdentifier) self.collectionView.dataSource = self self.collectionView.delegate = self let layout_collectionView = ["collectionView":self.collectionView] let collectionView_constraintsH = NSLayoutConstraint.constraintsWithVisualFormat("H:|-(-16.0)-[collectionView]-(-16.0)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: layout_collectionView) let collectionView_constraintsV = NSLayoutConstraint.constraintsWithVisualFormat("V:|-(-16.0)-[collectionView]-(-16.0)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: layout_collectionView) self.view.addConstraints(collectionView_constraintsH) self.view.addConstraints(collectionView_constraintsV) /// frameView // let frameView = UIView() // frameView.translatesAutoresizingMaskIntoConstraints = false // self.view.addSubview(frameView) // frameView.backgroundColor = UIColor(white: 1.0, alpha: 0.3) // let layout_frameView = ["frameView":frameView,"superView":self.view] // let frameView_constraintsX = NSLayoutConstraint.constraintsWithVisualFormat("H:[frameView(300.)]-(<=0)-[superView]", options: NSLayoutFormatOptions.AlignAllCenterY, metrics: nil, views: layout_frameView) // let frameView_constraintsY = NSLayoutConstraint.constraintsWithVisualFormat("V:[frameView(200.0)]-(<=0)-[superView]", options: NSLayoutFormatOptions.AlignAllCenterX, metrics: nil, views: layout_frameView) // self.view.addConstraints(frameView_constraintsX) // self.view.addConstraints(frameView_constraintsY) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) self.collectionView.layoutIfNeeded() /// 在开始的时候就滚动到中间一组 let max_offset_y = self.collectionView.contentSize.height - self.collectionView.bounds.height // self.start_offset_y = floor(max_offset_y / 2.0 / 30.0) * 30.0 + 30.0 * CGFloat(self.images.count) self.start_offset_y = floor(max_offset_y / 2.0 / 30.0) * 30.0 - 30.0 * CGFloat(self.images.count) NSLog("start_offset_y:%f", start_offset_y) self.collectionView.setContentOffset(CGPointMake(0.0, start_offset_y), animated: false) } override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) } override func viewDidDisappear(animated: Bool) { super.viewDidDisappear(animated) } } extension ViewController { /// 准备轮转的图片 func prepageImages(){ var images : [UIImage] = [] for a in 0..<11 { let i = UIImage(named: "\(a)")! images.append(i) } self.images = images } } extension ViewController:UICollectionViewDataSource, UICollectionViewDelegate { func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { return 1 } func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { // return 11 return self.carouselImages.count } func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! CardsCollectionViewCell // let image = UIImage(named: "\(indexPath.row)") // cell.label.text = "\(indexPath.row)" let image = self.carouselImages[indexPath.row] cell.contentImageView.image = image return cell } } extension ViewController:UIScrollViewDelegate { /// 滚动的时候判断边界距离进行跳转 func scrollViewDidScroll(scrollView: UIScrollView) { let translate = scrollView.contentOffset.y - self.start_offset_y NSLog("scroll:%f, %f", scrollView.contentOffset.y, translate) // if translate >= 30.0 { // scrollView.setContentOffset(CGPointMake(0.0, self.start_offset_y), animated: false) // } let target_scroll_y : CGFloat = 30.0 * CGFloat(self.images.count) if abs(translate) >= target_scroll_y { scrollView.setContentOffset(CGPointMake(0.0, self.start_offset_y), animated: false) } } } ================================================ FILE: CardsAnimationDemo.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 68DB5B161BEF7CF100F3B97B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68DB5B151BEF7CF100F3B97B /* AppDelegate.swift */; }; 68DB5B181BEF7CF100F3B97B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68DB5B171BEF7CF100F3B97B /* ViewController.swift */; }; 68DB5B1B1BEF7CF100F3B97B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 68DB5B191BEF7CF100F3B97B /* Main.storyboard */; }; 68DB5B1D1BEF7CF100F3B97B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 68DB5B1C1BEF7CF100F3B97B /* Assets.xcassets */; }; 68DB5B201BEF7CF100F3B97B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 68DB5B1E1BEF7CF100F3B97B /* LaunchScreen.storyboard */; }; 68DB5B291BEF7D2A00F3B97B /* CardsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68DB5B271BEF7D2A00F3B97B /* CardsCollectionViewCell.swift */; }; 68DB5B2A1BEF7D2A00F3B97B /* CardsCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68DB5B281BEF7D2A00F3B97B /* CardsCollectionViewLayout.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 68DB5B121BEF7CF100F3B97B /* CardsAnimationDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CardsAnimationDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 68DB5B151BEF7CF100F3B97B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 68DB5B171BEF7CF100F3B97B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 68DB5B1A1BEF7CF100F3B97B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 68DB5B1C1BEF7CF100F3B97B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 68DB5B1F1BEF7CF100F3B97B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 68DB5B211BEF7CF100F3B97B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 68DB5B271BEF7D2A00F3B97B /* CardsCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardsCollectionViewCell.swift; sourceTree = ""; }; 68DB5B281BEF7D2A00F3B97B /* CardsCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardsCollectionViewLayout.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 68DB5B0F1BEF7CF100F3B97B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 68DB5B091BEF7CF100F3B97B = { isa = PBXGroup; children = ( 68DB5B141BEF7CF100F3B97B /* CardsAnimationDemo */, 68DB5B131BEF7CF100F3B97B /* Products */, ); sourceTree = ""; }; 68DB5B131BEF7CF100F3B97B /* Products */ = { isa = PBXGroup; children = ( 68DB5B121BEF7CF100F3B97B /* CardsAnimationDemo.app */, ); name = Products; sourceTree = ""; }; 68DB5B141BEF7CF100F3B97B /* CardsAnimationDemo */ = { isa = PBXGroup; children = ( 68DB5B271BEF7D2A00F3B97B /* CardsCollectionViewCell.swift */, 68DB5B281BEF7D2A00F3B97B /* CardsCollectionViewLayout.swift */, 68DB5B151BEF7CF100F3B97B /* AppDelegate.swift */, 68DB5B171BEF7CF100F3B97B /* ViewController.swift */, 68DB5B191BEF7CF100F3B97B /* Main.storyboard */, 68DB5B1C1BEF7CF100F3B97B /* Assets.xcassets */, 68DB5B1E1BEF7CF100F3B97B /* LaunchScreen.storyboard */, 68DB5B211BEF7CF100F3B97B /* Info.plist */, ); path = CardsAnimationDemo; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 68DB5B111BEF7CF100F3B97B /* CardsAnimationDemo */ = { isa = PBXNativeTarget; buildConfigurationList = 68DB5B241BEF7CF100F3B97B /* Build configuration list for PBXNativeTarget "CardsAnimationDemo" */; buildPhases = ( 68DB5B0E1BEF7CF100F3B97B /* Sources */, 68DB5B0F1BEF7CF100F3B97B /* Frameworks */, 68DB5B101BEF7CF100F3B97B /* Resources */, ); buildRules = ( ); dependencies = ( ); name = CardsAnimationDemo; productName = CardsAnimationDemo; productReference = 68DB5B121BEF7CF100F3B97B /* CardsAnimationDemo.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 68DB5B0A1BEF7CF100F3B97B /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0710; LastUpgradeCheck = 0710; ORGANIZATIONNAME = "秦 道平"; TargetAttributes = { 68DB5B111BEF7CF100F3B97B = { CreatedOnToolsVersion = 7.1; }; }; }; buildConfigurationList = 68DB5B0D1BEF7CF100F3B97B /* Build configuration list for PBXProject "CardsAnimationDemo" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 68DB5B091BEF7CF100F3B97B; productRefGroup = 68DB5B131BEF7CF100F3B97B /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 68DB5B111BEF7CF100F3B97B /* CardsAnimationDemo */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 68DB5B101BEF7CF100F3B97B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 68DB5B201BEF7CF100F3B97B /* LaunchScreen.storyboard in Resources */, 68DB5B1D1BEF7CF100F3B97B /* Assets.xcassets in Resources */, 68DB5B1B1BEF7CF100F3B97B /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 68DB5B0E1BEF7CF100F3B97B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 68DB5B2A1BEF7D2A00F3B97B /* CardsCollectionViewLayout.swift in Sources */, 68DB5B181BEF7CF100F3B97B /* ViewController.swift in Sources */, 68DB5B161BEF7CF100F3B97B /* AppDelegate.swift in Sources */, 68DB5B291BEF7D2A00F3B97B /* CardsCollectionViewCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 68DB5B191BEF7CF100F3B97B /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 68DB5B1A1BEF7CF100F3B97B /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 68DB5B1E1BEF7CF100F3B97B /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 68DB5B1F1BEF7CF100F3B97B /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 68DB5B221BEF7CF100F3B97B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.1; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 68DB5B231BEF7CF100F3B97B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.1; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; name = Release; }; 68DB5B251BEF7CF100F3B97B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = CardsAnimationDemo/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = adow.CardsAnimationDemo; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 68DB5B261BEF7CF100F3B97B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = CardsAnimationDemo/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = adow.CardsAnimationDemo; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 68DB5B0D1BEF7CF100F3B97B /* Build configuration list for PBXProject "CardsAnimationDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( 68DB5B221BEF7CF100F3B97B /* Debug */, 68DB5B231BEF7CF100F3B97B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 68DB5B241BEF7CF100F3B97B /* Build configuration list for PBXNativeTarget "CardsAnimationDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( 68DB5B251BEF7CF100F3B97B /* Debug */, 68DB5B261BEF7CF100F3B97B /* Release */, ); defaultConfigurationIsVisible = 0; }; /* End XCConfigurationList section */ }; rootObject = 68DB5B0A1BEF7CF100F3B97B /* Project object */; } ================================================ FILE: CardsAnimationDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 adow Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # CardsAnimationDemo ![CardsAnimationDemo.gif](http://7vihfk.com1.z0.glb.clouddn.com/CardsAnimationDemo.gif) [实现效果视频](http://v.youku.com/v_show/id_XMTM4MTYyNTY2NA==.html) ## 介绍 CardsAnimationDemo 源于有一天我在网上看到的一篇文章: http://www.cocoachina.com/ios/20151013/13700.html,作者详细的介绍了如何实现这个卡片动画的全部过程。 我写这个 Demo 是同样做了一遍,唯一的不同是,我不是直接操作所有 `UIView` 和 `CALayer` 的 `transform3D` 属性来实现整个效果的,而是使用 `UICollectionView` 来完成所有的视图管理和实现(当然其实内部的实现也不过就是操作了 `transform3D` 属性)。 因此我将使用 `UICollectionView` 来完成整个翻转动画实现,并将做成可以无限轮转的样子。 我想说的是这个项目并不是一个可以直接拿来使用的组件,仅仅是一个作为研究的实验产品,虽然我觉得要并入到现成的项目中应该不算很麻烦。 ## 组成 由于使用 `UICollectionView` 来实现所有的卡片视图管理。同其他使用 `UICollectionView` 的代码一样,我们主要的工作都集中在 Layout(我这里定义为 `CardsCollectionViewLayout`),我这里也定义了一个 `CardsCollectionViewCell`, 其中只有一些简单的布局代码(显示那张图片)。 `ViewController` 是整个 App 的 `rootViewController`, 他只做了很少的一部分工作,基本上就是作为 `UICollectionView` 的 `dataSource` 和 `delegate` 存在的。 还有需要说明的是整个项目中的代码中使用了 Autolayout,因此如果把这些代码应用到你的项目中时,需要考虑是不是有布局的兼容性问题。在我的其他项目中,我大范围使用 Cartography 和 SnapKit 作为布局工具,他们让我大大减少写约束代码的时间。但在这个项目中,我直接写了约束代码而不是使用第三方工具,毕竟我不想在这里依赖任何第三方库。 ## UICollectionView 为什么使用 `UICollectionView`? 而不是像那篇文章里的那样直接通过手势来修改所有的卡片View/Layer 的属性? 我曾经也做个类似的项目,通过手势驱动,计算每个屏幕中出现的 UIView, 上下偏移和缩放,我们只需要知道手势移动的距离,知道每个 卡片 View 之间的距离和缩放的关系,就可以计算出每个卡片在配合移动过程中应该有的状态。事实上,使用 `UICollectionView` 中的 layout 部分代码和这个几乎是一样的,但是当你实现完整个项目之后你会突然发现,我通过手势完成的大部分代码居然就是 `UIScrollView` 中同样的功能,我特么居然就自己写了一个 `UIScrollView` 样的东西出来不是吗?既然这样我们为啥就不直接使用 `UIScrollView` 呢? 另一个显而易见的原因是,使用 `UICollectionView` 便于管理各个卡片视图,我们可以在 `UICollectionViewCell` 中完成自己的卡片布局,并且可以重用,这是最重要的。 ## 翻转 我们来看看这个卡片翻转动画,其实很简单,卡片从后面往前面移动,当移动到一个位置的时候,他不再继续往前移动了,而是往下完成一个翻转动画,然后就不见了。所以我们很明确的就是需要完成这个翻转的过程。 我们只可以对 `CALayer` 进行翻转,因为只有 `CALayer` 有 `transofrm3D` 属性(`CGTransform3D`), `UIView` 只有一个 `transform` 属性 (`CGAffineTransofrm`)。所以我们要做一个 UIView 的翻转效果就只要直接对这个 UIView 的 layer 属性设置 `transform3D` 就可以了。 需要注意的是,直接使用 x 轴的翻转是看不出透视效果的 (不信你直接用 `CATransform3DMakeRotate` 创建一个 x 变化来看看),我们还得设置一个透视的变化; 创建一个透视的变化: func makePerspectiveTransform() -> CATransform3D { var transform = CATransform3DIdentity; transform.m34 = 1.0 / -2000; return transform; } 现在我们就可以完成一个带透视的 x 轴 翻转效果 let transform_perctive = self.makePerspectiveTransform() let transform_3d = CATransform3DRotate(transform_perctive, radians, 1.0, 0.0, 0.0) 然后把这个 transofrm_3d 设置在 layer 的 `transform3D` 属性上就可以了。 ## CardsCollectionViewLayout 和 卡片翻转动画 UICollectionViewLayout 的确才是 UICollectionView 魔术的精髓所在,因为有 layout,才使得他区别于 UITableView,layout 真正控制了 UICollectionView 中所有 cell 的位置。 ### CardsCollectionViewLayout CardsViewControllerViewLayout 继承自 UICollectionFlowLayout, 但其实并没有使用任何 UICollectionFlowLayout 的方法和属性,因为我们的 cell 的坐标都是单独计算出来的。 需要注意的是, layout 并不是直接修改 cell 的 `frame`, `bounds`, `transform` 这些属性来实现 cell 的布局的,而是通过 `UICollectionViewLayoutAttributes` 对象来设置相应的 cell 的位置属性。 像大多数 Layout 的实现那样,我们需要覆盖几个方法: `func collectionViewContentSize() -> CGSize` 用来告诉 `UICollectionView` 内容区域的大小,因为我们并不是挨着整齐排列的,所以并不能把每个 cell 的大小相加就可以的,如果设置的太小就会让很多 cell 并不能出来,因为滚动区太小了,出不来。如果设置的太大又会拖动了一下就到空的地方去了。还好我们这里并不需要精确的计算整个内容去的大小(其实也可以计算出来),因为我们要做成无限轮转的滚动,他的原理就在于,当我们往上或者往下滚动到一个位置时,会突然跳转到另一个位置,因为我们仔细编排了每个 cell 的位置,所以使得这个跳转的过程看不出来而已。我们将在无限轮转的部分来讨论这个实现。 override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool { return true } 这个必须要设置为 true,因为我们需要在滚动的时候实时修改 layout。 `override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes?` 用来告诉 `UICollectionView` 每个 cell 的 `UICollectionViewLayoutAttributes` 对象,其中可以设置 `frame`, `bounds`, `transform`, `transform3D`, `alpha`, `zIndex` 等属性,他们会在对应的 cell 中被应用到实际的显示中。 `override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?` 用来告诉 `UICollectionView` 中一个指定区域内的 cell 的 `UICollectionViewLayoutAttributes` 集合,他一般就是当前可见范围内的 cell 的 attributes 集合。 我们简化了上面两个方法的实现,而把主要的工作都放在了 `override func prepareLayout()` 中,在这里,我们会根据 `UIScrollView` 的 `contentOffset.y` 来计算出每个 cell 的位置,缩放,和翻转的关系,为每个 cell 创建 `UICollectionViewLayoutAttributes`,并把他们保存到一个外部的数组中,所以 `layoutAttributesForItemAtIndexPath` 和 `layoutAttributesForElementsInRect` 只是单纯的读取这个数组而已。`UICollectionView` 熟练的你肯定会发现这里其实有很多可以优化性能的地方。 ### prepareLayout() 的实现 所有丑陋的代码都在 `prepareLayout()` 中。 每当滚动条拖动并导致更新 layout 时, `prepareLayout()` 会被首先调用,然后会根据位置来调用 `layoutAttributesForItemAtIndexPath` 或者 `layoutAttributesForElementsInRect`。而我把所有的 cell 对应的 attributes 对象的创建都放进了 `prepareLayout` 中。 我们先来讨论下 cell 之间的关系: 屏幕中间的 cell 是第一个,沿着 y 轴往上的时候,一个个 cell 都被放在后面,每个 cell 都沿着 y 轴上移 30 个 位置,并且缩小 0.1, 为了让cell相互遮挡,我们为每个 cell 设置了 `zIndex` ,为了看上去更加舒服点,又为每个 cell 的 layou 设置了 shadow, 这样就伪造出了一个 3D 透视的效果。为了方便计算,我们使用一个变量 (ratio) 来标识每个 cell 之间的关系(前后 ratio 缩小 0.1),ratio 等于 1.0 的时候,cell 在屏幕的中间; ratio 等于 0.0 的时候,就是他看不见了,ratio < 0.0 的时候也是应该看不见的;ratio 是需要大于 1.0 的,感觉他应该是越来越大才对,但是我们这里是需要翻转然后消失的,所以 ratio >1.0 的时候,屏幕中间的这个 cell 开始翻转,当到底 ratio = 1.1 的时候,翻转到另一面并消失了。 由于外部有 `UIScrollView`,当我们滚动的时候,我们不需要让所有的 cell 跟着 `UIScrollView` 直接移动,这样只会让所有的 cell 都上下移动而已。我们只需要记住,确定每个 cell 的位置是依靠 ratio,每个 cell 的 ratio 是相互递减的,所以我们只要在滚动 `UIScrollView` 的时候同时修改 ratio 就可以实现所有的 cell 在跟着滚动条变化。 请原谅我实在没有办法把这里的数学描述的更加清楚了,也许我的代码里写的一样的不清楚。但我们通过 ratio 的修改来设置每个 cell 的位置,ratio 在 0 到 1.0 的时候就是沿着 y 移动并放大缩小,当 ratio 在 1.0 到 1.1 的时候对这个 cell 进行翻转和透明度的变化,仅此而已。 `prepareLayout()` 的代码: override func prepareLayout() { super.prepareLayout() var array : [UICollectionViewLayoutAttributes] = [] let offset_y : CGFloat = self.collectionView!.contentOffset.y let max_offset_y = self.collectionView!.contentSize.height - self.collectionView!.bounds.size.height start_offset_y = floor(max_offset_y / 2.0 / 30.0) * 30.0 let reverse_offset_y : CGFloat = start_offset_y - offset_y self.numberOfItems = self.collectionView!.numberOfItemsInSection(0) for a in 0.. 1.0 { /// alpha, 从 1.0 到 0.0 var alpha = (1.1 - ratio) * 10.0 alpha = min(alpha, 1.0) alpha = max(alpha, 0.0) attributes.alpha = alpha /// rotate, 翻转角度从 0 到 -180.0 之间, angle_ratio 从 0.0 到 1.0 var angle_ratio = 1.0 - (1.1 - ratio) * 10.0 angle_ratio = min(angle_ratio, 1.0) angle_ratio = max(angle_ratio , 0.0) /// 不使用 180°,因为这样会从反面翻转过来 let angle : CGFloat = -179.999 * angle_ratio /// 转换成弧度 let radians : CGFloat = angle * CGFloat(M_PI) / 180.0 /// 实现 3D 翻转 let transform_perctive = self.makePerspectiveTransform() let transform_3d = CATransform3DRotate(transform_perctive, radians, 1.0, 0.0, 0.0) attributes.transform3D = transform_3d // print("a:\(a),ratio:\(ratio),alpha:\(alpha),angle:\(angle)") } /// 小于 0 的会反过来,就不用显示了 if ratio > 0 { array.append(attributes) } } self.attributesList = array } #### 对了还要说一下 anchorPoint `anchorPoint` 是每个 CALayer 用来确定相互位置关系时的 `锚点`, 问题在于这个点不大好描述,所以我这里就不展开说了,强烈建议大家看一下这个属性的作用。这里需要说明的是, `anchorPoint` 会影响旋转,因为旋转都是绕着 `anchorPoint` 进行的,而默认的 `anchorPoint` 是 (0.5, 0.5) 也就是中间,所以当我们直接做 x 轴翻转的时候会看到的效果其实不是我们想要的,我们需要的是绕着卡片的底部进行翻转。所以需要把 cell 的 `anchorPoint` 设置为底部 (0.5, 1.0), 但是 `anchorPoint` 是用来确定 CALayer 和他父层 CALayer 之间定位关系的点,修改了 `anchorPoint` 会把位置也改了,所以我们还需要同时修改 layer 的 `frame`用来补偿因为修改 `anchorPoint` 而偏移的位置。 在 `CardsCollectionViewCell` 中,`applyLayoutAttributes` 来设置 layer 的 `anchorPoint`,这样保证在每个使用 attributes 时都有一个正确的 `anchorPoint`。 override func applyLayoutAttributes(layoutAttributes: UICollectionViewLayoutAttributes) { super.applyLayoutAttributes(layoutAttributes) self.layer.anchorPoint = CGPointMake(0.5, 1.0) } 同时需要注意在 `prepareLayout` 中对每个 cell 进行位置计算时增加偏移。 var center_y : CGFloat = self.collectionView!.bounds.height / 2.0 + offset_y + self.cardHeight / 2.0 /// 以中间为固定位置,要加上因为修改 anchor 的 y 偏移 ### 改善拖动和吸附位置 因为我们根据 `UIScrollView` 的 滚动位置来确定每个 cell 的位置和翻转状态的,因此如果 `UIScrollView` 滚动到一个不精确的位置,那就可能只看到有个页面翻转了一半就停在那里了。这样实在太奇怪了,所以我们需要让 `UIScrollView` 滚动到一个位置的时候吸附的停在我们需要的位置上。 有的时候我们可以使用 `pagingEnabled` 属性,这里用起来有点麻烦,还好我还发现了另一个方法,`targetContentOffsetForProposedContentOffset`, 可以告诉你预计结束的时候的 `contentOffset` 的位置,你可以根据这个值改一下,并且返回一个修改过的值并让滚动条最后停在这个位置上(太神奇了)。由于我们每个 cell 间的 y 轴距离是 30 个 Point, 所以只需要让滚动条停止在最近的 30 的整数倍上面就可以了。 override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { // print("offset:\(proposedContentOffset)") /// 每个 cell 之间的 y 距离是30.0,所以要保证最后停在 30.0 的整数倍上面 var targetContentOffset = proposedContentOffset let (total,more) = divmod(targetContentOffset.y, b: y_distance_in_cells) if more > 0.0 { if more >= y_distance_in_cells / 2.0 { targetContentOffset.y = ceil(total) * y_distance_in_cells } else { targetContentOffset.y = floor(total) * y_distance_in_cells } } return targetContentOffset } ## 无限轮转 轮转图是上古时代企业网站最喜欢的工具,到了 App 中由于屏幕限制依旧随处可见。他们就像单曲循环一样有时让人感到厌烦。 我们这里也使用轮转,这样你无论往上还是往下都可以随意的滚动。 轮转的原理都是一样的,在你滚动到一个边界值的时候(最大或者最小),他就突然跳转到另一边去,这样就重新开始滚动了,我们只是没发现这个过程而已。所以,我们要准备用来轮转的图要比实际使用的多一些。 我们这里有 10 张图做的轮转效果,每张图(cell)之间的 y 轴距离是 30, 所以最大的 y 轴 滚动距离应该是 300, 所以每当超过这个值的时候,就跳转到开始的地方,另一个方向也是一样的。 但是 `UIScrollView` 不可以超出滚动区域很多的地方,所以我们不可以滚动到 `conentOffset.y <0` 很远的距离,一松开就会弹回到 0。 因此我们的 cell 一开始就不是在 `UICollectionView` 顶上开始绘制的,实际是在 `contentSize` 的中间开始绘制,这样你可以往上和往下进行滚动。同时在开始的时候滚动条就应该定位到这个位置。 为了让整个滚动的跳转看不出啥破绽,我准备了足够多的轮转图片,也就是把实际轮转的内容数量乘以 3 倍(其实完全用不着这么多),这样前后都有足够的 cell 做显示。 因此,我原来轮转的图片是 0 - 9 一共 10 张,实际我准备了 0 - 29 一共 30 张,而我在 UIScrollView 中开始时的是 9-18,当滚动到第19张时,会跳转到第 9 张,当滚动到第 8 张时会跳转到第 18 张. func scrollViewDidScroll(scrollView: UIScrollView) { let translate = scrollView.contentOffset.y - self.start_offset_y NSLog("scroll:%f, %f", scrollView.contentOffset.y, translate) let target_scroll_y : CGFloat = 30.0 * CGFloat(self.images.count) if abs(translate) >= target_scroll_y { scrollView.setContentOffset(CGPointMake(0.0, self.start_offset_y), animated: false) } } ## Demo 最后实现的代码: [CardsAnimationDemo](https://github.com/adow/CardsAnimationDemo) ## 参考 * [如何实现炫酷的卡片式动画!](http://www.cocoachina.com/ios/20151013/13700.html)