如何創(chuàng)建一個3D側(cè)邊欄動畫

原攻略來自于:How To Create a Cool 3D Sidebar Animation Like in Taasky

本篇文章為我自己的總結(jié)、翻譯版,你可以點(diǎn)擊上面的鏈接找到原文(如果你不是簡書看到的,可以點(diǎn)擊這里查看我的原文)

在今天的文章中,由于我的英文能力有限,就不會對原文進(jìn)行逐句翻譯了,我會以自己的理解來講解這篇文章的精髓,教你一步步來實(shí)現(xiàn)最終的效果

另外由于我個人的能力有限,可能有些地方出現(xiàn)遺漏或者理解出錯,歡迎指正下,共同學(xué)習(xí)。
最終效果:

最終效果
最終效果

由于之前作者是用swift老版本的初始工程來講解的,你可以來這里下載swift版本轉(zhuǎn)換好的源文件開始今天的學(xué)習(xí)
(ps:接下來的工程我都用swift3.0實(shí)現(xiàn)了一遍,不過仍可能出現(xiàn)粘貼的是老代碼未及時更新的問題,不過swift版本的問題也不難。)

下載好后,Taasky_origin就是我們要開始的初始文件了。

點(diǎn)擊Run運(yùn)行,你可以看到如下效果:


首頁、詳情頁
首頁、詳情頁

看起來像是一個工作之余和朋友喝喝咖啡之類的App

我們現(xiàn)在要做如下的事情

  • 首先你要創(chuàng)建一個ScrollView來包含左側(cè)的側(cè)邊欄按鈕部分和右邊的詳情部分
  • 然后添加一個按鈕來控制側(cè)邊欄的顯示和隱藏
  • 接著實(shí)現(xiàn)一個3D 效果來完成平滑的折疊效果
  • 最后當(dāng)你觸摸的時候,我們順帶添加一個菜單旋轉(zhuǎn)的動畫,和我們顯示、隱藏側(cè)邊欄同步起來
我們來分析一下
具體來看
具體來看
  • 當(dāng)菜單顯示的時候我們實(shí)際看到的是紫色部分的區(qū)域
  • 當(dāng)菜單隱藏的時候我們看到的是綠色的區(qū)域

接下來我們會用到ContainerView ,并通過自動布局添加約束的方式來完成我們最終的布局,最終我們要讓storyboard出現(xiàn)下面的畫面,提前來預(yù)覽下吧:


最終我們將會看到的storyboard
最終我們將會看到的storyboard

不要著急,我們接下來會一步步實(shí)現(xiàn)的。

給UIScrollView添加約束
  • 接下來我們需要做的是:創(chuàng)建一個新的控制器來協(xié)調(diào)菜單控制器和詳情控制器,在這個新的控制器中我們添加一個UIScrollView,然后給ScrollView添加 兩個container views ,分別嵌入菜單控制器和詳情控制器。

  • 創(chuàng)建一個新的ContainerViewController繼承自UIViewController,語言選擇Swift

  • 在Main.storyboard拖拽一個新的UIViewController,在Inspector中修改下之前Class\Custom Class 為 ContainerViewController



    把背景改為黑色:


  • 到了添加ScrollView的時候了,我們拖拽一個ScrollView,并把大小拉至填充整個屏幕,在右側(cè)屬性檢測器中去掉顯示指示條,并且把Delays Content Touches去掉勾選,這樣當(dāng)你選中的時候就不會有那么一點(diǎn)延遲了



    設(shè)置ScrollView 的delegate為當(dāng)前控制器



    給ScrollView添加四個約束,記得去掉勾選Constrain to margins(不要在意下圖中的寬高值的問題):

    添加的約束是:

  • Trailing Space to: Superview

  • Leading Space to: Superview

  • Top Space to: Superview

  • Bottom Space to: Bottom Layout Guide


    添加完后的樣子
    添加完后的樣子
  • 該給ScrollView添加內(nèi)容了,拖拽一個UIView給ScrollView,讓它的大小和ScrollView的大小一樣,然后再通過右側(cè)屬性檢測器給這個UIView的寬度增加80,舉例:我在7Plus的尺寸下編輯,ScrollView由于完全占滿屏幕所以寬度是414,那么我們剛剛添加的UIView的尺寸就是494了(這個時候我們還沒有添加布局哦)

