多任务卡片动画实现原理

实现效果


控件 — UICollectionView


这个动画是用 UICollectionView 实现的,简单讲下 UICollectionView 的工作原理。这里用到的 UICollectionView 也就3部分:ViewController(简称VC)、UICollectionViewCellUICollectionViewLayout

  1. ViewController
    在VC里,UICollectionView 的用法跟 UITableView 的用法类似。这里的初始化方法与 UITableview 有所不同,多了个 collectionViewLayout 属性,每个 collectionView 都会绑定一个 UICollectionViewLayout 对象, collectionView 根据这个 layout 对象来布局 cell

  2. UICollectionViewCell
    这里用的 Cell 实现起来和 UITableViewCell 没什么大区别,我们只要实现它的 initwithFrame 的初始化方法即可,然后实现你想要的布局。

  1. UICollectionViewLayout
    UICollectionViewLayout

UICollectionViewLayout 是让 UICollectionView 千变万化的精髓所在,所以上面的动画的重点也就是在 layout 里实现的。
系统提供了一个 UICollectionViewFlowLayout ,是一个流式布局,可以通过设置 scrollDirection 来指定滚动方向,如果这个系统提供的布局不能满足我们的需求,那我们就要自己实现一个 UICollectionViewLayout 的子类来达到我们想要的效果了,接下来说下自定义的 layout 需要重写哪几个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 当layout第一次展示时调用,显式或隐式地调用invalidatedlayout也会调用prepareLayout,During each layout update, the collection view calls this method first to give your layout object a chance to prepare for the upcoming layout operation
- (void)prepareLayout;

// 返回collectionView的contentSize,来决定collectionView的滚动范围
- (CGSize)collectionViewContentSize;

// Returns the content offset to use after an animated layout update or change。如果你的动画有两个layout的切换,那么这个方法至关重要,用它来返回一个目标contentOffset,能保证动画的正常表现。
-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset;

// Returns the layout attributes for all of the cells and views in the specified rectangle.
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect;

// 重中之重的方法,用来计算指定indexPath所对应的布局信息。这里的布局信息是一个UICollectionViewLayoutAttributes对象,我们可以通过frame、center、transform3D、transform来控制cell的表现状态。最后将这个attributes对象返回给上层使用。
-(UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath;

动画实现细节


上面是关于 UICollectionView 的一些初步介绍,还有很多使用细节待君探索。接下来说下我们这个动画的实现细节吧。

上面的动画中,包括了位移、缩放、透明度三个方面的变化,我们先来谈谈重点的位移部分吧。

思路:动画的效果是,随着手指的滑动, cell 从上往下慢慢位移,位移的过程中速度越来越快;相反,如果往上滑动, cell 移动的速度就慢慢变小。这里的自变量是 collectionView.contentOffset.y(竖直方向),因变量是 cell.center.y (或者说 cell.frame.origin.y ),所以需要为他们之间找一个函数,这个函数需要满足上面的动画效果。

根据变化的规律,先确定一个初步的函数:

它符合从小到大变化递增的特性,可以通过一些在线工具查看函数的图像来比较直观的看到函数的变化规律

可以看到这个函数是有两段的,那我们需要的只是左边这部分,因为当我们手指下滑的时候, collectionView.contentOffset.y 实际上是变小的,而 cell.origin.y 却是在变大的,所以左边半部分的变化正是我们想要的。

然而

并不能满足要求,需要将它①向右拉伸m个点、②向上拉伸n个点,也就是要构造一个函数:

这里解释一下m、n的值的含义。

做上述两步变化,m = 600,n = 500,生成如下函数

函数与 x 轴交点是 (600,0),与 y 轴交点是 (0,500)。
(0,500) 比较好理解,就是当 collectionView.contentOffset.y 等于 0 的时候, cell 对应的y坐标为 500。
(600,0) 也不难理解,就是当我们手指往上滑 600 个点,使 collectionView.contentOffset.y=600 时,这个时候 cell.origin.y 会等于 0。

函数的具体应用


定义第 0 个 cell 的位移函数

假设第 0 个 cell(简称 cell0 )对应函数的 m、n 分别为 n0 = 250,m0 = 1000,即当 collectionView.contentOffset.y=0 时, cell0.origin.y=250 ;当我们往上滑1000个点, collectionView.contentOffset.y=1000 时, cell0.origin.y=0 。同理,
ni 则表示当 contentOffset.y=0 时,第 i 个 cell 的 y 坐标。mi 则表示当 contentOffset.y=mi 时,第 i 个 cell 的 y 坐标为 0。

所以对于第 0 个 cell ,我们可以给出一个函数来计算它的 y 坐标:

这里 x 是指 collectionView.contentOffset.y ,y0 是指 cell0.origin.y

生成第 i 个 cell 的位移函数

定义了cell0 的位置函数,就可以以一定规律生成 cell1、cell2…… 的位置函数了,也就是生成每个cell 的 m、n 值。

mi 的计算

我们可以定义,当手指往下滑动140个点时,第1个 cell 会运动到第2个 cell的位置,以此类推,每个 cell 会运动到下一个 cell 的位置。所以我们定义 $m_i=m_{i-1}+140$ 也就是 $m_i=m_0 + itimes140$; (ps:这里140决定了 cell 之间的间距,当然可以根据需求改变这个值来调整视觉效果)

ni 的计算

需要通过 $y_0=((1000-x)/1000)^4times250$ 来计算。我们知道,当 x=0 时,$y_0=n_0=250$。在上一步计算时,我们定义了手指往下滑140时第0个 cell 会运动到第1个 cell 的位置,也就是说 cell1 的位置 $n_1$ 可以由 $y_0=((1000-x)/1000)^4times250$ 得到,**这里 x 的值应该是手指从 0 下滑140个点,也就是 collectionView.contentOffset.y=-140 ,所以x=(-140)**。(ps:往下滑动时 contentOffset.y 是递减的,所以这里的x是负的140。)所以

同理,可以推出 ni 的公式:

yi 的公式

ok,mi 和ni 都可由 m0、n0 得到,那么 yi 的公式

就可以转化成 m0 和 n0 的表达式,即

虽然这个函数看起来挺长的,但是其中 m0和n0 都是我们定的初始值,140 也是我们定义的常量。变量 x 就是 contentOffset.y 。所以到此我们已经能根据手指的滑动,计算出每个 cell 的 y 坐标,从而实现了这个滚动动画。

公式体现在下面的函数里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#pragma mark -- 公式
//根据下标、当前偏移量来获取对应的y坐标
-(CGFloat)getOriginYWithOffsetY:(CGFloat)offsetY row:(NSInteger)row
{
// 公式: y0 = ((m0 - x)/m0)^4*n0
// 公式: yi=((m0 + i*140-x)/(m0 + i*140))^4*((m0+140*i)/m0)^4*n0
CGFloat x = offsetY; //这里offsetY就是自变量x
CGFloat ni = [self defaultYWithRow:row];
CGFloat mi = self.m0+row*self.deltaOffsetY;
CGFloat tmp = mi - x;
tmp = fmaxf(0, tmp); //不小于0
CGFloat y = powf((tmp)/mi, 4)*ni;
// NSLog(@"%d--y:%f ",(int)row,y);
return y;
}

//获取当contentOffset.y=0时每个cell的y值
-(CGFloat)defaultYWithRow:(NSInteger)row
{
CGFloat x0 = 0; //初始状态
CGFloat xi = x0 - self.deltaOffsetY*row;
CGFloat ni = powf((self.m0 - xi)/self.m0, 4)*self.n0;
// NSLog(@"defaultY-%d: %f",(int)row,ni);
return ni;
}

转自