
前言
注冊(cè)、登錄功能是應(yīng)用標(biāo)識(shí)用戶身份、記錄并保存用戶數(shù)據(jù)、提供個(gè)性化服務(wù)的重要方法,但是對(duì)于用戶來(lái)說(shuō),看到復(fù)雜的登錄、注冊(cè)表單就已經(jīng)讓人厭惡至極了,還要去亂成一團(tuán)的腦海中提取出相應(yīng)的賬號(hào)密碼,最后還要正確無(wú)誤地在鍵盤上一個(gè)一個(gè)地輸入字母。。。是不是要生無(wú)可戀到?jīng)]有脾氣了,感覺(jué)人生已經(jīng)走到崩潰懸崖的邊緣了呢? readme 在登錄和注冊(cè)中恰到好處地加入了動(dòng)畫,這種人性化的設(shè)計(jì)讓人眼前一亮,如果嘗試多點(diǎn)幾下觸發(fā)動(dòng)畫,感覺(jué)可以玩一整天啊。
輸入 「Email 」時(shí)攤開雙手,輸入「Password」時(shí)遮住眼睛在無(wú)形中傳達(dá)了:
- 用遮眼、睜眼的動(dòng)畫巧妙地表達(dá)了:「Email」是明文輸入,而「Password」是密文輸入;
- 密碼輸入的安全性,貓頭鷹遮住眼睛好像在提示用戶:輸入密碼時(shí)要注意防止他人偷窺以竊取你的賬號(hào)和密碼,如果此時(shí)有小伙伴在邊上,看到這個(gè)動(dòng)畫,他也應(yīng)該明白:這時(shí)候我需要回避一下。
- 向使用者傳達(dá):服務(wù)提供方非常注重用戶隱私,用戶輸入的密碼服務(wù)提供方是不會(huì)隨意偷看的;
- 可愛(ài)、簡(jiǎn)潔而有趣的貓頭鷹動(dòng)畫減輕并降低了用戶輸入賬號(hào)密碼時(shí)的反感和焦慮,也傳達(dá)了服務(wù)提供方非常具有創(chuàng)意,對(duì)產(chǎn)品及其注重人性化體驗(yàn)。

動(dòng)畫分解
將貓頭鷹動(dòng)畫錄屏,并提取出關(guān)鍵幀圖片,查看手指的變化:

說(shuō)明:
- 兩手抓起的圖片為【放下時(shí)的左右手】
- 遮住眼睛的圖片為【抬起時(shí)的左右手】
觀察分解的圖片,從打開雙手狀態(tài) -> 遮住眼睛狀態(tài):
【放下時(shí)的左右手】初始化時(shí)位置分別在貓頭鷹頭像圖片左右兩側(cè),正常大小。動(dòng)畫開始后,向右平移的同時(shí)逐漸縮小為0;
【抬起時(shí)的左右手】初始化時(shí)位置在【放下時(shí)的左右手】圖片的中心點(diǎn)位置,但是大小為 0。動(dòng)畫開始后,向右平移的同時(shí)放大至正常大小并遮住眼睛;
-
注意到,雙手遮住眼睛后,還有一張【眼睛圖片】會(huì)覆蓋住眼睛白色的部分,
眼睛圖片并不是簡(jiǎn)單的設(shè)置隱藏/顯示:
self.owlEyeImgView.hidden = YES;來(lái)添加的。注意到第四張分解圖片,貓頭鷹的白色部分是有一點(diǎn)點(diǎn)灰色漸變的,在動(dòng)畫中可以通過(guò)設(shè)置alpha值來(lái)設(shè)置眼睛圖片。
遮住眼睛狀態(tài)->打開雙手狀態(tài)的動(dòng)畫則與上所述相反。
準(zhǔn)備素材
從 readme 登錄頁(yè)面下載圖片素材:
Safari 瀏覽器 - 開發(fā) - 顯示頁(yè)面資源 - 打開圖像并下載:

核心方法
設(shè)置一個(gè)枚舉類型表示貓頭鷹當(dāng)前的動(dòng)畫狀態(tài):
// 貓頭鷹動(dòng)畫
typedef NS_ENUM(NSUInteger, RMIOLoginViewOwlAnimationState) {
RMIOLoginViewOwlAnimationStateDefaule, // 默認(rèn)初始狀態(tài)
RMIOLoginViewOwlAnimationStateDown, // 睜眼狀態(tài)(輸入用戶名時(shí))
RMIOLoginViewOwlAnimationStateUp, // 遮眼狀態(tài)(輸入密碼時(shí))
};
我將抬手動(dòng)畫和打開雙手的兩個(gè)動(dòng)畫分別封裝在兩個(gè)方法中,方便復(fù)用:
抬起左右手遮眼動(dòng)畫方法
// 抬起左右手動(dòng)畫:打開雙手狀態(tài) -> 遮住眼睛狀態(tài)
- (void)armUpImageAnimation {
[UIView animateWithDuration:KOwlAnimationDuration animations:^{
// 眼睛圖片 alpha 值從0變?yōu)?
self.owlEyeImgView.alpha = 1;
// 【放下時(shí)的左右手】向右平移并縮小到0
CGRect armDownLeftRect = CGRectMake(_faceRect.origin.x +29, 148, 0, 0);
self.armDownLeftImgView.frame = armDownLeftRect;
CGRect armDownRightRect = CGRectMake(CGRectGetMaxX(_faceRect) - 29, 148, 0, 0);
self.armDownRightImgView.frame = armDownRightRect;
// 抬起時(shí)的左右手】向右平移并放大
CGRect armUpLeftRect = CGRectMake(_faceRect.origin.x - 6, 108, 51, 42);
self.armUpLeftImgView.frame = armUpLeftRect;
CGRect armUpRightRect = CGRectMake(_faceRect.origin.x + 60, 107, 51, 43);
self.armUpRightImgView.frame = armUpRightRect;
}];
}
放下左右手睜眼動(dòng)畫方法
// 放下左右手動(dòng)畫:遮住眼睛狀態(tài)->打開雙手狀態(tài)
- (void)armDownImageAnimation {
[UIView animateWithDuration:KOwlAnimationDuration animations:^{
// 眼睛圖片 alpha 值從1變?yōu)?
self.owlEyeImgView.alpha = 0;
// 【放下時(shí)的左右手】向左平移還原
CGRect armDownLeftRect = CGRectMake(_faceRect.origin.x - 36, 134, 43, 25);
self.armDownLeftImgView.frame = armDownLeftRect;
CGRect armDownRightRect = CGRectMake(CGRectGetMaxX(_faceRect), 134, 43, 26);
self.armDownRightImgView.frame = armDownRightRect;
// 抬起時(shí)的左右手】向左平移并縮小到0
CGRect armUpLeftRect = CGRectMake(_faceRect.origin.x - 15, 150, 0, 0);
self.armUpLeftImgView.frame = armUpLeftRect;
CGRect armUpRightRect = CGRectMake(CGRectGetMaxX(_faceRect) + 15, 150, 0, 0);
self.armUpRightImgView.frame = armUpRightRect;
}];
}
需要實(shí)現(xiàn) UITextFieldDelegate 部分協(xié)議以調(diào)用動(dòng)畫方法:
#pragma mark - UITextFieldDelegate
// 開始編輯
- (void)textFieldDidBeginEditing:(UITextField *)textField {
// 1.開始輸入用戶名
if (textField.tag == KUsernameTextFieldTag) {
switch (self.owlAnimationState) {
case RMIOLoginViewOwlAnimationStateDefaule: {
self.owlAnimationState = RMIOLoginViewOwlAnimationStateDown;
break;
}
case RMIOLoginViewOwlAnimationStateDown: {
break;
}
case RMIOLoginViewOwlAnimationStateUp: {
self.owlAnimationState = RMIOLoginViewOwlAnimationStateDown;
[self armDownImageAnimation];
break;
}
}
}
// 2.開始輸入密碼
if (textField.tag == KPasswordTextFieldTag) {
switch (self.owlAnimationState) {
case RMIOLoginViewOwlAnimationStateDefaule:
case RMIOLoginViewOwlAnimationStateDown: {
self.owlAnimationState = RMIOLoginViewOwlAnimationStateUp;
[self armUpImageAnimation];
break;
}
case RMIOLoginViewOwlAnimationStateUp: {
break;
}
}
}
}
/*
結(jié)束編輯
每當(dāng)密碼輸入結(jié)束編輯時(shí),需要打開雙手。
遮住眼睛->打開雙手:
* 密碼輸入時(shí),返回到賬號(hào)輸入,打開雙手。
* 密碼輸入時(shí),點(diǎn)擊空白部分或點(diǎn)擊登錄,打開雙手。
*/
- (void)textFieldDidEndEditing:(UITextField *)textField {
if (textField.tag == KPasswordTextFieldTag) {
if (self.owlAnimationState == RMIOLoginViewOwlAnimationStateUp) {
self.owlAnimationState = RMIOLoginViewOwlAnimationStateDown;
[self armDownImageAnimation];
}
}
}
// 用戶點(diǎn)擊鍵盤"Return"按鈕
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[self endEditing:YES];
return YES;
}
看看實(shí)現(xiàn)效果如何:
bingo!I Do It!??????

放慢10倍速度看看:

源碼已上傳 GitHub
參考
君子性非異也,善假于物也——荀子
參考了各位大佬的Demo源碼,還有各種資料:
-
WSLoginView ??
這個(gè) Demo 可以研究研究,本文的動(dòng)畫邏輯也大都參考這個(gè)。但仔細(xì)觀察你會(huì)發(fā)現(xiàn)因?yàn)樗玫膱D片與視圖層次結(jié)構(gòu)沖突的原因,貓頭鷹的鼻尖沒(méi)有辦法露出來(lái)(左右兩邊的小手本來(lái)也沒(méi)有露出來(lái)),我想了許久,最后使用 Sketch 畫了一個(gè)鼻子貼在上面。
WSLoginView
畫一個(gè)鼻子給你:
owl-mouth@2x.png
關(guān)于像素對(duì)齊
我在設(shè)置貓頭鷹圖片 Frame 時(shí)的方法:
self.faceRect = CGRectMake(CGFloatPixelRound((kScreenWidth - 116) / 2), 69, 116, 92); //第 203 行
// 【原因解析】
// 在 iPhone 7 (尺寸為 375*667)上的值為:(129.5,69,116,92),因?yàn)?Scale = 2,所以實(shí)際像素值為(259,138,232,184),像素對(duì)齊
// 在 iPhone X (尺寸為 375*812)上的值為(129.666667,69.000000,116.000000,92.000000),因?yàn)?Scale = 3 ,所以實(shí)際像素值為 (389,207,348,276),像素對(duì)齊
//-------------------------
// 如果不使用像素對(duì)齊函數(shù),那么
// 在 iPhone 7 上的值為(129.5,69,116,92),實(shí)際像素值為:(259,138,232,184),像素對(duì)齊
// 在 iPhone X 上的值為(129.5,69,116,92),實(shí)際像素值為:(388.5,207,348,276),像素沒(méi)有對(duì)齊??
// 【總結(jié)】
// 設(shè)置 CGRect 值的時(shí)候盡量小心,特別是如果你用到了除法(如 KScreenSize / 5),可能會(huì)導(dǎo)致最終取出來(lái)的值不是整數(shù),
// 就會(huì)造成像素不對(duì)齊,就會(huì)增加 CPU/GPU 的消耗,會(huì)影響性能或者說(shuō)頁(yè)面不會(huì)以 60 FPS 進(jìn)行渲染,造成頁(yè)面卡頓。
其中有一個(gè) CGFloatPixelRound() 函數(shù)可以設(shè)置像素對(duì)齊值(CGFloat),來(lái)自于 YYKit 框架的 YYCGUtilities.h 類中,里面還有其他像設(shè)置像素對(duì)齊點(diǎn)(CGPoint)、像素對(duì)齊大?。–GSize)、像素對(duì)齊矩形(CGRect)等方法:
// 像素對(duì)齊值
CGFloatPixelFloor(CGFloat value)
CGFloatPixelRound(CGFloat value)
CGFloatPixelCeil(CGFloat value)
CGFloatPixelHalf(CGFloat value)
// 像素對(duì)齊點(diǎn)
CGPointPixelFloor(CGPoint point)
CGPointPixelRound(CGPoint point)
CGPointPixelCeil(CGPoint point)
CGPointPixelHalf(CGPoint point)
// 像素對(duì)齊大小
CGSizePixelFloor(CGSize size)
CGSizePixelRound(CGSize size)
CGSizePixelCeil(CGSize size)
CGSizePixelHalf(CGSize size)
// 像素對(duì)齊矩形
CGRectPixelFloor(CGRect rect)
CGRectPixelRound(CGRect rect)
CGRectPixelCeil(CGRect rect)
CGRectPixelHalf(CGRect rect)
// 像素對(duì)齊邊緣插入量
UIEdgeInsetPixelFloor(UIEdgeInsets insets)
UIEdgeInsetPixelCeil(UIEdgeInsets insets)
其中底層調(diào)用的 Floor()、Round()、Ceil() 分別是數(shù)學(xué)函數(shù):
- floor() 取不大于傳入值的最大整數(shù)
- round() 四舍五入
- ceil() 返回大于或者等于指定表達(dá)式的最小整數(shù)
示例:
scale = 2.000000 時(shí):
"CGFloatPixelCeil(2.1)" = 5; "CGFloatPixelCeil(2.25)" = 5; "CGFloatPixelCeil(2.45)" = 5; "CGFloatPixelFloor(2.1)" = 4; "CGFloatPixelFloor(2.25)" = 4; "CGFloatPixelFloor(2.45)" = 4; "CGFloatPixelHalf(2.1)" = "4.5"; "CGFloatPixelHalf(2.25)" = "4.5"; "CGFloatPixelHalf(2.45)" = "4.5"; "CGFloatPixelRound(2.1)" = 4; "CGFloatPixelRound(2.25)" = 5; "CGFloatPixelRound(2.45)" = 5;
最后的話
興趣是最好的老師,寫代碼可能是一件枯燥的事。看到這個(gè)動(dòng)畫的第一眼就是:哇!好酷!如果在 iOS 上實(shí)現(xiàn)這個(gè)動(dòng)畫應(yīng)該會(huì)很好玩吧!
所以做這個(gè)Demo的時(shí)候可以一直盯著電腦研究搗鼓好久,就連吃飯、走路的時(shí)候也會(huì)想著初始化時(shí)各個(gè)圖片應(yīng)該放在哪里?如何正確獲取及調(diào)整圖片的位置呢?應(yīng)該是什么狀態(tài)的?動(dòng)畫的路徑是如何進(jìn)行的呢?觸摸點(diǎn)擊事件的邏輯是怎樣的呢?所以實(shí)現(xiàn)起來(lái)一點(diǎn)也不費(fèi)力,做完還會(huì)有滿滿的成就感。。。
So,愿永遠(yuǎn)年輕,永遠(yuǎn)熱淚盈眶。