屏幕快照
  • 修改添加的UIView的Document\Label為Content View(主要是為了方便追蹤各個view)
    Open the Identity Inspector for the content view you just added and set
    給剛才的UIView添加約束


  • 修改一下Trailing Space的 Constant 為0(??,好吧在上面一步你完全可以直接設(shè)置好的,多做一步只是為了讓新手熟悉一下...)



    設(shè)置好后,你會發(fā)現(xiàn)有布局的警告,這是因為對于ScrollView的布局僅僅設(shè)置上下左右是不夠的,還要對Content View的寬高做設(shè)置,這樣才能決定ScrollView的 content size.

  • 接下來我們來設(shè)置寬和高,選中Content View,按住control鍵拖拽至View,如下:



    在右側(cè)屬性檢測器中修改寬度的常量為80



    設(shè)置常量為80的意思是我們的Content View會比整個View(也相當(dāng)于屏幕的寬度)的寬度寬80
    好了,現(xiàn)在再看看,發(fā)現(xiàn)警告不見了吧,哈哈,太棒了!
添加菜單和詳情的Container Views

拖拽一個UIContainer View到Content View上,在屬性檢測器中設(shè)置寬度為80,并且給 Document\Label設(shè)置為Menu Container View,如下:


不要在意圖中的600,因為Xcode版本的問題,現(xiàn)在的Xcode8已經(jīng)不是默認(rèn)600X600了,我們要設(shè)置的是高度和Content View一樣,寬度為80,左上角對齊父視圖就對了
添加詳情Container View:同樣再拖拽一個UIContainer View放在菜單Container View右邊,給它的Document\Label 設(shè)置為Detail Container View



這個詳情Container View的寬度和父視圖的寬度是一樣的,現(xiàn)在你應(yīng)該得到如下的視圖:



添加Container View 會自帶一個控制器,把它們刪掉:
現(xiàn)在來設(shè)置布局吧:

對于Menu Container View:相對于它的父視圖以及詳情 Container View共5個約束:



對于 Detail Container View:添加了3個約束:



通過移動箭頭改一下 Initial view controller

把菜單控制器和詳情控制器嵌套進(jìn)來:選中Menu Container View拖拽到Navigation Controller,并選擇embed



設(shè)置好嵌套之后,你的storyboard會變成如下:

嵌套進(jìn)來的控制器統(tǒng)統(tǒng)都收縮到80的寬度了。
調(diào)整一下menu view controller上UIImageView的寬度

刪掉 menu和 detail 之間的 segue,并給detail view controller添加一個Navigation Controller 通過上面菜單欄的按鈕:Editor\Embed In\Navigation Controller



給這個新的navigation controller 設(shè)置如下屬性:



在屬性檢測器中設(shè)置View Controller\Layout\Adjust Scroll View Insets選中(這個會避免內(nèi)容被bar覆蓋):

我們要讓Detail View Controller嵌套在Detail Container View中,


現(xiàn)在你就可以運(yùn)行一下了,運(yùn)行結(jié)果應(yīng)該是這個樣子:



現(xiàn)在我們可以隨意滑動ScrollView了, 接下來我們要修改一下只讓它顯示整個側(cè)邊菜單和隱藏側(cè)邊菜單, 并且讓它不能滑出邊界

  1. 在ScrollView的屬性檢測器中選擇Scrolling\Paging Enabled
  2. 不要選擇Bounce\Bounces

再次build&run 你會發(fā)現(xiàn):


但是還有一點(diǎn)點(diǎn)問題,當(dāng)你試圖隱藏菜單框的時候,它有時又重新彈回并顯示出來了,這是一個 paging的問題,詳細(xì)的你可以參考這里: this problem discussion on StackOverflow.
先留著這個問題,我們先來解決點(diǎn)擊左側(cè)按鈕沒有相應(yīng)的事件變化的問題(和我們最上面演示的圖作比較)

這或許會使你感到驚訝,因為我們并沒有改變?nèi)魏蜗嚓P(guān)的代碼,接著往下看:
先修改一下細(xì)節(jié)的問題:
在 MenuViewController.swift的 viewDidLoad() 加入以下代碼:

override func viewDidLoad() {
  super.viewDidLoad()
  // Remove the drop shadow from the navigation bar
  navigationController!.navigationBar.clipsToBounds = true
}

這能消除掉navigation bar下極小的細(xì)縫,雖然是個很小的細(xì)節(jié),但是也能對整個APP填色不少。

