iOS开发—创建一个 iOS 图书打开动画:第 1 部分

2,509 阅读14分钟

在这个由两部分组成的教程系列中,您将有一个漂亮的 iOS 书籍打开动画和翻页动画:

  • 在第 1 段中,您将学习如何自定义组合视图并应用深度和按压以使应用程序看起来更象部分。
  • 在第 2 段中,以不同的方式在不同的学习方式之间创建自定义转换,并集成以部分在视图之间自然、不同的转换。

本教程适用于中高级开发人员;您将使用自定义转换和自定义组合视图布局。

入门

在结尾下载本教程的入门项目;提取 zip 文件的内容,然后在 Xcode 中打开Paper.xcodeproj 。

在点击中创建并运行项目您将看到以下内容:

01.gif

该应用程序您几乎完全制作;您可以滚动浏览您的图书馆,并选择有最喜欢的书籍之一进行查看。但是,最后一次阅读一本并排的书是什么时候?一点 UICollectionView 的专业知识,你就可以把照片装扮得非常漂亮!

项目结构

下面是入门项目最重要部分的简要介绍:

数据模型文件夹包含三个文件:

  • Books.plist包含样本书籍数据。每本书都包含一个图像封面以及一幅代表图画的图像。
  • BookStore.swift是一个单例,在应用程序的生命周期中只创建一次。BookStore的工作是从Books.plist加载数据并创建 Book 对象。
  • Book.swift是一个类,用于存储与书籍相关的信息,例如书籍封面、每页索引的图像以及页数。

Books文件夹包含两个文件:

  • BooksViewController.swiftUICollectionViewController.此类负责水平显示您的书籍列表。
  • BookCoverCell.swift封面是您所有的书籍;它由BooksViewController使用。

图书目录中,您将找到以下内容:

  • BookViewController.swift 也是UICollectionViewController的目的是在您从 BooksViewController中选择一本书时显示该书的页面。
  • BookPageCell.swiftBookViewController**使用显示一本一本被的所有页面。

这是最后一个文件夹Helpers中的内容:

  • UIImage+Helpers.swift是该扩展包含一种方法,一种圆角图像,另一种用于将图像缩小到给定大小。UIImage

就这样!我们的审查 - 是时候放下一些代码了!

自定义地图

首先,您需要覆盖BooksViewController的集合的默认布局。一个面积显示整整一个屏幕的大书展示。将其缩小一点以看起来更顺畅,如下:

02.gif

当您在滚动时,最靠近屏幕的中心会变大,以显示它是选择。您继续时,书会显示滚动图像,以您将其被带到一边时活动。

在 App* s下创建一个名为Layout的组。然后选择iOS 组,然后选择新的文件布局,然后**选择文件**...,以成为UICollectionViewFlowLayout的子类,并将语言设置为Swift*。

使用您的新布局,您需要指示BooksViewController的集合视图。

打开Main.storyboard,点击BooksViewController然后点击Collection View。在Attributes Inspector中,将Layout设置为CustomClass**设置为BooksLayout ,如下所示

03.png

打开 BooksLayout.swift并在**BooksLayout类声明添加以下代码。

让PageWidth : Clo GF = 362
让Height : CGFloat  = 568   

这两个特征将用于设置单元格的大小。

现在在类花中添加以下括号:

需要初始化(编码器aDecoder:NSCoder) {
   super . init(编码器:aDecoder)
  滚动方向=  
  UICollectionViewScrollDirection 水平方向//1 
  itemSize = CGSizeMake ( PageWidth , PageHeight ) //2 
  minimumInteritemSpacing = 10 //3    
}

下面是代码的作用:

  1. 将集合视图的滚动视图设置为水平。
  2. 将单元格的大小设置为362的页面宽度和568的高度。
  3. 将单元格之间的元件设置为10

,在后面添加以下代码init(coder:)

