ARKit教程16_第十二章:高級(jí)用戶交互

前言

對(duì)ARKit感興趣的同學(xué),可以訂閱ARKit教程專(zhuān)題
源代碼地址在這里

正文

在之前的章節(jié),我們學(xué)習(xí)了:

  • 檢測(cè)一個(gè)矩形。
  • 檢測(cè)一個(gè)QR碼。
  • 在檢測(cè)到的矩形和QR碼上面添加一個(gè)平面。
  • 在平面上面顯示內(nèi)容。

在這一章,我們將會(huì)學(xué)到:

  • 通過(guò)使用故事板而不是獨(dú)立的視圖控制器來(lái)改善用戶交互。
  • 切換全屏模式。
  • 檢測(cè)預(yù)定義的圖像。

我們會(huì)采用上一章的代碼繼續(xù)做開(kāi)發(fā)。不過(guò)我們需要移除掉一些無(wú)用的代碼。

首先,我們把BillboardViewController.xib移除掉,然后再把BillboardView.swift.移除掉。

打開(kāi)BillboardViewController.swift并刪除對(duì)剛剛刪除的BillboardView類(lèi)的引用。此外,刪除BillboardViewController類(lèi)中的其余代碼,但保留其定義。你最終會(huì)得到一個(gè)像這樣的空類(lèi):

class BillboardViewController: UIViewController { 
}

接下來(lái),刪除實(shí)現(xiàn)UICollectionViewDelegateFlowLayout協(xié)議的擴(kuò)展。

打開(kāi)ViewController.swift并找到setBillboardImages(_ :)。找到后,刪除整個(gè)方法。

此外,我們可以刪除BillboardViewDelegate協(xié)議的擴(kuò)展名。找到以下代碼并將其刪除:

extension ViewController: BillboardViewDelegate { 
    func billboardViewDidSelectPlayVideo( 
        _ view: BillboardView) {
        createVideo()
    }
}

最后,找到addBillboardNode()。刪除以下代碼行:

let images = [ 
    "logo_1", "logo_2", "logo_3", "logo_4", "logo_5" 
].map { UIImage(named: $0)! }
setBillboardImages(images)

之后我們需要添加一些新的文件。我們首先創(chuàng)建一個(gè)Billboard.storyboard

請(qǐng)注意,它由導(dǎo)航控制器和具有三個(gè)不同自定義單元的集合視圖控制器組成。

以上三個(gè)cell用于顯示:

  • 視頻播放器
  • 圖片
  • 網(wǎng)頁(yè)視圖

為此,我們需要?jiǎng)?chuàng)建三個(gè)cell。

  • ImageCell:用于顯示一張圖片。

    import UIKit
    
    class ImageCell: UICollectionViewCell {
        @IBOutlet weak var imageView: UIImageView!
    
        func show(image: UIImage) {
              imageView.image = image
        }
    }
    
  • WebBrowserCell: 用于展示一個(gè)網(wǎng)頁(yè)視圖。

    import UIKit
    
    class WebBrowserCell: UICollectionViewCell {
        @IBOutlet weak var webBrowser: UIWebView!
    
      func go(to urlString: String) {
          guard let url = URL(string: urlString) else { return }
          let request = URLRequest(url: url)
          webBrowser.loadRequest(request)
        }
    }
    
  • VideoCell:用于播放視頻。

    import UIKit
    import SpriteKit
    import AVFoundation
    import ARKit
    
    class VideoCell: UICollectionViewCell {
        @IBOutlet weak var playButton: UIButton!
        @IBOutlet weak var playerContainer: UIView!
    
      func configure(videoUrl: String, sceneView: ARSCNView, billboard: BillboardContainer) {
      }
         @IBAction func play() {
         }
     }
    

創(chuàng)建基于故事板的廣告牌

新廣告牌使用故事板,因此它與之前的實(shí)現(xiàn)有很大不同。打開(kāi)ViewController.swift并滾動(dòng)到renderer(_:nodeFor:)。

在上一次迭代中,我們?cè)诶锩鎰?chuàng)建了BillboardViewController
addBillboardNode()。由于實(shí)現(xiàn)現(xiàn)在更復(fù)雜,而不是再次將代碼添加到該方法,最好使用新方法。

let billboardNode = addBillboardNode()之后添加如下代碼:

createBillboardController()

在上一次迭代中,我們?cè)?strong>BillboardViewController里面創(chuàng)建了
addBillboardNode()方法。由于實(shí)現(xiàn)現(xiàn)在更復(fù)雜,而不是再次將代碼添加到該方法,最好使用新方法。

