构建Jakub Antalik的音乐播放器

优质
小牛编辑
130浏览
2023-12-01

Jakub是斯洛伐克的一名出色的设计师,已经设计了一些经常发布到Dtibbble去的非常有创造力的界面。其中一个作品非常打动我,那是一个很有趣的例子,证明了界面上每次操作一个元素的断断续续的动画是如何抓住用户的眼球的。在本指南的早期我展示了一个他设计的音乐播放器,含有一些很酷的内置动画,这里我们再看一下。

所以这里他明显使用到的技术是什么?他操纵了动画的开始时间。通过让每个元素比另一个元素慢一点动画到屏幕上的位置,并按照行的顺序操作屏幕上的每一个元素,就形成了一个非常整齐的波浪效果,感觉就像每个元素都被前一个元素用橡皮筋带动的一样。

让我们重建他音乐播放器概念的第二个屏幕:歌曲列表。

首先,我们需要重建设计来切片元素并且尽可能整齐地分开动画它们。我拉出我选择的设计工具:Photoshop,然后开始工作。musicplayer.psd文件是放置该设计文件的地方,如果你喜欢的话可以打开来检出它。我不会详细说明如何用Photoshop创建这个设计,但文件和设计都足够简单和直接。

这里是我重建的第二个屏幕的歌曲列表。

如果你仔细观察原始的动画,会发现有8个分开动画的不同元素。

  1. 黑色箭头和“Dance Club”文本
  2. “Ministry of Fun”文本
  3. “Add a Song”按钮
  4. 五首歌对应的五行

这8个元素(或元素组,因为箭头和“Dance Club”文本是一起动画的)是通过不同的开始时间递进进入视图的,这就是我们要在动画中获取的非常酷的波浪感效果。

首先我们整理一下计划。我需要做的是分开添加这些元素到界面上,这样我就可以分开动画它们了。如果这是一个真实的app,有着真实流入的数据,这个界面最可能是一个UITableView或者UICollectionView来获取一个好的、结构化的展示行的方式。从高层面来概括这两个视图类型的话,就是你实现你需要定义的它们的接口方法,来返回一些数据到界面上,比如返回行高的方法,或者返回一个只有一行的视图的方法。因为我们没有数据,而且我的主要目的是演示如何构建动画,我就仅仅是保存一些Photoshop里设计的图片并手动将这些图片添加到界面上去,从顶部的箭头和“Dance Club”文本开始。

// 定义一个变量来获取屏幕的宽度,我们会经常用到这个值。
CGFloat windowWidth = self.window.bounds.size.width;

// 将背景添加到界面上
UIImageView *backgroundView = [[UIImageView alloc] initWithFrame:self.window.bounds];
backgroundView.image = [UIImage imageNamed:@"background"];
[self.window addSubview:backgroundView];

// 添加箭头和文本
UIImageView *arrowView =
    [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, windowWidth, 45)];
arrowView.image = [UIImage imageNamed:@"arrow"];
[self.window addSubview:arrowView];

这里没什么特别的,只是简单地添加一些视图到我们原型的主屏幕上。名为@“background”的图片是大的渐变的图片,作为其他视图的背景。@“arrow”图片是用Photoshop做出来的包含箭头和“Dance Club”文本的图片,因为我会同时动画它们,所以将它们简单地放在一个图片里。

这里是目前界面看起来的样子。

现在让我们添加更多的视图!

// “Ministry of Fun”图片
UIImageView *ministryView =
    [[UIImageView alloc] initWithFrame:CGRectMake(0, 57, windowWidth, 28)];
ministryView.image = [UIImage imageNamed:@"ministry"];
[self.window addSubview:ministryView];

// 添加一个歌曲按钮
UIButton *addButton = [UIButton buttonWithType:UIButtonTypeCustom];
[addButton setImage:[UIImage imageNamed:@"add-button"] forState:UIControlStateNormal];
[addButton setImage:[UIImage imageNamed:@"add-button-pressed"]
    forState:UIControlStateHighlighted];