函数覆盖prepareLayout(){
   super .prepareLayout()
  
  //我们滚动集合的视图。
  //1 
  collectionView ? .decelerationRate =  UIScrollViewDecelerationRateFast
  
  //2
  集合视图?.contentInset =  UIEdgeInsets (
    顶部:0,
    左:collectionView !.bounds.width /  2  -  PageWidth  /  2,
    底部:0,
    右:collectionView ! .bounds.width /  2  -  PageWidth  /  2
  )
}

prepareLayout()让您有机会在为每个单元格提出任何布局之前执行任何计算。

  1. 设置用户头像后显示停止滚动的速度。将设置为滚动查看将添加到它的UIScrollViewDecelerationRateFast位置。尝试通过查看NormalvsFast有什么不同!
  2. 集合视图的插件,以便第一本书的封面始终居中。

现在您需要处理每个单元格的布局信息。

在下面添加以下代码prepareLayout()

覆盖func  layoutAttributesForElementsInRect ( rect : CGRect ) -> [ AnyObject ]?{
   //1 
  var array =  super .layoutAttributesForElementsInRect(rect) as![ UICollectionViewLayoutAttributes ]
  
  //2
  对于目录中的属性{
    //3 
    var frame = attributes.frame
     //4 
    var distance =  abs (collectionView ! .contentOffset.x + collectionView ! .contentInset.left - frame.origin.x)
     //5 
    var scale =  0.7  *  min ( max ( 1  - distance / (collectionView ! .bounds.width), 0.75 ), 1 )
     //6 
    attributes.transform = CGAffineTransformMakeScale(规模,规模)
  }
  
  返回目录
}

layoutAttributesForElementsInRect(_:)返回一个物品,它为每个单元UICollectionViewLayoutAttributes格提供。

  1. 调用的超类属性会layoutAttributesForElementsInRect返回一个列表,其中包含每个单元格的所有默认布局。
  2. 循环遍历中的每个属性。
  3. 抓取当前单元格属性的框架。
  4. 书籍计算封面(即单元格)与屏幕中心之间的距离。
  5. 根据上面的距离,在 075 和 1 之间计算你缩放所有的封面。然后,将缩放0.7的封面以使它们保持美观和美观。
  6. 最后,将剧情简介。

,在后面添加以下代码layoutAttributesForElementsInRect(_:)

覆盖函数 shouldInvalidateLayoutForBoundsChange ( newBounds : CGRect ) -> Bool {
  返回真
}

true在滚动时更改其属性,这非常适合重新计算单元格的属性。UICollectionView

制造并运行您的应用程序;会看到中间的那本书比其他书大:

04.gif

滚动浏览书籍以每本书的封面如何放大和缩小。,但是这本书可以卡进去查看,如果选择,那不是很好吗?

您将添加的下一个方法将完成这一点!

抓拍到一本书

targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:)确定集合视图在哪一个滚动停止,并返回建议的一点测量以设置集合视图的contentOffset

在后面添加以下代码shouldInvalidateLayoutForBoundsChange(_:)

覆盖func  targetContentOffsetForProposedOffset (建议Offset : CGPoint , withScrollingVelocity Speed  : CGPoint ) -> CGPoint {
   // 将单元格瞄准中心
  //1 
  var newOffset =  CGPoint ()
   //2 
  var layout = collectionView ! .collectionViewLayout为! UICollectionViewFlowLayout 
  //3 
  var width = layout.itemSize.width + layout.minimumLineSpacing
   //4 
  var偏移量=提议的内容偏移量.x +集合视图!.contentInset.left
  
  //5 
  if velocity.x >  0 {
     //ceil 返回下一个最大的数字
    offset = width * ceil(offset / width)
  } else  if velocity.x ==  0 { //6 
    //对参数进行四舍五入
    offset = width * round(offset / width)
  } else  if velocity.x <  0 { //7 
    //删除参数的小数部分
    offset = width * floor(offset / width)
  }
  //8 
  newOffset.x = offset - collectionView !.contentInset.left
  newOffset.y =建议的ContentOffset.y //y 将始终相同... 
  return newOffset
}