func createBillboardController() { 
    // 1 
    DispatchQueue.main.async {

    // 2 
    let navController = UIStoryboard(name: "Billboard", bundle: nil) .instantiateInitialViewController() as! UINavigationController

    // 3 
    let billboardViewController = navController.visibleViewController as! BillboardViewController

    // 4 
    billboardViewController.sceneView = self.sceneView billboardViewController.billboard = self.billboard

    // 5 
    billboardViewController.willMove( toParentViewController: self) self.addChildViewController(billboardViewController) self.view.addSubview(billboardViewController.view)

    // 6 
     self.show(viewController: billboardViewController)
    }
}

上面的代碼作用如下:

  • 1: 切換到主線程,以便我們可以處理用戶界面更新。
  • 2: 創(chuàng)建Billboard故事板的初始視圖控制器的實(shí)例,它是一個(gè)導(dǎo)航控制器。這里的強(qiáng)制轉(zhuǎn)換是一個(gè)很好的調(diào)試工具:如果應(yīng)用程序崩潰,則意味著存在開(kāi)發(fā)錯(cuò)誤 - 初始視圖控制器很可能不是預(yù)期的導(dǎo)航控制器。
  • 3: 導(dǎo)航控制器的根視圖控制器是BillboardViewController。再次強(qiáng)制轉(zhuǎn)換非常方便,以防我們?cè)诠适掳逯懈哪承﹥?nèi)容并忘記更新代碼。
  • 4: 廣告牌將使用場(chǎng)景視圖和廣告牌容器,因此最好在這里設(shè)置它們;但是,我們必須添加兩個(gè)相應(yīng)的屬性。
  • 5: 要在顯示新視圖控制器之前準(zhǔn)備它,我們需要:
    ??a:告訴新視圖控制器它將移動(dòng)到ViewController。
    ??b: 將新視圖控制器添加到ViewController。
    ??c: 將新視圖控制器的視圖添加到ViewController的視圖中作為子視圖。
  • 6: 新視圖控制器已準(zhǔn)備好顯示,因此我們將其傳遞給show(viewController:)

這樣就完成了廣告牌的創(chuàng)建;但是,show方法尚未實(shí)現(xiàn)。在剛剛創(chuàng)建的方法之后添加如下代碼:

private func show(viewController: BillboardViewController) { 
    let material = SCNMaterial() material.isDoubleSided = true 
    material.cullMode = .front
    material.diffuse.contents = viewController.view
    billboard?.viewController = viewController 
    billboard?.billboardNode?.geometry?.materials = [material]
}

這與我們?cè)谏弦徽碌?strong>setBillboardImages()中所做的類(lèi)似。

打開(kāi)BillboardViewController,添加如下代碼:

var sceneView: ARSCNView?

var billboard: BillboardContainer?

構(gòu)建并運(yùn)行應(yīng)用程序。注意一旦檢測(cè)到QR碼,應(yīng)用程序就會(huì)崩潰。看看Xcode控制臺(tái)會(huì)發(fā)現(xiàn)一個(gè)無(wú)法識(shí)別的選擇器被發(fā)送到廣告牌視圖控制器:

[RazeAd.BillboardViewController collectionView:numberOfItemsInSection:]: unrecognized selector sent to instance 0x1030e4600

RazeAd[3180:1334506] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[RazeAd.BillboardViewController collectionView:numberOfItemsInSection:]:unrecognized selector sent to instance 0x1030e4600'

發(fā)生這種情況是因?yàn)楣适掳逍枰獜V告牌視圖控制器是UICollectionViewController的子類(lèi),當(dāng)我們打開(kāi)故事板文件時(shí)可能會(huì)注意到它。

這是一個(gè)簡(jiǎn)單的方法:修改BillboardViewController類(lèi)并使其繼承自UICollectionViewController而不是UIViewController

class BillboardViewController: UICollectionViewController

與廣告牌互動(dòng)

加載廣告牌故事板后,就可以專(zhuān)注于在Billboard ViewController類(lèi)中進(jìn)行連接。

最初的步驟是提供集合視圖;這意味著覆蓋UICollectionViewDataSource協(xié)議定義的一些方法。

打開(kāi)BillboardViewController.swift并且在最后添加如下代碼:

// UICollectionViewDataSource 
extension BillboardViewController { 
}

我們需要告訴集合視圖三件事:每個(gè)部分的部分?jǐn)?shù)量,每個(gè)部分的項(xiàng)目數(shù)以及每個(gè)項(xiàng)目要顯示的單元格。

在擴(kuò)展中添加如下代碼:

override func numberOfSections( 
    in collectionView: UICollectionView) -> Int { 
    return 3 
}

每個(gè)單元格類(lèi)型都作為單獨(dú)的部分處理。這在處理圖像序列時(shí)很有用。

接下來(lái)是在每個(gè)case下面添加以下方法:

override func collectionView(
    _ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let currentSection = Section(rawValue: section) else { return 0 }
switch currentSection { 
case .images:
return images.count case .video:
return 1 case .webBrowser:
return 1 
    }
}

