整合營銷服務(wù)商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          谷歌發(fā)布安卓折疊屏手機應(yīng)用設(shè)計規(guī)范

          ndroid 的覆蓋范圍在遞增,體驗也在變得越來越好,現(xiàn)已有超過 2.5 億臺大屏設(shè)備搭載了 Android 系統(tǒng),包括平板電腦、可折疊設(shè)備以及 Chrome OS 設(shè)備。如何適配不同的屏幕尺寸并保障良好的體驗,一直以來都是開發(fā)者的一大難題。尤其隨著可折疊設(shè)備等新興產(chǎn)品的涌現(xiàn),適配工作也愈發(fā)迫切。

          谷歌官方發(fā)文,將重點介紹 Material Design 指南中更新的相關(guān)內(nèi)容,并提供一些建議來幫助開發(fā)者按照自適應(yīng)界面的原則來構(gòu)建應(yīng)用,從而解決在平板電腦和可折疊設(shè)備上的適配問題。

          如果您更喜歡通過視頻了解本文內(nèi)容,請點擊下方:

          △ 折疊屏上應(yīng)用設(shè)計規(guī)范

          Compose

          https://developer.android.google.cn/jetpack/compose/nav-adaptive

          設(shè)計指南

          2021 年年初,我們在 Material Design 網(wǎng)站上發(fā)布了針對大屏設(shè)備的指南文檔。Android 開發(fā)者峰會期間我們更新了一些內(nèi)容,以幫助開發(fā)者為可折疊設(shè)備等更多其他類型的設(shè)備做好準(zhǔn)備。

          https://m3.material.io/foundations/adaptive-design/overview

          深入理解布局

          深入理解布局指南介紹了布局容器的相關(guān)概念,它提供了一個整體框架,可幫助開發(fā)者思考如何在屏幕上排列導(dǎo)航欄、工具欄和內(nèi)容等界面元素。

          https://material.io/design/layout/understanding-layout.html#principles

          △ 布局的三個主要區(qū)域

          指南中的組合部分帶您了解如何充分利用屏幕空間以保障可讀性,并且以尊重用戶心智模型的方式在不同的場景下合理排布重要內(nèi)容和操作選項。包括適當(dāng)縮放以展示更多內(nèi)容,如示例中的副標(biāo)題和日期,以及較小的組合技術(shù),例如在緊湊型的布局中對內(nèi)容進行視覺分組并保持其相關(guān)性等。

          https://material.io/design/layout/understanding-layout.html#composition

          △ 組合指南中涉及的部分布局方式

          以 Fortnightly 示例應(yīng)用為例,它在平板電腦上的界面布局十分均衡,這得益于它遵從了指南里對容器的建議。而且可以看到,F(xiàn)ortnightly 使用了視覺分隔線 (Visual Divider) 用于分隔最新新聞,在屏幕的另一邊,則利用留白和排版對不同類別的新聞報道進行分組。

          △ Fortnightly 遵循指南對內(nèi)容進行分隔和分組

          網(wǎng)格系統(tǒng)

          現(xiàn)在,許多應(yīng)用將屏幕視作一個大畫布或單欄,以水平和垂直的方式按相互關(guān)系繪制元素,有些應(yīng)用也會在一側(cè)整體留出邊距。這一做法在小屏上或許行得通,當(dāng)屏幕尺寸較大時就會出現(xiàn)明顯的問題。網(wǎng)格系統(tǒng)則將您的布局劃分為一系列欄,從而幫助您在規(guī)范網(wǎng)格中設(shè)計更具表現(xiàn)力的布局。在布局中使用欄式網(wǎng)格 (如下圖),能夠讓大屏設(shè)備的體驗呈現(xiàn)更貼心,更組織有序的印象,使得設(shè)備和內(nèi)容更自然地融為一體。

          △ 欄式網(wǎng)格

          您可以通過這些欄將屏幕劃分為不同區(qū)域,用于容納相關(guān)的信息和操作,進而改善信息層次結(jié)構(gòu)。如下圖所示,這里分了三個區(qū)域,這些區(qū)域?qū)凑赵O(shè)計者期望用戶閱讀的順序,把用戶的注意力吸引到這些區(qū)域?qū)?yīng)在屏幕的主要信息片段或信息組上。最重要的一點是,欄式網(wǎng)格提供了一種合理的方式來思考當(dāng)屏幕尺寸變大或變小時如何將內(nèi)容進行重排,從而幫助您對不同的屏幕尺寸作出一致響應(yīng)。

          △ 使用欄式網(wǎng)格將屏幕劃分為三個主要區(qū)域

          在本例中,三個主要區(qū)域通過重排來保持相同的信息層次結(jié)構(gòu),但以更加人性化的方式在小屏幕上顯示。

          △ 使用欄式網(wǎng)格在不同屏幕尺寸中對內(nèi)容進行重排

          記住網(wǎng)格系統(tǒng)有助于您選擇組件行為,在不同的布局中,以對設(shè)備尺寸和場景最有意義的方式?jīng)Q定替換還是更改組件。例如,在大屏設(shè)備上,您可使用 Navigation rail (左側(cè)邊欄導(dǎo)航條) 代替底部導(dǎo)航 (Bottom navigation),兩者功能相同,視覺表現(xiàn)方式也類似,但 Navigation rail 能夠更加人性化地排布頁面。手機上的全屏對話框 (Full-screen dialog) 在大屏幕上可以采用簡單對話框 (Simple dialog) 替代,以保持用戶當(dāng)前操作的上下文。

          △ 在大屏上使用簡單對話框 (右) 代替全屏對話框 (左)

          Navigation rail

          https://m3.material.io/components/navigation-rail/overview

          底部導(dǎo)航 (Bottom navigation)

          https://m3.material.io/components/bottom-navigation

          尺寸類別

          請記住,替換組件時,首先要滿足用戶的功能性和人性化需求。找到調(diào)整界面的正確閾值,這是實現(xiàn)響應(yīng)式界面的重要步驟。因此我們定義了新斷點值,這有助于將設(shè)備劃分到預(yù)設(shè)的尺寸類別中,這些尺寸代表了市場上實際設(shè)備的尺寸。它們有助于將應(yīng)用版面的原始尺寸轉(zhuǎn)換為離散的標(biāo)準(zhǔn)化組,您可以據(jù)此做出更高層次的界面決策。例如,幾乎所有標(biāo)準(zhǔn)手機在豎屏模式下都采用了較小 (Compact) 寬度和中等 (Medium) 高度的組合,由于普遍使用垂直滾動,對大多數(shù)應(yīng)用而言,根據(jù)寬度的尺寸類別進行適配就已足夠。

          △ 基于寬度的尺寸類別

          △ 基于高度的尺寸類

          這些尺寸類將作為新的 API 出現(xiàn)在 1.1 版 Jetpack Window Manager 庫中。從 Android Studio Bumblebee 開始,我們還以參考設(shè)備 (Reference devices) 的形式,將尺寸類別整合到工具中,在此基礎(chǔ)上實現(xiàn)界面有利于保持一致性,操作也更加簡單。而且開發(fā)者不需要去檢查實際物理尺寸或屏幕方向,或其他容易出錯的標(biāo)識。您在設(shè)計和構(gòu)建不同的尺寸類別時,請想想人們會如何手持和觸摸這些類別所代表的設(shè)備。關(guān)注設(shè)備的形狀和尺寸,有助于您打造出更加人性化的體驗。例如,在平板電腦或大屏手機上,如果不完全調(diào)整握持姿勢,人們可能很難觸及屏幕的頂部區(qū)域,因此請將重要操作和內(nèi)容放在容易觸及的區(qū)域中。

          尺寸類

          https://developer.android.google.cn/guide/topics/large-screens/support-different-screen-sizes#window_size_classes

          Window Manager

          https://developer.android.google.cn/jetpack/androidx/releases/window

          Android Studio Bumblebee

          https://developer.android.google.cn/studio

          規(guī)范布局

          規(guī)范布局提供了一系列通用布局方案,對設(shè)計大屏幕應(yīng)用非常有幫助。第一種是列表 / 詳情,或列表網(wǎng)格視圖的簡單組合,同時在開始展示內(nèi)容的屏幕起始側(cè),設(shè)置 / 不設(shè)置導(dǎo)航容器。

          △ 列表 / 詳情布局

          支持面板可用于人們需要集中精力的體驗中,例如文檔。在屏幕尾側(cè)或底部添加一塊面板,以便于使用工具或上下文控件。

          △ 支持面板

          信息流是新聞或社交類應(yīng)用中的常見模式,模板采用圖塊 (Tile) 的形式來吸引用戶發(fā)現(xiàn)更多內(nèi)容。這種交互與移動手機一樣 —— 打開一項即表示打開一個新頁面,但這種體驗更具沉浸感,而且專為大屏幕尺寸而設(shè)計。

          △ 信息流

          主頁橫幅優(yōu)先將內(nèi)容排列在屏幕頂部,并在內(nèi)容周圍和下方設(shè)計了支持元素,這對以媒體為中心的應(yīng)用來說,是非常棒的體驗。

          △ 主頁橫幅

          規(guī)范布局實踐

          采用響應(yīng)式界面不僅僅是為不同屏幕尺寸提供并行結(jié)構(gòu),應(yīng)用還要足夠靈活,這樣才能根據(jù)各種需要調(diào)整尺寸,例如旋轉(zhuǎn)設(shè)備、多窗口模式以及折疊和非折疊姿態(tài)。因此在運行期間,應(yīng)用可從一個尺寸類別過渡到另一個尺寸類別,并再次過渡回去。重要的是,不要將尺寸類別視作完全獨立的桶,應(yīng)用也需保證連續(xù)性 (即不中斷用戶體驗),所以應(yīng)用狀態(tài)或數(shù)據(jù)不能丟失。

          △ 響應(yīng)式界面可根據(jù)屏幕尺寸變化而調(diào)整內(nèi)容布局

          設(shè)想一下,當(dāng)您調(diào)整瀏覽器窗口大小時,如果瀏覽器回退了一個頁面,或者重定向到另一個頁面,又或者修改了歷史記錄,這種體驗非常奇怪。因此,每個頁面都應(yīng)足夠靈活,而且應(yīng)當(dāng)能夠在尺寸過渡期間保持狀態(tài)不變,這個時候規(guī)范布局就能發(fā)揮重要作用。針對每個頁面,您可以思考一下,當(dāng)屏幕尺寸變大時,可以添加什么內(nèi)容。當(dāng)屏幕尺寸變小時,可以刪除哪些內(nèi)容。然后再選擇合適的策略。這可能意味著您需要重新審視導(dǎo)航圖,尤其是當(dāng)您目前的設(shè)計以手機為主時更應(yīng)如此。

          如需構(gòu)建響應(yīng)式界面,我們應(yīng)該優(yōu)先考慮界面中長駐元素的位置,例如導(dǎo)航元素。遵循 Material 指南,我們可以根據(jù)寬度的尺寸類別提供替代布局,將導(dǎo)航調(diào)整到最方便使用的位置。例如,小屏幕采用底部導(dǎo)航視圖,中等屏幕采用 Navigation rail,大屏幕采用完整導(dǎo)航視圖。請大家注意,這些布局采用的是寬度限定符 "-w",而非最小寬度限定符 "-sw"。剩余空間用于排列內(nèi)容,我們可以在這些空間應(yīng)用規(guī)范布局。

          列表 / 詳情

          對列表 / 詳情而言,AndroidX 中有個名為 SlidingPaneLayout 的專用控件,使用前需為它的兩個子元素指定 layout_width,在運行期間,SlidingPaneLayout 會判斷是否有足夠空間同時展示兩個窗格:

          <SlidingPaneLayout …>
                <FragmentCOntainerView
                        android : id=”@+id/list_pane”
                        android : layout_width=”300dp”
                        android : layout_weight=”1”
                        …  />
          
                <FragmentCOntainerView
                        android : id=”@+id/detail_pane”
                        android : layout_width=”360dp”
                        android : layout_weight=”2”
          
          <SlidingPaneLayout …>

          △ SlidingPaneLayout 布局示例

          當(dāng)屏幕空間足夠,則兩個窗格至少都要達到指定的寬度,剩余空間可通過 layout_weight 分配,如左圖所示;如果空間不足,如右圖所示,則每個窗格都使用父視圖的全寬,詳情窗格將被滑到一邊,或直接覆蓋第一個窗格。

          △ SlidingPaneLayout 中空間分配結(jié)果

          viewModel.selectedItemFlow.collect { item ->
          // 更新詳情窗格的內(nèi)容
          detailPane.showItem(item)
          // 將詳細信息窗格滑動到視圖中
          // 如果并排放置兩個窗格
          // 并不會產(chǎn)生實際效果
          slidingPaneLayout.openPane()
          }

          如上代碼所示,您可以通過代碼控制滑動窗格,當(dāng)用戶從列表中選擇一個項目,我們從 ViewModel 的 Kotlin 流中接收到該項目,然后更新詳情窗格的內(nèi)容,并通過調(diào)用 openPane 將其滑入視圖。在 Trackr 應(yīng)用中效果如下圖所示:

          關(guān)于如何使用 SlidingPaneLayout 實現(xiàn)雙窗格布局的相關(guān)內(nèi)容,請參閱 Android 開發(fā)者網(wǎng)站: 創(chuàng)建雙窗格布局,該頁面還介紹了其他內(nèi)容,例如集成系統(tǒng)返回按鈕以實現(xiàn)側(cè)滑回退窗格等。

          Trackr 應(yīng)用

          https://github.com/android/trackr

          創(chuàng)建雙窗格布局

          https://developer.android.google.cn/guide/topics/ui/layout/twopane

          信息流

          我們可以通過信息流沉浸式地展示一個數(shù)據(jù)集,因此 RecyclerView 是非常適合的選擇,我們可以通過改變 RecyclerView 使用的 LayoutManager 來改變其展現(xiàn)形式。LinearLayoutManager 適合用于較小型寬度,但在中等寬度和展開型寬度場景下,頁面內(nèi)容則會出現(xiàn)過度拉伸和變形的情況,這時改用 GridLayoutManager,或 StaggeredGridLayoutManager 甚至 FlexBoxLayoutManager,可能會更合適。

          △ 通過更換 RecyclerView 的 LayoutManager 來改變其展現(xiàn)形式

          主頁橫幅

          我們還可以改變單項布局,使某些項比其他項更高或更寬,以此凸顯其重要性,打造更有趣的視覺效果。在主頁橫幅布局中,我們強調(diào)某個特定元素,重新排布它周圍的其他支持元素。當(dāng)然我們有很多方法可以實現(xiàn)這一點,但 ConstraintLayout 的靈活性最大,因為它提供了很多種方式來約束子元素的尺寸,以及相對于其他子元素的位置。在如下媒體類示例應(yīng)用,它的首圖限制在 16:9 的寬高比內(nèi),描述窗格占 60% 寬度,剩余空間留給其他元素。約束條件可以改變甚至還可以用 MotionLayout 設(shè)置動畫,它是一個特殊的 ConstraintLayout。

          △ 主頁橫幅示例

          對于支持面板而言,從 LinearLayout 到 ConstraintLayout 的任何布局控件,都可以當(dāng)作容器來定位面板。如下圖所示,我們考慮一件事,當(dāng)過渡到小屏幕尺寸時,面板上的內(nèi)容應(yīng)該放在哪里。我們有許多可選方案,比如使用屏幕尾側(cè)的側(cè)邊抽屜式導(dǎo)航欄,或者使用上滑式底部動作條,或者使用選項菜單,甚至可以將內(nèi)容完全隱藏起來。

          適配可折疊設(shè)備

          可折疊設(shè)備不僅配備了更大的屏幕,它們還可以根據(jù)設(shè)備的折疊方式和用戶的使用方式調(diào)整設(shè)備的方向 / 姿勢。

          目前有三種常見的設(shè)備形態(tài): 折疊、未折疊和桌面模式 (懸停)。另外,我們稍后也將看到其他理論上存在的狀態(tài),例如書本模式。

          △ 折疊設(shè)備的三種常見姿態(tài)

          與其他大屏幕設(shè)備一樣,我們需要多想想用戶會怎樣握持未折疊設(shè)備?如平板電腦,部分屏幕區(qū)域難以用大拇指觸及,用戶也很難騰出整只手來自由操控屏幕。用戶輕易就能觸及屏幕的底部角落,但可能無法觸及屏幕最頂端,尤其是在豎屏模式下。這意味著如果您使用 Navigation rail 這類組件,將導(dǎo)航按鈕居中或固定在屏幕底部,這會更便于用戶的操作。

          △ 大屏設(shè)備中的用戶操作熱區(qū)

          同時,我們還需要考慮鉸鏈位置對交互的影響。鉸鏈會帶來明顯的觸覺差異,甚至兩個屏幕會存在物理分離。因此,請您避免將按鈕和其他重要操作項直接放在鉸鏈區(qū)域。大多數(shù)設(shè)備上的鉸鏈區(qū)域?qū)挾燃s為 48 dp,在桌面模式下也請避免將界面元素放在鉸鏈區(qū)域,因為在這種設(shè)備模式下,用戶幾乎無法使用該區(qū)域的任何功能。

          △ 鉸鏈區(qū)域

          當(dāng)設(shè)備從折疊模式轉(zhuǎn)換到非折疊模式時,有兩種主要的技術(shù)方案可用于設(shè)計布局。第一種是擴大屏幕,該方案采用了一種簡單的響應(yīng)式布局,在該布局下應(yīng)用會擴展內(nèi)容并填充到屏幕上。通常情況下,我們會根據(jù)前面提到的 Material 指南來擴展欄式網(wǎng)格:

          https://m3.material.io/foundations/adaptive-design/foldables/compositions

          第二種是增加另一個頁面,根據(jù)您構(gòu)建的應(yīng)用不同,可以采用與列表 / 詳情或者以另一個面板補充主面板功能相同的方案。

          △ 情境 1: 擴大屏幕 (圖左) 情境 2: 增加頁面 (圖右)

          在這兩種情況下,根據(jù) material.io 的指南,您需要創(chuàng)建一個平均分布在鉸鏈區(qū)域兩側(cè)的八欄網(wǎng)格,當(dāng)添加 Navigation rail 等導(dǎo)航容器時,屏幕起始側(cè)會被壓縮以容納導(dǎo)航容器。

          △ 平均分布在鉸鏈兩側(cè)的八欄網(wǎng)格 (藍背景)

          適配示例

          現(xiàn)在我們來看如何在運行期間利用好折疊狀態(tài)。Jetpack Window Manager 庫提供了相應(yīng)的 API,可以檢測應(yīng)用窗口是否存在折疊。任何 Activity 都可以獲得一個 WindowInfoRepository 實例。然后,在 Started 和 Stopped 這兩種生命周期狀態(tài)之間,我們可以安全地從窗口布局信息流中收集信息。每當(dāng)流發(fā)射一個值時,我們都可以檢查 displayFeature,然后有針對性地尋找 FoldingFeature。

          override fun onCreate(savedInstanceState: Bundle?) {
                  super.onCreate(savedInstanceState)
          
                  val windowInfoRepo = windowInfoRepository()
          
                  // 在 STARTED 和 STOPPED 這兩種生命周期狀態(tài)之間安全地從 windowInfoRepo 中收集數(shù)據(jù)
                  lifecycleScope.launch(Dispatchers.Main) {
                      lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                          windowInfoRepo.windowLayoutInfo.collect { info ->
                              for (feature in info.displayFeatures) {
                                  val fold = feature as? FoldingFeature ?: continue
                                  // 使用 FoldingFeature
                              }
                          }
                      }
                  }
              }

          △ 識別折疊姿態(tài)

          掌握了折疊姿態(tài)的相關(guān)信息后,我們可以通過一些方法來查看設(shè)備是否處于前面提及的某種姿態(tài)。在書本模式下,設(shè)備的狀態(tài)為 HALF_OPENED,且其方向為 VERTICAL;在桌面模式下的狀態(tài)為 HALF_OPENED,且其方向為 HORIZONTAL。

          // 書本模式是半打開的垂直折疊模式
          fun FoldingFeature.isBookMode() =
              state == FoldingFeature.State.HALF_OPENED &&
                  orientation == FoldingFeature.Orientation.VERTICAL
          
          // 桌面模式是半打開的水平折疊模式
          fun FoldingFeature.isTableTopMode() =
              state == FoldingFeature.State.HALF_OPENED &&
                  orientation == FoldingFeature.Orientation.HORIZONTAL

          △ 書本模式于桌面模式的判定條件

          FoldingFeature 中還包含窗口中的折疊位置,當(dāng)折疊導(dǎo)致內(nèi)容視圖被割裂時,我們應(yīng)該及時更新布局參數(shù)。您可以做些調(diào)整,比如將支持面板置于一側(cè),或者在折疊的上半部分展示主頁橫幅。首先,我們需要知道內(nèi)容視圖在窗口中的位置,通過 getLocationInWindow 可以獲取位置信息。我們將使用這些坐標(biāo)以及寬度和高度創(chuàng)建一個 Rect 對象,這樣我們便得到了窗口坐標(biāo)空間中的視圖邊界。

          FoldingFeature 給出了在窗口的坐標(biāo)空間中的折疊邊界,因此我們可以直接檢查這兩個區(qū)域是否相交,如果相交,我們可以將 featureRect 的邊界轉(zhuǎn)換為視圖的坐標(biāo)空間并將其返回。順便說一下,如果您使用 SlidingPaneLayout 來實現(xiàn)列表 / 詳情布局,您會自動獲得對書本模式的支持。只要兩個窗格都能容納進去,SlidingPaneLayout 會將窗格置于折疊姿態(tài)的另一側(cè)。

          fun getFoldBoundsInView(
                 foldingFeature: FoldingFeature,
                 view: View
          ): Rect? {
                 // 獲取視圖在窗口坐標(biāo)空間中的邊界
                 val viewLocation = IntArray(2)
                 view.getLocationInWindow(viewLocation)
          
                 val (viewX, viewY) = viewLocation
                 val viewRect = Rect(
                     left = viewX, top = viewY
                     right = viewX + view.width, bottom = view + view.height
                 )
              …
          
                 //顯示功能的邊界已經(jīng)在窗口的坐標(biāo)空間中
                 // 檢查 view 的邊界和顯示功能的邊界是否相交
                 val featureRect = Rect(foldingFeature.bounds)
                 val intersects = featureRect. intersect (viewRect)
          
                 if (featureRect.isEmpty || ! intersects)
                     return null
                 }
          
                 // 將功能的邊界坐標(biāo)轉(zhuǎn)換為 view 的坐標(biāo)空間
                 featureRect.offset(-viewX, -viewY)
                 return featureRect
          }

          △ 獲取折疊的位置信息

          測試

          如果您的應(yīng)用存在與折疊狀態(tài)相關(guān)的特殊行為,您需要為此編寫單元測試。Jetpack Window Manager 里面有一條測試規(guī)則,支持在插樁測試期間模擬 FoldingFeature。由于測試需用到視圖,我們添加了 WindowLayoutInfoPublisherRule,以及 ActivityScenarioRule,兩者一起組成了一個測試規(guī)則鏈。在該測試方法中,我們通過 activityRule 獲取 Activity,然后創(chuàng)建窗口特性來模擬桌面模式,構(gòu)建 WindowLayoutInfo 對象并使用 publisherRule 發(fā)布該對象。之后,我們可以使用 Espresso 和 JUnit 斷言來檢查 Activity 在桌面模式下能否正常運行。

          private val publisherRule = WindowLayoutInfoPublisherRule()
          private val activityRule = ActivityScenarioRule (MyActivity: :class.java)
          
          @get :Rule
          val testRule = RuleChain.outerRule (publisherRule) .around(activityRule)
          
          @Test
          fun testDeviceOpen_TableTop(): Unit = testScope.runBlockingTest {
              activityRule.scenario.onActivity { activity ->
                  val feature = FoldingFeature (activity, HALF_OPENED, HORIZONTAL)
                  val testWindowInfo = WindowLayoutInfo.Builder( )
                          .setDisplayFeatures (listOf (feature))
                          .build()
          
                  publisherRule.overrideWindowLayoutInfo(testWindowInfo)
              }
                  // 編寫基于桌面模式的斷言
          }

          △ 測試折疊狀態(tài)

          界面測試存在一定難度,因為有些測試須在特定設(shè)備上進行。為此,Android Studio 正在增加對 Gradle 托管的虛擬設(shè)備的支持。您可以使用 7.1 及以上版本的 Android Gradle 插件來體驗該功能。

          在應(yīng)用級的 build.gradle 文件中的 testOptions 模塊下,指定虛擬設(shè)備配置文件,就像您平時在 Android Studio 管理和運行虛擬設(shè)備那樣。例如,這里使用的是 Pixel C 平板電腦鏡像,接下來 Gradle 會創(chuàng)建能夠在指定設(shè)備上執(zhí)行測試的目標(biāo),甚至還能根據(jù)需要下載設(shè)備鏡像。

          android {
              testoptions {
                  devices {
                      pixelCapi30 (ManagedVirtualDevice) {
                          device = "Pixel C" // 平板電腦設(shè)備
                          apilevel = 30
                          systemImageSource = "aosp" // 如需 GooglePlay 服務(wù),使用“google”
                          abi = "x86”
                      }
                  }
              }
          }
          
          #Gradle target = {device name} + {build variant} + "AndroidTest"
          ./gradlew pixelCapi30debugAndroidTest

          △ 虛擬設(shè)備配置

          為便于區(qū)分哪些測試是針對哪些設(shè)備的,我們將創(chuàng)建自定義注解 LargeScreenTest,并用該注解來標(biāo)記測試函數(shù)。運行前面的 Gradle 命令時,我們會為 AndroidTestRunner 添加一項參數(shù),確保只運行具有此注釋的測試。若您不使用注釋,也可以使用 TestRunner 的其他過濾選項,比如運行特定類中的測試。將這些特性加以組合,我們可以為測試設(shè)置一致運行配置。

          annotation class LargeScreenTest
          
          @RunWith(AndroidJUnit4: :class)
          class MyActivityTest {
          
              @Test @LargeScreenTest
              fun largeScreenDeviceTest() {
                  // 在平板電腦設(shè)備上測試界面
              }
          }
          
          # 只運行帶有指定注解的測試
          . /gradlew pixelCapi30debugAndroidTest \
              -Pandroid.testInstrumentationRunnerArguments.annotation=com.mypkg.LargeScreenTest

          △ 使用自定義注解為指定設(shè)備編寫測試

          更多信息

          除了讓屏幕上的內(nèi)容看起來更大之外,大屏幕還帶來了一些其他機會,幫助您的應(yīng)用大放異彩。在多窗口模式下,您的應(yīng)用可以與其他應(yīng)用并排使用,除了響應(yīng)式調(diào)整之外,還可以考慮如何讓應(yīng)用在這種模式下發(fā)揮更大作用,比如支持拖拽等。這種小功能可以提高用戶的工作效率,用戶便更樂意使用您的應(yīng)用。

          △ 多窗口模式效果

          多窗口模式

          https://developer.android.google.cn/guide/topics/ui/multi-window

          除了通過觸摸進行交互外,大屏幕設(shè)備還支持其他交互形式。設(shè)備的屏幕尺寸越大,用戶就越有可能使用鍵盤、手寫筆、鼠標(biāo)、游戲手柄或其他外接設(shè)備。如果您想提高應(yīng)用在這些情況下的易用性,可以計劃支持其中一些輸入方式,如需了解更多詳情,請參閱文章《是時候為各式設(shè)備適配完善的輸入支持了》。

          在如此多樣化的硬件生態(tài)系統(tǒng)中,您可能很難擁有各種形狀和尺寸的設(shè)備,如今 Android SDK 為可折疊設(shè)備提供了模擬器圖像,這些模擬器允許您隨時將折疊狀態(tài)更改為鉸鏈的角度。即將推出的 Android Studio Chipmunk 也會配備可調(diào)整尺寸的模擬器,允許您自由改變應(yīng)用窗口的尺寸,每個開發(fā)者都可以在幾乎任何類型的設(shè)備中試用他們的應(yīng)用。

          △ Android Studio Chipmunk 中的可調(diào)整尺寸的模擬器

          Android Studio Chipmunk

          https://developer.android.google.cn/studio/preview

          我們也一直在 Android Studio 中開發(fā)新工具,希望為大家開發(fā)大屏幕應(yīng)用提供支持。新的 Layout Validation 工具可以在覆蓋了各種尺寸類別的參考設(shè)備上預(yù)覽布局,并提示問題區(qū)域 (例如文本使用了長行),以及為不同斷點推薦不同界面組件。

          △ Android Studio 中的 Layout Validation

          最后,我們在 Android 開發(fā)者網(wǎng)站上列出了針對大屏幕的應(yīng)用質(zhì)量指南,指南中的前面部分介紹的是基本兼容性預(yù)期,比如應(yīng)用是否同時支持橫屏和豎屏模式,后面幾部分重點介紹支持各種屏幕類型和狀態(tài),并使用特定屏幕類型或狀態(tài)打造不同的體驗。

          大屏幕的應(yīng)用質(zhì)量指南

          https://developer.android.google.cn/docs/quality-guidelines/large-screens-app-quality

          我們希望大家都能夠利用今天分享的內(nèi)容,并參考新的質(zhì)量指南,構(gòu)建出在各種屏幕尺寸下都能讓用戶心動的應(yīng)用。

          、傳統(tǒng)布局

          盒狀模型結(jié)合 display 屬性、float 浮動以及 position 定位屬性設(shè)計的各式傳統(tǒng)布局形式。

          2、說再多不如動手實踐,下面舉三個例子

          html 部分代碼:

           <section>
           2 <!-- 傳統(tǒng)布局-例1結(jié)構(gòu):盒子模型 -->
           3 <div class="layout-one">
           4 <div class="header">header</div>
           5 <div class="banner">banner</div>
           6 <div class="content">main-content</div>
           7 <div class="footer">footer</div>
           8 </div>
           9 
          10 <!-- 傳統(tǒng)布局-例2結(jié)構(gòu):盒子模型 + float -->
          11 <div class="layout-two">
          12 <div class="header">header</div>
          13 <div class="banner">banner</div>
          14 <div class="content">
          15 <div class="content-left">content-left</div>
          16 <div class="content-right">content-right</div>
          17 </div>
          18 <div class="footer">footer</div>
          19 </div>
          20 
          21 <!-- 傳統(tǒng)布局-例3結(jié)構(gòu) + float + position -->
          22 <div class="layout-three">
          23 <div class="header">header</div>
          24 <div class="main-banner">banner</div>
          25 <div class="main-content">
          26 <div class="content1">content1</div>
          27 <div class="content2">content2</div>
          28 </div>
          29 <div class="footer">footer</div>
          30 </div>
          31 </section>
          

          css樣式部分代碼:

          1 /* 基本樣式 */
           2 section {
           3 width: 1200px;
           4 height: 300px;
           5 margin: 0 auto;
           6 padding: 10px;
           7 }
           8 .layout-one, .layout-two, .layout-three {
           9 float: left;
          10 margin-left: 20px;
          11 }
          12 div {
          13 width: 300px;
          14 }
          15 
          16 /* 可復(fù)用樣式 */
          17 .header {
          18 height: 25px;
          19 text-align: center;
          20 background-color: bisque;
          21 line-height: 25px;
          22 }
          23 .banner {
          24 height: 50px;
          25 text-align: center;
          26 line-height: 50px;
          27 background-color: aquamarine;
          28 }
          29 .footer {
          30 height: 25px;
          31 text-align: center;
          32 line-height: 25px;
          33 background-color: black;
          34 color: #ffffff;
          35 }
          36 
          37 /* 結(jié)構(gòu)1 基礎(chǔ)樣式 */
          38 .layout-one .content {
          39 height: 60px;
          40 text-align: center;
          41 line-height: 60px;
          42 background-color: aqua;
          43 }
          44 
          45 /* 結(jié)構(gòu)2 基礎(chǔ)樣式 */
          46 .layout-two .content {
          47 height: 60px;
          48 text-align: center;
          49 line-height: 60px;
          50 background-color: aqua;
          51 }
          52 .layout-two .content-left {
          53 width: 100px;
          54 float: left;
          55 border-right: 1px solid #000000;
          56 }
          57 .layout-two .content-right {
          58 width: 199px;
          59 float: left;
          60 }
          61 
          62 /* 結(jié)構(gòu)3 基礎(chǔ)樣式 */
          63 .main-banner {
          64 width: 200px;
          65 height: 50px;
          66 margin: 0 auto;
          67 background-color: aquamarine;
          68 text-align: center;
          69 line-height: 50px;
          70 }
          71 .main-content {
          72 position: relative;
          73 width: 200px;
          74 height: 60px;
          75 margin: 0 auto;
          76 text-align: center;
          77 line-height: 60px;
          78 }
          79 .content1 {
          80 width: 60px;
          81 height: 60px;
          82 position: absolute;
          83 top: 0px;
          84 left: 30px;
          85 background-color: aqua;
          86 }
          87 .content2 {
          88 width: 60px;
          89 height: 60px;
          90 position: absolute;
          91 top: 0px;
          92 right: 30px;
          93 background-color: aqua;
          94 }
          

          頁面效果:

          通過上述的三個例子,我們可以發(fā)現(xiàn):

          • 盒子的多重嵌套,雖然讓我們可以方便的劃分區(qū)域,但卻增加了網(wǎng)頁結(jié)構(gòu)的復(fù)雜性,這將使后期網(wǎng)站的維護變得十分的困難,從而增加維護成本;
          • 合理的網(wǎng)頁布局可以讓我們在網(wǎng)站開發(fā)的過程中得到事半功倍的效果
          • 浮動的應(yīng)用以及結(jié)合定位屬性的使用,使得我們的網(wǎng)頁布局花樣變得豐富多彩,但同時也帶來了不少問題,比如:在定位的過程中對“距離”的要求變得十分精確等等
          • 網(wǎng)站設(shè)計與布局的其中一個也是很重要的要求便是能在不同的設(shè)備上較好的展現(xiàn)對應(yīng)的內(nèi)容,但如上的傳統(tǒng)布局在一定程度上在這方面不是很好,無法很好的進行響應(yīng)屏幕分辨率的變化!

          為了更近時代的進步,所以我們需要更深入的去學(xué)習(xí),如:響應(yīng)式設(shè)計中的,流式布局,彈性布局等等!加油吧,騷年!

          每天進步一點點,相信積累的力量!

          web前端其實很簡單!

          如何學(xué)習(xí)呢?看下下面

          領(lǐng)取方法:

          關(guān)注“IT金勻” 然后私信回復(fù)“前端”即可

          請記得給金勻先來個“評論+轉(zhuǎn)發(fā)”

          我們的開發(fā)工程中經(jīng)常會使用到各種圖,所謂的圖就是由節(jié)點和節(jié)點之間的連接所形成的系統(tǒng),數(shù)學(xué)上專門有一個分支叫圖論(Graph Theroy)。利用圖我們可以做很多工具,比如思維導(dǎo)圖,流程圖,狀態(tài)機,組織架構(gòu)圖,等等。今天我要做的是用開源的HTML5工具來快速構(gòu)造一個做圖的工具。

          工具選擇

          工預(yù)善其事,必先利其器。第一件事是選擇一件合適的工具,開源時代,程序員還是很幸福的,選擇很多。

          • flowchart.js http://adrai.github.io/flowchart.js/ , 基于SVG創(chuàng)建Flow Chart
          • go.js http://www.gojs.net/latest/index.html go.js 提供一整套的JS工具 ,支持各種交互式圖表的創(chuàng)建。有免費版和收費版
          • joint.js http://www.jointjs.com/ joint.js 是另一個創(chuàng)建圖標(biāo)庫的工具,也提供免費版和商業(yè)版
          • jsPlumb http://www.jsplumb.org/ jsPlumb是一套開源的流程圖創(chuàng)建工具 ,小巧精悍,使用簡單
          • d3 http://d3js.org 在html5領(lǐng)域,d3可謂是最好的可視化基礎(chǔ)庫,提供方面的DOM操作,非常強大。

          最終,我選擇了jsPlumb,因為它完全開源,使用很簡單,用D3的話可能會多花很多功夫。joint.js也不錯。大家可以根據(jù)自己的需要選擇。

          構(gòu)建靜態(tài)應(yīng)用

          下面我們一步一步的來使用jsPlumb來創(chuàng)建我們的流程圖工具。

          第一步是等待DOM和jsPlumb初始化完畢,類似document.ready()和jquery.ready(), 要使用jsPlumb, 需要把代碼放在這個函數(shù)里:

          jsPlumb.ready(function()?{
          ????//?...?your?code?goes?here?...
          }


          創(chuàng)建一個jsPlumb的實例,并初始化jsPlumb的配置參數(shù):

          //Initialize?JsPlumb
          var?color?=?"#E8C870";
          var?instance?=?jsPlumb.getInstance({
          ????//?notice?the?'curviness'?argument?to?this?Bezier?curve.??the?curves?on?this?page?are?far?smoother
          ????//?than?the?curves?on?the?first?demo,?which?use?the?default?curviness?value.??????
          ????Connector?:?[?"Bezier",?{?curviness:50?}?],
          ????DragOptions?:?{?cursor:?"pointer",?zIndex:2000?},
          ????PaintStyle?:?{?strokeStyle:color,?lineWidth:2?},
          ????EndpointStyle?:?{?radius:5,?fillStyle:color?},
          ????HoverPaintStyle?:?{strokeStyle:"#7073EB"?},
          ????EndpointHoverStyle?:?{fillStyle:"#7073EB"?},
          ????Container:"container-id"
          ?});


          這里給給出了一些配置包括,連接線(這里配置了一個貝塞爾曲線),線的風(fēng)格,連接點得風(fēng)格。Container需要配置一個對應(yīng)的DIV容器的id。(這里也可以使用setContainer的方法)

          下面我們要創(chuàng)建一個節(jié)點(node),每一個節(jié)點可以用一個DIV來實現(xiàn)。我這里提供了一個函數(shù)來創(chuàng)建節(jié)點。

          function?addNode(parentId,?nodeId,?nodeLable,?position)?{
          ??var?panel?=?d3.select("#"?+?parentId);
          ??panel.append('div').style('width','120px').style('height','50px')
          ????.style('position','absolute')
          ????.style('top',position.y).style('left',position.x)
          ????.style('border','2px?#9DFFCA?solid').attr('align','center')
          ????.attr('id',nodeId).classed('node',true)
          ????.text(nodeLable);
          
          ??return?jsPlumb.getSelector('#'?+?nodeId)[0];
          }


          這里做的事情就是創(chuàng)建了一個DIV元素,并放在對應(yīng)的容器的制定位置上,注意為了支持拖拽的功能,必須使用position:absolute 。

          我使用D3來操作DOM,大家可能會更習(xí)慣JQuery,這純屬個人喜好的問題。

          最后返回創(chuàng)建節(jié)點的實例引用,這是的selector使用了jsPlumb.getSelector()方法,它和JQuery的selector是一樣的,這樣用的好處是你可以使用不同的DOM操作庫,例如Vanilla

          下面我使用一個函數(shù)來創(chuàng)建端點/錨點(anchor),錨點就是節(jié)點上的連接點,用于連接不同的節(jié)點。

          function?addPorts(instance,?node,?ports,?type)?{
          ??//Assume?horizental?layout
          ??var?number_of_ports?=?ports.length;
          ??var?i?=?0;
          ??var?height?=?$(node).height();??//Note,?jquery?does?not?include?border?for?height
          ??var?y_offset?=?1?/?(?number_of_ports?+?1);
          ??var?y?=?0;
          
          ??for?(?;?i?<?number_of_ports;?i++?)?{
          ????var?anchor?=?[0,0,0,0];
          ????var?paintStyle?=?{?radius:5,?fillStyle:'#FF8891'?};
          ????var?isSource?=?false,?isTarget?=?false;
          ????if?(?type?===?'output'?)?{
          ??????anchor[0]?=?1;
          ??????paintStyle.fillStyle?=?'#D4FFD6';
          ??????isSource?=?true;
          ????}?else?{
          ??????isTarget?=true;
          ????}
          
          ????anchor[1]?=?y?+?y_offset;
          ????y?=?anchor[1];
          
          ????instance.addEndpoint(node,?{
          ??????uuid:node.getAttribute("id")?+?"-"?+?ports[i],
          ??????paintStyle:?paintStyle,
          ??????anchor:anchor,
          ??????maxConnections:-1,
          ??????isSource:isSource,
          ??????isTarget:isTarget
          ????});
          ??}
          }


          instance是jsPlumb的實例

          node是我們用addNode方法創(chuàng)建的Node實例

          ports,是一個string的數(shù)組,指定端點的個數(shù)和名字

          type,可能是output或者input,指定端點的種類,一個節(jié)點的輸出端口可以連接另一個節(jié)點的輸入端口。

          這里anchor是一個四維數(shù)組,0維和1維分別是錨點在節(jié)點x軸和y軸的偏移百分比。我這里希望把端口畫在節(jié)點的左右兩側(cè),并按照端口的數(shù)量均勻分布。

          最后使用instance.addEndpoint來創(chuàng)建端點。注意這里只要指定isSource和isTarget就可以用drag&drop的方式來連接端點,非常方便。

          下面一步我們提供一個函數(shù)來連接端點:

          function?connectPorts(instance,?node1,?port1,?node2?,?port2)?{
          ??//?declare?some?common?values:
          ??var?color?=?"gray";
          ??var?arrowCommon?=?{?foldback:0.8,?fillStyle:color,?width:5?},
          ??//?use?three-arg?spec?to?create?two?different?arrows?with?the?common?values:
          ??overlays?=?[
          ????[?"Arrow",?{?location:0.8?},?arrowCommon?],
          ????[?"Arrow",?{?location:0.2,?direction:-1?},?arrowCommon?]
          ??];
          
          ??var?uuid_source?=?node1.getAttribute("id")?+?"-"?+?port1;
          ??var?uuid_target?=?node2.getAttribute("id")?+?"-"?+?port2;
          
          ??instance.connect({uuids:[uuid_source,?uuid_target]});
          }


          node1和node2是源節(jié)點和目標(biāo)節(jié)點的引用,port1和port2是源端口和目標(biāo)端口的名字。

          使用instance.connect方法來創(chuàng)建連接。 overlays用來添加連接線的箭頭效果或者其他風(fēng)格,我這里沒有使用,因為覺得都不是很好看。大家如果要用,只要把overlays加入到instance.connect的方法參數(shù)就可以了。

          調(diào)用以上方法來創(chuàng)建節(jié)點,端點和連接線。

          var?node1?=?addNode('container-id','node1',?'node1',?{x:'80px',y:'20px'});
          var?node2?=?addNode('container-id','node2',?'node2',?{x:'280px',y:'20px'});
          
          addPorts(instance,?node1,?['out1','out2'],'output');
          addPorts(instance,?node2,?['in','in1','in2'],'input');
          
          connectPorts(instance,?node1,?'out2',?node2,?'in');


          這里我們創(chuàng)建了兩個節(jié)點,第一個節(jié)點有兩個輸出端口,第二個節(jié)點有三個輸入端口,然后把第一個節(jié)點的out2端口連接到第二個端點的in端口。效果如下:

          最后我們給節(jié)點增加drag&drop的功能,這樣我們就可以拖動這些節(jié)點來改變圖的布局了。

          instance.draggable($('.node'));


          這里似乎依賴于JQuery-UI,我還不是很清楚。

          交互式創(chuàng)建節(jié)點

          我們已經(jīng)初步具有了創(chuàng)建圖的功能,可是節(jié)點的創(chuàng)建必須通過程序,我們希望用交互的方式來創(chuàng)建節(jié)點。

          通常我們希望有一個tree view的控件,讓后通過拖拽來創(chuàng)建對應(yīng)類型的節(jié)點。這里我使用了這個開源的tree view,基于bootstrap https://github.com/jonmiles/bootstrap-treeview

          我們先創(chuàng)建一個tree view:

          function?getTreeData()?{
          ??var?tree?=?[
          ????{
          ??????text:?"Nodes",
          ??????nodes:?[
          ????????{
          ??????????text:?"Node1",
          ????????},
          ????????{
          ??????????text:?"Node2"
          ????????}
          ??????]
          ????}
          ??];?
          
          ??return?tree;
          }
          //Initialize?Control?Tree?View
          $('#control-panel').treeview({data:?getTreeData()});


          樹上有兩個節(jié)點:

          然后我實現(xiàn)從樹上拖拽對應(yīng)的節(jié)點,到流程圖上的邏輯。

          //Handle?drag?and?drop
          $('.list-group-item').attr('draggable','true').on('dragstart',?function(ev){
          ??//ev.dataTransfer.setData("text",?ev.target.id);
          ??ev.originalEvent.dataTransfer.setData('text',ev.target.textContent);
          ??console.log('drag?start');
          });
          
          $('#container-id').on('drop',?function(ev){
          ??//avoid?event?conlict?for?jsPlumb
          ??if?(ev.target.className.indexOf('_jsPlumb')?>=?0?)?{
          ????return;
          ??}
          
          ??ev.preventDefault();
          ??var?mx?=?''?+?ev.originalEvent.offsetX?+?'px';
          ??var?my?=?''?+?ev.originalEvent.offsetY?+?'px';
          
          ??console.log('on?drop?:?'?+?ev.originalEvent.dataTransfer.getData('text'));
          ??var?uid?=?new?Date().getTime();
          ??var?node?=?addNode('flow-panel','node'?+?uid,?'node',?{x:mx,y:my});
          ??addPorts(instance,?node,?['out'],'output');
          ??addPorts(instance,?node,?['in1','in2'],'input');
          ??instance.draggable($(node));
          }).on('dragover',?function(ev){
          ??ev.preventDefault();
          ??console.log('on?drag?over');
          });


          這里要注意的是要避免和jsPlumb拖拽端點的邏輯沖突,當(dāng)檢測到target是jsPlumb對象是需要直接從drop方法中退出以執(zhí)行對應(yīng)的jsPlumb的drop邏輯。

          好了,一個繪制流程圖的軟件工具初步完工。

          我把代碼放在oschina的代碼托管服務(wù)上了, 大家有興趣可以去試試。


          主站蜘蛛池模板: 在线观看一区二区精品视频| A国产一区二区免费入口| 人妻久久久一区二区三区| 中文字幕乱码一区二区免费| 狠狠色婷婷久久一区二区| 奇米精品一区二区三区在| 国产人妖视频一区二区| 亚洲福利一区二区精品秒拍| 国产精品无码一区二区三区不卡| 国产成人精品无码一区二区老年人 | 日韩精品中文字幕无码一区| 国产美女在线一区二区三区| 国产主播福利一区二区| 一区二区精品视频| 寂寞一区在线观看| 麻豆一区二区免费播放网站| 日本一区二区三区在线视频观看免费| 一区二区免费国产在线观看| 亚洲AV无码一区二区三区系列| 亚洲一区二区影视| 亚洲国产高清在线一区二区三区 | 亚洲人成人一区二区三区| 中文无码一区二区不卡αv| 精品女同一区二区三区在线 | 精品一区精品二区| 久久久老熟女一区二区三区| 精品国产一区二区三区免费看 | 亚洲一区精品视频在线| 色系一区二区三区四区五区| 农村乱人伦一区二区| 国产一区二区三区在线看片| 亚洲AV日韩AV天堂一区二区三区| 国产情侣一区二区| 一区二区三区四区精品视频| 国产SUV精品一区二区88L| 亚洲视频一区二区在线观看| 区三区激情福利综合中文字幕在线一区| 日韩人妻一区二区三区免费| 无码少妇一区二区浪潮av| 亚洲Av永久无码精品一区二区| 国产丝袜无码一区二区三区视频|