自適應布局可以保證窗口尺寸在一定范圍內變化時,頁面的顯示是正常的。但是將窗口尺寸變化較大時(如窗口寬度從400vp變化為1000vp),僅僅依靠自適應布局可能出現圖片異常放大或頁面內容稀疏、留白過多等問題,此時就需要借助響應式布局能力調整頁面結構。
響應式布局是指頁面內的元素可以根據特定的特征(如窗口寬度、屏幕方向等)自動變化以適應外部容器變化的布局能力。響應式布局中最常使用的特征是窗口寬度,可以將窗口寬度劃分為不同的范圍(下文中稱為斷點)。當窗口寬度從一個斷點變化到另一個斷點時,改變頁面布局(如將頁面內容從單列排布調整為雙列排布甚至三列排布等)以獲得更好的顯示效果。
當前系統提供了如下三種響應式布局能力,后文中我們將依次展開介紹。
斷點
斷點以應用窗口寬度為切入點,將應用窗口在寬度維度上分成了幾個不同的區間即不同的斷點,在不同的區間下,開發者可根據需要實現不同的頁面布局效果。具體的斷點如下所示。
說明:
- 以設備屏幕寬度作為參照物,也可以實現類似的效果。考慮到應用可能以非全屏窗口的形式顯示,以應用窗口寬度為參照物更為通用。
- 開發者可以根據實際使用場景決定適配哪些斷點。如xs斷點對應的一般是智能穿戴類設備,如果確定某頁面不會在智能穿戴設備上顯示,則可以不適配xs斷點。
- 可以根據實際需要在lg斷點后面新增xl、xxl等斷點,但注意新增斷點會同時增加UX設計師及應用開發者的工作量,除非必要否則不建議盲目新增斷點。
系統提供了多種方法,判斷應用當前處于何種斷點,進而可以調整應用的布局。常見的監聽斷點變化的方法如下所示:
- 獲取窗口對象并監聽窗口尺寸變化
- 通過媒體查詢監聽應用窗口尺寸變化
- 借助柵格組件能力監聽不同斷點的變化
本小節中,先介紹如何通過窗口對象監聽斷點變化,后續的媒體查詢及柵格章節中,將進一步展開介紹另外兩種方法。
通過窗口對象監聽斷點變化的核心是獲取窗口對象及注冊窗口尺寸變化的回調函數。
- 在UIAbility的 onWindowStageCreate 生命周期回調中,通過 窗口 對象獲取啟動時的應用窗口寬度并注冊回調函數監聽窗口尺寸變化。將窗口尺寸的長度單位 由px換算為vp 后,即可基于前文中介紹的規則得到當前斷點值,此時可以使用 狀態變量 記錄當前的斷點值方便后續使用。
// MainAbility.ts
import window from '@ohos.window'
import display from '@ohos.display'
import UIAbility from '@ohos.app.ability.UIAbility'
export default class MainAbility extends UIAbility {
private windowObj?: window.Window
private curBp: string = ''
//...
// 根據當前窗口尺寸更新斷點
private updateBreakpoint(windowWidth: number) :void{
// 將長度的單位由px換算為vp
let windowWidthVp = windowWidth / display.getDefaultDisplaySync().densityPixels
let newBp: string = ''
if (windowWidthVp < 320) {
newBp = 'xs'
} else if (windowWidthVp < 600) {
newBp = 'sm'
} else if (windowWidthVp < 840) {
newBp = 'md'
} else {
newBp = 'lg'
}
if (this.curBp !== newBp) {
this.curBp = newBp
// 使用狀態變量記錄當前斷點值
AppStorage.setOrCreate('currentBreakpoint', this.curBp)
}
}
onWindowStageCreate(windowStage: window.WindowStage) :void{
windowStage.getMainWindow().then((windowObj) => {
this.windowObj = windowObj
// 獲取應用啟動時的窗口尺寸
this.updateBreakpoint(windowObj.getWindowProperties().windowRect.width)
// 注冊回調函數,監聽窗口尺寸變化
windowObj.on('windowSizeChange', (windowSize)=>{
this.updateBreakpoint(windowSize.width)
})
});
// ...
}
//...
}
- 在頁面中,獲取及使用當前的斷點。
@Entry
@Component
struct Index {
@StorageProp('currentBreakpoint') curBp: string = 'sm'
build() {
Flex({justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center}) {
Text(this.curBp).fontSize(50).fontWeight(FontWeight.Medium)
}
.width('100%')
.height('100%')
}
}
- 運行及驗證效果。
媒體查詢
在實際應用開發過程中,開發者常常需要針對不同類型設備或同一類型設備的不同狀態來修改應用的樣式。媒體查詢提供了豐富的媒體特征監聽能力,可以監聽應用顯示區域變化、橫豎屏、深淺色、設備類型等等,因此在應用開發過程中使用的非常廣泛。
本小節僅介紹媒體查詢跟斷點的結合,即如何借助媒體查詢能力,監聽斷點的變化,讀者可以自行查閱官網中關于 媒體查詢 的相關介紹了解更詳細的用法。
說明: 類Web開發范式,支持在js文件和css文件中使用媒體查詢,請查看 js媒體查詢 了解詳細用法。
示例:
通過媒體查詢,監聽應用窗口寬度變化,獲取當前應用所處的斷點值。
1.對通過媒體查詢監聽斷點的功能做簡單的封裝,方便后續使用
// common/breakpointsystem.ets
import mediaQuery from '@ohos.mediaquery'
declare interface BreakPointTypeOption<T> {
xs?: T
sm?: T
md?: T
lg?: T
xl?: T
xxl?: T
}
export class BreakPointType<T> {
options: BreakPointTypeOption<T>
constructor(option: BreakPointTypeOption<T>) {
this.options = option
}
getValue(currentBreakPoint: string) {
if (currentBreakPoint === 'xs') {
return this.options.xs
} else if (currentBreakPoint === 'sm') {
return this.options.sm
} else if (currentBreakPoint === 'md') {
return this.options.md
} else if (currentBreakPoint === 'lg') {
return this.options.lg
} else if (currentBreakPoint === 'xl') {
return this.options.xl
} else if (currentBreakPoint === 'xxl') {
return this.options.xxl
} else {
return undefined
}
}
}
interface Breakpoint {
name: string
size: number
mediaQueryListener?: mediaQuery.MediaQueryListener
}
export class BreakpointSystem {
private currentBreakpoint: string = 'md'
private breakpoints: Breakpoint[] = [
{ name: 'xs', size: 0 }, { name: 'sm', size: 320 },
{ name: 'md', size: 600 }, { name: 'lg', size: 840 }
]
private updateCurrentBreakpoint(breakpoint: string) {
if (this.currentBreakpoint !== breakpoint) {
this.currentBreakpoint = breakpoint
AppStorage.Set<string>('currentBreakpoint', this.currentBreakpoint)
console.log('on current breakpoint: ' + this.currentBreakpoint)
}
}
public register() {
this.breakpoints.forEach((breakpoint: Breakpoint, index) => {
let condition:string
if (index === this.breakpoints.length - 1) {
condition = '(' + breakpoint.size + 'vp<=width' + ')'
} else {
condition = '(' + breakpoint.size + 'vp<=width<' + this.breakpoints[index + 1].size + 'vp)'
}
console.log(condition)
breakpoint.mediaQueryListener = mediaQuery.matchMediaSync(condition)
breakpoint.mediaQueryListener.on('change', (mediaQueryResult) => {
if (mediaQueryResult.matches) {
this.updateCurrentBreakpoint(breakpoint.name)
}
})
})
}
public unregister() {
this.breakpoints.forEach((breakpoint: Breakpoint) => {
if(breakpoint.mediaQueryListener){
breakpoint.mediaQueryListener.off('change')
}
})
}
}
2.在頁面中,通過媒體查詢,監聽應用窗口寬度變化,獲取當前應用所處的斷點值
// MediaQuerySample.ets
import { BreakpointSystem, BreakPointType } from 'common/breakpointsystem'
@Entry
@Component
struct MediaQuerySample {
@StorageLink('currentBreakpoint') private currentBreakpoint: string = "md";
@State private icon: Resource = $r('app.media.md')
private breakpointSystem: BreakpointSystem = new BreakpointSystem()
aboutToAppear() {
this.breakpointSystem.register()
}
aboutToDisappear() {
this.breakpointSystem.unregister()
}
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Image(new BreakPointType({sm:$r('app.media.sm'), md:$r('app.media.md'), lg:$r('app.media.lg')}).getValue(this.currentBreakpoint)!)
.height(100)
.width(100)
.objectFit(ImageFit.Contain)
Text(this.currentBreakpoint)
.fontSize(24)
.margin(10)
}
.width('100%')
.height('100%')
}
}
柵格布局
簡介
柵格是多設備場景下通用的輔助定位工具,通過將空間分割為有規律的柵格。柵格可以顯著降低適配不同屏幕尺寸的設計及開發成本,使得整體設計和開發流程更有秩序和節奏感,同時也保證多設備上應用顯示的協調性和一致性,提升用戶體驗。
柵格的樣式由Margin、Gutter、Columns三個屬性決定。
Margin是相對應用窗口、父容器的左右邊緣的距離,決定了內容可展示的整體寬度。
Gutter是相鄰的兩個Column之間的距離,決定內容間的緊密程度。
Columns是柵格中的列數,其數值決定了內容的布局復雜度。
單個Column的寬度是系統結合Margin、Gutter和Columns自動計算的,不需要也不允許開發者手動配置。
柵格布局就是柵格結合了斷點,實現柵格布局能力的組件叫柵格組件。在實際使用場景中,可以根據需要配置不同斷點下柵格組件中元素占據的列數,同時也可以調整Margin、Gutter、Columns的取值,從而實現不同的布局效果。
說明:
- ArkUI在API 9對柵格組件做了重構,推出了新的柵格組件 GridRow ,同時原有的 GridContainer組件 及 柵格設置 已經廢棄。
- 本文中提到的柵格組件,如無特別說明,都是指GridRow和GridCol組件。
柵格組件的斷點
柵格組件提供了豐富的斷點定制能力。
(一)開發者可以修改斷點的取值范圍,支持啟用最多6個斷點。
基于本文斷點小節介紹的推薦值,柵格組件默認提供xs、sm、md、lg四個斷點。
柵格組件支持開發者修改斷點的取值范圍,除了默認的四個斷點,還支持開發者啟用xl和xxl兩個額外的斷點。
說明: 斷點并非越多越好,通常每個斷點都需要開發者“精心適配”以達到最佳顯示效果。
示例1:
修改默認的斷點范圍,同時啟用xl和xxl斷點。
圖片右下角顯示了當前設備屏幕的尺寸(即應用窗口尺寸),可以看到隨著窗口尺寸發生變化,柵格的斷點也相應發生了改變。(為了便于理解,下圖中將設備的DPI設置為160,此時1vp=1px)
@Entry
@Component
struct GridRowSample1 {
@State private currentBreakpoint: string = 'unknown'
build() {
// 修改斷點的取值范圍同時啟用更多斷點,注意,修改的斷點值后面必須加上vp單位。
GridRow({breakpoints: {value: ['600vp', '700vp', '800vp', '900vp', '1000vp'],
reference: BreakpointsReference.WindowSize}}) {
GridCol({span:{xs: 12, sm: 12, md: 12, lg:12, xl: 12, xxl:12}}) {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text(this.currentBreakpoint).fontSize(50).fontWeight(FontWeight.Medium)
}
}
}.onBreakpointChange((currentBreakpoint: string) => {
this.currentBreakpoint = currentBreakpoint
})
}
}
(二)柵格斷點默認以窗口寬度為參照物,同時還允許開發者配置為以柵格組件本身的寬度為參照物。
柵格既可以用于頁面整體布局的場景,也可以用于頁面局部布局的場景。考慮到在實際場景中,存在應用窗口尺寸不變但是局部區域尺寸發生了變化的情況,柵格組件支持以自身寬度為參照物響應斷點變化具有更大的靈活性。
示例2:
以柵格組件寬度為參考物響應斷點變化。滿足窗口尺寸不變,而部分內容區需要做響應式變化的場景。
為了便于理解,下圖中自定義預覽器的設備屏幕寬度設置為650vp。示例代碼中將側邊欄的變化范圍控制在[100vp, 600vp],那么右側的柵格組件寬度相對應在[550vp, 50vp]之間變化。根據代碼中對柵格斷點的配置,柵格組件寬度發生變化時,其斷點相應的發生改變。
@Entry
@Component
struct GridRowSample2 {
@State private currentBreakpoint: string = 'unknown';
build() {
// 用戶可以通過拖拽側邊欄組件中的分隔線,調整側邊欄和內容區的寬度。
SideBarContainer(SideBarContainerType.Embed)
{
// 側邊欄,尺寸變化范圍 [100vp, 600vp]
Column(){}.width('100%').backgroundColor('#19000000')
// 內容區,尺寸變化范圍 [550vp, 50vp]
GridRow({breakpoints: {value: ['100vp', '200vp', '300vp', '400vp', '500vp'],
reference: BreakpointsReference.ComponentSize}}) {
GridCol({span:{xs: 12, sm: 12, md: 12, lg:12, xl: 12, xxl:12}}) {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text(this.currentBreakpoint).fontSize(50).fontWeight(FontWeight.Medium)
}
}
}.onBreakpointChange((currentBreakpoint: string) => {
this.currentBreakpoint = currentBreakpoint;
}).width('100%')
}
// 側邊欄拖拽到最小寬度時,不自動隱藏
.autoHide(false)
.sideBarWidth(100)
// 側邊欄的最小寬度
.minSideBarWidth(100)
// 側邊欄的最大寬度
.maxSideBarWidth(600)
}
}
(三)柵格組件的斷點發生變化時,會通過onBreakPointChange事件通知開發者。
在之前的兩個例子中,已經演示了onBreakpointChange事件的用法,此處不再贅述。
柵格組件的columns、gutter和margin
柵格組件columns默認為12列,gutter默認為0,同時支持開發者根據實際需要定義不同斷點下的columns數量以及gutter長度。特別的,在柵格組件實際使用過程中,常常會發生多個元素占據的列數相加超過總列數而折行的場景。柵格組件還允許開發者分別定義水平方向的gutter(相鄰兩列之間的間距)和垂直方向的gutter(折行時相鄰兩行之間的間距)。
考慮到 組件通用屬性 中已經有margin和padding,柵格組件不再單獨提供額外的margin屬性,直接使用通用屬性即可。借助margin或者padding屬性,均可以控制柵格組件與父容器左右邊緣的距離,但是二者也存在一些差異:
margin區域在柵格組件的邊界外,padding區域在柵格組件的邊界內。
柵格組件的backgroundColor會影響padding區域,但不會影響margin區域。
總的來講,margin在組件外而padding在組件內,開發者可以根據實際需要進行選擇及實現目標效果。
示例3:
不同斷點下,定義不同的columns和gutter。
@Entry
@Component
struct GridRowSample3 {
private bgColors: ResourceColor[] = [
$r('sys.color.ohos_id_color_palette_aux1'),
$r('sys.color.ohos_id_color_palette_aux2'),
$r('sys.color.ohos_id_color_palette_aux3'),
$r('sys.color.ohos_id_color_palette_aux4'),
$r('sys.color.ohos_id_color_palette_aux5'),
$r('sys.color.ohos_id_color_palette_aux6')
]
build() {
// 配置不同斷點下columns和gutter的取值
GridRow({columns: {sm: 4, md: 8, lg: 12},
gutter: {x: {sm: 8, md: 16, lg: 24}, y: {sm: 8, md: 16, lg: 24}}}) {
ForEach(this.bgColors, (bgColor:ResourceColor)=>{
GridCol({span: {sm: 2, md: 2, lg: 2}}) {
Row().backgroundColor(bgColor).height(30).width('100%')
}
})
}
}
}
示例4:
通過通用屬性margin或者padding,均可以控制柵格組件與其父容器左右兩側的距離,但padding區域計算在柵格組件內而margin區域計算在柵格組件外。此外,借助onBreakpointChange事件,還可以改變不同斷點下margin或padding值。
@Entry
@Component
struct GridRowSample4 {
@State private gridMargin: number = 0
build() {
Column() {
Row().width('100%').height(30)
// 使用padding控制柵格左右間距
GridRow() {
GridCol({span:{xs: 12, sm: 12, md: 12, lg:12}}) {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text("padding").fontSize(24).fontWeight(FontWeight.Medium)
}.backgroundColor('#19000000').width('100%')
}
}
.height(50)
.borderWidth(2)
.borderColor('#F1CCB8')
.padding({left: this.gridMargin, right: this.gridMargin})
// 借助斷點變化事件配置不同斷點下柵格組件的左右間距值
.onBreakpointChange((currentBreakpoint: string) => {
if (currentBreakpoint === 'lg' || currentBreakpoint === 'md') {
this.gridMargin = 24
} else {
this.gridMargin = 12
}
})
Row().width('100%').height(30)
// 使用margin控制柵格左右間距
GridRow() {
GridCol({span:{xs: 12, sm: 12, md: 12, lg:12}}) {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text("margin").fontSize(24).fontWeight(FontWeight.Medium)
}.backgroundColor('#19000000').width('100%')
}
}
.height(50)
.borderWidth(2)
.borderColor('#F1CCB8')
.margin({left: this.gridMargin, right: this.gridMargin})
}
}
}
柵格組件的span、offset和order
柵格組件(GridRow)的直接孩子節點只可以是柵格子組件(GridCol),GridCol組件支持配置span、offset和order三個參數。這三個參數的取值按照"xs -> sm -> md -> lg -> xl -> xxl"的向后方向具有繼承性(不支持向前方向的繼承性),例如將sm斷點下span的值配置為3,不配置md斷點下span的值,則md斷點下span的取值也是3。
示例5:
通過span參數配置GridCol在不同斷點下占據不同的列數。特別的,將md斷點下和6的span配置為0,這樣在md斷點下3和6不會渲染和顯示。
class Obj {
public index: number = 1;
public color: Resource = $r('sys.color.ohos_id_color_palette_aux1')
}
@Entry
@Component
struct GridRowSample6 {
private elements: Obj[] = [
{index: 1, color: $r('sys.color.ohos_id_color_palette_aux1')},
{index: 2, color: $r('sys.color.ohos_id_color_palette_aux2')},
{index: 3, color: $r('sys.color.ohos_id_color_palette_aux3')},
{index: 4, color: $r('sys.color.ohos_id_color_palette_aux4')},
{index: 5, color: $r('sys.color.ohos_id_color_palette_aux5')},
{index: 6, color: $r('sys.color.ohos_id_color_palette_aux6')},
]
build() {
GridRow() {
ForEach(this.elements, (item:Obj)=>{
GridCol({span: {sm: 6, md: 4, lg: 3}, offset: {sm: 0, md: 2, lg: 1} }) {
Row() {
Text('' + item.index).fontSize(24)
}
.justifyContent(FlexAlign.Center)
.backgroundColor(item.color).height(30).width('100%')
}
})
}
}
}
示例6:
通過offset參數,配置GridCol相對其前一個兄弟間隔的列數。
class Obj {
public index: number = 1;
public color: Resource = $r('sys.color.ohos_id_color_palette_aux1')
}
@Entry
@Component
struct GridRowSample7 {
private elements: Obj[] = [
{index: 1, color: $r('sys.color.ohos_id_color_palette_aux1')},
{index: 2, color: $r('sys.color.ohos_id_color_palette_aux2')},
{index: 3, color: $r('sys.color.ohos_id_color_palette_aux3')},
{index: 4, color: $r('sys.color.ohos_id_color_palette_aux4')},
{index: 5, color: $r('sys.color.ohos_id_color_palette_aux5')},
{index: 6, color: $r('sys.color.ohos_id_color_palette_aux6')},
]
build() {
GridRow() {
ForEach(this.elements, (item:Obj)=>{
GridCol({span: {sm: 6, md: 4, lg: 3}, order: {lg: (6-item.index)}}) {
Row() {
Text('' + item.index).fontSize(24)
}
.justifyContent(FlexAlign.Center)
.backgroundColor(item.color).height(30).width('100%')
}
})
}
}
}
示例7:
通過order屬性,控制GridCol的順序。在sm和md斷點下,按照1至6的順序排列顯示;在lg斷點下,按照6至1的順序排列顯示。
class Obj {
public index: number = 1;
public color: Resource = $r('sys.color.ohos_id_color_palette_aux1')
}
@Entry
@Component
struct GridRowSample7 {
private elements: Obj[] = [
{index: 1, color: $r('sys.color.ohos_id_color_palette_aux1')},
{index: 2, color: $r('sys.color.ohos_id_color_palette_aux2')},
{index: 3, color: $r('sys.color.ohos_id_color_palette_aux3')},
{index: 4, color: $r('sys.color.ohos_id_color_palette_aux4')},
{index: 5, color: $r('sys.color.ohos_id_color_palette_aux5')},
{index: 6, color: $r('sys.color.ohos_id_color_palette_aux6')},
]
build() {
GridRow() {
ForEach(this.elements, (item:Obj)=>{
GridCol({span: {sm: 6, md: 4, lg: 3}, order: {lg: (6-item.index)}}) {
Row() {
Text('' + item.index).fontSize(24)
}
.justifyContent(FlexAlign.Center)
.backgroundColor(item.color).height(30).width('100%')
}
})
}
}
}
示例8:
僅配置sm和lg斷點下span、offset和order參數的值,則md斷點下這三個參數的取值與sm斷點相同(按照“sm->md->lg”的向后方向繼承)。
class Obj {
public index: number = 1;
public color: Resource = $r('sys.color.ohos_id_color_palette_aux1')
}
@Entry
@Component
struct GridRowSample8 {
private elements: Obj[] = [
{index: 1, color: $r('sys.color.ohos_id_color_palette_aux1')},
{index: 2, color: $r('sys.color.ohos_id_color_palette_aux2')},
{index: 3, color: $r('sys.color.ohos_id_color_palette_aux3')},
{index: 4, color: $r('sys.color.ohos_id_color_palette_aux4')},
{index: 5, color: $r('sys.color.ohos_id_color_palette_aux5')},
{index: 6, color: $r('sys.color.ohos_id_color_palette_aux6')},
]
build() {
GridRow() {
ForEach(this.elements, (item:Obj)=>{
// 不配置md斷點下三個參數的值,則其取值與sm斷點相同
GridCol({span: {sm:4, lg: 3}, offset: {sm: 2, lg: 1},
order: {sm: (6-item.index), lg: item.index}}) {
Row() {
Text('' + item.index).fontSize(24)
}
.justifyContent(FlexAlign.Center)
.backgroundColor(item.color).height(30).width('100%')
}
})
}
}
}
柵格組件的嵌套使用
柵格組件可以嵌套使用以滿足復雜場景的需要。
示例9:
class Obj {
public index: number = 1;
public color: Resource = $r('sys.color.ohos_id_color_palette_aux1')
}
@Entry
@Component
struct GridRowSample9 {
private elements: Obj[] = [
{index: 1, color: $r('sys.color.ohos_id_color_palette_aux1')},
{index: 2, color: $r('sys.color.ohos_id_color_palette_aux2')},
{index: 3, color: $r('sys.color.ohos_id_color_palette_aux3')},
{index: 4, color: $r('sys.color.ohos_id_color_palette_aux4')},
{index: 5, color: $r('sys.color.ohos_id_color_palette_aux5')},
{index: 6, color: $r('sys.color.ohos_id_color_palette_aux6')},
]
build() {
GridRow() {
GridCol({span: {sm: 12, md: 10, lg: 8}, offset: {sm: 0, md: 1, lg: 2}}) {
GridRow() {
ForEach(this.elements, (item:Obj)=>{
GridCol({span: {sm: 6, md: 4, lg: 3}}) {
Row() {
Text('' + item.index).fontSize(24)
}
.justifyContent(FlexAlign.Center)
.backgroundColor(item.color).height(30).width('100%')
}
})
}
.backgroundColor('#19000000')
.height('100%')
}
}
}
}
總結
如前所述,柵格組件提供了豐富的自定義能力,功能異常靈活和強大。只需要明確柵格在不同斷點下的Columns、Margin、Gutter及span等參數,即可確定最終布局,無需關心具體的設備類型及設備狀態(如橫豎屏)等。柵格可以節約設計團隊與開發團隊的溝通成本,提升整體開發效率。