我們寫(xiě)的應(yīng)用程序往往都不是靜態(tài)的,因?yàn)樗鼈冃枰m應(yīng)用戶的需求以及為執(zhí)行各種任務(wù)而改變狀態(tài)。
在這些狀態(tài)之間轉(zhuǎn)換時(shí),清晰的揭示正在發(fā)生什么是非常重要的。而不是在頁(yè)面之間跳躍,動(dòng)畫(huà)幫助我們解釋用戶從哪里來(lái),要到哪里去。
鍵盤(pán)在 view 中滑進(jìn)滑出給了我們一個(gè)錯(cuò)覺(jué),讓我們以為它是簡(jiǎn)單的被隱藏在屏幕下方的,并且是手機(jī)很自然的一個(gè)部分。View controller 轉(zhuǎn)場(chǎng)加強(qiáng)了我們的應(yīng)用程序的導(dǎo)航結(jié)構(gòu),并且給了用戶正在移向哪個(gè)方向的提示。微妙的反彈和碰撞使界面栩栩如生,并且激發(fā)出了物理的質(zhì)感。要是沒(méi)有這些的話,我們就只有一個(gè)沒(méi)有視覺(jué)修飾的干巴巴環(huán)境了。
動(dòng)畫(huà)是敘述你的應(yīng)用的故事的絕佳方式,在了解動(dòng)畫(huà)背后的基本原理之后,設(shè)計(jì)它們會(huì)輕松很多。
在這篇文章 (以及這個(gè)話題中其余大多數(shù)文章) 中,我們將特別地針對(duì) Core Animation 進(jìn)行探討。雖然你將看到的很多東西也可以用更高層級(jí)的 UIKit 的方法來(lái)完成,但是 Core Animation 將會(huì)讓你更好的理解正在發(fā)生什么。它以一種更明確的方式來(lái)描述動(dòng)畫(huà),這對(duì)這篇文章讀者以及你自己的代碼的讀者來(lái)說(shuō)都非常有用。
在看動(dòng)畫(huà)如何與我們?cè)谄聊簧峡吹降膬?nèi)容交互之前,我們需要快速瀏覽一下 Core Animation 的 CALayer,這是動(dòng)畫(huà)產(chǎn)生作用的地方。
你大概知道 UIView 實(shí)例,以及 layer-backed 的 NSView,修改它們的 layer 來(lái)委托強(qiáng)大的 Core Graphics 框架來(lái)進(jìn)行渲染。然而,你務(wù)必要理解,當(dāng)把動(dòng)畫(huà)添加到一個(gè) layer 時(shí),是不直接修改它的屬性的。
取而代之,Core Animation 維護(hù)了兩個(gè)平行 layer 層次結(jié)構(gòu): model layer tree(模型層樹(shù)) 和 presentation layer tree(表示層樹(shù))。前者中的 layers 反映了我們能直接看到的 layers 的狀態(tài),而后者的 layers 則是動(dòng)畫(huà)正在表現(xiàn)的值的近似。
實(shí)際上還有所謂的第三個(gè) layer 樹(shù),叫做 rendering tree(渲染樹(shù))。因?yàn)樗鼘?duì) Core Animation 而言是私有的,所以我們?cè)谶@里不討論它。
考慮在 view 上增加一個(gè)漸出動(dòng)畫(huà)。如果在動(dòng)畫(huà)中的任意時(shí)刻,查看 layer 的 opacity 值,你是得不到與屏幕內(nèi)容對(duì)應(yīng)的透明度的。取而代之,你需要查看 presentation layer 以獲得正確的結(jié)果。
雖然你可能不會(huì)去直接設(shè)置 presentation layer 的屬性,但是使用它的當(dāng)前值來(lái)創(chuàng)建新的動(dòng)畫(huà)或者在動(dòng)畫(huà)發(fā)生時(shí)與 layers 交互是非常有用的。
通過(guò)使用 -[CALayer presentationLayer] 和 -[CALayer modelLayer],你可以在兩個(gè) layer 之間輕松切換。
可能最常見(jiàn)的情況是將一個(gè) view 的屬性從一個(gè)值改變?yōu)榱硪粋€(gè)值,考慮下面這個(gè)例子:
http://wiki.jikexueyuan.com/project/objc/images/12-1.gif" alt="" />
在這里,我們讓紅色小火箭的 x-position 從 77.0 變?yōu)?455.0,剛好超過(guò)它的 parent view 的邊。為了填充所有路徑,我們需要確定我們的火箭在任意時(shí)刻所到達(dá)的位置。這通常使用線性插值法來(lái)完成:
http://wiki.jikexueyuan.com/project/objc/images/12-15.png" alt="" />
也就是說(shuō),對(duì)于動(dòng)畫(huà)給定的一個(gè)分?jǐn)?shù) t,火箭的 x 坐標(biāo)就是起始點(diǎn)的 x 坐標(biāo) 77,加上一個(gè)到終點(diǎn)的距離 ?x = 378 乘以該分?jǐn)?shù)的值。
使用 CABasicAnimation,我們可以如下實(shí)現(xiàn)這個(gè)動(dòng)畫(huà):
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @77;
animation.toValue = @455;
animation.duration = 1;
[rocket.layer addAnimation:animation forKey:@"basic"];
請(qǐng)注意我們要?jiǎng)赢?huà)的鍵路徑,也就是 position.x,實(shí)際上包含一個(gè)存儲(chǔ)在 position 屬性中的 CGPoint 結(jié)構(gòu)體成員。這是 Core Animation 一個(gè)非常方便的特性。請(qǐng)務(wù)必查看支持的鍵路徑的完整列表。
然而,當(dāng)我們運(yùn)行該代碼時(shí),我們意識(shí)到火箭在完成動(dòng)畫(huà)后馬上回到了初始位置。這是因?yàn)樵谀J(rèn)情況下,動(dòng)畫(huà)不會(huì)在超出其持續(xù)時(shí)間后還修改 presentation layer。實(shí)際上,在結(jié)束時(shí)它甚至?xí)粡氐滓瞥?/p>
一旦動(dòng)畫(huà)被移除,presentation layer 將回到 model layer 的值,并且因?yàn)槲覀儚奈葱薷脑?layer 的 position 屬性,所以我們的飛船將重新出現(xiàn)在它開(kāi)始的地方。
這里有兩種解決這個(gè)問(wèn)題的方法:
第一種方法是直接在 model layer 上更新屬性。這是推薦的的做法,因?yàn)樗沟脛?dòng)畫(huà)完全可選。
一旦動(dòng)畫(huà)完成并且從 layer 中移除,presentation layer 將回到 model layer 設(shè)置的值,而這個(gè)值恰好與動(dòng)畫(huà)最后一個(gè)步驟相匹配。
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @77;
animation.toValue = @455;
animation.duration = 1;
[rocket.layer addAnimation:animation forKey:@"basic"];
rocket.layer.position = CGPointMake(455, 61);
或者,你可以通過(guò)設(shè)置動(dòng)畫(huà)的 fillMode 屬性為 kCAFillModeForward 以留在最終狀態(tài),并設(shè)置removedOnCompletion 為 NO 以防止它被自動(dòng)移除:
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @77;
animation.toValue = @455;
animation.duration = 1;
animation.fillMode = kCAFillModeForward;
animation.removedOnCompletion = NO;
[rectangle.layer addAnimation:animation forKey:@"basic"];
Andy Matuschak 指出了,如果將已完成的動(dòng)畫(huà)保持在 layer 上時(shí),會(huì)造成額外的開(kāi)銷(xiāo),因?yàn)殇秩酒鲿?huì)去進(jìn)行額外的繪畫(huà)工作。
值得指出的是,實(shí)際上我們創(chuàng)建的動(dòng)畫(huà)對(duì)象在被添加到 layer 時(shí)立刻就復(fù)制了一份。這個(gè)特性在多個(gè) view 中重用動(dòng)畫(huà)時(shí)這非常有用。比方說(shuō)我們想要第二個(gè)火箭在第一個(gè)火箭起飛不久后起飛:
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.byValue = @378;
animation.duration = 1;
[rocket1.layer addAnimation:animation forKey:@"basic"];
rocket1.layer.position = CGPointMake(455, 61);
animation.beginTime = CACurrentMediaTime() + 0.5;
[rocket2.layer addAnimation:animation forKey:@"basic"];
rocket2.layer.position = CGPointMake(455, 111);
設(shè)置動(dòng)畫(huà)的 beginTime 為未來(lái) 0.5 秒將只會(huì)影響 rocket2,因?yàn)閯?dòng)畫(huà)在執(zhí)行語(yǔ)句 [rocket1.layer addAnimation:animation forKey:@"basic"]; 時(shí)已經(jīng)被復(fù)制了,并且之后 rocket1 也不會(huì)考慮對(duì)動(dòng)畫(huà)對(duì)象的改變。
不妨看一看 David 的 關(guān)于動(dòng)畫(huà)時(shí)間的一篇很棒的文章,通過(guò)它可以學(xué)習(xí)如何更精確的控制你的動(dòng)畫(huà)。
我決定再使用 CABasicAnimation 的 byValue 屬性創(chuàng)建一個(gè)動(dòng)畫(huà),這個(gè)動(dòng)畫(huà)從 presentation layer 的當(dāng)前值開(kāi)始,加上 byValue 的值后結(jié)束。這使得動(dòng)畫(huà)更易于重用,因?yàn)槟悴恍枰_的指定可能無(wú)法提前知道的 from- 和 toValue 的值。
fromValue, byValue 和 toValue 的不同組合可以用來(lái)實(shí)現(xiàn)不同的效果,如果你需要?jiǎng)?chuàng)建一個(gè)可以在你的不同應(yīng)用中重用的動(dòng)畫(huà),你可以查看文檔。
這很容易想到一個(gè)場(chǎng)景,你想要為你的動(dòng)畫(huà)定義超過(guò)兩個(gè)步驟,我們可以使用更通用的 CAKeyframeAnimation,而不是去鏈接多個(gè) CABasicAnimation 實(shí)例。
關(guān)鍵幀(keyframe)使我們能夠定義動(dòng)畫(huà)中任意的一個(gè)點(diǎn),然后讓 Core Animation 填充所謂的中間幀。
比方說(shuō)我們正在制作我們下一個(gè) iPhone 應(yīng)用程序上的登陸表單,我們希望當(dāng)用戶輸入錯(cuò)誤的密碼時(shí)表單會(huì)晃動(dòng)。使用關(guān)鍵幀動(dòng)畫(huà),看起來(lái)大概像下面這樣:
http://wiki.jikexueyuan.com/project/objc/images/12-16.gif" alt="" />
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position.x";
animation.values = @[ @0, @10, @-10, @10, @0 ];
animation.keyTimes = @[ @0, @(1 / 6.0), @(3 / 6.0), @(5 / 6.0), @1 ];
animation.duration = 0.4;
animation.additive = YES;
[form.layer addAnimation:animation forKey:@"shake"];
values 數(shù)組定義了表單應(yīng)該到哪些位置。
設(shè)置 keyTimes 屬性讓我們能夠指定關(guān)鍵幀動(dòng)畫(huà)發(fā)生的時(shí)間。它們被指定為關(guān)鍵幀動(dòng)畫(huà)總持續(xù)時(shí)間的一個(gè)分?jǐn)?shù)。
請(qǐng)注意我是如何選擇不同的值從 0 到 10 和從 10 到 -10 轉(zhuǎn)換以維持恒定的速度的。
設(shè)置 additive 屬性為 YES 使 Core Animation 在更新 presentation layer 之前將動(dòng)畫(huà)的值添加到 model layer 中去。這使我們能夠?qū)λ行问降男枰碌脑刂赜孟嗤膭?dòng)畫(huà),且無(wú)需提前知道它們的位置。因?yàn)檫@個(gè)屬性從 CAPropertyAnimation 繼承,所以你也可以在使用 CABasicAnimation 時(shí)使用它。
雖然用代碼實(shí)現(xiàn)一個(gè)簡(jiǎn)單的水平晃動(dòng)并不難,但是沿著復(fù)雜路徑的動(dòng)畫(huà)就需要我們?cè)陉P(guān)鍵幀的 values 數(shù)組中存儲(chǔ)大量 box 化的 CGPoint。 值得慶幸的是,CAKeyframeAnimation 提供了更加便利的 path 屬性作為代替。
舉個(gè)例子,我們?nèi)绾巫屢粋€(gè) view 做圓周運(yùn)動(dòng):
http://wiki.jikexueyuan.com/project/objc/images/12-2.gif" alt="" />
CGRect boundingRect = CGRectMake(-150, -150, 300, 300);
CAKeyframeAnimation *orbit = [CAKeyframeAnimation animation];
orbit.keyPath = @"position";
orbit.path = CFAutorelease(CGPathCreateWithEllipseInRect(boundingRect, NULL));
orbit.duration = 4;
orbit.additive = YES;
orbit.repeatCount = HUGE_VALF;
orbit.calculationMode = kCAAnimationPaced;
orbit.rotationMode = kCAAnimationRotateAuto;
[satellite.layer addAnimation:orbit forKey:@"orbit"];
使用 CGPathCreateWithEllipseInRect(),我們創(chuàng)建一個(gè)圓形的 CGPath 作為我們的關(guān)鍵幀動(dòng)畫(huà)的 path。
使用 calculationMode 是控制關(guān)鍵幀動(dòng)畫(huà)時(shí)間的另一種方法。我們通過(guò)將其設(shè)置為 kCAAnimationPaced,讓 Core Animation 向被驅(qū)動(dòng)的對(duì)象施加一個(gè)恒定速度,不管路徑的各個(gè)線段有多長(zhǎng)。將其設(shè)置為 kCAAnimationPaced 將無(wú)視所有我們已經(jīng)設(shè)置的 keyTimes。
設(shè)置 rotationMode 屬性為 kCAAnimationRotateAuto 確保飛船沿著路徑旋轉(zhuǎn)。作為對(duì)比,如果我們將該屬性設(shè)置為 nil 那動(dòng)畫(huà)會(huì)是什么樣的呢。
http://wiki.jikexueyuan.com/project/objc/images/12-3.gif" alt="" />
你可以使用帶路徑的動(dòng)畫(huà)來(lái)實(shí)現(xiàn)幾個(gè)有趣的效果;資深 objc.io 作者 Ole Begemann 寫(xiě)了一篇文章,闡述了如何將 CAShapeLayer 與基于路徑的動(dòng)畫(huà)組合起來(lái)使用,并只用幾行代碼來(lái)創(chuàng)建酷炫的繪圖動(dòng)畫(huà)。
讓我們?cè)俅蝸?lái)看看第一個(gè)例子:
http://wiki.jikexueyuan.com/project/objc/images/12-4.gif" alt="" />
你會(huì)發(fā)現(xiàn)我們的火箭的動(dòng)畫(huà)有一些看起來(lái)非常不自然的地方。那是因?yàn)槲覀冊(cè)诂F(xiàn)實(shí)世界中看到的大部分運(yùn)動(dòng)需要時(shí)間來(lái)加速或減速。對(duì)象瞬間達(dá)到最高速度,然后再立即停止往往看起來(lái)非常不自然。除非你在讓機(jī)器人跳舞,但這很少是想要的效果。
為了給我們的動(dòng)畫(huà)一個(gè)存在慣性的感覺(jué),我們可以使用我們上面提到的參數(shù)因子來(lái)進(jìn)行插值。然而,如果我們接下來(lái)需要為每個(gè)需要加速或減速的行為創(chuàng)建一個(gè)新的插值函數(shù),這將是一個(gè)很難擴(kuò)展的方法。
取而代之,常見(jiàn)的做法是把要進(jìn)行動(dòng)畫(huà)的屬性的插值從動(dòng)畫(huà)的速度中解耦出來(lái)。這樣一來(lái),給動(dòng)畫(huà)提速會(huì)產(chǎn)生一種小火箭加速運(yùn)動(dòng)的效果,而不用改變我們的插值函數(shù)。
我們可以通過(guò)引入一個(gè) 時(shí)間函數(shù) (timing function) (有時(shí)也被稱為 easing 函數(shù))來(lái)實(shí)現(xiàn)這個(gè)目標(biāo)。該函數(shù)通過(guò)修改持續(xù)時(shí)間的分?jǐn)?shù)來(lái)控制動(dòng)畫(huà)的速度。
http://wiki.jikexueyuan.com/project/objc/images/12-17.png" alt="" />
最簡(jiǎn)單的 easing 函數(shù)是 linear。它在整個(gè)動(dòng)畫(huà)上維持一個(gè)恒定的速度。在 Core Animation 中,這個(gè)功能由 CAMediaTimingFunction 類(lèi)表示。
http://wiki.jikexueyuan.com/project/objc/images/12-5.gif" alt="" />
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @50;
animation.toValue = @150;
animation.duration = 1;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[rectangle.layer addAnimation:animation forKey:@"basic"];
rectangle.layer.position = CGPointMake(150, 0);
Core Animation 附帶了一些 linear 之外的內(nèi)置 easing 函數(shù),如:
kCAMediaTimingFunctionEaseIn):kCAMediaTimingFunctionEaseOut):kCAMediaTimingFunctionEaseInEaseOut):kCAMediaTimingFunctionDefault):在一定限度內(nèi),你也可以使用 +functionWithControlPoints:::: 創(chuàng)建自己的 easing 函數(shù)。通過(guò)傳遞 cubic Bézier 曲線的兩個(gè)控制點(diǎn)的 x 和 y 坐標(biāo),你可以輕松的創(chuàng)建自定義 easing 函數(shù),比如我為我們的紅色小火箭選擇的那個(gè)。
這個(gè)方法因?yàn)橛腥齻€(gè)無(wú)名參數(shù)而聲名狼藉,我們并不推薦在你的 API 中使用這種蛋疼的寫(xiě)法。
http://wiki.jikexueyuan.com/project/objc/images/12-10.gif" alt="" />
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @77;
animation.toValue = @455;
animation.duration = 1;
animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.5:0:0.9:0.7];
[rocket.layer addAnimation:animation forKey:@"basic"];
rocket.layer.position = CGPointMake(150, 0);
我不打算講太多關(guān)于 Bézier 曲線的細(xì)節(jié),在計(jì)算機(jī)圖形學(xué)中,它們是創(chuàng)建平滑曲線的常用技術(shù)。你可能在基于矢量的繪圖工具,比如 Sketch 或 Adobe Illustrator 中見(jiàn)過(guò)它們。
http://wiki.jikexueyuan.com/project/objc/images/12-11.png" alt="" />
傳遞給 +functionWithControlPoints:::: 的值有效地控制了控制點(diǎn)的位置。所得到的定時(shí)函數(shù)將基于得到的路徑來(lái)調(diào)整動(dòng)畫(huà)的速度。x 軸代表時(shí)間的分?jǐn)?shù),而 y 軸是插值函數(shù)的輸入值。
遺憾的是,由于這些部分被鎖定在 [0–1] 的范圍內(nèi),我們不可能用它來(lái)創(chuàng)建一些像預(yù)期動(dòng)作 (Anticipation,一種像目標(biāo)進(jìn)發(fā)前先回退一點(diǎn),到達(dá)目標(biāo)后還過(guò)沖一會(huì)兒,見(jiàn)下圖) 這樣的常見(jiàn)效果。
我寫(xiě)了一個(gè)小型庫(kù),叫做 RBBAnimation,它包含一個(gè)允許使用 更多復(fù)雜 easing 函數(shù) 的自定義子類(lèi) CAKeyframeAnimation,包括反彈和包含負(fù)分量的 cubic Bézier 函數(shù):
http://wiki.jikexueyuan.com/project/objc/images/12-12.gif" alt="" />
RBBTweenAnimation *animation = [RBBTweenAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @50;
animation.toValue = @150;
animation.duration = 1;
animation.easing = RBBCubicBezier(0.68, -0.55, 0.735, 1.55);
http://wiki.jikexueyuan.com/project/objc/images/12-13.gif" alt="" />
RBBTweenAnimation *animation = [RBBTweenAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @50;
animation.toValue = @150;
animation.duration = 1;
animation.easing = RBBEasingFunctionEaseOutBounce;
對(duì)于某些復(fù)雜的效果,可能需要同時(shí)為多個(gè)屬性進(jìn)行動(dòng)畫(huà)。想象一下,在一個(gè)媒體播放程序中,當(dāng)切換到到隨機(jī)曲目時(shí)我們讓隨機(jī)動(dòng)畫(huà)生效??雌饋?lái)就像下面這樣:
http://wiki.jikexueyuan.com/project/objc/images/12-14.gif" alt="" />
你可以看到,我們需要同時(shí)對(duì)上面的封面的 position,rotation 和 z-position 進(jìn)行動(dòng)畫(huà)。使用 CAAnimationGroup 來(lái)動(dòng)畫(huà)其中一個(gè)封面的代碼大概如下:
CABasicAnimation *zPosition = [CABasicAnimation animation];
zPosition.keyPath = @"zPosition";
zPosition.fromValue = @-1;
zPosition.toValue = @1;
zPosition.duration = 1.2;
CAKeyframeAnimation *rotation = [CAKeyframeAnimation animation];
rotation.keyPath = @"transform.rotation";
rotation.values = @[ @0, @0.14, @0 ];
rotation.duration = 1.2;
rotation.timingFunctions = @[
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]
];
CAKeyframeAnimation *position = [CAKeyframeAnimation animation];
position.keyPath = @"position";
position.values = @[
[NSValue valueWithCGPoint:CGPointZero],
[NSValue valueWithCGPoint:CGPointMake(110, -20)],
[NSValue valueWithCGPoint:CGPointZero]
];
position.timingFunctions = @[
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]
];
position.additive = YES;
position.duration = 1.2;
CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
group.animations = @[ zPosition, rotation, position ];
group.duration = 1.2;
group.beginTime = 0.5;
[card.layer addAnimation:group forKey:@"shuffle"];
card.layer.zPosition = 1;
我們使用 CAAnimationGroup 得到的一個(gè)好處是可以將所有動(dòng)畫(huà)作為一個(gè)對(duì)象暴露出去。如果你要在應(yīng)用程序中的多個(gè)地方用工廠對(duì)象創(chuàng)建的重用的動(dòng)畫(huà)的話,這將會(huì)非常有用。
你也可以使用動(dòng)畫(huà)組同時(shí)控制所有動(dòng)畫(huà)組成部分的時(shí)間。
都現(xiàn)在了,你應(yīng)該已經(jīng)聽(tīng)說(shuō)過(guò) UIKit Dynamics 了,這是 iOS 7 中引入的一個(gè)物理模擬框架,它允許你使用約束和力來(lái)為 views 做動(dòng)畫(huà)。與 Core Animation 不同,它與你在屏幕上看到的內(nèi)容交互更為間接,但是它的動(dòng)態(tài)特性讓你可以在事先不知道結(jié)果時(shí)創(chuàng)建動(dòng)畫(huà)。
Facebook 最近開(kāi)源了 Paper 背后的動(dòng)畫(huà)引擎 Pop。從概念上講,它介于 Core Animation 和 UIKit Dynamics 之間。它完美的使用了彈簧(spring)動(dòng)畫(huà),并且能夠在動(dòng)畫(huà)運(yùn)行時(shí)操控目標(biāo)值,而無(wú)需替換它。Pop 也可以在 OS X 上使用,并且允許我們?cè)诿總€(gè) NSObject 的子類(lèi)中為任意屬性進(jìn)行動(dòng)畫(huà)。