只有一個(gè)視頻和一個(gè)Web瀏覽器,因此對(duì)于這些,我們返回1。但是,可能有多個(gè)圖像,因此我們返回images.count以確定需要多少行。

最后,我們必須添加返回給定索引路徑的單元格的方法:

override func collectionView(

    _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

    // 1 
    guard let currentSection =
    Section(rawValue: indexPath.section) else { fatalError("Unexpected collection view section") }

    // 2 
    let cellType: Cell switch currentSection { 
    case .images:
    cellType = .cellImage case .video:
    cellType = .cellVideo case .webBrowser:
    cellType = .cellWebBrowser }

    // 3 
    let cell = collectionView.dequeueReusableCell( withReuseIdentifier: cellType.rawValue, for: indexPath)

    // 4 
    switch cell { 
    case let imageCell as ImageCell:
         let image = UIImage(named: images[indexPath.row])! imageCell.show(image: image)

    case let videoCell as VideoCell:

    let videoUrl = "https://www.rmp-streaming.com/media/bbb-360p.mp4" if let sceneView = sceneView, let billboard = billboard { videoCell.configure( videoUrl: videoUrl, sceneView: sceneView, billboard: billboard ) } break

    case let webBrowserCell as WebBrowserCell: 
    webBrowserCell.go(to: "https://www.raywenderlich.com")

    default:

    fatalError("Unrecognized cell") }

    return cell

}

這是一個(gè)很長(zhǎng)的,雖然簡(jiǎn)單的實(shí)現(xiàn):

  • 1:Section枚舉用于標(biāo)識(shí)每個(gè)部分。如果其原始值無(wú)法識(shí)別某個(gè)部分,則會(huì)拋出fatalError。
  • 2: 另一個(gè)名為Cell的枚舉,用于識(shí)別細(xì)胞。
  • 3: Cell枚舉使用String類(lèi)型的原始值來(lái)定義單元標(biāo)識(shí)符。指定索引路徑的單元格已出列。
  • 4: 我們可以根據(jù)細(xì)胞類(lèi)型使用適當(dāng)?shù)臄?shù)據(jù)配置細(xì)胞。

我們可能已經(jīng)注意到圖像單元格的代碼引用了不存在的圖像屬性。此屬性將包含要在廣告牌中顯示的圖像名稱(chēng)列表。

billboard屬性之后,添加以下內(nèi)容:

var images: [String] = [ "logo_1", "logo_2", "logo_3", "logo_4", "logo_5" ]

目前,我們已完成廣告牌視圖控制器。構(gòu)建并運(yùn)行應(yīng)用程序。

第一個(gè)單元是視頻播放器 - 它還沒(méi)有實(shí)現(xiàn),所以不要指望它能夠工作。但是,我們可以向右滑動(dòng)以顯示圖像,并且可以訪問(wèn)最后一個(gè)單元格中的Web瀏覽器,該單元格將加載www.raywenderlich.com

顯示視頻播放器

視頻單元實(shí)現(xiàn)

打開(kāi)VideoCell.swift添加如下代碼:

// 1 
var isPlaying = false 
// 2 
var videoNode: SKVideoNode!

var spriteScene: SKScene!

// 3 
var videoUrl: String!

// 4 
var player: AVPlayer?

// 5 
weak var billboard: BillboardContainer? 
weak var sceneView: ARSCNView?

上面代碼作用如下:

  • 1: isPlaying是指示視頻當(dāng)前是否正在播放的標(biāo)志。
  • 2: 在創(chuàng)建過(guò)程中,將ARKit節(jié)點(diǎn)存儲(chǔ)到videoNode中,將SpriteKit場(chǎng)景存儲(chǔ)到spriteScene中。
  • 3: 這包含目標(biāo)視頻的URL。
  • 4: 這是負(fù)責(zé)播放視頻的AVPlayer。
  • 5: 這些是對(duì)廣告牌容器和ARKit場(chǎng)景視圖的引用。

其中一些屬性可以在configure方法中初始化,該方法當(dāng)前為空。將此內(nèi)容添加到其正文:

self.videoUrl = videoUrl 
self.billboard = billboard 
self.sceneView = sceneView

要播放視頻,用戶將點(diǎn)按“播放”按鈕;這個(gè)按鈕已經(jīng)連接到空的play()動(dòng)作方法,我們只需要提供實(shí)現(xiàn)。添加以下代碼:

guard let billboard = billboard else { return }

if billboard.isFullScreen {

} else {

    // 1 
    createVideoPlayerAnchor() 
    // 2 
    billboard.videoPlayerDelegate?.didStartPlay() 
    // 3
    playButton.isEnabled = false
}

上面代碼作用如下:

  • 1: 創(chuàng)建視頻播放器錨點(diǎn)。
  • 2: 通知代表視頻開(kāi)始播放。
  • 3: 禁用播放按鈕以防止雙擊觸發(fā)相同的操作兩次。

