实现 twitter iOS app UI

本文章翻译自Yari D’areglia

在使用了 Twitter 的 iOS 应用一段时间后,我开始以开发者的眼光,注意到它副标题的移动,控件的交互非常有意思。这激起了我的好奇心,Twitter 开发者是怎么实现的?

让我们先大概看下效果图,是不是相当的优雅?看起来就像系统默认的图,但是如果你仔细看的话,你会发现更多。视图有重叠,整体随着 ScrollView 滑动放大缩小,协调流畅地变换… 我被迷住了,大爱这效果了。

让我们开始试试!

首先,先看看这次实现的最终效果图:

preview

结构描述

在我们探究代码之前,先看看 UI 的结构。

先查看 Main.storyboard 文件。这 ViewController 可以看到两个物体。第一个是头部(包括白色 label),第二个是 scrollView,包含头像和其他相关信息(用户名和关注按钮)。最后底部有一个叫做 Sizer 的 view ,只是为了增加 ScrollView 的内容,以便能够竖直地滑动。

layers

就像你看到的,这个结构非常的简单。只要你注意到头部是在 ScrollView 之外的,而不是和其他视图放在一起。这样使整个结构更加的灵活,便于修改。

LET’S CODE

如果你看了最终的效果图,有2个效果你应该可以轻松实现:

  • 1)下拉(当 ScrollView 已经滑到最顶部)
  • 2)上下滑动

第二个过程要被分解成4个步骤

  • 2.1)上滑,header 的图片上移,直到高度和导航栏一样,然后大小保持不变。
  • 2.2)上滑,头像变小
  • 2.3)当 header 固定,头像移动到 header 背后
  • 2.4)当写着用户姓名的 label 到达 header,一个新的白色 label 就显示在 header 的中间。header 的图片产生毛玻璃效果。

打开 ViewController.swift,让我们开始一步步实现。

创建控制器

很明显,首先我们需要获得 Scrollview 的偏移 offset,这通过 UIScrollViewDelegate 的代理方法 scrollViewDidScroll 很容易实现。

对 view 实现大小变换,最简单就是使用 Core Animation 实现同类型的3D变换,将新的值赋予 layer.transform 属性即可。

这里有一篇关于 core Animation 的文章,有兴趣的话可以看看:

这是在 scrollViewDidScroll 方法下代码的前几行:

var offset = scrollView.contentOffset.y
var avatarTransform = CATransform3DIdentity
var headerTransform = CATransform3DIdentity

这里我们获得竖直方向上的 offset,然后初始化 2个 transformation,后面会用到。

下拉

开始下拉操作:

if offset < 0 {

let headerScaleFactor:CGFloat = -(offset) / header.bounds.height
let headerSizevariation = ((header.bounds.height * (1.0 + headerScaleFactor)) - header.bounds.height)/2.0
headerTransform = CATransform3DTranslate(headerTransform, 0, headerSizevariation, 0)
headerTransform = CATransform3DScale(headerTransform, 1.0 + headerScaleFactor, 1.0 + headerScaleFactor, 0)

header.layer.transform = headerTransform
}

首先确认 offset.y 是否是负值的,这意味着下拉操作开始,header图片开始放大。
剩下的就是简单的数学计算了。

headerScaleFactor 通过比例关系计算而来。也就是说偏移量为 header高度的2倍,那么 ScaleFactor 就为2.0。
只要注意它的顶部到屏幕边缘的距离是固定的,图片按比例放大。

第二个部分我们需要处理上下滑动。让我们看看如何一个一个实现。

header(第一阶段)

现在当前的偏移是大于0的。头部需要随着偏移竖直地滑动,知道达到预定的高度(这个在后面“毛玻璃”会讲到)。

headerTransform = CATransform3DTranslate(headerTransform, 0, max(-offset_HeaderStop, -offset), 0)

这次代码相当简单。只要移动header到预定的位置。

说起来惭愧,我太懒了,把一些值写死,比如 offset_HeaderStop。应该有更优雅地写法来计算控件的位置。

头像(avatar)

头像缩放跟我们的下拉操作是相同的逻辑。但在这个例子,头像的底部固定而不是顶部。这里代码很相似,除了缩放动画。

// Avatar -----------

let avatarScaleFactor = (min(offset_HeaderStop, offset)) / avatarImage.bounds.height / 1.4 // Slow down the animation
let avatarSizeVariation = ((avatarImage.bounds.height * (1.0 + avatarScaleFactor)) - avatarImage.bounds.height) / 2.0
avatarTransform = CATransform3DTranslate(avatarTransform, 0, avatarSizeVariation, 0)
avatarTransform = CATransform3DScale(avatarTransform, 1.0 - avatarScaleFactor, 1.0 - avatarScaleFactor, 0)

如你所见,我们使用了 min 函数去停止头像缩放,当 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 的动画

let labelTransform = CATransform3DMakeTranslation(0, max(-distance_W_LabelHeader, offset_B_LabelHeader - offset), 0)
headerLabel.layer.transform = labelTransform

这里有2个写死的变量(羞愧。。):当偏移量为 offset_B_LabelHeader,黑色用户名的 label 到达了 header 的底部。

shame1

distance_W_LabelHeader 是 header 底部到白色 label 中心的距离。

shame2

变换计算的逻辑是:当黑色 label 接触到 header,白色 label 显示,当到达 header 的中心停止。

max(-distance_W_LabelHeader, offset_B_LabelHeader - offset)

毛玻璃

最后一个效果是头部的毛玻璃。我找了3个不同的库去解决这个问题,甚至尝试过超级简单的 openGL ES -_-。但是实时更新毛玻璃效果总会存在延迟现象。

机智的我发现,只要计算毛玻璃效果一次,然后覆盖在未转化的图片上,改变它的透明度即可。相信 Twitter 的开发者也是这么实现的。

viewDidAppear 下,我们计算出了毛玻璃效果的 header,然后设置透明度为0,隐藏它:

// Header - Blurred Image

headerBlurImageView = UIImageView(frame: header.bounds)
headerBlurImageView?.image = UIImage(named: "header_bg")?.blurredImageWithRadius(10, iterations: 20, tintColor: UIColor.clearColor())
headerBlurImageView?.contentMode = UIViewContentMode.ScaleAspectFill
headerBlurImageView?.alpha = 0.0
header.insertSubview(headerBlurImageView, belowSubview: headerLabel)

blurred view 这个 view 使用了第三方库 FBBlurView

scrollViewDidScroll 方法下,我们根据 offset 更改透明度即可。

headerBlurImageView?.alpha = min (1.0, (offset - offset_B_LabelHeader)/distance_W_LabelHeader)

这个计算的逻辑就是要使最大值不超过1,毛玻璃效果必须在黑色label到达头部的时候开始变化,当白色label在最终位置的时候停止变化。

这就是全部

希望你能享受这个教学过程。学习如何重新实现有趣的动画效果对我来说相当有意思。

你可以在这里下载 demo