以下是用户抬起手指后计算书籍封面建议偏移量的方法:

  1. 新建一个CGPoint名为newOffset.
  2. 获取集合视图的当前布局。
  3. 获取单元格的总宽度。
  4. 计算相对于屏幕中心的当前偏移量。
  5. 如果velocity.x > 0,则用户正在向右滚动。将offset/width其视为您要滚动到的图书索引。
  6. 如果velocity.x = 0,则用户没有在滚动中投入足够的精力,并且同一本书仍然处于选中状态。
  7. 如果velocity.x < 0,则用户正在向左滚动。
  8. 更新新的x偏移量并返回。这保证了一本书将始终居中。

构建并运行您的应用程序;再次滚动它们,您应该注意到滚动动作要快得多:

要完成此布局,您需要创建一种机制来限制用户仅单击中间的书。截至目前,您目前可以点击任何书籍,无论其位置如何。

打开BooksViewController.swift并将以下代码放在注释下// MARK: Helpers

func  selectedCell () -> BookCoverCell ? {
  如果 让indexPath = collectionView ?.indexPathForItemAtPoint( CGPointMake (collectionView ! .contentOffset.x + collectionView ! .bounds.width /  2 , collectionView ! .bounds.height /  2 )) {
     if  let cell = collectionView ? .cellForItemAtIndexPath(indexPath)作为? BookCoverCell {
      返回单元格
    }
  }
  返回 零
}

selectedCell()将始终返回中间单元格。

接下来,替换openBook(_:)为以下内容:

func  openBook () {
  让vc =故事板? .instantiateViewControllerWithIdentifier( "BookViewController" )为! BookViewController 
  vc.book = selectedCell() ?.book
   // UICollectionView 在后台线程上加载它的单元格,因此请确保在将其传递给动画处理程序之前加载它
  dispatch_async(dispatch_get_main_queue(), { () -> Void  in 
    self .navigationController ? .pushViewController(vc, animated: true )
    返回
  })
}

这只是使用selectedCell您编写的新方法,而不是将 abook作为参数。

接下来,替换collectionView(_:didSelectItemAtIndexPath:)为以下内容:

覆盖 func  collectionView ( collectionView : UICollectionView , didSelectItemAtIndexPath  indexPath : NSIndexPath ) {
  打开书()
}

这只是删除了在所选索引处打开图书的代码;现在您将始终在屏幕中央打开这本书。

构建并运行您的应用程序;您现在会注意到视图中心的书始终是打开的书。

你已经完成了BooksLayout。是时候让屏幕上的书更逼真了,让用户在书中翻页!

翻书版式

这是您要拍摄的最终效果:

05.gif

现在看起来更像一本书!

在Book组下创建一个名为Layout的组。接下来,右键单击Layout文件夹并选择New File...,然后选择iOS\Source\Cocoa Touch Class模板并单击Next。将类命名为BookLayout,使其成为UICollectionViewFlowLayout的子类,并将Language设置为Swift

和以前一样,您的图书收藏视图需要使用新布局。打开Main.storyboard并选择Book View Controller Scene。选择集合视图并将Layout设置为Custom。最后,将布局设置为BookLayout,如下图:

06.png

打开BookLayout.swiftBookLayout并在类声明上方添加以下代码:

私有 让 PageWidth : CGFloat  =  362
私有 让 PageHeight : CGFloat  =  568
私有 var numberOfItems =  0

您将使用这些常量变量来设置每个单元格的大小;此外,您还可以跟踪书中的总页数。

接下来,在类声明中添加以下代码:

覆盖 func  prepareLayout () {
  超级.prepareLayout()
  集合视图?.decelerationRate =  UIScrollViewDecelerationRateFast 
  numberOfItems = collectionView !.nu​​mberOfItemsInSection( 0 )
  集合视图?.pagingEnabled = 真
}

这与您在 中所做的类似,但BooksLayout有以下区别:

  1. 将减速率设置为UIScrollViewDecelerationRateFast以增加滚动视图减速的速率。
  2. 获取当前书籍的页数。
  3. 启用分页;这让视图以集合视图框架宽度的固定倍数滚动(而不是默认的连续滚动)。

仍然在BookLayout.swift中,添加以下代码:

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

同样,true每次用户滚动时,return 都会更新布局。

接下来,通过覆盖为集合视图指定内容大小,collectionViewContentSize()如下所示:

覆盖 func  collectionViewContentSize () -> CGSize {
   return  CGSizeMake (( CGFloat (numberOfItems /  2 )) * collectionView ! .bounds.width, collectionView ! .bounds.height)
}

这将返回内容区域的整体大小。内容的高度将始终保持不变,但内容的整体宽度是项目数(即页面)除以 2 乘以屏幕宽度。你除以二的原因是书页是双面的。页面两边都有内容。

就像您在 中所做的那样BooksLayout,您需要覆盖layoutAttributesForElementsInRect(_:)以便可以将分页效果添加到您的单元格。

在后面添加以下代码collectionViewContentSize()

覆盖 func  layoutAttributesForElementsInRect ( rect : CGRect ) -> [ AnyObject ] ?{
   //1 
  var数组:[ UICollectionViewLayoutAttributes ] = []
  
  //2 
  for i in  0  ...  max ( 0 , numberOfItems -  1 ) {
     //3 
    var indexPath =  NSIndexPath (forItem: i, inSection: 0 )
     //4 
    var attributes = layoutAttributesForItemAtIndexPath(indexPath)
     if attributes !=  nil {
       //5
      数组+= [属性]
    }
  }
  //6
  返回数组
}

不是像在 中那样计算此方法中的属性BooksLayout,而是将此任务留给layoutAttributesForItemAtIndexPath(_:),因为在本书实现中的任何给定时间,所有单元格都在可见矩形内。

下面逐行解释:

  1. 创建一个新数组来保存UICollectionViewLayoutAttributes.
  2. 遍历集合视图中的所有项目(页面)。
  3. 对于集合视图中的每个项目,创建一个NSIndexPath.
  4. 获取当前的属性indexPath。你layoutAttributesForItemAtIndexPath(_:)很快就会覆盖。
  5. 将属性添加到您的数组中。
  6. 返回所有单元格属性。

处理页面几何

在你直接开始实现layoutAttributesForItemAtIndexPath(_:). :]

07.png

上图显示,每一页都以书脊为旋转轴翻转。图表上的比率范围从*-1.01.0*。为什么?好吧,想象一本书放在桌子上,书脊代表 0.0。当您从左向右翻页时,“翻转”比率从 -1.0(全左)变为 1.0(全右)。

因此,您可以用以下比率表示您的翻页:

  • 0.0表示页面呈90度角,垂直于表格。
  • +/- 0.5表示页面与桌子成45度角。
  • +/- 1.0表示页面与表格平行。

请注意,由于角度旋转是逆时针方向的,因此角度的符号将与比率的符号相反。

首先,在 之后添加以下辅助方法layoutAttributesForElementsInRect(_:)

//MARK: - 属性逻辑助手

func  getFrame ( collectionView : UICollectionView ) -> CGRect {
   var frame =  CGRect ()
  
  frame.origin.x = (collectionView.bounds.width /  2 ) - ( PageWidth  /  2 ) + collectionView.contentOffset.x
  frame.origin.y = (collectionViewContentSize().height -  PageHeight ) /  2 
  frame.size.width =  PageWidth 
  frame.size.height =  PageHeight
  
  返回帧
}