廣告牌容器沒(méi)有isFullScreen屬性,但我們可以快速添加它。打開(kāi)BillboardContainer.swift并添加以下內(nèi)容:

var isFullScreen = false

視頻播放器代理方法

在最后一個(gè)代碼段中,我們通過(guò)委托方法調(diào)用發(fā)送了通知;此委托存儲(chǔ)在廣告牌容器中,因此我們需要添加協(xié)議定義。仍然在BillboardContainer.swift中,在import語(yǔ)句之后添加以下內(nèi)容:

protocol VideoPlayerDelegate: class { 
    func didStartPlay() func didEndPlay() 
}

這兩種方法用于在視頻開(kāi)始或停止時(shí)通知應(yīng)用。

現(xiàn)在,我們需要保留對(duì)要通知的目標(biāo)實(shí)體的引用。將此屬性添加到BillboardContainer

weak var videoPlayerDelegate: VideoPlayerDelegate?

要使代理有效,必須將此屬性設(shè)置為要通知的實(shí)例。在這種情況下,實(shí)例是ViewController,因?yàn)樗潜仨殞?duì)視頻播放器狀態(tài)的變化作出反應(yīng)的實(shí)體。

打開(kāi)ViewController.swift并找到createBillboard,然后在廣告牌實(shí)例化后添加如下代碼:

billboard?.videoPlayerDelegate = self

除了設(shè)置代理之外,還必須實(shí)現(xiàn)它。在文件的末尾,添加以下代碼以處理視頻播放器狀態(tài)更改:

extension ViewController: VideoPlayerDelegate { 
    func didStartPlay() { 
        // 1 
        billboard?.billboardNode?.isHidden = true 
    }

    func didEndPlay() { 
        // 2 
        billboard?.billboardNode?.isHidden = false 
    }
}

上面代碼作用如下:

  • 1: 在視頻播放器啟動(dòng)時(shí)隱藏廣告牌。
  • 2: 視頻播放器停止時(shí)再次顯示廣告牌。

回到視頻單元格

VideoCellplay()處理程序中調(diào)用的createVideoPlayerAnchor()方法仍未實(shí)現(xiàn)。打開(kāi)VideoCell.swift,在configure()方法之后添加以下內(nèi)容:

func createVideoPlayerAnchor() {
    guard let billboard = billboard else { return } 
    guard let sceneView = sceneView else { return }
    // 1 
    let center = billboard.plane.center *matrix_float4x4(SCNMatrix4MakeRotation( Float.pi / 2.0, 0.0, 0.0, 1.0))
    let anchor = ARAnchor(transform: center)
    // 2 
    sceneView.session.add(anchor: anchor)
    // 3 
    billboard.videoAnchor = anchor
}

上面代碼作用如下:

  • 1: 創(chuàng)建一個(gè)以廣告牌中心為中心的新錨點(diǎn),并圍繞z軸應(yīng)用通常的90度旋轉(zhuǎn)。
  • 2: 將新錨添加到ARKit場(chǎng)景。
  • 3: 保留對(duì)錨點(diǎn)的引用,因?yàn)樯院髮⑿枰玫剿?/li>

視頻播放器創(chuàng)建邏輯更新

我們需要添加一些代碼來(lái)將新創(chuàng)建的錨與SceneKit節(jié)點(diǎn)相關(guān)聯(lián)。 ARSCNView的委托是ViewController類(lèi),因此我們必須在其中創(chuàng)建新節(jié)點(diǎn)。但是,為了保持一致性,最好將該任務(wù)保留在VideoCell中,最簡(jiǎn)單的方法是通過(guò)委托實(shí)現(xiàn)。

打開(kāi)ViewController.swift并找到renderer(_:nodeFor)方法。在switch語(yǔ)句中,我們將看到已經(jīng)存在處理視頻節(jié)點(diǎn)創(chuàng)建的情況;但是,它使用的是舊實(shí)現(xiàn)。

我們需要?jiǎng)h除包含以下方法的實(shí)現(xiàn):

  • addVideoPlayerNode()
  • createVideo()
  • removeVideo()

找到這三種方法并且刪除它們。

現(xiàn)在,我們?cè)?strong>renderer方法中,找到如下代碼:

node = addVideoPlayerNode()

把這一行代碼替換為:

node = billboard.videoNodeHandler?.createNode()

如前所述,我們將使用代理來(lái)公開(kāi)視頻單元格方法。打開(kāi)BillboardContainer.swift,在videoPlayerDelegate屬性之前添加:

weak var videoNodeHandler: VideoNodeHandler?

要關(guān)閉循環(huán),我們需要定義VideoNodeHandler協(xié)議。在import語(yǔ)句后添加如下代碼:

protocol VideoNodeHandler: class { 
    func createNode() -> SCNNode? 
    func removeNode() 
}