[addButton setFrame:CGRectMake(0, 102, windowWidth, 45)];
[self.window addSubview:addButton];

我添加“Ministry of Fun”图片视图(使用我用Photoshop分割出来的PNG图片)到界面上然后为“Add a Song”按钮创建一个UIButton。我本可以懒一点,不将按钮做成一个真的UIButton,而是使用一个UIImageView,但我想演示如何为一个自定义的UIButton设置点击的和普通的图片。只需要调用同样的一个 -setImage:forState:方法,但给它传输不同的属性。你可以随便调用它来设置不同的状态属性,来覆盖用户对按钮的每一个可能的操作。接着我设置按钮的位置并将它添加到界面上。

这里是目前状态的界面,以及点击按钮时不同状态的演示。

我们UIControlStateHighlighted状态的图片只是将白色边框换成了白色的填充。

现在让我们添加我们的行。它们也都是UIImageView,所以也只用直接在背景图片上放置就可以了。

// Katy Perry 行
UIImageView *firstRow =
    [[UIImageView alloc] initWithFrame:CGRectMake(0, 170, windowWidth, 80)];
firstRow.image = [UIImage imageNamed:@"1st-row"];
[self.window addSubview:firstRow];

// Shakira 行
UIImageView *secondRow =
    [[UIImageView alloc] initWithFrame:CGRectMake(0, 170+80, windowWidth, 80)];
secondRow.image = [UIImage imageNamed:@"2nd-row"];
[self.window addSubview:secondRow];

// Pitbull 行
UIImageView *thirdRow =
    [[UIImageView alloc] initWithFrame:CGRectMake(0, 170+160, windowWidth, 80)];
thirdRow.image = [UIImage imageNamed:@"3rd-row"];
[self.window addSubview:thirdRow];

// Lana del Rey 行
UIImageView *fourthRow =
    [[UIImageView alloc] initWithFrame:CGRectMake(0, 170+240, windowWidth, 80)];
fourthRow.image = [UIImage imageNamed:@"4th-row"];
[self.window addSubview:fourthRow];

// HEX 行
UIImageView *fifthRow =
    [[UIImageView alloc] initWithFrame:CGRectMake(0, 170+320, windowWidth, 80)];
fifthRow.image = [UIImage imageNamed:@"5th-row"];
[self.window addSubview:fifthRow];

你可能注意到每一行frame的Y坐标(垂直位置)都有一个小方程式。每一行都是80px高,所以放置它们每一行的时候我都在Y坐标上加了80。我也可以使用Auto Layout来做,但对这个例子来说就有点过于复杂了。

这里是在添加动画前的样子。

但等一下,我们并不想要在第一次进入的看到这样的界面。这次练习的目的在于让每个元素都动画到它们的位置上,也就是说它们不应该立即出现在它们的最终位置。我要做的是从屏幕的右边开始每一个元素,然后我会让每个元素的左边动画到屏幕的左边,来到最终的位置。

让我们回到我们的视图设置代码并修改每个元素的frame,这样它们的X轴坐标就不再是0了,而是屏幕的宽度。这样就会让每个元素的左边界并齐屏幕的右边界,用户就看不到了。

// 添加箭头和顶部的文字
UIImageView *arrowView =
   [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 0, windowWidth, 45)];
arrowView.image = [UIImage imageNamed:@"arrow"];
[self.window addSubview:arrowView];

// Ministry of Fun 文字
UIImageView *ministryView =
    [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 57, windowWidth, 56/2)];
ministryView.image = [UIImage imageNamed:@"ministry"];
[self.window addSubview:ministryView];

// Add a Song 按钮
UIButton *addButton = [UIButton buttonWithType:UIButtonTypeCustom];
[addButton setImage:[UIImage imageNamed:@"add-button"]
    forState:UIControlStateNormal];
[addButton setImage:[UIImage imageNamed:@"add-button-pressed"]
    forState:UIControlStateHighlighted];
[addButton setFrame:CGRectMake(windowWidth, 102, windowWidth, 45)];
[self.window addSubview:addButton];

