Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
ndroid 的覆蓋范圍在遞增,體驗也在變得越來越好,現已有超過 2.5 億臺大屏設備搭載了 Android 系統,包括平板電腦、可折疊設備以及 Chrome OS 設備。如何適配不同的屏幕尺寸并保障良好的體驗,一直以來都是開發者的一大難題。尤其隨著可折疊設備等新興產品的涌現,適配工作也愈發迫切。
谷歌官方發文,將重點介紹 Material Design 指南中更新的相關內容,并提供一些建議來幫助開發者按照自適應界面的原則來構建應用,從而解決在平板電腦和可折疊設備上的適配問題。
如果您更喜歡通過視頻了解本文內容,請點擊下方:
△ 折疊屏上應用設計規范
Compose
https://developer.android.google.cn/jetpack/compose/nav-adaptive
2021 年年初,我們在 Material Design 網站上發布了針對大屏設備的指南文檔。Android 開發者峰會期間我們更新了一些內容,以幫助開發者為可折疊設備等更多其他類型的設備做好準備。
https://m3.material.io/foundations/adaptive-design/overview
深入理解布局
深入理解布局指南介紹了布局容器的相關概念,它提供了一個整體框架,可幫助開發者思考如何在屏幕上排列導航欄、工具欄和內容等界面元素。
https://material.io/design/layout/understanding-layout.html#principles
△ 布局的三個主要區域
指南中的組合部分帶您了解如何充分利用屏幕空間以保障可讀性,并且以尊重用戶心智模型的方式在不同的場景下合理排布重要內容和操作選項。包括適當縮放以展示更多內容,如示例中的副標題和日期,以及較小的組合技術,例如在緊湊型的布局中對內容進行視覺分組并保持其相關性等。
https://material.io/design/layout/understanding-layout.html#composition
△ 組合指南中涉及的部分布局方式
以 Fortnightly 示例應用為例,它在平板電腦上的界面布局十分均衡,這得益于它遵從了指南里對容器的建議。而且可以看到,Fortnightly 使用了視覺分隔線 (Visual Divider) 用于分隔最新新聞,在屏幕的另一邊,則利用留白和排版對不同類別的新聞報道進行分組。
△ Fortnightly 遵循指南對內容進行分隔和分組
網格系統
現在,許多應用將屏幕視作一個大畫布或單欄,以水平和垂直的方式按相互關系繪制元素,有些應用也會在一側整體留出邊距。這一做法在小屏上或許行得通,當屏幕尺寸較大時就會出現明顯的問題。網格系統則將您的布局劃分為一系列欄,從而幫助您在規范網格中設計更具表現力的布局。在布局中使用欄式網格 (如下圖),能夠讓大屏設備的體驗呈現更貼心,更組織有序的印象,使得設備和內容更自然地融為一體。
△ 欄式網格
您可以通過這些欄將屏幕劃分為不同區域,用于容納相關的信息和操作,進而改善信息層次結構。如下圖所示,這里分了三個區域,這些區域將按照設計者期望用戶閱讀的順序,把用戶的注意力吸引到這些區域對應在屏幕的主要信息片段或信息組上。最重要的一點是,欄式網格提供了一種合理的方式來思考當屏幕尺寸變大或變小時如何將內容進行重排,從而幫助您對不同的屏幕尺寸作出一致響應。
△ 使用欄式網格將屏幕劃分為三個主要區域
在本例中,三個主要區域通過重排來保持相同的信息層次結構,但以更加人性化的方式在小屏幕上顯示。
△ 使用欄式網格在不同屏幕尺寸中對內容進行重排
記住網格系統有助于您選擇組件行為,在不同的布局中,以對設備尺寸和場景最有意義的方式決定替換還是更改組件。例如,在大屏設備上,您可使用 Navigation rail (左側邊欄導航條) 代替底部導航 (Bottom navigation),兩者功能相同,視覺表現方式也類似,但 Navigation rail 能夠更加人性化地排布頁面。手機上的全屏對話框 (Full-screen dialog) 在大屏幕上可以采用簡單對話框 (Simple dialog) 替代,以保持用戶當前操作的上下文。
△ 在大屏上使用簡單對話框 (右) 代替全屏對話框 (左)
Navigation rail
https://m3.material.io/components/navigation-rail/overview
底部導航 (Bottom navigation)
https://m3.material.io/components/bottom-navigation
尺寸類別
請記住,替換組件時,首先要滿足用戶的功能性和人性化需求。找到調整界面的正確閾值,這是實現響應式界面的重要步驟。因此我們定義了新斷點值,這有助于將設備劃分到預設的尺寸類別中,這些尺寸代表了市場上實際設備的尺寸。它們有助于將應用版面的原始尺寸轉換為離散的標準化組,您可以據此做出更高層次的界面決策。例如,幾乎所有標準手機在豎屏模式下都采用了較小 (Compact) 寬度和中等 (Medium) 高度的組合,由于普遍使用垂直滾動,對大多數應用而言,根據寬度的尺寸類別進行適配就已足夠。
△ 基于寬度的尺寸類別
△ 基于高度的尺寸類
這些尺寸類將作為新的 API 出現在 1.1 版 Jetpack Window Manager 庫中。從 Android Studio Bumblebee 開始,我們還以參考設備 (Reference devices) 的形式,將尺寸類別整合到工具中,在此基礎上實現界面有利于保持一致性,操作也更加簡單。而且開發者不需要去檢查實際物理尺寸或屏幕方向,或其他容易出錯的標識。您在設計和構建不同的尺寸類別時,請想想人們會如何手持和觸摸這些類別所代表的設備。關注設備的形狀和尺寸,有助于您打造出更加人性化的體驗。例如,在平板電腦或大屏手機上,如果不完全調整握持姿勢,人們可能很難觸及屏幕的頂部區域,因此請將重要操作和內容放在容易觸及的區域中。
尺寸類
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
規范布局
規范布局提供了一系列通用布局方案,對設計大屏幕應用非常有幫助。第一種是列表 / 詳情,或列表網格視圖的簡單組合,同時在開始展示內容的屏幕起始側,設置 / 不設置導航容器。
△ 列表 / 詳情布局
支持面板可用于人們需要集中精力的體驗中,例如文檔。在屏幕尾側或底部添加一塊面板,以便于使用工具或上下文控件。
△ 支持面板
信息流是新聞或社交類應用中的常見模式,模板采用圖塊 (Tile) 的形式來吸引用戶發現更多內容。這種交互與移動手機一樣 —— 打開一項即表示打開一個新頁面,但這種體驗更具沉浸感,而且專為大屏幕尺寸而設計。
△ 信息流
主頁橫幅優先將內容排列在屏幕頂部,并在內容周圍和下方設計了支持元素,這對以媒體為中心的應用來說,是非常棒的體驗。
△ 主頁橫幅
采用響應式界面不僅僅是為不同屏幕尺寸提供并行結構,應用還要足夠靈活,這樣才能根據各種需要調整尺寸,例如旋轉設備、多窗口模式以及折疊和非折疊姿態。因此在運行期間,應用可從一個尺寸類別過渡到另一個尺寸類別,并再次過渡回去。重要的是,不要將尺寸類別視作完全獨立的桶,應用也需保證連續性 (即不中斷用戶體驗),所以應用狀態或數據不能丟失。
△ 響應式界面可根據屏幕尺寸變化而調整內容布局
設想一下,當您調整瀏覽器窗口大小時,如果瀏覽器回退了一個頁面,或者重定向到另一個頁面,又或者修改了歷史記錄,這種體驗非常奇怪。因此,每個頁面都應足夠靈活,而且應當能夠在尺寸過渡期間保持狀態不變,這個時候規范布局就能發揮重要作用。針對每個頁面,您可以思考一下,當屏幕尺寸變大時,可以添加什么內容。當屏幕尺寸變小時,可以刪除哪些內容。然后再選擇合適的策略。這可能意味著您需要重新審視導航圖,尤其是當您目前的設計以手機為主時更應如此。
如需構建響應式界面,我們應該優先考慮界面中長駐元素的位置,例如導航元素。遵循 Material 指南,我們可以根據寬度的尺寸類別提供替代布局,將導航調整到最方便使用的位置。例如,小屏幕采用底部導航視圖,中等屏幕采用 Navigation rail,大屏幕采用完整導航視圖。請大家注意,這些布局采用的是寬度限定符 "-w",而非最小寬度限定符 "-sw"。剩余空間用于排列內容,我們可以在這些空間應用規范布局。
列表 / 詳情
對列表 / 詳情而言,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 布局示例
當屏幕空間足夠,則兩個窗格至少都要達到指定的寬度,剩余空間可通過 layout_weight 分配,如左圖所示;如果空間不足,如右圖所示,則每個窗格都使用父視圖的全寬,詳情窗格將被滑到一邊,或直接覆蓋第一個窗格。
△ SlidingPaneLayout 中空間分配結果
viewModel.selectedItemFlow.collect { item -> // 更新詳情窗格的內容 detailPane.showItem(item) // 將詳細信息窗格滑動到視圖中 // 如果并排放置兩個窗格 // 并不會產生實際效果 slidingPaneLayout.openPane() }
如上代碼所示,您可以通過代碼控制滑動窗格,當用戶從列表中選擇一個項目,我們從 ViewModel 的 Kotlin 流中接收到該項目,然后更新詳情窗格的內容,并通過調用 openPane 將其滑入視圖。在 Trackr 應用中效果如下圖所示:
關于如何使用 SlidingPaneLayout 實現雙窗格布局的相關內容,請參閱 Android 開發者網站: 創建雙窗格布局,該頁面還介紹了其他內容,例如集成系統返回按鈕以實現側滑回退窗格等。
Trackr 應用
https://github.com/android/trackr
創建雙窗格布局
https://developer.android.google.cn/guide/topics/ui/layout/twopane
信息流
我們可以通過信息流沉浸式地展示一個數據集,因此 RecyclerView 是非常適合的選擇,我們可以通過改變 RecyclerView 使用的 LayoutManager 來改變其展現形式。LinearLayoutManager 適合用于較小型寬度,但在中等寬度和展開型寬度場景下,頁面內容則會出現過度拉伸和變形的情況,這時改用 GridLayoutManager,或 StaggeredGridLayoutManager 甚至 FlexBoxLayoutManager,可能會更合適。
△ 通過更換 RecyclerView 的 LayoutManager 來改變其展現形式
主頁橫幅
我們還可以改變單項布局,使某些項比其他項更高或更寬,以此凸顯其重要性,打造更有趣的視覺效果。在主頁橫幅布局中,我們強調某個特定元素,重新排布它周圍的其他支持元素。當然我們有很多方法可以實現這一點,但 ConstraintLayout 的靈活性最大,因為它提供了很多種方式來約束子元素的尺寸,以及相對于其他子元素的位置。在如下媒體類示例應用,它的首圖限制在 16:9 的寬高比內,描述窗格占 60% 寬度,剩余空間留給其他元素。約束條件可以改變甚至還可以用 MotionLayout 設置動畫,它是一個特殊的 ConstraintLayout。
△ 主頁橫幅示例
對于支持面板而言,從 LinearLayout 到 ConstraintLayout 的任何布局控件,都可以當作容器來定位面板。如下圖所示,我們考慮一件事,當過渡到小屏幕尺寸時,面板上的內容應該放在哪里。我們有許多可選方案,比如使用屏幕尾側的側邊抽屜式導航欄,或者使用上滑式底部動作條,或者使用選項菜單,甚至可以將內容完全隱藏起來。
可折疊設備不僅配備了更大的屏幕,它們還可以根據設備的折疊方式和用戶的使用方式調整設備的方向 / 姿勢。
目前有三種常見的設備形態: 折疊、未折疊和桌面模式 (懸停)。另外,我們稍后也將看到其他理論上存在的狀態,例如書本模式。
△ 折疊設備的三種常見姿態
與其他大屏幕設備一樣,我們需要多想想用戶會怎樣握持未折疊設備?如平板電腦,部分屏幕區域難以用大拇指觸及,用戶也很難騰出整只手來自由操控屏幕。用戶輕易就能觸及屏幕的底部角落,但可能無法觸及屏幕最頂端,尤其是在豎屏模式下。這意味著如果您使用 Navigation rail 這類組件,將導航按鈕居中或固定在屏幕底部,這會更便于用戶的操作。
△ 大屏設備中的用戶操作熱區
同時,我們還需要考慮鉸鏈位置對交互的影響。鉸鏈會帶來明顯的觸覺差異,甚至兩個屏幕會存在物理分離。因此,請您避免將按鈕和其他重要操作項直接放在鉸鏈區域。大多數設備上的鉸鏈區域寬度約為 48 dp,在桌面模式下也請避免將界面元素放在鉸鏈區域,因為在這種設備模式下,用戶幾乎無法使用該區域的任何功能。
△ 鉸鏈區域
當設備從折疊模式轉換到非折疊模式時,有兩種主要的技術方案可用于設計布局。第一種是擴大屏幕,該方案采用了一種簡單的響應式布局,在該布局下應用會擴展內容并填充到屏幕上。通常情況下,我們會根據前面提到的 Material 指南來擴展欄式網格:
https://m3.material.io/foundations/adaptive-design/foldables/compositions
第二種是增加另一個頁面,根據您構建的應用不同,可以采用與列表 / 詳情或者以另一個面板補充主面板功能相同的方案。
△ 情境 1: 擴大屏幕 (圖左) 情境 2: 增加頁面 (圖右)
在這兩種情況下,根據 material.io 的指南,您需要創建一個平均分布在鉸鏈區域兩側的八欄網格,當添加 Navigation rail 等導航容器時,屏幕起始側會被壓縮以容納導航容器。
△ 平均分布在鉸鏈兩側的八欄網格 (藍背景)
現在我們來看如何在運行期間利用好折疊狀態。Jetpack Window Manager 庫提供了相應的 API,可以檢測應用窗口是否存在折疊。任何 Activity 都可以獲得一個 WindowInfoRepository 實例。然后,在 Started 和 Stopped 這兩種生命周期狀態之間,我們可以安全地從窗口布局信息流中收集信息。每當流發射一個值時,我們都可以檢查 displayFeature,然后有針對性地尋找 FoldingFeature。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val windowInfoRepo = windowInfoRepository() // 在 STARTED 和 STOPPED 這兩種生命周期狀態之間安全地從 windowInfoRepo 中收集數據 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 } } } } }
△ 識別折疊姿態
掌握了折疊姿態的相關信息后,我們可以通過一些方法來查看設備是否處于前面提及的某種姿態。在書本模式下,設備的狀態為 HALF_OPENED,且其方向為 VERTICAL;在桌面模式下的狀態為 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 中還包含窗口中的折疊位置,當折疊導致內容視圖被割裂時,我們應該及時更新布局參數。您可以做些調整,比如將支持面板置于一側,或者在折疊的上半部分展示主頁橫幅。首先,我們需要知道內容視圖在窗口中的位置,通過 getLocationInWindow 可以獲取位置信息。我們將使用這些坐標以及寬度和高度創建一個 Rect 對象,這樣我們便得到了窗口坐標空間中的視圖邊界。
FoldingFeature 給出了在窗口的坐標空間中的折疊邊界,因此我們可以直接檢查這兩個區域是否相交,如果相交,我們可以將 featureRect 的邊界轉換為視圖的坐標空間并將其返回。順便說一下,如果您使用 SlidingPaneLayout 來實現列表 / 詳情布局,您會自動獲得對書本模式的支持。只要兩個窗格都能容納進去,SlidingPaneLayout 會將窗格置于折疊姿態的另一側。
fun getFoldBoundsInView( foldingFeature: FoldingFeature, view: View ): Rect? { // 獲取視圖在窗口坐標空間中的邊界 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 ) … //顯示功能的邊界已經在窗口的坐標空間中 // 檢查 view 的邊界和顯示功能的邊界是否相交 val featureRect = Rect(foldingFeature.bounds) val intersects = featureRect. intersect (viewRect) if (featureRect.isEmpty || ! intersects) return null } // 將功能的邊界坐標轉換為 view 的坐標空間 featureRect.offset(-viewX, -viewY) return featureRect }
△ 獲取折疊的位置信息
如果您的應用存在與折疊狀態相關的特殊行為,您需要為此編寫單元測試。Jetpack Window Manager 里面有一條測試規則,支持在插樁測試期間模擬 FoldingFeature。由于測試需用到視圖,我們添加了 WindowLayoutInfoPublisherRule,以及 ActivityScenarioRule,兩者一起組成了一個測試規則鏈。在該測試方法中,我們通過 activityRule 獲取 Activity,然后創建窗口特性來模擬桌面模式,構建 WindowLayoutInfo 對象并使用 publisherRule 發布該對象。之后,我們可以使用 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) } // 編寫基于桌面模式的斷言 }
△ 測試折疊狀態
界面測試存在一定難度,因為有些測試須在特定設備上進行。為此,Android Studio 正在增加對 Gradle 托管的虛擬設備的支持。您可以使用 7.1 及以上版本的 Android Gradle 插件來體驗該功能。
在應用級的 build.gradle 文件中的 testOptions 模塊下,指定虛擬設備配置文件,就像您平時在 Android Studio 管理和運行虛擬設備那樣。例如,這里使用的是 Pixel C 平板電腦鏡像,接下來 Gradle 會創建能夠在指定設備上執行測試的目標,甚至還能根據需要下載設備鏡像。
android { testoptions { devices { pixelCapi30 (ManagedVirtualDevice) { device = "Pixel C" // 平板電腦設備 apilevel = 30 systemImageSource = "aosp" // 如需 GooglePlay 服務,使用“google” abi = "x86” } } } } #Gradle target = {device name} + {build variant} + "AndroidTest" ./gradlew pixelCapi30debugAndroidTest
△ 虛擬設備配置
為便于區分哪些測試是針對哪些設備的,我們將創建自定義注解 LargeScreenTest,并用該注解來標記測試函數。運行前面的 Gradle 命令時,我們會為 AndroidTestRunner 添加一項參數,確保只運行具有此注釋的測試。若您不使用注釋,也可以使用 TestRunner 的其他過濾選項,比如運行特定類中的測試。將這些特性加以組合,我們可以為測試設置一致運行配置。
annotation class LargeScreenTest @RunWith(AndroidJUnit4: :class) class MyActivityTest { @Test @LargeScreenTest fun largeScreenDeviceTest() { // 在平板電腦設備上測試界面 } } # 只運行帶有指定注解的測試 . /gradlew pixelCapi30debugAndroidTest \ -Pandroid.testInstrumentationRunnerArguments.annotation=com.mypkg.LargeScreenTest
△ 使用自定義注解為指定設備編寫測試
除了讓屏幕上的內容看起來更大之外,大屏幕還帶來了一些其他機會,幫助您的應用大放異彩。在多窗口模式下,您的應用可以與其他應用并排使用,除了響應式調整之外,還可以考慮如何讓應用在這種模式下發揮更大作用,比如支持拖拽等。這種小功能可以提高用戶的工作效率,用戶便更樂意使用您的應用。
△ 多窗口模式效果
多窗口模式
https://developer.android.google.cn/guide/topics/ui/multi-window
除了通過觸摸進行交互外,大屏幕設備還支持其他交互形式。設備的屏幕尺寸越大,用戶就越有可能使用鍵盤、手寫筆、鼠標、游戲手柄或其他外接設備。如果您想提高應用在這些情況下的易用性,可以計劃支持其中一些輸入方式,如需了解更多詳情,請參閱文章《是時候為各式設備適配完善的輸入支持了》。
在如此多樣化的硬件生態系統中,您可能很難擁有各種形狀和尺寸的設備,如今 Android SDK 為可折疊設備提供了模擬器圖像,這些模擬器允許您隨時將折疊狀態更改為鉸鏈的角度。即將推出的 Android Studio Chipmunk 也會配備可調整尺寸的模擬器,允許您自由改變應用窗口的尺寸,每個開發者都可以在幾乎任何類型的設備中試用他們的應用。
△ Android Studio Chipmunk 中的可調整尺寸的模擬器
Android Studio Chipmunk
https://developer.android.google.cn/studio/preview
我們也一直在 Android Studio 中開發新工具,希望為大家開發大屏幕應用提供支持。新的 Layout Validation 工具可以在覆蓋了各種尺寸類別的參考設備上預覽布局,并提示問題區域 (例如文本使用了長行),以及為不同斷點推薦不同界面組件。
△ Android Studio 中的 Layout Validation
最后,我們在 Android 開發者網站上列出了針對大屏幕的應用質量指南,指南中的前面部分介紹的是基本兼容性預期,比如應用是否同時支持橫屏和豎屏模式,后面幾部分重點介紹支持各種屏幕類型和狀態,并使用特定屏幕類型或狀態打造不同的體驗。
大屏幕的應用質量指南
https://developer.android.google.cn/docs/quality-guidelines/large-screens-app-quality
我們希望大家都能夠利用今天分享的內容,并參考新的質量指南,構建出在各種屏幕尺寸下都能讓用戶心動的應用。
、傳統布局
盒狀模型結合 display 屬性、float 浮動以及 position 定位屬性設計的各式傳統布局形式。
2、說再多不如動手實踐,下面舉三個例子
html 部分代碼:
<section> 2 <!-- 傳統布局-例1結構:盒子模型 --> 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 <!-- 傳統布局-例2結構:盒子模型 + 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 <!-- 傳統布局-例3結構 + 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 /* 可復用樣式 */ 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 /* 結構1 基礎樣式 */ 38 .layout-one .content { 39 height: 60px; 40 text-align: center; 41 line-height: 60px; 42 background-color: aqua; 43 } 44 45 /* 結構2 基礎樣式 */ 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 /* 結構3 基礎樣式 */ 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 }
頁面效果:
通過上述的三個例子,我們可以發現:
為了更近時代的進步,所以我們需要更深入的去學習,如:響應式設計中的,流式布局,彈性布局等等!加油吧,騷年!
每天進步一點點,相信積累的力量!
web前端其實很簡單!
如何學習呢?看下下面
領取方法:
關注“IT金勻” 然后私信回復“前端”即可
請記得給金勻先來個“評論+轉發”
我們的開發工程中經常會使用到各種圖,所謂的圖就是由節點和節點之間的連接所形成的系統,數學上專門有一個分支叫圖論(Graph Theroy)。利用圖我們可以做很多工具,比如思維導圖,流程圖,狀態機,組織架構圖,等等。今天我要做的是用開源的HTML5工具來快速構造一個做圖的工具。
工預善其事,必先利其器。第一件事是選擇一件合適的工具,開源時代,程序員還是很幸福的,選擇很多。
最終,我選擇了jsPlumb,因為它完全開源,使用很簡單,用D3的話可能會多花很多功夫。joint.js也不錯。大家可以根據自己的需要選擇。
下面我們一步一步的來使用jsPlumb來創建我們的流程圖工具。
第一步是等待DOM和jsPlumb初始化完畢,類似document.ready()和jquery.ready(), 要使用jsPlumb, 需要把代碼放在這個函數里:
jsPlumb.ready(function()?{ ????//?...?your?code?goes?here?... }
創建一個jsPlumb的實例,并初始化jsPlumb的配置參數:
//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" ?});
這里給給出了一些配置包括,連接線(這里配置了一個貝塞爾曲線),線的風格,連接點得風格。Container需要配置一個對應的DIV容器的id。(這里也可以使用setContainer的方法)
下面我們要創建一個節點(node),每一個節點可以用一個DIV來實現。我這里提供了一個函數來創建節點。
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]; }
這里做的事情就是創建了一個DIV元素,并放在對應的容器的制定位置上,注意為了支持拖拽的功能,必須使用position:absolute 。
我使用D3來操作DOM,大家可能會更習慣JQuery,這純屬個人喜好的問題。
最后返回創建節點的實例引用,這是的selector使用了jsPlumb.getSelector()方法,它和JQuery的selector是一樣的,這樣用的好處是你可以使用不同的DOM操作庫,例如Vanilla
下面我使用一個函數來創建端點/錨點(anchor),錨點就是節點上的連接點,用于連接不同的節點。
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方法創建的Node實例
ports,是一個string的數組,指定端點的個數和名字
type,可能是output或者input,指定端點的種類,一個節點的輸出端口可以連接另一個節點的輸入端口。
這里anchor是一個四維數組,0維和1維分別是錨點在節點x軸和y軸的偏移百分比。我這里希望把端口畫在節點的左右兩側,并按照端口的數量均勻分布。
最后使用instance.addEndpoint來創建端點。注意這里只要指定isSource和isTarget就可以用drag&drop的方式來連接端點,非常方便。
下面一步我們提供一個函數來連接端點:
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是源節點和目標節點的引用,port1和port2是源端口和目標端口的名字。
使用instance.connect方法來創建連接。 overlays用來添加連接線的箭頭效果或者其他風格,我這里沒有使用,因為覺得都不是很好看。大家如果要用,只要把overlays加入到instance.connect的方法參數就可以了。
調用以上方法來創建節點,端點和連接線。
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');
這里我們創建了兩個節點,第一個節點有兩個輸出端口,第二個節點有三個輸入端口,然后把第一個節點的out2端口連接到第二個端點的in端口。效果如下:
最后我們給節點增加drag&drop的功能,這樣我們就可以拖動這些節點來改變圖的布局了。
instance.draggable($('.node'));
這里似乎依賴于JQuery-UI,我還不是很清楚。
我們已經初步具有了創建圖的功能,可是節點的創建必須通過程序,我們希望用交互的方式來創建節點。
通常我們希望有一個tree view的控件,讓后通過拖拽來創建對應類型的節點。這里我使用了這個開源的tree view,基于bootstrap https://github.com/jonmiles/bootstrap-treeview
我們先創建一個tree view:
function?getTreeData()?{ ??var?tree?=?[ ????{ ??????text:?"Nodes", ??????nodes:?[ ????????{ ??????????text:?"Node1", ????????}, ????????{ ??????????text:?"Node2" ????????} ??????] ????} ??];? ??return?tree; } //Initialize?Control?Tree?View $('#control-panel').treeview({data:?getTreeData()});
樹上有兩個節點:
然后我實現從樹上拖拽對應的節點,到流程圖上的邏輯。
//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拖拽端點的邏輯沖突,當檢測到target是jsPlumb對象是需要直接從drop方法中退出以執行對應的jsPlumb的drop邏輯。
好了,一個繪制流程圖的軟件工具初步完工。
我把代碼放在oschina的代碼托管服務上了, 大家有興趣可以去試試。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。