上面代碼作用如下:

  • createNode(): 當(dāng)廣告視圖控制器需要視頻播放器的新SCNNode時(shí),由廣告視圖控制器調(diào)用。
  • removeNode(): 必須刪除節(jié)點(diǎn)時(shí)調(diào)用。

打開(kāi)VideController.swift并在touchesBegun(_:with :)中找到有問(wèn)題的代碼行。將其替換為以下內(nèi)容:

billboard?.videoNodeHandler?.removeNode()

刪除廣告牌后,我們需要重置之前添加的videoNodeHandler屬性。

找到removeBillboard()并在最后一行之前插入以下代碼行,其中billboard屬性重置為nil

billboard?.videoNodeHandler = nil

運(yùn)行應(yīng)用程序。然后掃描QR碼,當(dāng)前的效果是點(diǎn)擊視頻播放器中的播放按鈕......廣告牌消失,但不顯示視頻。

實(shí)現(xiàn)視頻節(jié)點(diǎn)處理程序

打開(kāi)VideoCell.swift。在configure(videoUrl:sceneView:billboard :)結(jié)束時(shí),設(shè)置屬性:

billboard.videoNodeHandler = self

VideoHandlerProtocol分別定義了兩種方法來(lái)為視頻播放器創(chuàng)建和刪除SceneKit節(jié)點(diǎn)。

完成后,構(gòu)建并運(yùn)行應(yīng)用程序。掃描二維碼后,點(diǎn)擊播放按鈕;廣告牌消失,并由顯示視頻的簡(jiǎn)約視頻播放器取代。

點(diǎn)按屏幕上的任意位置即可關(guān)閉視頻播放器并返回廣告牌。

全屏

觸發(fā)全屏模式

我們可以通過(guò)多種方式使用全屏模式,例如使用按鈕或放大/縮小手勢(shì)。也可以使用雙擊,這個(gè)項(xiàng)目中,我們就采用雙擊手勢(shì)。

打開(kāi)BillboardViewController.swift并且在images屬性之后添加如下代碼:

// 1 
let doubleTapGesture = UITapGestureRecognizer()
override func viewDidLoad() { 
    super.viewDidLoad()
    // 2 
    doubleTapGesture.numberOfTapsRequired = 2 doubleTapGesture.addTarget( self, action: #selector(didDoubleTap)) view.addGestureRecognizer(doubleTapGesture)
}
// 3 
@objc func didDoubleTap() { 
    guard let billboard = billboard else { return } 
    if billboard.isFullScreen { 
        restoreFromFullScreen() 
} else { 
    showFullScreen() 
    } 
}

如果之前使用過(guò)手勢(shì),那么這些代碼可能看起來(lái)很熟悉。我們正在使用點(diǎn)擊手勢(shì)識(shí)別器來(lái)檢測(cè)廣告牌視圖控制器上的雙擊:

  • 1: 此屬性用于保留對(duì)手勢(shì)的引用,以防我們要禁用或以其他方式更改手勢(shì)。
  • 2: 我們?cè)趘iewDidLoad()中配置并激活點(diǎn)擊手勢(shì)。 numberOfTapsRequired設(shè)置為2**表示我們要捕獲雙擊。
  • 3: 在雙擊處理程序中,切換全屏模式。這兩種情況都是由接下來(lái)要實(shí)施的各種方法處理的。

進(jìn)入全屏

當(dāng)廣告牌以ARKit模式顯示時(shí),其導(dǎo)航控制器 - 以及整個(gè)堆棧 - 是ViewController的子節(jié)點(diǎn),因?yàn)檫@是顯示ARKit的視圖控制器。要進(jìn)入全屏模式,我們必須將廣告牌的視圖與其超級(jí)視圖分離,并將其附加到父視圖控制器的視圖中。

創(chuàng)建廣告牌視圖控制器時(shí),我們將其視圖添加到ViewControllershow(viewController :)方法中的SCNMaterial

private func show(viewController: BillboardViewController) { 
    let material = SCNMaterial() 
    ...
    material.diffuse.contents = viewController.view ...
}

BillboardViewController的末尾,添加以下代碼:

extension BillboardViewController {
    func showFullScreen() { 
        guard let billboard = billboard else { return } 
        guard billboard.isFullScreen == false else { return }
        // 1 
        guard let mainViewController = parent as? ViewController else { return }
        self.mainViewController = mainViewController mainView = view.superview
        // 2 
        willMove(toParentViewController: nil) 
        view.removeFromSuperview() 
        removeFromParentViewController()
        // 3
        willMove(toParentViewController: mainViewController) 
        mainViewController.view.addSubview(view) 
        mainViewController.addChildViewController(self)
        // 4 
         billboard.isFullScreen = true
    }
}

