- 原文地址:How It’s Made: I/O Photo Booth
- 原文作者:Very Good Ventures Team
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:霜羽 Hoarfroster
- 校對者:Chorer
我們(Very Good Ventures 開發者們)與 Google 合作,為今年的 Google I/O 帶來了互動體驗:Photo Booth!你現在可以與知名的谷歌吉祥物 Flutter's Dash、Android Jetpack、Chrome 的 Dino 和 Firebase 的 Sparky 合影,並用諸如派對帽、披薩、時髦眼鏡等等的貼紙裝飾照片!最後,你還可以在社交媒體上分享照片,或者選擇下載照片以更新你的個人資料照片!
Flutter Dash、Firebase Sparky、Android Jetpack 和 Chrome Dino
我們使用 Flutter Web 和 Firebase 構建了 I/O Photo Booth 這個軟體,因為 Flutter 現在提供了對 Web 應用程式的支援,我們認為這將是讓今年虛擬 Google I/O 的來自全球各地的與會者輕鬆訪問此應用程式的好方法。Flutter 的網路支援消除了我們必須從應用程式商店安裝應用程式的限制,還使我們可以靈活地在指定的裝置上執行它:移動端、桌面端或平板電腦端。這讓我們可以無需下載,只需使用任何裝置上的任何適當的瀏覽器,瀏覽我們給出的頁面即可體驗 I/O Photo Booth!
儘管 I/O Photo Booth 旨在提供 Web 體驗,但所有程式碼都是使用與平臺無關的架構編寫的。當對相機外掛等元素的原生支援可用於各自的平臺時,相同的程式碼適用於所有平臺(桌面端、網路端和移動端)。
使用 Flutter 製作虛擬照相亭
為 Web 構建 Flutter 相機外掛
第一個挑戰是在 Web 上為 Flutter 構建一個相機外掛。最初,我們聯絡了 Baseflow 團隊,因為他們維護著現有的開源 Flutter 相機外掛。雖然 Baseflow 致力於為 iOS 和 Android 構建一流的相機外掛支援,但我們很高興使用 聯合外掛。我們儘可能貼近官方外掛介面,以便我們可以在準備好時將其合併回官方外掛。
我們確定了兩個對於在 Flutter 中構建 I/O Photo Booth 相機體驗至關重要的 API。
- 初始化相機: 該應用程式首先需要訪問我們裝置上的相機。在桌上型電腦上,這可能是網路攝像頭,而在移動裝置上,我們選擇了前置攝像頭。我們還指定解析度為 1080p,以根據我們的裝置最大限度地提高相機質量。
- 拍照: 我們使用了內建的
HtmlElementView
。它可以使用平臺檢視來渲染原生 Web 元素作為 Flutter 小部件。在這個專案中,我們渲染了一個VideoElement
作為原生 HTML 元素 —— 也就是我們之前在螢幕上看到的你拍你的照片。我們還使用了CanvasElement
作為另一個 HTML 元素呈現方式,讓我們可以在單擊拍照按鈕時從媒體流中捕獲影像。
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,
);
}
複製程式碼
相機許可權
在我們讓 Flutter Camera 外掛可以在 Web 上執行後,我們建立了一個抽象來根據相機許可權顯示不同的 UI。例如,在等待選擇是否允許使用瀏覽器攝像頭時,或者如果沒有可用的攝像頭可供訪問,我們可以顯示一條說明性訊息。
Camera(
controller: _controller,
placeholder: (_) => const SizedBox(),
preview: (context, preview) => PhotoboothPreview(
preview: preview,
onSnapPressed: _onSnapPressed,
),
error: (context, error) => PhotoboothError(error: error),
)
複製程式碼
在此抽象中,placeholder
在應用程式等待我們授予相機許可權時返回初始 UI(const SizedBox()
)。preview
授予許可權後返回 UI,並提供攝像機的實時視訊流。錯誤構建器允許我們在發生錯誤時捕獲錯誤並呈現相應的錯誤訊息。
映象照片
我們的下一個挑戰是映象照片。如果我們保留原樣使用相機拍攝照片,那麼我們看到的將不是平時在照鏡子時所看到的那樣,而有些裝置開放了介面如反轉按鈕來處理這個。所以如果你用前置攝像頭拍照,拍攝照片時,我們會看到映象版本。
在我們的第一種方法中,我們嘗試捕捉預設的相機檢視,然後圍繞 y 軸應用 180 度變換。這似乎有效,但後來我們遇到了一個問題,即 Flutter 偶爾會覆蓋轉換,導致視訊恢復到未映象的版本.
在 Flutter 團隊的幫助下,我們通過將 VideoElement
包裝在 DivElement
中並更新 VideoElement
元素,讓 VideoElement
填滿 DivElement
的寬度和高度,解決了這個問題。這允許我們將映象應用到視訊元素,而無需 Flutter 覆蓋變換效果,因為父元素是一個 div
。這種方法為我們提供了所需的映象相機檢視!
非映象檢視
映象檢視
堅持嚴格的縱橫比
對大螢幕執行嚴格的 4:3 寬高比,對小螢幕執行嚴格的 3:4 寬高比,這比看起來更難!強制執行此比例非常重要,既要遵守 Web 應用程式的整體設計,又要確保照片在社交媒體上分享時看起來畫素完美。這是一項具有挑戰性的任務,因為裝置上內建攝像頭的縱橫比差異很大。
為了強制執行嚴格的縱橫比,應用程式首先使用 JavaScript getUserMedia
API。我們使用這個 API,提供給 VideoElement
流,也就是我們在相機檢視中看到的(當然是映象的)。我們還應用了 object-fit
CSS 屬性來確保視訊元素覆蓋其父容器。我們使用 Flutter 的內建 AspectRatio
小部件設定縱橫比。因此,相機不會對顯示的縱橫比做出任何假設;它始終返回支援的最大解析度,然後符合 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 Photo Booth 體驗的很大一部分是與我們最喜歡的 Google 朋友合影並新增道具。我們可以在照片中拖放好友和道具,也可以調整大小和旋轉它們,直到獲得我們喜歡的影像。不難注意到,將朋友新增到螢幕時,我們可以拖動它們並調整其大小。朋友們也有動畫 —— 我們使用了雪碧圖來實現這種效果。
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 小部件上,在本例中,這是朋友和道具。這個小部件使用了 LayoutBuilder
根據視口的約束來處理小部件的縮放。在其中,我們使用 GestureDetectors
連線到 onScaleStart
、onScaleUpdate
和 onScaleEnd
回撥。這些回撥提供有關反映我們對朋友和道具所做更改所需的手勢的詳細資訊。
Transform
小部件和 4D 矩陣轉換將根據我們所做的各種手勢處理縮放和旋轉朋友和道具,如由多個 GestureDetector
報告。
Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..scale(scale)
..rotateZ(angle),
child: _DraggablePoint(...),
)
複製程式碼
最後,我們建立了一個單獨的包來確定我們的裝置是否支援觸控輸入。可拖動、可調整大小的小部件會根據觸控功能進行調整。在具有觸控輸入的裝置上,可調整大小的錨點和旋轉圖示不可見,因為我們可以通過捏和平移來直接操縱影像,而在沒有觸控輸入的裝置(例如我們的桌面裝置)上,新增了錨點和旋轉圖示以適應單擊和拖動。
優先考慮網路上的 Flutter
使用 Flutter 進行 Web 優先開發
這是我們使用 Flutter 構建的首批純 Web 專案之一,它與移動應用程式具有不同的特性。
我們需要確保該應用程式對任何裝置上的任何瀏覽器都具有響應性和自適應性。也就是說,我們必須確保 I/O Photo Booth 可以根據瀏覽器大小進行縮放,並且能夠處理移動和 Web 輸入。我們通過幾種方式做到了這一點:
- 響應式調整大小: 我們應該能夠將瀏覽器的大小調整為所需的大小,並且 UI 應相應地做出響應。如果我們的瀏覽器視窗為縱向,則相機將從具有 4:3 縱橫比的橫向檢視翻轉為具有 3:4 縱橫比的縱向檢視。
- 響應式設計: 桌面瀏覽器的設計在右側顯示 Dash、Android Jetpack、Dino 和 Sparky,對於移動裝置,它們顯示在頂部。桌面設計也使用了攝像頭右側的抽屜,移動端使用了 BottomSheet 類。
- 自適應輸入: 如果從桌面訪問 I/O Photo Booth,則滑鼠點選被視為輸入裝置。而如果使用的是平板電腦或手機,則使用觸控作為輸入。在調整貼紙大小並將其放置在照片中時,這一點尤其重要。移動裝置端支援捏合和平移,桌面端支援單擊和拖動。
可擴充套件架構
我們還使用了自己的方法為此應用程式構建可擴充套件的移動應用程式。我們以強大的基礎開始 I/O Photo Booth,包括聲音空值安全性、國際化以及從第一次提交開始的 100% 單元和小部件測試覆蓋率。我們使用了 flutter_bloc 進行狀態管理,因為它允許輕鬆測試業務邏輯並觀察應用程式中的所有狀態變化。這對於開發人員日誌和可追溯性特別有用,因為我們可以準確地看到狀態之間的變化並更快地隔離問題。
我們還實現了一個功能驅動的 monorepo 結構。例如,貼紙、分享和實時相機預覽都在它們自己的資料夾中實現,其中每個資料夾包含其各自的 UI 元件和業務邏輯。這些與外部依賴項整合,例如位於包子目錄中的相機外掛。這種架構允許我們的團隊並行處理多個功能,而不會中斷其他人的工作,最大限度地減少合併衝突,並使我們能夠有效地重用程式碼。例如,UI 元件庫是一個單獨的包,名為 photobooth_ui
,相機外掛也是單獨的。
通過將元件分成獨立的包,我們可以提取和開源與此特定專案無關的各個元件。甚至 UI 元件庫包也可以為 Flutter 社群開源,類似於 Material 和 Cupertino 元件庫。
Firebase + Flutter = 完美匹配
Firebase 身份驗證、儲存、託管等
Photo Booth 利用 Firebase 生態系統進行各種後端整合。firebase_auth
包 支援在應用啟動後立即匿名登入使用者。每個會話都使用 Firebase 身份驗證來建立一個具有唯一 ID 的匿名使用者。
當我們開啟共享頁面時,Firebase 就能夠起作用了。我們可以下載照片以儲存為個人資料圖片,也可以直接分享到社交媒體。如果我們選擇下載照片,它會本地儲存在我們的裝置上。如果選擇分享照片,應用會使用 firebase_storage
package 將照片儲存在 Firebase 中,以便我們可以稍後檢索它,以填充社交帖子。
我們在 Firebase 儲存桶上定義了 Firebase 安全規則 以使照片在建立後不可變。這可以防止其他使用者修改或刪除儲存桶中的照片。此外,我們使用 Google Cloud 提供的 物件生命週期管理 來定義刪除所有 30 天前的物件的規則,但我們也可以請求按照應用程式中列出的說明儘快刪除自己的照片。
此應用程式還使用了 Firebase Hosting 來快速安全地託管網路應用程式。action-hosting-deploy GitHub Action 允許我們根據目標分支自動部署到 Firebase 託管。當我們將更改合併到主分支時,該操作會觸發一個工作流,該工作流構建應用程式的開發風格並將其部署到 Firebase 託管。類似地,當我們將更改合併到釋出分支時,該操作會觸發生產部署。GitHub Action 與 Firebase Hosting 的結合使我們的團隊能夠快速迭代並始終預覽最新版本。
最後,我們使用 Firebase Performance Monitoring 來監控關鍵的 Web 效能指標。
使用 Cloud Functions 進行社交
在生成自己的社交帖子之前,我們首先需要確保照片每一個畫素都看起來足夠完美。最終影像包括一個漂亮的框架以紀念 I/O Photo Booth,並被裁剪為 4:3 或 3:4 的縱橫比,以便在社交帖子上看起來很棒。
我們使用 OffscreenCanvas
API 或 [CanvasElement
](developer.mozilla.org/ en-US/docs/Web/HTML/Element/canvas) 作為 polyfill 合成原始照片以及包含我們的朋友和道具的圖層,並生成可以下載的單個影像。image_compositor
包 處理此處理步驟。
然後,我們利用 Firebase 強大的 Cloud Functions 來協助將照片分享到社交媒體。當我們單擊共享按鈕時,我們將被帶到所選平臺上的一個新選項卡,其中包含一個預先填充的帖子。該帖子有一個重定向到我們編寫的雲函式的 URL。瀏覽器在分析 URL 時,會檢測到雲函式生成的動態元資訊。此資訊允許瀏覽器在我們的社交帖子中顯示照片的精美預覽影像以及指向共享頁面的連結,我們的關注者可以在其中檢視照片並導航回 I/O Photo Booth 應用程式以獲取他們自己的照片。
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。
成品效果
這個專案是構建應用程式的網路優先方法的一個很好的例子。與我們使用 Flutter 構建移動應用程式的經驗相比,我們構建這個 Web 應用程式的工作流程是如此相似,這讓我們感到驚喜。我們必須考慮諸如視口大小、響應能力、觸控與滑鼠輸入、影像載入時間、瀏覽器相容性以及為 Web 構建所帶來的所有其他考慮因素等元素。但是,我們仍然使用相同的模式、架構和編碼標準編寫 Flutter 程式碼。我們在為 Web 構建時感到賓至如歸。Flutter 軟體包的工具和不斷髮展的生態系統,包括 Firebase 工具套件,使 I/O Photo Booth 成為可能。
在 I/O Photo Booth 工作的非常好的 Ventures 團隊
我們已經開源了所有程式碼。檢視 GitHub 上的 photo_booth 專案!
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。