对于每一页,您计算相对于集合视图中间的框架。getFrame(_:)将使每一页的边缘与书脊对齐。唯一改变的变量是集合视图在x方向上的内容偏移量。

接下来,在 之后添加以下方法getFrame(_:)

func  getRatio ( collectionView : UICollectionView , indexPath : NSIndexPath ) -> CGFloat {
   //1 
  let page =  CGFloat (indexPath.item - indexPath.item %  2 ) *  0.5
  
  //2 
  var ratio: CGFloat  =  - 0.5  + page - (collectionView.contentOffset.x / collectionView.bounds.width)
  
  //3
  如果比率>  0.5 {
    比率=  0.5  +  0.1  * (比率-  0.5 )
    
  } else  if ratio <  - 0.5 {
    比率=  - 0.5  +  0.1  * (比率+  0.5 )
  }
  
  回报率
}

上面的方法计算页面的比例。依次查看每个评论部分:

  1. 计算书中一页的页码——记住书中的页面是双面的。乘以0.5可以为您提供您所在的确切页面。
  2. ratio根据您正在翻页的加权百分比计算。
  3. 您需要将页面限制在*-0.50.5范围之间的比率。乘以0.1*会在每个页面之间创建一个间隙,使其看起来像它们重叠。

计算出比率后,您将使用它来计算翻页的角度。

在后面添加以下代码getRatio(_:indexPath:)

func  getAngle ( indexPath : NSIndexPath , ratio : CGFloat ) -> CGFloat {
   // 设置旋转
  var angle: CGFloat  =  0
  
  //1 
  if indexPath.item %  2  ==  0 {
     // 书脊在页面左侧
    angle = ( 1 - ratio) *  CGFloat ( - M_PI_2 )
  } else {
     //2 
    // 书脊在页面的右边
    angle = ( 1  + ratio) *  CGFloat ( M_PI_2 )
  }
  //3 
  // 确保奇偶页面没有完全相同的角度
  angle +=  CGFloat (indexPath.row %  2 ) /  1000 
  //4
  返回角度
}

有一些数学运算正在进行,但是当你分解它时它并没有那么糟糕:

  1. 检查当前页面是否是偶数。这意味着该页面位于书脊的右侧。向右翻页是逆时针方向,书脊右侧的页面具有负角度。回想一下,您定义的比率介于*-0.50.5*之间。
  2. 如果当前页面是奇数,则该页面位于书脊的左侧。向左翻页是顺时针方向,书脊左侧的页面具有正角度。
  3. 为每一页添加一个小角度,使页面有一些分离。
  4. 返回旋转的角度。

一旦你有了角度,你需要转换每一页。添加以下方法:

func  makePerspectiveTransform () -> CATransform3D {
   var transform =  CATransform3DIdentity 
  transform.m34 =  1.0  /  - 2000
  返回变换
}

修改变换矩阵的m34给每一页增加一点透视。

现在是时候应用旋转了。添加以下代码:

func  getRotation ( indexPath : NSIndexPath , ratio : CGFloat ) -> CATransform3D {
   var transform = makePerspectiveTransform()
   var angle = getAngle(indexPath, ratio: ratio)
  transform =  CATransform3DRotate (transform, angle, 0 , 1 , 0 )
  返回变换
}

在这里,您使用前面的两个辅助方法来计算变换和角度,并创建一个CATransform3D沿 y 轴应用于页面的方法。

现在您已经设置了所有辅助方法,您终于可以为每个单元格创建属性了。在之后添加以下方法layoutAttributesForElementsInRect(_:)