// Katy Perry 行
UIImageView *firstRow =
    [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 170, windowWidth, 80)];
firstRow.image = [UIImage imageNamed:@"1st-row"];
[self.window addSubview:firstRow];

// Shakira 行
UIImageView *secondRow =
    [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 170+80, windowWidth, 80)];
secondRow.image = [UIImage imageNamed:@"2nd-row"];
[self.window addSubview:secondRow];

// Pitbull 行
UIImageView *thirdRow =
    [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 170+160, windowWidth, 80)];
thirdRow.image = [UIImage imageNamed:@"3rd-row"];
[self.window addSubview:thirdRow];

// Lana del Rey 行
UIImageView *fourthRow =
    [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 170+240, windowWidth, 80)];
fourthRow.image = [UIImage imageNamed:@"4th-row"];
[self.window addSubview:fourthRow];

// HEX 行
UIImageView *fifthRow =
    [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 170+320, windowWidth, 80)];
fifthRow.image = [UIImage imageNamed:@"5th-row"];
[self.window addSubview:fifthRow];

你可以想象一下现在所有元素都移动到屏幕的右边去的界面样式,现在只显示了背景图片。

现在所有内容都在屏幕外并且准备好动画了,策略是让每个元素都动画到左边,一次一个,每个都有所延迟,这样就会产生一种波浪的感觉。为了好玩,我们试试使用基于block的UIView动画方法来让我们的元素动画到屏幕上。

这里是第一个动画block,我们会将箭头和“Dance Club”图片滑动到左边。

[UIView animateWithDuration:1.1 delay:0 usingSpringWithDamping:0.3
    initialSpringVelocity:0 options:0 animations:^{
    [arrowView setFrame:CGRectMake(0, 0, windowWidth, 45)];
} completion:NULL];

这个基于block的动画有1.1秒的持续时间和0.3的弹簧阻尼。持续时间是动画完成需要的时间,而阻尼是iOS 7在UIView动画方法中提供的一个弹簧属性,用来控制弹簧的弹力。JNWSpringAnimation提供了三个属性来控制弹簧的物理性质,但Apple值提供了一个,即damping属性。damping需要时一个0到1之间的值,越接近0,弹簧动作就越有弹性,越接近1,就越没有弹性,直到完全没有弹性,变成一个平滑的淡入。

让我们看看这个duration和damping值产生的动作。

恩,有点不太对。动画太快也太跳跃了。这种类型的弹性动画带来了一些焦虑。这是一个关于仅仅使用一个弹簧动画并不能提升你的app整体用户体验的很好的例子。每种类型的动画都给你的用户带来了一些感受,而这个带来了错误地感受。

让我们将持续时间提升到2.1秒并看看感觉。

比起Jakub的原始动画,这个又太弹了,我们的damping值也需要调整。让我们将damping从0.3提升到0.6,如我之前所说,它更靠近1这个不弹的值。我们还是需要一点弹性,现在让我们来看看它怎么样了。

好了,不是太坏。你可以发现当你使用iOS 7提供的弹簧动画方法时,它直接提供了一些值来获取你想要的感觉。NSWSpringAnimation给出的弹簧属性更容易理解,至少对我来说是这样,因为它们都操作了弹簧动作方程的不同属性。iOS 7的基于block的动画中的damping值实际上是一个解释值,这意味着苹果无论获取到你输入的什么值,都会做一些复杂的计算来操作这个值并将其放入弹簧动作方程式中。你可以说苹果操作了这个值,因为它在0和1之间改变弹性。而在实际的弹簧动作方程中,动作的时间(它到达平衡点或者最终位置的时间)是由弹簧的其他属性决定的,它不是你去设置然后强制弹簧遵循的。苹果的动画方法有一个你需要设置的持续时间,所以你在以一种并非完全遵循物理法则管理下的弹簧动作。这就是为什么我倾向于用JSWSpringAniamtion(或者Facebook Pop,我会马上提及),因为它们有着更加自然、逼真的弹簧动画。