當(dāng)我們點(diǎn)擊MenuViewController中的cell的時候 應(yīng)該設(shè)置DetailViewController 的 menuItem 屬性,但是現(xiàn)在DetailViewController已經(jīng)不直接和MenuViewController連接了,所以沒什么反應(yīng)
ContainerViewController將會扮演MenuViewController和DetailViewController之間的協(xié)調(diào)者的角色
給ContainerViewController.swift添加一個屬性:
private var detailViewController: DetailViewController?
在ContainerViewController.swift中實(shí)現(xiàn) prepareForSegue(_:sender:) 方法:

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "DetailViewSegue" {
        let navigationController = segue.destination as! UINavigationController
        detailViewController = navigationController.topViewController as? DetailViewController
        }
}

在將DetailViewController嵌套進(jìn)container View中的時候會生成一條Storyboard Embed Segue的線,你選中這條線,并設(shè)置它的Identifier為DetailViewSegue:



在ContainerViewController中聲明一個menuItem屬性,并設(shè)置屬性觀測器:

var menuItem: NSDictionary? {
  didSet {
    if let detailViewController = detailViewController {
      detailViewController.menuItem = menuItem
    }
  }
}

由于MenuViewController 和DetailViewController之間已經(jīng)沒有segue連接了,但是當(dāng)選中MenuViewController中的cell的時候仍然需要作出相應(yīng),我們把事件的相應(yīng)從prepareForSegue(:sender:)移動到tableView(:didDeselectRowAtIndexPath:)中。

刪掉prepareForSegue(_:sender:)中的代碼,改成如下:

// MARK: UITableViewDelegate
 override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        tableView.deselectRow(at: indexPath, animated: true)
        let menuItem = menuItems[indexPath.row] as! NSDictionary
        (navigationController!.parent as! ContainerViewController).menuItem = menuItem
    }

這樣我們就在選中menu中的cell的時候設(shè)置ContainerViewController的menuItem屬性了,并且觸發(fā)了屬性觀測器從而設(shè)置了DetailViewController的menuItem屬性

我們在MenuViewController.swift中的viewDidLoad()添加:

(navigationController!.parentViewController as! ContainerViewController).menuItem = 
  (menuItems[0] as! NSDictionary)

這是為了app第一次啟動的時候給DetailViewController設(shè)置默認(rèn)的圖片
現(xiàn)在運(yùn)行app,會發(fā)現(xiàn)如下效果了:


現(xiàn)在我們要顯示和隱藏左側(cè)菜單

當(dāng)我們選中菜單的時候,我們要隱藏掉菜單
為了實(shí)現(xiàn)這個目的,我們要設(shè)置Scroll View 的content
在ContainerViewController.swift 中連接 Scroll View并命名為scrollView
具體操作如下:



現(xiàn)在在ContainerViewController.swift 添加 hideOrShowMenu(_:animated:)方法

// MARK: ContainerViewController
func hideOrShowMenu(show: Bool, animated: Bool) {
  let menuOffset = menuContainerView.bounds.width
  scrollView.setContentOffset(show ? CGPoint.zero : CGPoint(x: menuOffset, y: 0), animated: animated)
}

menuOffset 的值是 80 ,當(dāng)true的時候,那么origin就是(0,0),這個時候菜單是可見的,同理,當(dāng)origin時(80,0)的時候菜單是隱藏的

在ContainerViewController的menuItem屬性觀測器中添加

