在使用`Twitter`的APP后,我已开发者的视觉并注意到整体与部分之间相互协调是件极其有意思的事情。这引起了我的好奇心:这是怎么做到的?
让我们具体地讨论下这个视图布局:此效果不优雅吗?它看起开就像本应如此,但你仔细的观察后就会发现更多。随着`Scrollview`的偏移,图层的覆盖,动作和比例缩放是那么的平滑连贯… … 实在是太喜欢这个效果了。
So,就让我们立刻实现这个效果吧。
首先,先看下最终效果:
### 结构描述
—
在写代码之前,我想给你一个关于如何构建UI的简单意见。
打开`Main.storyboard`文件,在这个控制器里面你会发现2个主要的对象。第一个是一个呈现`Header`的视图,第二个是`Scrollview`,它包含了`Avatar`和账号相关的其他信息,如:`username`标签和`Follow`按钮。还有一个被叫做`Sizer`的视图,它是为了确保`Scrollview`拥有足够大的垂直滑动的空间。
就像你看到的那样,这个结构非常的简单。稍微注意一下就可发现`Header`的外部放置了一个`Scrollview`,而不是与其他元素放置在一起。虽然没必严格如此,但这样会使它的结构变动更加灵活。
### 编码
—
如果你仔细的看了最后的动画,将会注意到你要管理2个不同的动作:
1. 向下拉(当`Scrollview`已经停靠在屏幕的顶部的时候)
2. 上下滑动
第二个动作可以细分为4个小步骤:
+ 向上滑动,一直到导航条默认的大小并停靠在屏幕的顶部。
+ 向上滑动,`Avatar`开始逐渐变小。
+ 当`Header`被固定后,`Avatar`会移动到它的下边。
+ 当`username`标签抵达`Header`的顶部时,一个新的白色`Label`将会从`Header`中心的底部展现。这时`Header`的背景图片将会用高斯模糊渲染。
打开`ViewController`让我们一个一个的实现这些步骤。
### 构建管理者
—
首先要做的事情很明显,就是获取关于`Scrollview`的偏移量`offset`。我们可以通过`UIScrollViewDelegate`协议实现`scrollViewDidScroll`方法。
在一个`View`上执行最简单地动画方式是使用`Core Animation`逐渐的进行三维变换,并给`layer.transform`赋予新值。
关于`Core Animation`可以参考这篇文章
http://www.thinkandbuild.it/playing-around-with-core-graphics-core-animation-and-touch-events-part-1/
这些是`scrollViewDidScroll:`方法的第一部分
CGFloat offset = scrollView.contentOffset.y;
CATransform3D avatarTransform = CATransform3DIdentity;
CATransform3D headerTransform = CATransform3DIdentity;
在这里我们获取一个当前垂直偏移量`offset`,并初始化2个`CATransform3D`变量。
### 下拉
—
下拉动作的管理:
if (offset < 0) {
CGFloat headerScaleFactor = -(offset) / header.bounds.size.height;
CGFloat headerSizevariation = (header.bounds.size.height * (1.0 + headerScaleFactor) - header.bounds.size.height) / 2.0;
headerTransform = CATransform3DTranslate(headerTransform, 0, headerSizevariation, 0);
headerTransform = CATransform3DScale(headerTransform, 1.0 + headerScaleFactor, 1.0 + headerScaleFactor, 0);
header.layer.transform = headerTransform;
}
首先,我们检查`offset`是否为负数:用户在下拉的过程中,将会进入`Scrollview`的弹性区域。
剩下的代码就是简单的数学逻辑。
`Header`的扩大是因为它的上边缘固定于屏幕的顶部,而底部的图片在等比缩放。
`the transformation is made by scaling and subsequently translating to the top for a value equal to the size variation of the view. `实际上,移动`ImageView`图层的中点到顶部并同时缩放它,你可以获得相同的效果。
`headerScaleFactor`是用来被计算的一部分。我们想用`offset`适当的对`Header`进行缩放。换句话说,当`offset`是`Header`高度的2倍时,`headerScaleFactor`必须是2.0。
我们需要管理的第二个动作是上下滑动。让我们看看,如何一步步通过UI的主要元素完成变换的。
### 头部(第一阶段)
—
当前的`offset`应该大于0。`Header`应该随`offset`进行垂直变换,直到它期望的高度(我们后面将会讲解`Header`的高斯模糊)。
headerTransform = CATransform3DTranslate(headerTransform, 0, MAX(-offset_HeaderStop, -offset), 0);
这句代码非常简单。我们只需定义一个让`Header`在此停止移动的最小值。
让我感到羞愧的是我比较懒!所以我写死了一些数值,像`offset_HeaderStop`。其实,我们可以通过计算UI元素的位置来获取相同的效果。下次有空再改吧。
### 头像
—
`Avatar`的缩放与我们处理下拉的逻辑一样,只是在这种情况下,图片是到达底部而不是顶部。这段代码和上边的比较相似,除了减小缩放的比例为1.4。
// Avatar -----------
CGFloat avatarScaleFactor = MIN(offset_HeaderStop, offset) / avatarImage.bounds.size.height / 1.4;
CGFloat avatarSizevariation = (avatarImage.bounds.size.height * (1.0 + avatarScaleFactor) - avatarImage.bounds.size.height) / 2.0;
avatarTransform = CATransform3DTranslate(avatarTransform, 0, avatarSizevariation, 0);
avatarTransform = CATransform3DScale(avatarTransform, 1.0-avatarScaleFactor, 1.0-avatarScaleFactor, 0);
就像你看到的,当`Header`停止变化时,我们用`MIN`函数来使`Avatar`的缩放停止。
此时,我们根据当前`offset`设置最顶层的图层。除非`offset`小于等于`offset_HeaderStop`,最顶层的图层是`Avatar`,否则是`Header`。
if (offset <= offset_HeaderStop) {
if (avatarImage.layer.zPosition < header.layer.zPosition) {
header.layer.zPosition = 0;
}
} else {
if (avatarImage.layer.zPosition >= header.layer.zPosition) {
header.layer.zPosition = 2;
}
}
}
### 白色Label
—
这段代码是白色`Label`的动画:
// ------------ Label
CATransform3D labelTransform = CATransform3DMakeTranslation(0, MAX(-distance_W_LabelHeader, offset_B_LabelHeader - offset), 0);
headerLabel.layer.transform = labelTransform;
这里有2个令我感到羞愧的变量值:当`offset`等于`offset_B_LabelHeader`时,黑色的`username`标签刚到触碰到`Header`的底部。
distance_W_LabelHeader
是Header
底部与白色Label
终点之间的距离。
这个变换是通过此逻辑计算:黑色Label
触碰到Header
,白色Label
就会立即出现,并且到达Header
中点位置就停止移动。所以我们使用下面代码创建Y
值:
MAX(-distance_W_LabelHeader, offset_B_LabelHeader - offset)
高斯模糊
最后一个效果是
Header
的模糊。为了得到合适的解决方案,我用了3个不同的库… … 我也尝试过用OpenGL ES
创建基类,但实时更新模糊总是非常缓慢。
然后我意识到我可以对模糊仅仅计算一次,将不模糊和模糊的图片进行重叠,只是改变alpha
值。我非常确信,Twitter
就是这样做的。
在viewDidAppear
中,我们计算Header
的模糊值并隐藏它,设置alpha
值为0。
// Header - Blurred Image
headerBlurImageView = [[UIImageView alloc] initWithFrame:header.bounds];
headerBlurImageView.image = [[UIImage imageNamed:@"header_bg"] blurredImageWithRadius:10 iterations:20 tintColor:[UIColor clearColor]];
headerBlurImageView.contentMode = UIViewContentModeScaleAspectFill;
headerBlurImageView.alpha = 0.0;
[header insertSubview:headerBlurImageView belowSubview:headerLabel];
header.clipsToBounds = YES;
模糊视图是用过FXBlurView
实现的。
在scrollViewDidScroll:
方法中,我们只需根据offset
设置alpha
:
// ------------ Blur
headerBlurImageView.alpha = MIN(1.0, (offset - offset_B_LabelHeader) / distance_W_LabelHeader);
这个计算的背后逻辑是:alpha
最大值是1,当黑色Label
触碰到Header
时模糊效果开始出现,当白色到达最终位置时,也将停止继续模糊。
就这样!
我希望你喜欢这个教程。学习如何重现这种很棒的动画效果对我来说是很大的乐趣。
Swift代码:Download Source
OC代码:Download Source