上面作用如下:

  • 1; 保持對(duì)父視圖控制器和超級(jí)視圖的引用。退出全屏模式時(shí),我們需要它們才能恢復(fù)視圖。
  • 2: 從各自的父母中刪除視圖控制器及其視圖。
  • 3: 再次將視圖控制器添加到其上一個(gè)父級(jí),并將其視圖添加到父級(jí)視圖(而不是SCNMaterial)。
  • 4: 設(shè)置標(biāo)記以跟蹤全屏模式是打開(kāi)還是關(guān)閉。

現(xiàn)在,我們需要添加用于跟蹤原始父視圖控制器和父視圖的兩個(gè)屬性。在廣告牌屬性后添加它們:

weak var mainViewController: AdViewController? 
weak var mainView: UIView?

退出全屏

要退出全屏模式,必須執(zhí)行相反操作:從ViewController視圖中刪除視圖,并在進(jìn)入全屏模式之前將其放回視圖所具有的任何父類(lèi)。

回想一下,我們?cè)?strong>mainView屬性中保存了這個(gè)值。

打開(kāi)BillboardViewController.swift文件,在showFullScreen()之后添加此代碼:

func restoreFromFullScreen() {
    guard let billboard = billboard else { return } 
    guard billboard.isFullScreen == true else { return } 
    guard let mainViewController = mainViewController else { return } 
    guard let mainView = mainView else { return }
    // 1 
    willMove(toParentViewController: nil) 
    view.removeFromSuperview() 
    removeFromParentViewController()
    // 2 
    willMove(toParentViewController: mainViewController)
    mainView.addSubview(view) mainViewController.addChildViewController(self)
    // 3 
    billboard.isFullScreen = false 
    self.mainViewController = nil 
    self.mainView = nil
}

上面的代碼作用如下:

  • 1: 從父視圖中刪除BillboardViewController的視圖,在本例中是ViewController的視圖。
  • 2:將視圖放回到原始視圖中,該視圖在全屏顯示時(shí)存儲(chǔ)在mainView屬性中。
  • 3: 重置全屏圖像,以及對(duì)主視圖控制器和主視圖的引用。

運(yùn)行應(yīng)用程序并執(zhí)行常規(guī)的QR檢測(cè)。然后,雙擊廣告牌以轉(zhuǎn)換到全屏。

到此為止,視頻播放器還不能用。

修復(fù)視頻播放器

打開(kāi)VideoCell.swift。在play()中,將if billboard.isFullScreen分支留空,插入以下代碼:

if isPlaying == false {
    // 1 
    createVideoPlayerView()
    playButton.setImage( #imageLiteral(resourceName: "arKit-pause"), for: .normal) 
} else { 
    // 2 
    stopVideo()
    playButton.setImage( #imageLiteral(resourceName: "arKit-play"), for: .normal)
} 
// 3 
isPlaying = !isPlaying

上面的代碼作用如下:

  • 1: 如果沒(méi)有播放視頻,請(qǐng)創(chuàng)建視頻播放器視圖并更改按鈕的圖標(biāo)以顯示paused圖像。

  • 2: 如果當(dāng)前正在播放視頻,請(qǐng)將其停止并將按鈕的圖標(biāo)恢復(fù)為play圖像。

  • 3: 切換isPlaying標(biāo)志,以跟蹤視頻播放狀態(tài)。

接下來(lái)需要做的是創(chuàng)建視頻播放器。在createVideoPlayerAnchor()之后添加如下代碼:

func createVideoPlayerView() {
    if player == nil { 
        guard let url = URL(string: videoUrl) else { return } 
        player = AVPlayer(url: url) 
        let layer = AVPlayerLayer(player: player) 
        layer.frame = playerContainer.bounds 
        playerContainer.layer.addSublayer(layer) 
    }
    player?.play()
}

如果不存在視頻播放器我們會(huì)創(chuàng)建一個(gè)視頻播放器并且開(kāi)始播放。接下來(lái),我們還需要一個(gè)停止播放的方法:

func stopVideo() { 
    player?.pause() 
}

上面的這個(gè)方法會(huì)讓播放器暫停播放。

構(gòu)建并運(yùn)行應(yīng)用程序。掃描QR碼并雙擊廣告牌進(jìn)入全屏模式。然后,點(diǎn)擊play按鈕播放視頻;再次點(diǎn)擊它可暫停視頻。

檢測(cè)參考圖像

ARKit 1.5引入了一些新功能:能夠檢測(cè)自定義圖像。因此,我們可能希望讓?xiě)?yīng)用程序識(shí)別一個(gè)或多個(gè)預(yù)定義圖像,而不是檢測(cè)白色矩形或QR碼。

在下一節(jié)中,我們將通過(guò)使用自定義圖像檢測(cè)替換QR代碼檢測(cè)。但是,要實(shí)現(xiàn)此目的,有兩個(gè)先決條件:

  • Xcode版本大于或等于9.3。
  • iOS 系統(tǒng)版本大于或等于11.3。

我們添加一個(gè)或多個(gè)參考圖像。