var menuItem: NSDictionary? {
  didSet {
    hideOrShowMenu(false, animated: true)
    // ...

運(yùn)行app,將會出現(xiàn)如下效果:



好了這個時候我們來解決paging 的問題吧(滑動來隱藏側(cè)邊欄的時候,側(cè)邊欄會彈出來的BUG)
我們將通過遵守UIScrollViewDelegate協(xié)議來解決這個問題
給ContainerViewController添加協(xié)議:

class ContainerViewController: UIViewController, UIScrollViewDelegate {

添加協(xié)議方法并實(shí)現(xiàn):

// MARK: - UIScrollViewDelegate
func scrollViewDidScroll(scrollView: UIScrollView) {
  /*
  Fix for the UIScrollView paging-related issue mentioned here:
  http://stackoverflow.com/questions/4480512/uiscrollview-single-tap-scrolls-it-to-top
  */
        scrollView.isPagingEnabled = scrollView.contentOffset.x < (scrollView.contentSize.width - scrollView.frame.width)
}

對于這個問題的詳細(xì)探究就不在本文的范圍之內(nèi)了,有興趣的同學(xué)可以進(jìn)入這個鏈接看看:這里

Bulid&Run,你會發(fā)現(xiàn)解決了這個問題:


添加左上角的按鈕

通過點(diǎn)擊按鈕來隱藏和展示左側(cè)欄,創(chuàng)建一個HamburgerView.swift繼承于UIView
其內(nèi)部實(shí)現(xiàn):

class HamburgerView: UIView {
 
  let imageView: UIImageView! = UIImageView(image: UIImage(named: “Hamburger”))
 
  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)!
    configure()
  }
 
  required override init(frame: CGRect) {
    super.init(frame: frame)
    configure()
  }
 
  // MARK: Private
 
  private func configure() {
    imageView.contentMode = UIViewContentMode.center
    addSubview(imageView)
  }
 
}

我們來給DetailViewController.swift添加一個屬性:
var hamburgerView: HamburgerView?
在 viewDidLoad()中創(chuàng)建hamburgerView并賦在navigation bar上

let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: “hamburgerViewTapped”)
hamburgerView = HamburgerView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
hamburgerView!.addGestureRecognizer(tapGestureRecognizer)
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: hamburgerView!)

實(shí)現(xiàn)點(diǎn)擊方法:hamburgerViewTapped()
在這個方法中我們將調(diào)用ContainerViewController的hideOrShowMenu(_:animated:)方法,但是我們應(yīng)該傳入什么值呢?

我們給ContainerViewController添加一個bool類型的屬性來記錄左側(cè)菜單是否顯示

在ContainerViewController.swift下面添加屬性:
var showingMenu = false
我們重寫viewDidLayoutSubviews() 來控制展示或者隱藏菜單欄,這樣的好處是旋轉(zhuǎn)的時候也能及時響應(yīng):

override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()
      hideOrShowMenu(show: showingMenu, animated: false)
}

那么我們就不需要ContainerViewController.swift中的viewDidLoad()了,刪了它們吧
別忘了我們還要在DetailViewController.swift中添加響應(yīng)事件:

    func hamburgerViewTapped() {
        let navigationController = parent as! UINavigationController
        let containerViewController = navigationController.parent as! ContainerViewController
        containerViewController.hideOrShowMenu(show: !containerViewController.showingMenu, animated: true)
    }

每當(dāng)我們點(diǎn)擊的時候需要通過!containerViewController.showingMenu來控制是否顯示,就像button的選中非選中那樣。

我們要在hideOrShowMenu方法中及時修改一下我們的showingMenu的狀態(tài),在hideOrShowMenu方法下面添加如下:
showingMenu = show
B&R(Build & Run)你會看到如下的效果:

還有一個問題就是當(dāng)你滑動展示菜單欄或者隱藏菜單欄的時候,再去點(diǎn)擊左上角的button來響應(yīng)事件需要點(diǎn)擊兩次,這是為什么呢?

這是因為我們滑動ScrollView的時候 并沒有更新showingMenu
為了修正這個問題,你需要實(shí)現(xiàn)UIScrollViewDelegate另一個方法
在ContainerViewController中添加:

func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
  let menuOffset = CGRectGetWidth(menuContainerView.bounds)
  showingMenu = !CGPointEqualToPoint(CGPoint(x: menuOffset, y: 0), scrollView.contentOffset)
  println(“didEndDecelerating showingMenu \(showingMenu)”)
}

當(dāng)ScrollView的content offset等于菜單欄寬度(80)的時候,也就是菜單欄是隱藏的,設(shè)置showingMenu為false,反之同理

B&R 讓我們來看看當(dāng)停下來的時候是否會如預(yù)期那樣?看起來和期望好像有一點(diǎn)點(diǎn)的差池,這點(diǎn)差池有點(diǎn)依賴于滑動的速度,當(dāng)我在模擬器上測試的時候,只有滾動很慢的情況下達(dá)到預(yù)期,但是在真實(shí)設(shè)備上只有滾動很快才會達(dá)到預(yù)期。

好吧,那就把上面的代碼全部都移動到scrollViewDidScroll(_:)中,這個方法會不斷的調(diào)用,相對來說更可靠點(diǎn)


添加3D效果

