轉(zhuǎn)自我自己的 blog

這一篇 blog 是專門介紹如何仿寫知乎日?qǐng)?bào)的側(cè)邊欄菜單,主要介紹如何構(gòu)建 UI 以及一個(gè)漸隱的圖層效果,和如何控制側(cè)邊欄的顯示和隱藏。
#界面
首先是畫 UI。側(cè)邊欄的 UI 比較簡(jiǎn)單,由上到下分別是:
- 頭像和登錄按鈕
- 收藏、消息和設(shè)置按鈕
- 專欄的列表
- 離線下載和切換主題的按鈕
用 xib 做出來(lái)就是這樣子:

值得一提的是專欄列表的底部,在滑動(dòng)的時(shí)候會(huì)有一個(gè)漸隱的效果,如圖所示:

實(shí)現(xiàn)這個(gè)效果的第一個(gè)想法是在 UITableView 的底部改一個(gè)帶漸變效果的透明 UIView,但是原版中處于漸隱效果的底部 Cell 也是可以點(diǎn)擊的,這樣子遮罩的 UIView 還要處理一下點(diǎn)擊事件,麻煩,棄用!第二個(gè)想法就和 Part 2 中一樣,給 UITableView 設(shè)置一個(gè) CAGradientLayer,專門在底部加一個(gè)漸變的效果,但是試驗(yàn)了幾次,發(fā)現(xiàn)漸變的效果十分不理想,所以也放棄了。
最后的解決方案,也是我無(wú)意中發(fā)現(xiàn)的。UIView 的 layer 有個(gè)屬性是 mask, 根據(jù)文檔里面寫的,這也是個(gè) CALayer,用來(lái)給 layer 的 alpha 通道設(shè)置遮罩。但是,這個(gè)屬性必須在滾動(dòng)的時(shí)候不斷重新設(shè)置,不然遮罩會(huì)固定在一個(gè)地方。所以更新的代碼就在 scrollViewDidScroll: 里:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
UIColor *backgroundColor = [UIColor colorWithRed:0.106 green:0.125 blue:0.141 alpha:1];
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.menuTableView.bounds;
gradientLayer.colors =
@[ (id)[UIColor clearColor].CGColor, (id)backgroundColor.CGColor];
gradientLayer.endPoint = CGPointMake(0.5, 0.8);
gradientLayer.startPoint = CGPointMake(0.5, 1);
self.menuTableView.layer.mask = gradientLayer;
}
這段代碼實(shí)現(xiàn)的就是每次滾動(dòng)的時(shí)候,都新建一個(gè) CAGradientLayer,設(shè)置好大小、漸變的顏色和位置,然后將其賦給 UITableView 的 layer 的 mask。但是這樣肯定會(huì)有性能問題,可我也沒想到其他的方法,如果有更好的解決方案還請(qǐng)賜教!
#View 層次
弄好了 UI,接下來(lái)就是交互操作了。顯示側(cè)邊欄有兩個(gè)方式:一是點(diǎn)擊主頁(yè)面左上角的菜單按鈕,二是向右滑動(dòng)。同樣隱藏側(cè)邊欄也有兩個(gè)方式:一是點(diǎn)擊主頁(yè)面的任意區(qū)域,二是向左滑動(dòng)。兩個(gè)的根本區(qū)別就是,一個(gè)是 tap,一個(gè)是 slide。
這里先說(shuō)一個(gè) Part 1 中遺留的東西,就是主頁(yè)面的 view (以下簡(jiǎn)稱 main view)是 HomeViewController 的 view(下面簡(jiǎn)稱 root view) 的一個(gè) subview。為什么要多套這一層呢?我最開始做的時(shí)候是沒有這一層的,也就是 root view 就是主頁(yè)面的 View,側(cè)邊欄的 view (以下簡(jiǎn)稱 side view)是作為 root view 的 subview 放在 root view 的左側(cè)。移動(dòng)兩個(gè) view 的動(dòng)畫代碼就是這個(gè)樣子的(半偽代碼,示意而已):
[UIView animateWithDuration:kSideMenuAnimationDuration
animations:^{
self.view.left = 225;
self.sideMenuVC.view.left = 0;
}];
就是 root view 和 side view 同時(shí)向右移動(dòng)一段距離。但是這樣做的話,root view 向右移動(dòng)后,side view 也無(wú)法顯示出來(lái),只是一個(gè)同樣大小的黑色區(qū)域。我用 Reveal 看了一下,移動(dòng)后 root view 的 frame 也右移了,也就是 origin 不在屏幕的左上角,而 side view 仍然是在 root view 的 bounds 之外(我猜想這是無(wú)法顯示的原因)。
嘗試之后,我的解決方法就是,main view 和 side view 都作為 root view 的 subview,移動(dòng)的只是 main view 和 side view, 而 root view 是固定不動(dòng)的,這就是多了一層的原因。
#Tap 操作
首先說(shuō)如何通過點(diǎn)擊來(lái)顯示和隱藏側(cè)邊欄。因?yàn)槲宜械?UI 都是靠 AutoLayout 來(lái)控制的,所以側(cè)邊欄和主頁(yè)面的位置變換的動(dòng)畫也是用 AutoLayout。
HomeViewController 在加載的時(shí)候把 side view 加入到 root view 中,設(shè)置好位置和大小,這里的代碼就不貼了。下面是點(diǎn)擊菜單按鈕顯示側(cè)邊欄的代碼:
- (IBAction)showSideMenu:(UIButton *)sender {
self.homeViewLeft.constant = 225;
self.homeViewRight.constant = -225;
[self.homeView setNeedsUpdateConstraints];
[UIView animateWithDuration:kSideMenuAnimationDuration
animations:^{
self.sideMenuVC.view.left = 0;
[self.view layoutIfNeeded];
}
completion:^(BOOL finished) {
// setup transparent view for tapping to hide the side menu
self.tapView = [[UIView alloc] initWithFrame:self.homeView.bounds];
self.tapView.backgroundColor = [UIColor clearColor];
[self.tapView addGestureRecognizer:self.tapToHideSideMenu];
[self.homeView addSubview:self.tapView];
self.isShowSideMenu = YES;
}];
}
代碼中的 homeViewLeft 和 homeViewRight 是從 StoryBoard 中拖過來(lái)的約束,通過改變這兩個(gè)約束的值來(lái)移動(dòng) main view。
原版中側(cè)邊欄滑出后,移到右側(cè)的 main view 就不可以上下滑動(dòng)內(nèi)容了,只可以點(diǎn)擊或滑動(dòng)隱藏側(cè)邊欄菜單。所以在上面的代碼中,動(dòng)畫結(jié)束后,新建一個(gè) tapView 蓋在 main view 上面,這樣就無(wú)法直接操作 main view 了,然后給這個(gè) tapView 添加一個(gè) tap 的手勢(shì),這個(gè)手勢(shì)就是用來(lái)隱藏側(cè)邊欄的。下面是隱藏的代碼:
- (void)hideSideMenu {
[UIView animateWithDuration:kSideMenuAnimationDuration
animations:^{
self.homeView.left = 0;
self.sideMenuVC.view.left = -255;
}
completion:^(BOOL finished) {
[self.tapView removeGestureRecognizer:self.tapToHideSideMenu];
[self.tapView removeFromSuperview];
self.isShowSideMenu = NO;
}];
}
這個(gè)動(dòng)畫里沒有使用 AutoLayout,還是用 frame 來(lái)操作位置。在隱藏動(dòng)畫結(jié)束后,順便把 tapView 干掉。
#Slide 操作
另外一種交互就是向左向右滑動(dòng),這里就用到了 UIPanGestureRecognizer。
滑動(dòng)的細(xì)節(jié)有兩點(diǎn):
- side view 和 main view 隨著滑動(dòng)手勢(shì)同步移動(dòng)。
- 如果向右滑動(dòng)到某一閾值,side view 就自動(dòng)完全顯示出來(lái)。反之,side view 就完全隱藏。
具體實(shí)現(xiàn)就是給 main view 添加一個(gè) 'UIPanGestureRecognizer',手勢(shì)的 action 代碼如下:
- (void)handlePanGesture:(UIPanGestureRecognizer *)recognizer {
CGFloat offsetX = [recognizer translationInView:self.homeView].x;
if (offsetX > 0 && offsetX < kSideMenuWidth) {
self.sideMenuVC.view.right = offsetX;
self.homeViewLeft.constant = offsetX;
self.homeViewRight.constant = -offsetX;
[self.homeView layoutIfNeeded];
}
if (recognizer.state == UIGestureRecognizerStateEnded) {
if (offsetX >= kSideMenuWidth / 2) {
[self showSideMenu:nil];
} else {
[self hideSideMenu];
}
}
}
這段代碼中,第一個(gè) if 是用 AutoLayout 和 frame 根據(jù)滑動(dòng)距離同時(shí)移動(dòng)兩個(gè) view;第二個(gè) if 就是手勢(shì)結(jié)束時(shí)判斷是要隱藏還是顯示,如果超過側(cè)邊欄寬度的一半,就進(jìn)行顯示動(dòng)畫,反之就進(jìn)行隱藏動(dòng)畫。
代碼請(qǐng)猛戳 github