選擇參考圖像

第一步是讓?xiě)?yīng)用知道需要檢測(cè)哪些圖像。在Xcode中,打開(kāi)Assets.xcassets目錄,然后單擊位于窗口右下角的+按鈕。將顯示一個(gè)彈出菜單,其中包含一長(zhǎng)串選項(xiàng)。找到這兩個(gè):New AR Resource GroupNew AR Reference Image

我們將使用前者創(chuàng)建組,后者將新圖像添加到AR資源組。 AR資源組是為ARKit創(chuàng)建的特殊組類(lèi)型。它在運(yùn)行時(shí)加載,ARKit*使用它來(lái)確定它應(yīng)該檢測(cè)哪些圖像。

創(chuàng)建一個(gè)新的AR資源組并將其命名為RMK-ARKit-triggers。然后,將圖像添加到新創(chuàng)建的組:

  • 1: 右鍵選擇logo_3圖片
  • 2: 選擇Show in Finder。
  • 3: 在Xcode打開(kāi)的Finder窗口中,拖動(dòng)圖像文件并將其放入RMK-ARKit-triggers組。
  • 4: 對(duì)logo_4圖像重復(fù)步驟1-3

選擇組中兩個(gè)圖像之一,然后查看“屬性”檢查器。除了圖像名稱(chēng),它還包含兩個(gè)可編輯的參數(shù):大小和度量單位。

尺寸和度量單位屬性用于為現(xiàn)實(shí)世界中的圖像的物理尺寸提供ARKit。 ARKit使用這些測(cè)量值來(lái)確定相機(jī)的正確視角和距離。

為寬度和高度輸入0.2,并驗(yàn)證Meters是選定的度量單位,如下頁(yè)所示。如果在A4Letter紙張上最大化打印,這大致是圖像的大小。重復(fù)其他圖像。


請(qǐng)注意,兩個(gè)圖像現(xiàn)在在其各自的右下角顯示警告圖標(biāo)。將鼠標(biāo)懸停在警告圖標(biāo)上,然后單擊以顯示更多信息:

上面警告包含的意思如下:

  • 1: 圖像彼此太相似了。
  • 2: 圖像顏色直方圖不足夠,這意味著沒(méi)有均勻的顏色分布。
  • 3: 圖像具有相同顏色的大部分。

看看你添加的圖像,它們并不完全符合第二和第三個(gè)要求 - 但不要擔(dān)心,這些圖片能用。

注冊(cè)參考圖像

此時(shí),應(yīng)用程序不知道這些圖像的存在 - 你必須告訴它們。這是在ARKit配置期間完成的。

打開(kāi)ViewController.swift并導(dǎo)航到viewWillAppear(_ :)。使用以下代碼行創(chuàng)建配置:

// Create a session configuration 
let configuration = ARWorldTrackingConfiguration() 
configuration.worldAlignment = .camera

// Run the view's session 
sceneView.session.run(configuration)

ARWorldTrackingConfiguration繼承的ARConfiguration類(lèi)具有名為detectionImages的屬性;我們可以使用此屬性來(lái)指定希望ARKit識(shí)別的一組圖像。

可檢測(cè)圖像捆綁在ARReferenceImage的實(shí)例中,該實(shí)例添加了一些元數(shù)據(jù),即:

  • name
  • physicalSize。
    如前所述,在創(chuàng)建AR資源組時(shí),可以分配兩者。

在上面顯示的代碼段中,在運(yùn)行會(huì)話的最后一行之前,插入此代碼以加載和設(shè)置參考圖像:

// 1 
var triggerImages = ARReferenceImage.referenceImages( inGroupNamed: "RMK-ARKit-triggers", bundle: nil) 
// 2 
configuration.detectionImages = triggerImages

第一行加載之前添加到RMK-ARKit-triggers AR資源組的圖像。第二行將這些圖像分配給配置。

需要注意的是referenceImages(inGroupNamed:bundle :)如何返回Set <ARReferenceImage>而不是數(shù)組。

提供一個(gè)hook

現(xiàn)在,我們需要ARKit在識(shí)別參考圖像時(shí)通知應(yīng)用程序。識(shí)別后,ARKit會(huì)自動(dòng)為會(huì)話添加錨點(diǎn)。

添加錨點(diǎn)時(shí),有三種可能的方法可以通知:

  • ARSessionDelegate委托的session(_:didAdd :)

  • 查看(_:didAdd:for :) ARSKViewDelegate委托。

  • ARSCNViewDelegate委托的rendered(_:didAdd:for:)。

    sceneView.session.delegate = self

向下滾動(dòng)以找到ARSessionDelegate擴(kuò)展,并在最后添加:

// 1 
func session(_ session: ARSession,
    didAdd anchors: [ARAnchor]) { 
    // 2 
    if let imageAnchor = anchors.compactMap({ $0 as? ARImageAnchor }).first {
        // 3
        self.createBillboard(center: imageAnchor.transform,size: imageAnchor.referenceImage.physicalSize) 
    }
}