首先要添加透視效果:
在ContainerViewController.swift中添加如下代碼:

  func transformForFraction(fraction:CGFloat) -> CATransform3D {
        var identity = CATransform3DIdentity
        identity.m34 = -1.0 / 1000.0;
        let angle = Double(1.0 - fraction) * -M_PI_2
        let xOffset = menuContainerView.bounds.width * 0.5
        let rotateTransform = CATransform3DRotate(identity, CGFloat(angle), 0.0, 1.0, 0.0)
        let translateTransform = CATransform3DMakeTranslation(xOffset, 0.0, 0.0)
        return CATransform3DConcat(rotateTransform, translateTransform)
    }

分析一下transformForFraction(_:):的作用:

  • 當(dāng) fraction為0的是菜單欄完全隱藏,當(dāng)fraction為1的時候菜單欄完全顯示
  • CATransform3DIdentity 是4x4的單位juzhen
  • CATransform3DIdentity的m34屬性控制著透視的量
  • CATransform3DRotate 用弧度制 來控制繞y軸的旋轉(zhuǎn)量,-90表明垂直于當(dāng)前的x-y平面,0表明平行于x-y平面
  • rotateTransform是單位矩陣經(jīng)傳入m34 值按照一定弧度選擇變換之后的矩陣
  • translateTransform 是將菜單欄向右移動其一半寬度距離變換而來的矩陣
  • CATransform3DConcat 將上面的兩個矩陣進(jìn)行了連鎖變化

注意: m34 通常是1除以一個值來表示,這個值表達(dá)的含義是你在z軸上觀察x-y平面的位置(單位是像素),負(fù)數(shù)表明觀察者是在屏幕前,而正數(shù)表示觀察者在屏幕后面。

在觀察者和觀察的對象之間畫線形成3D透視效果。觀察者移動的越遠(yuǎn),那么透視效果越不明顯。你可以試試改變值1000到500或者2000來看看菜單欄會發(fā)生什么樣的變化.

在scrollViewDidScroll(_:):下添加如下代碼:

 let multiplier = 1.0 / menuContainerView.bounds.width
        let offset = scrollView.contentOffset.x * multiplier
        let fraction = 1.0 - offset
        menuContainerView.layer.transform = transformForFraction(fraction: fraction)
        menuContainerView.alpha = fraction

值是從0到1的,0表示完全顯示,1表示完全隱藏菜單欄
這樣的話fraction 完全依賴于已經(jīng)顯示的菜單欄的寬度來改變其值(0~1),同時我們還通過fraction來調(diào)整菜單欄的alpha 來改變它的明暗情況

B&R 我們可以看到我們的3D效果了


很顯然還有一點(diǎn)錯誤,我們的連接點(diǎn)好像出問題了,這是因為view默認(rèn)狀態(tài)的anchorPoint是view的中心

我們來修改anchorPoint使其在右側(cè)邊緣中心:

在ContainerViewController.swift的viewDidLayoutSubviews()中添加如下code:
menuContainerView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
B&R 你會看到:

還有一件事:

我們來給左上角的按鈕(以下稱MenuBtn)添加動畫吧
在HamburgerView.swift中添加:
func rotate(fraction: CGFloat) { let angle = Double(fraction) * M_PI_2 imageView.transform = CGAffineTransformMakeRotation(CGFloat(angle)) }
在ContainerViewController.swift中的scrollViewDidScroll(_:)中添加如下代碼:

if let detailViewController = detailViewController { if let rotatingView = detailViewController.hamburgerView { rotatingView.rotate(fraction) } }
B&R

最終效果
最終效果

Cool!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,220評論 4 61
  • 原文地址在這里.原文 去年,讀者們投票選出了Top5的iOS7最佳動畫,當(dāng)然也很想看到有關(guān)這些動畫如何實(shí)現(xiàn)的教程。...
    bu再等閱讀 743評論 0 0
  • 一 慌慌張張地考完普通話,在預(yù)備火鍋面前早已被拋到九霄云外。2016年冬天的第一場火鍋,...
    碰我就炸氣閱讀 332評論 0 0
  • 有一天,給丹妞講《狐貍和烏鴉》的故事,末了,我跟她說:“可憐的小烏鴉,輕信了狐貍的話,害得自己要挨餓了?!钡ゆせ匚?..
    劉忙不盲閱讀 4,103評論 0 1

友情鏈接更多精彩內(nèi)容