覆盖 func  layoutAttributesForItemAtIndexPath ( indexPath : NSIndexPath ) -> UICollectionViewLayoutAttributes!{
   //1 
  var layoutAttributes =  UICollectionViewLayoutAttributes (forCellWithIndexPath: indexPath)
		
  //2 
  var frame = getFrame(collectionView ! )
  layoutAttributes.frame =框架
		
  //3 
  var ratio = getRatio(collectionView ! , indexPath: indexPath)
	
  //4
  如果比率>  0  && indexPath.item %  2  ==  1 
     || ratio <  0  && indexPath.item %  2  ==  0 {
     // 确保封面始终可见
    if indexPath.row !=  0 {
       return  nil
    }
  }	
  //5 
  var rotation = getRotation(indexPath, ratio: min ( max (ratio, - 1 ), 1 ))
  layoutAttributes.transform3D =旋转
		
  //6
  如果indexPath.row ==  0 {
    layoutAttributes.zIndex =  Int .max
  }
		
  返回布局属性
}

您将为集合视图中的每个单元格调用此方法:

  1. 为给定的单元格创建一个UICollectionViewLayoutAttributes对象 NSIndexPath
  2. 使用您创建的方法设置属性的框架,getFrame以确保它始终与书脊对齐。
  3. getRatio使用您之前编写的 计算集合视图中项目的比率。
  4. 检查当前页面是否在比率的阈值内。如果不是,请不要显示单元格。出于优化目的(并且出于常识),您不会显示页面的背面,而只会显示正面的页面 - 除非它是您始终显示的书的封面。
  5. 使用您计算的给定比率应用旋转和变换。
  6. 检查是否indexPath是第一页。如果是这样,请确保它zIndex始终位于其他页面的顶部以避免闪烁效果。

构建并运行你的应用程序,打开你的一本书,翻阅它......哇,什么?

08.png

页面似乎锚定在它们的中心——而不是边缘!

09.png

如图所示,每个页面的锚点对于xy*都设置为**0.5 。*你能告诉你需要做什么来解决这个问题吗?

10.png

很明显,您需要将页面的锚点更改为其边缘。如果页面位于书的右侧,则锚点应为*(0, 0.5)。但如果页面位于书的左侧,则锚点应为(1, 0.5)*。

打开BookPageCell.swift并添加以下代码:

override  func  applyLayoutAttributes ( layoutAttributes : UICollectionViewLayoutAttributes !) {
   super .applyLayoutAttributes(layoutAttributes)
   //1 
  if layoutAttributes.indexPath.item %  2  ==  0 {
     //2 
    layer.anchorPoint =  CGPointMake ( 0 , 0.5 )
    isRightPage =  true 
    } else { //3 
      //4 
      layer.anchorPoint =  CGPointMake ( 1 , 0.5 )
      isRightPage = 假
    }
    //5 
    self .updateShadowLayer()
}

在这里您覆盖applyLayoutAttributes(_:),它应用在BookLayout中创建的布局属性。

这是非常简单的代码:

  1. 检查当前单元格是否是偶数。这意味着该书的书脊位于页面的左侧。
  2. 将锚点设置在单元格的左侧并设置isRightPagetrue。此变量可帮助您确定页面的圆角应该在哪里。
  3. 如果当前单元格是奇数,则书脊位于页面右侧。
  4. 将锚点设置在单元格的右侧并设置isRightPagefalse
  5. 最后,更新当前页面的阴影层。

构建并运行您的应用程序;翻阅页面,事情应该看起来更好一点:

11.gif

这就是本教程的第一部分!花一些时间沉浸在您所创造的荣耀中——这是一个非常酷的效果!

结论

您可以从第 1 部分下载包含所有源代码的已完成项目

您从集合视图的默认布局开始,并学会了自定义新布局以将其变成真正令人惊叹的东西!使用此应用程序的人会觉得他们正在翻阅一本真正的书。正是这些小事将普通的阅读器应用程序变成了人们可以真正感受到联系的东西。

但是,您还没有完成!在本教程的第 2 部分中,您将让这个应用程序变得更好、更直观,您将在其中探索关闭和打开的图书视图之间的自定义转换。

这里也推荐一些面试相关的内容!