上面代碼作用如下:

  • 1: 當(dāng)錨點(diǎn)添加到會(huì)話時(shí),ARKit調(diào)用此委托方法;識(shí)別參考圖像時(shí)創(chuàng)建新錨點(diǎn)。
  • 2: 由于該方法接收到一組錨點(diǎn),因此我們首先按類(lèi)型過(guò)濾它們并僅保留ARImageAnchor的實(shí)例;然后你拿第一個(gè)。
  • 3: 我們可以使用該錨點(diǎn)創(chuàng)建廣告牌。

打開(kāi)AdViewController.swift文件,找到createBillboard()的當(dāng)前實(shí)現(xiàn),并在它之后添加此重載:

func createBillboard(center: matrix_float4x4, size: CGSize) { 
    // 1 
    let plane = RectangularPlane(center: center, size: size)

    // 2 
    let rotation = SCNMatrix4MakeRotation(Float.pi / 2, -1.0, 0.0, 0.0)

    // 3 
    let rotatedCenter = plane.center * matrix_float4x4(rotation) 
    let anchor = ARAnchor(transform: rotatedCenter)

    billboard = BillboardContainer( billboardAnchor: anchor, plane: plane) 
    billboard?.videoPlayerDelegate = self 
    sceneView.session.add(anchor: anchor)

    print("New billboard created")
}

上面代碼作用如下:

  • 1: 我們不必使用4個(gè)矩形頂點(diǎn),而是具有參考圖像的中心,因此我們可以使用RectangularPlane初始化程序的正確重載。
  • 2: 旋轉(zhuǎn)矩陣是不同的。而不是圍繞z軸旋轉(zhuǎn)90度,我們需要旋轉(zhuǎn)相同的度數(shù),但圍繞x軸,以及相反的方向。這是SceneKit節(jié)點(diǎn)通常需要的調(diào)整。
  • 3: 方法的其余部分與另一個(gè)相同。

注意:如果參考圖像的大小不正確,ARKit無(wú)論如何都會(huì)識(shí)別它。但是,觀點(diǎn)可能會(huì)發(fā)生變化。如果圖像較大,ARKit可能認(rèn)為它比它更接近,而如果它更小,它可以看得更遠(yuǎn)。

在運(yùn)行時(shí)添加參考圖像

能夠檢測(cè)預(yù)定義圖像非常棒,但是必須將其與應(yīng)用程序捆綁在一起可能是一個(gè)相當(dāng)大的限制,因?yàn)橐滤?,我們必須發(fā)布新版本的應(yīng)用程序。
ViewController.swift中,向上滾動(dòng)到viewWillAppear(_ :)并找到創(chuàng)建triggerImages變量的行。在該行之后添加此代碼:

// 1 
let image = UIImage(named: "logo_2")! 
// 2
let referenceImage = ARReferenceImage(image.cgImage!, orientation: .up, physicalWidth: 0.2) 
// 3 
triggerImages?.insert(referenceImage)

上面的代碼作用如下:

  • 1: 從圖像資源中找到的logo_2圖像創(chuàng)建UIImage。
  • 2: 為它創(chuàng)建一個(gè)參考圖像,通過(guò)0.2米的物理寬度。系統(tǒng)計(jì)算物理高度。
  • 3: 將新創(chuàng)建的參考圖像添加到加載到triggerImages變量中的組。

參考圖像檢測(cè)是不同的用例,并不代替QR碼檢測(cè)。使用QR碼,我們可以存儲(chǔ)附加值的自定義數(shù)據(jù)。

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

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

  • 生平第一次素描,而目還是頭,亂紅老師的講解和一步步人物頭像的立體活現(xiàn),我感覺(jué)到的是驚訝和感動(dòng),就一支小小的鉛筆和一...
    謙玥兒閱讀 840評(píng)論 4 9
  • 風(fēng)。風(fēng)而后是葉而后是熟紅的棗子,而后是泥土與黏在一旁亮白的混凝土地面上干癟的蚯蚓。 鴉片白與霉斑青的路,路上印著一...
    反光物閱讀 311評(píng)論 0 1
  • 我愛(ài)上你了 是, 這一次, 無(wú)法自拔 喜歡看你的眼睛 喜歡看你調(diào)皮的表情 喜歡你打哈欠時(shí)撐大的鼻孔 喜歡看你寬敞的...
    尚靈心閱讀 310評(píng)論 0 3
  • 1.我是因?yàn)槭裁粗档帽粣?ài)? 雖然有那么多人愛(ài)我,但我心里不安定! 爛故事又在起作用了。 (1)我會(huì)覺(jué)得愛(ài)我的人是因...
    weeklybright閱讀 331評(píng)論 0 1

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