现在,让我们从上到下动画屏幕上的其他元素。每个都需要比前一个开始得稍微慢一点。同时我想要控制app启动后动画开始的时间,来看看我如何管理。

CGFloat initialDelay = 1.0f;
CGFloat stutter = 0.3f;

// 动画箭头图片
[UIView animateWithDuration:2.1 delay:initialDelay
    usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
    [arrowView setFrame:CGRectMake(0, 0, windowWidth, 45)];
} completion:NULL];

// 动画Ministry of Fun文字
[UIView animateWithDuration:2.1 delay:initialDelay + (1 * stutter)
    usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
    [ministryView setFrame:CGRectMake(0, 57, windowWidth, 28)];
} completion:NULL];

我设置了两个CGFloat变量,一个initialDelay值来存储延迟时间,一个stutter来存储每个动画之间细微的延迟。这个数字对我们动画效果整体的感觉和流动感都非常重要。动画之间太长的延时会让他们觉得不连贯,太短就不足以形成我们想要构建的波浪效果。

回到代码:第一个动画的delay属性就是initialDelay变量的值,因为这是来到屏幕上的第一个动画。第二个动画block的delay值为initialDelay+(1*stutter)。这表示它会等待开始的延迟时间,然后会等待stutter值乘以1的时间。接下来的所有动画都会遵循这个公式作为延时,并且每次都会加1倍stutter。这可以确保他们的动画之间都是同样的延时。

这里是现在看起来的样子。

我觉得这个看起来不错。老实说,只动画两个元素很难看出波浪效果是不是好的,因为你无法获取一个整体的真实感受,除非动画一系列的元素。所以让我们动画屏幕上的其他元素。

[UIView animateWithDuration:2.1 delay:initialDelay + (2 * stutter)
    usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
    [addButton setFrame:CGRectMake(0, 102, windowWidth, 45)];
} completion:NULL];

[UIView animateWithDuration:2.1 delay:initialDelay + (3 * stutter)
    usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
    [firstRow setFrame:CGRectMake(0, 170, windowWidth, 80)];
} completion:NULL];

[UIView animateWithDuration:2.1 delay:initialDelay + (4 * stutter)
    usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
    [secondRow setFrame:CGRectMake(0, 170+80, windowWidth, 80)];
} completion:NULL];

[UIView animateWithDuration:2.1 delay:initialDelay + (5 * stutter)
    usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
    [thirdRow setFrame:CGRectMake(0, 170+160, windowWidth, 80)];
} completion:NULL];

[UIView animateWithDuration:2.1 delay:initialDelay + (6 * stutter)
    usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
    [fourthRow setFrame:CGRectMake(0, 170+240, windowWidth, 80)];
} completion:NULL];

[UIView animateWithDuration:2.1 delay:initialDelay + (7 * stutter)
    usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
    [fifthRow setFrame:CGRectMake(0, 170+320, windowWidth, 80)];
} completion:NULL];

现在我们动画了所有的元素到位置上了,让我们看看效果。

对我来说感觉还不太对。动画的延时还是有点太长了,破坏了想要的波浪感。看起来一点也没有流动感。让我们降低延时,把stutter变量的值从0.3降为0.15来看看效果。

很接近了,但我认为我们可以再缩小一点点延迟时间来让它更有天然的流动感,就像每个元素都牵引了下一个。让我们将stutter变量降为0.6。

现在我们有些成果了。我认为它看起来很棒并且有非常好的波浪动作。让我们和Jakub原始的动作做一些比较。

看起来我们匹配得很接近!所以从这个例子中学到了什么呢?

  • 基于block的UIView动画方法中的弹簧的damping值是一个抽象值,对获取一个好的感觉并没有什么用。这就是为什么我喜欢用真实的弹簧动作(不需要设置持续时间的),比如JSWSpringAnimation提供的那种。
  • 当实现一个像这个一样内置的动画时,调整动画之间的延时是得到一个好的波浪形动作的关键点。

我在我自己的iPhone app Interesting中也使用了波浪形的动画。来看看我的app的动画并构建它。