做者 / Very Good Ventures Teamhtml
咱們 (Very Good Ventures 團隊) 與 Google 合做,在今年的 Google I/O 大會上推出了 照相亭互動體驗 (I/O Photo Booth)。您能夠與深受喜好的 Google 吉祥物合影: Flutter 的 Dash、Android Jetpack、Chrome 的 Dino 和 Firebase 的 Sparky,並用各類貼紙裝飾照片,包括派對帽、披薩、時髦眼鏡等。固然,您也能夠經過社交媒體下載並分享,或者用做您的我的頭像!git
△ Flutter 的 Dash、Firebase 的 Sparky、Android Jetpack 和 Chrome 的 Dinogithub
咱們使用 Flutter web 和 Firebase 構建了 I/O 照相亭。由於 Flutter 如今支持打造 Web 應用,咱們認爲這將是一個很好的方式,可讓世界各地的與會者在今年的線上 Google I/O 大會上輕鬆訪問這一應用。Flutter web 消除了必須經過應用商店安裝應用的障礙,同時用戶還能夠靈活選擇運行應用的設備: 移動設備、桌面設備或平板電腦。所以,只要能使用瀏覽器,用戶即可無需下載直接使用 I/O 照相亭。web
儘管 I/O 照相亭旨在提供 Web 體驗,但全部代碼均採用與平臺無關的架構編寫而成。當相機插件等原生功能的支持在各個平臺就緒後,這套代碼便可在全部平臺 (桌面、Web 和移動設備) 通用。canvas
構建 Web 版 Flutter 相機插件後端
第一個挑戰即在 Web 上爲 Flutter 構建攝像頭插件。最初,咱們聯繫了 Baseflow 團隊,由於他們負責維護現有的開源 Flutter 攝像頭插件。Baseflow 致力於構建適用於 iOS 和 Android 的一流攝像頭插件支持,咱們也很樂於與其合做,使用 聯合插件 方法爲插件提供 Web 支持。咱們儘量符合官方插件接口,以便咱們能夠在準備就緒時將其合併回官方插件。api
咱們肯定了兩個對於在 Flutter 中構建 I/O 照相亭相機體驗相當重要的 API。瀏覽器
Future<CameraImage> takePicture() async { final videoWidth = videoElement.videoWidth; final videoHeight = videoElement.videoHeight; final canvas = html.CanvasElement( width: videoWidth, height: videoHeight, ); canvas.context2D ..translate(videoWidth, 0) ..scale(-1, 1) ..drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight); final blob = await canvas.toBlob(); return CameraImage( data: html.Url.createObjectUrl(blob), width: videoWidth, height: videoHeight, ); }
攝像頭權限安全
在 Web 上完成 Flutter 攝像頭插件後,咱們建立了一個抽象佈局,以根據相機權限顯示不一樣的界面。例如,在等待您容許或拒絕使用瀏覽器攝像頭時,或者若是沒有可供訪問的攝像頭時,咱們能夠顯示一條說明性消息。網絡
Camera( controller: _controller, placeholder: (_) => const SizedBox(), preview: (context, preview) => PhotoboothPreview( preview: preview, onSnapPressed: _onSnapPressed, ), error: (context, error) => PhotoboothError(error: error), )
在上面的抽象佈局中,placeholder 會在應用等待您授予攝像頭權限時返回初始界面。Preview 則會在您授予權限後返回真實的界面,並顯示攝像頭的實時視頻流。結尾的 Error 構造語句則能夠在錯誤發生時捕獲錯誤並顯示相應的消息。
生成鏡像照片
咱們的下一個挑戰是生成鏡像照片。若是咱們照原樣使用攝像頭拍攝的照片,那麼您看到的內容將與您在照鏡子時所看到的內容不同。某些設備會提供專門處理這一問題的設置選項,因此,若是您用前置攝像頭拍照,您看到的實際上是照片的鏡像版本。
在咱們的第一種方法中,咱們嘗試捕捉默認的攝像頭視圖,而後圍繞 y 軸對其進行 180 度翻轉。這種方法彷佛有效,但後來咱們遇到了 一個問題,即 Flutter 偶爾會覆蓋這個翻轉,致使視頻恢復到未鏡像的版本。
在 Flutter 團隊的幫助下,咱們將 VideoElement 放在 DivElement 中,並更新 VideoElement 以填充 DivElement 的寬度和高度,解決了這個問題。這樣一來,咱們可以爲視頻元素應用鏡像,同時由於父元素是 div,因此不會被 Flutter 覆蓋翻轉效果。如此一來,咱們便得到了所需的鏡像攝像頭視圖!
△ 未鏡像的視圖
△ 鏡像視圖
保持寬高比
在大屏幕上保持 4:3 寬高比,以及在小屏幕上保持 3:4 寬高比,這個操做起來比看起來更難!保持寬高比很是重要,既要符合 Web 應用的總體設計,又要確保在社交媒體上分享照片時,令其中的像素呈現出清晰的本色效果。這是一項具備挑戰性的任務,由於不一樣設備上內置攝像頭的寬高比差別很大。
爲了強制保持寬高比,應用首先使用 JavaScript getUserMedia API 從設備攝像頭請求可能的最大分辨率。隨後,咱們將此 API 傳遞到 VideoElement 流中,這即是您在攝像頭視圖中看到的內容 (固然是已鏡像的版本)。咱們還應用了 object-fit CSS 屬性來確保視頻元素能蓋住其父級容器。咱們使用 Flutter 自帶的 AspectRatio widget 來設置寬高比。所以,攝像頭不會對顯示的寬高比作出任何假設;它始終返回支持的最大分辨率,而後遵照 Flutter 提供的約束條件 (在本例中爲 4:3 或 3:4)。
final orientation = MediaQuery.of(context).orientation; final aspectRatio = orientation == Orientation.portrait ? PhotoboothAspectRatio.portrait : PhotoboothAspectRatio.landscape; return Scaffold( body: _PhotoboothBackground( aspectRatio: aspectRatio, child: Camera( controller: _controller, placeholder: (_) => const SizedBox(), preview: (context, preview) => PhotoboothPreview( preview: preview, onSnapPressed: () => _onSnapPressed( aspectRatio: aspectRatio, ), ), error: (context, error) => PhotoboothError(error: error), ), ), );
經過拖放添加貼紙
I/O 照相亭的一大重要體驗在於與您最喜歡的 Google 吉祥物合影並添加道具。您可以在照片中拖放吉祥物和道具,以及調整大小和旋轉,直到得到您喜歡的圖像。您也會發現,在將吉祥物添加到屏幕上時,您能夠拖動它們並調整其大小。吉祥物們仍是有動畫效果的——這種效果由 sprite sheet 來實現。
for (final character in state.characters) DraggableResizable( canTransform: character.id == state.selectedAssetId, onUpdate: (update) { context.read<PhotoboothBloc>().add( PhotoCharacterDragged( character: character, update: update, ), ); }, child: _AnimatedCharacter(name: character.asset.name), ),
爲調整對象的大小,咱們建立了可拖動、可調整大小且能夠容納其餘 Flutter widget 的 widget,在本例中,即爲吉祥物和道具。該 widget 會使用 LayoutBuilder,根據窗口的約束條件來處理 widget 的縮放。在內部,咱們使用 GestureDetector 以掛接到 onScaleStart、onScaleUpdate 和 onScaleEnd 事件。這些回調提供了必要的手勢詳細信息,以反映用戶對吉祥物和道具的操做。
經過多個 GestureDetector 回饋的數據,Transform widget 和 4D 矩陣變換便可根據用戶所作的各類手勢處理縮放,以及旋轉吉祥物和道具。
Transform( alignment: Alignment.center, transform: Matrix4.identity() ..scale(scale) ..rotateZ(angle), child: _DraggablePoint(...), )
最後,咱們建立了單獨的 package 來肯定您的設備是否支持觸摸輸入。可拖動、可調整大小的 widget 會根據觸摸功能作出相應的調整。在具備觸摸輸入功能的設備上,您並不能看到調整大小的錨點和旋轉圖標,由於您能夠經過雙指張合和平移手勢來直接操縱圖像;而在不支持觸摸輸入的設備 (例如您的桌面設備) 上,咱們則添加了錨點和旋轉圖標,以適應單擊和拖動操做。
使用 Flutter 針對 Web 進行開發
這是咱們使用 Flutter 構建的首批純 Web 項目之一,其與移動應用具備不一樣的特徵。
咱們須要確保該應用對任何設備上的任何瀏覽器都具備 響應性和自適應性。也就是說,咱們必須確保 I/O 照相亭能夠根據瀏覽器大小進行縮放,而且可以處理移動設備和 Web 端的輸入。咱們經過如下幾種方式作到了這一點:
可擴展架構
咱們還爲此應用構建了可擴展的移動應用。咱們的 I/O 照相亭在建立之初就具備穩固的基礎,包括良好的空安全性、國際化,以及從第一次提交開始就作到的 100% 單元和 widget 測試覆蓋率。咱們使用 flutter_bloc 進行狀態管理,由於它支持咱們輕鬆測試業務邏輯,並觀察應用中的全部狀態變化。這對於生成開發者日誌和確保可追溯性特別有用,由於咱們能夠準確地觀察到從一個狀態到另外一個狀態的變化,並更快地隔離問題。
咱們還實現了由功能驅動的單一代碼庫結構。例如,貼紙、分享和實時相機預覽,均在各自的文件夾中獲得實現,其中每一個文件夾包含其各自的界面組件和業務邏輯。這些功能也會用到外部依賴,例如位於 package 子目錄中的相機插件。利用這種架構,咱們的團隊可以在互不干擾的狀況下並行處理多個功能,最大限度地減小合併衝突,並有效地重用代碼。例如,界面組件庫是名爲 photobooth_ui 的單獨 package,相機插件也是單獨的。
經過將組件分紅獨立的 package,咱們能夠提取未與此特定項目綁定的各個組件,並將其開源。與 Material 和 Cupertino 組件庫相似,咱們甚至能夠將界面組件庫 package 作開源處理,以供 Flutter 社區使用。
Firebase Auth、存儲、託管等
照相亭利用 Firebase 生態系統進行各類後端集成。firebase_auth package 支持用戶在應用啓動後當即匿名登陸。每一個會話都使用 Firebase Auth 建立具備惟一 ID 的匿名用戶。
當您來到共享頁面時,此設置即會開始發揮做用。您能夠下載照片以保存爲我的頭像,也能夠直接將其分享到社交媒體。若是您下載照片,則該照片將存儲在您的本地設備上。若是您分享照片,咱們會使用 firebase_storage package 將照片存儲在 Firebase 中,以便稍後檢索並生成帖子經過社交媒體發佈。
咱們在 Firebase 的存儲分區上定義了 Firebase 安全規則,確保照片在建立後不可變。這能夠防止其餘用戶修改或刪除存儲分區中的照片。此外,咱們使用 Google Cloud 提供的 對象生命週期管理,定義了一個刪除 30 天前全部對象的規則,但您能夠按照應用中列出的說明請求儘快刪除您的照片。
此應用還使用 Firebase Hosting 快速安全地進行託管。咱們能夠藉助 action-hosting-deploy GitHub Action,根據目標分支,將應用自動部署到 Firebase Hosting。當咱們將變動合併到主分支時,該操做會觸發一個工做流,用於構建應用的特定開發版本,並將其部署到 Firebase Hosting。一樣,當咱們將變動合併到發佈分支時,該操做也會觸發部署生產版本。經過結合使用 GitHub Action 與 Firebase Hosting,咱們的團隊可以快速迭代,並始終獲得最新版本的預覽。
最後,咱們使用 Firebase 性能監測 來監控主要的 Web 性能指標。
使用 Cloud Functions 進行社交
在生成您的社交帖子以前,咱們首先會確保照片內容是像素級完美的。最終圖像包含漂亮的邊框,以呈現 I/O 照相亭特點,並按 4:3 或 3:4 的寬高比進行裁剪,以便在社交帖子上呈現出色的效果。
咱們使用 OffscreenCanvas API 或 CanvasElement 來合成原始照片、吉祥物和道具的圖層,並生成您能夠下載的單個圖像。這個處理步驟由 image_compositor package 負責執行。
而後,咱們利用 Firebase 強大的 Cloud Functions,來將照片分享到社交媒體。當您點擊分享按鈕時,系統會帶您前往新標籤頁,並在所選的社交平臺上自動生成待發布的帖子。該帖子還包含一個連接,鏈接到咱們編寫的 Cloud Functions。瀏覽器在分析網址時,會檢測 Cloud Functions 生成的動態元數據,並據此在您的社交帖子中顯示照片的精美預覽,以及一個指向分享頁面的連接,您的粉絲們能夠在該頁面上查看照片,並導航回 I/O 照相亭應用,以獲取他們本身的照片。
function renderSharePage(imageFileName: string, baseUrl: string): string { const context = Object.assign({}, BaseHTMLContext, { appUrl: baseUrl, shareUrl: `${baseUrl}/share/${imageFileName}`, shareImageUrl: bucketPathForFile(`${UPLOAD_PATH}/${imageFileName}`), }); return renderTemplate(shareTmpl, context); }
成品以下所示:
有關如何在 Flutter 項目中使用 Firebase 的更多信息,請查看 此 Codelab。
本項目詳細地示範瞭如何針對 Web 來構建應用的方法。令咱們感到驚喜的是,與使用 Flutter 構建移動應用的體驗相比,這個 Web 應用的構建工做流與之很是類似。咱們必須考慮窗口大小、自適應、觸摸與鼠標輸入、圖像加載時間、瀏覽器兼容性等元素,以及在構建 Web 應用時所必需考慮的其餘全部因素。可是,咱們仍然可使用相同的模式、架構和編碼標準來編寫 Flutter 代碼,這讓咱們在構建 Web 應用時感到很是自在。Flutter package 提供的工具和不斷髮展的生態系統,包括 Firebase 工具套件,幫助咱們實現了 I/O 照相亭。
△ 打造 I/O 照相亭的 Very Good Ventures 團隊
咱們已經開放了全部源代碼,歡迎你們前往 GitHub 查看 photo_booth 項目,也別忘了多多拍照秀出來哦!