一多特征定义
- 一多定义:一套代码工程,一次开发上架,多端按需部署
- 一多目标:支撑开发者高效开发多终端设备上的应用
- 开发模式:
- HAP包归一:代码工程和项目管理统一,UI自适应,HAP包多设备归一,适用于同泛类设备(手机、平板、折叠屏)内共用的基础应用开发。
- HAP包独立:代码工程和项目管理统一,UI设计独立,HAP包设备独立,适用于不同泛类设备应用(手机,电脑,智慧屏/车机,穿戴设备)以及同泛类设备内追求极致体验应用开发,代码最大化共享,同时各类设备进行最优化设计。
- 适用设备:基于设备屏幕,交互方式(触屏,键鼠,走焦)等差异对设备归类
布局能力
- 自适应布局:
- 元素可以根据相对关系自动变化以适应外部容器的变化,当前有7种自适应布局能力,这些布局可以独立使用,也可以多种布局叠加使用
- 响应式布局(重点 ):
- 元素可以根据特定的特征(如窗口宽度,屏幕方向)触发变化以适应外部容器变化的能。响应式布局能力基于断点、栅格、媒体查询特征(如设备类型、设备宽高)等。
自适应布局(1):拉伸能力
拉伸能力是指容器组件发生变化时,增加或减少的剩余空间全部分配给容器组件内指定区域。如两端组件固定,中间留白,可以通过Blank组件实现。
scss
Row() {
Text('左边')
.backgroundColor(Color.Red)
.height('100%')
.width(100)
.textAlign(TextAlign.Center)
Blank()
Text('右边')
.backgroundColor(Color.Green)
.height('100%')
.width(100)
.textAlign(TextAlign.Center)
}
.width('100%')
.height('100')
自适应布局(2):均分能力
均分能力是指容器组件尺寸发生变化时,增加或减少的空间均匀分配给容器组件内所有空白区域。如一个组件内有多个均匀分布的子组件,可以通过FlexAlign.SpaceEvenly
实现
scss
Flex({justifyContent:FlexAlign.SpaceEvenly}) {
Text('text one')
.backgroundColor(Color.Green)
Text('text one')
.backgroundColor(Color.Red)
Text('text one')
.backgroundColor(Color.Blue)
}
自适应布局(3):占比能力
占比能力是指子组件的宽高按照预设的比例,随父容器的变化而变化.如子组件按照1:1:1的能力占据父容器,可以使用layoutWeight
来设置。
scss
Row() {
Text('text one')
.textAlign(TextAlign.Center)
.backgroundColor(Color.Green)
.height('100%')
.layoutWeight(1)
Text('text one')
.textAlign(TextAlign.Center)
.backgroundColor(Color.Red)
.height('100%')
.layoutWeight(1)
Text('text one')
.textAlign(TextAlign.Center)
.backgroundColor(Color.Blue)
.height('100%')
.layoutWeight(1)
}
.width('100%')
.height(100)
自适应能力(4):缩放能力
缩放能力是指子组件的宽高按照预设的比例,随着容器组件发生变化,且变化过程中子组件的宽高比不变。如设置一个组件固定宽高比为0.5:1,可以使用aspectRatio
来实现
scss
Column() {
Text('text one')
.textAlign(TextAlign.Center)
.backgroundColor(Color.Green)
.width('50%')
// 宽高比 0.5:1
.aspectRatio(0.5)
}
自适应布局(5):延伸能力
延伸能力是指容器组件内的子组件,按照其在列表中的先后顺序,随着容器组件尺寸变化显示或隐藏。它可以根据显示区域的尺寸,显示不同数量的元素。可以使用List
或Scroll
来实现。
scss
private items:string[] = ['1','2','3','4','5','6','7']
@Builder function test5(items:string[]) {
List({space: 30}) {
ForEach(items,(item:string) => {
ListItem() {
Text(item)
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.size({
width: 150,
height: 150
})
}
})
}
.listDirection(Axis.Horizontal)
}
scss
scroller:Scroller = new Scroller()
@Builder function test6(items:string[]) {
Scroll(this.scroller) {
Row({space: 30}) {
ForEach(items,(item:string) => {
Text(item)
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.size({
width: 150,
height: 150
})
})
}
}
.scrollable(ScrollDirection.Horizontal)
}
自适应布局(6):隐藏能力
隐藏能力是指容器内的子组件,按照其预设的显示优先级,随容器组件尺寸变化显示或隐藏,其中相同显示优先级的子组件同时显示或隐藏。可以通过displayPriority
实现,值越高,优先级就越高
scss
@Builder function test7() {
Row({space: 20}) {
Text('1')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.size({
width: 50,
height: 50
})
.displayPriority(1)
Text('2')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.size({
width: 50,
height: 50
})
.displayPriority(2)
Text('3')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.size({
width: 50,
height: 50
})
.displayPriority(3)
Text('2')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.size({
width: 50,
height: 50
})
.displayPriority(2)
Text('1')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.size({
width: 50,
height: 50
})
.displayPriority(1)
}
.width('100%')
.height('100')
.justifyContent(FlexAlign.Center)
}
自适应布局(7):折行能力
折行能力是指容器组件尺寸发生变化,当布局方向尺寸不足以显示完整内容时动换行。常用于横竖屏适配或默认设备向平板切换的场景。可以使用FlexWrap.Wrap
实现
scss
@Builder function test8() {
//FlexWrap.Wrap:自动换行
Flex({wrap:FlexWrap.Wrap,}) {
ForEach(this.items,(item) => {
Text(item)
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.size({
width: 150,
height: 150
})
.margin(20)
})
}
.width('100%')
.height(150)
}
响应式布局(1):断点和媒体查询
断点:将窗口宽度划分为不同的范围(即断点),监听窗口尺寸变化,当断点改变时同步调整页面布局。
断点名称 | 取值范围 (vp) |
---|---|
xs | [0,320) |
sm | [320,600) |
md | [600,840) |
lg | [840,∞) |
kotlin
import mediaquery from '@ohos.mediaquery'
// 断点监听类
class BreakpointModel {
private currentBreakpoint:string = 'sm'
private xsListener:mediaquery.MediaQueryListener;
private smListener:mediaquery.MediaQueryListener;
private mdListener:mediaquery.MediaQueryListener;
private lgListener:mediaquery.MediaQueryListener;
private isBreakpointXS = (result:mediaquery.MediaQueryResult):void => {
if (result.matches) {
this.updateCurrentBreakpoint('xs')
}
}
private isBreakpointSM = (result:mediaquery.MediaQueryResult):void => {
if (result.matches) {
this.updateCurrentBreakpoint('sm')
}
}
private isBreakpointMD = (result:mediaquery.MediaQueryResult):void => {
if (result.matches) {
this.updateCurrentBreakpoint('md')
}
}
private isBreakpointLG = (result:mediaquery.MediaQueryResult):void => {
if (result.matches) {
this.updateCurrentBreakpoint('lg')
}
}
private updateCurrentBreakpoint(bp:string) {
if (this.currentBreakpoint !== bp) {
this.currentBreakpoint = bp
/***
* 应用级变量的状态管理
* 对监听属性currentBreakpoint进行存储
*/
AppStorage.Set<string>('currentBreakpoint',this.currentBreakpoint)
}
}
// 注册监听
register() {
// 监听屏幕水平宽度
this.xsListener = mediaquery.matchMediaSync('(width<320vp)')
// 通过回调改变currentBreakpoint的值
this.xsListener.on('change',this.isBreakpointXS)
this.smListener = mediaquery.matchMediaSync('(320vp<=width<600vp)')
this.smListener.on('change',this.isBreakpointSM)
this.mdListener = mediaquery.matchMediaSync('(600vp<=width<840vp)')
this.mdListener.on('change',this.isBreakpointMD)
this.lgListener = mediaquery.matchMediaSync('(840vp<=width)')
this.lgListener.on('change',this.isBreakpointLG)
}
// 关闭监听
unRegister() {
this.xsListener.off('change',this.isBreakpointXS)
this.smListener.off('change',this.isBreakpointSM)
this.mdListener.off('change',this.isBreakpointMD)
this.lgListener.off('change',this.isBreakpointLG)
}
}
@Entry
@Component
struct Breakpoint {
private bpModel:BreakpointModel = new BreakpointModel()
// 订阅监听属性currentBreakpoint
@StorageProp('currentBreakpoint') currentBreakpoint:string = 'sm'
aboutToAppear() {
this.bpModel.register()
}
aboutToDisappear() {
this.bpModel.unRegister()
}
build() {
Flex({justifyContent:FlexAlign.Center,alignItems:ItemAlign.Center}) {
Text(this.currentBreakpoint)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.size({width:'100%',height:'100%'})
}
}
媒体查询:媒体查询提供了丰富的媒体特征监听能力,可以监听应用显示区域变化、横竖屏、深浅色、设备类型等等。
less
import mediaquery from '@ohos.mediaquery';
import window from '@ohos.window';
import common from '@ohos.app.ability.common';
let portraitFunc = null;
@Entry
@Component
struct MediaQueryExample {
// 当前页面的索引
@State currentIndex: number = 0
// tabs控制器
private tabsController: TabsController = new TabsController()
// 是否竖屏
@State isSp:boolean = true
@State color: Color = Color.Green;
@State text: string = '竖屏';
// 当设备横屏时条件成立
listener = mediaquery.matchMediaSync('(orientation: landscape)');
// 当满足媒体查询条件时,触发回调
onPortrait(mediaQueryResult:mediaquery.MediaQueryResult) {
// 若设备为横屏状态,更改相应的页面布局
if (mediaQueryResult.matches) {
this.color = Color.Red;
this.text = '横屏';
this.isSp = false;
} else {
this.color = Color.Green;
this.text = '竖屏';
this.isSp = true;
}
}
aboutToAppear() {
// 绑定当前应用实例
portraitFunc = this.onPortrait.bind(this);
// 绑定回调函数
this.listener.on('change', portraitFunc);
}
// 改变设备横竖屏状态函数
private changeOrientation(isLandscape: boolean) {
// 获取UIAbility实例的上下文信息
let context = getContext(this) as common.UIAbilityContext;
// 调用该接口手动改变设备横竖屏状态
window.getLastWindow(context).then((lastWindow) => {
lastWindow.setPreferredOrientation(isLandscape ? window.Orientation.LANDSCAPE : window.Orientation.PORTRAIT)
});
}
build() {
// 根据横竖屏状态显示对应的tabs
Tabs({barPosition:this.isSp ? BarPosition.End : BarPosition.Start,index:this.currentIndex}) {
TabContent() {
Button('切换横竖屏')
.onClick(() => {
this.isSp = !this.isSp
this.changeOrientation(this.isSp)
})
}
.tabBar(this.tabBuilder('首页',0,$r('app.media.tabbar_shouye_nol'),$r('app.media.tabbar_shouye_sel')))
TabContent() {
}
.tabBar(this.tabBuilder('商城',1,$r('app.media.tabbar_shangcheng_nor'),$r('app.media.tabbar_shangcheng_sel')))
TabContent() {
}
.tabBar(this.tabBuilder('信用卡',2,$r('app.media.tabbar_xinyongka_nor'),$r('app.media.tabbar_xinyongka_sel')))
TabContent() {
}
.tabBar(this.tabBuilder('消息',3,$r('app.media.tabbar_xiaoxi_nor'),$r('app.media.tabbar_xiaoxi_sel')))
TabContent() {
}
.tabBar(this.tabBuilder('我的',4,$r('app.media.tabbar_wode_nor'),$r('app.media.tabbar_wode_sel')))
}
// 是否垂直时调用
.vertical(this.isSp ? false : true)
}
@Builder tabBuilder(title: string,
targetIndex: number,
normalImg: Resource,
selectedImg: Resource) {
Column() {
Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
.size({ width: 25, height: 25 })
Text(title)
.fontSize(12)
.fontColor(this.currentIndex === targetIndex ? Color.Red : 0x333333)
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentIndex = targetIndex
this.tabsController.changeIndex(this.currentIndex)
})
}
}
响应式布局(2):栅格布局
根据设备的水平宽度,将不同的屏幕尺寸划分为不同数量的栅格,来实现屏幕的自适应。且栅格和栅格之间可以设置一个间距。GridRow
为栅格容器组件,需与栅格子组件GridCol
在栅格布局场景中联合使用。
设置栅格的列数columns
scss
@Entry
@Component
struct GridRowCompone {
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
@State currentBreakpoint:string = 'xs'
@State columns:number = 2
// 根据断点返回栅格的数量
getGridRowColumns():number {
var columns:number = 8
if (this.currentBreakpoint === 'xs') {
columns = 2
} else if (this.currentBreakpoint === 'sm') {
columns = 4
} else if (this.currentBreakpoint === 'md') {
columns = 6
} else if (this.currentBreakpoint === 'lg') {
columns = 8
}
this.columns = columns
return columns
}
build() {
this.test1()
}
@Builder test1() {
Row() {
GridRow({
// 设置栅格的列数
columns: this.getGridRowColumns()
}) {
ForEach(this.bgColors, (item, index) => {
GridCol() {
Row() {
Text(`${index + 1}`)
.textAlign(TextAlign.Center)
.width('100%')
}.width('100%').height('50')
}.backgroundColor(item)
})
}
.width('100%').height('100%')
// 回调当前断点尺寸
.onBreakpointChange((breakpoint) => {
this.currentBreakpoint = breakpoint
console.info('xzq'+breakpoint)
})
}
.height('100%')
.width('100%')
}
}
**子组件排列方向 **
默认从左到右GridRowDirection.Row
,从右到左GridRowDirection.RowReverse
css
GridRow({ direction: GridRowDirection.Row }){}
子组件间距
GridRow中通过gutter属性设置子元素在水平和垂直方向的间距。
当gutter类型为number时,同时设置栅格子组件间水平和垂直方向边距且相等。下例中,设置子组件水平与垂直方向距离相邻元素的间距为10。
scss
GridRow({ gutter: 10 }){}
子组件GridCol
GridCol组件作为GridRow组件的子组件,通过给GridCol传参或者设置属性两种方式,设置span(占用列数),offset(偏移列数),order(元素序号)的值。
-
设置span的4种方式
lessGridCol({ span: 2 }){} GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }){} GridCol(){}.span(2) GridCol(){}.span({ xs: 1, sm: 2, md: 3, lg: 4 })
-
设置offset的四种方式
lessGridCol({ offset: 2 }){} GridCol({ offset: { xs: 2, sm: 2, md: 2, lg: 2 } }){} GridCol(){}.offset(2) GridCol(){}.offset({ xs: 1, sm: 2, md: 3, lg: 4 })
-
设置order的四种方式
lessGridCol({ order: 2 }){} GridCol({ order: { xs: 1, sm: 2, md: 3, lg: 4 } }){} GridCol(){}.order(2) GridCol(){}.order({ xs: 1, sm: 2, md: 3, lg: 4 })
-
设置一个栅格数为8,子组件占2个栅格的demo
scss
build() {
this.test2()
}
@Builder test2() {
GridRow({ columns: 8 }) {
ForEach(this.bgColors, (color, index) => {
GridCol({ span: 2 }) {
Row() {
Text(`${index}`)
}.width('100%').height('50vp')
}
.backgroundColor(color)
})
}
}
- 设置一个不同断点下,子组件占用不同数量的栅格demo
scss
build() {
this.test3()
}
@Builder test3() {
GridRow({ columns: 12 }) {
ForEach(this.bgColors, (color, index) => {
GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }) {
Row() {
Text(`${index}`)
}.width('100%').height('50vp')
}
.backgroundColor(color)
})
}
}
offset
栅格子组件相对于前一个子组件的偏移列数,默认为0。
- 当类型为number时,子组件偏移相同列数
scss
build() {
this.test4()
}
@Builder test4() {
GridRow() {
ForEach(this.bgColors, (color, index) => {
GridCol({ offset: 2 }) {
Row() {
Text('' + index)
}.width('100%').height('50vp')
}
.backgroundColor(color)
})
}
}
默认12列栅格,由于偏移列数为2,所以每个空白处占2格,子组件共4格
- 设置一个不同断点下,有不同偏移量的demo
scss
build() {
this.test5()
}
@Builder test5() {
GridRow() {
ForEach(this.bgColors, (color, index) => {
GridCol({ offset: { xs: 1, sm: 2, md: 3, lg: 4 } }) {
Row() {
Text('' + index)
}.width('100%').height('50vp')
}
.backgroundColor(color)
})
}
}
这个排列方式看懂了吗?由于当前屏幕属于lg的断点,所以每个空白处的偏移量为4,当第一行栅格占满后,会进行折行,然后继续进行偏移。
order
栅格子组件的序号,决定子组件排列次序。当子组件不设置order或者设置相同的order, 子组件按照代码顺序展示。当子组件设置不同的order时,order较小的组件在前,较大的在后。
当子组件部分设置order,部分不设置order时,未设置order的子组件依次排序靠前,设置了order的子组件按照数值从小到大排列。
scss
build() {
this.test6()
}
@Builder test6() {
GridRow() {
GridCol({ order: 4 }) {
Row() {
Text('1')
}.width('100%').height('50vp')
}.backgroundColor(Color.Red)
GridCol({ order: 3 }) {
Row() {
Text('2')
}.width('100%').height('50vp')
}.backgroundColor(Color.Orange)
GridCol({ order: 2 }) {
Row() {
Text('3')
}.width('100%').height('50vp')
}.backgroundColor(Color.Yellow)
GridCol({ order: 1 }) {
Row() {
Text('4')
}.width('100%').height('50vp')
}.backgroundColor(Color.Green)
}
}
- 设置一个不同断点下,子组件不同的排列顺序的demo
scss
build() {
this.test7()
}
@Builder test7() {
GridRow() {
GridCol({ order: { xs:1, sm:5, md:3, lg:7}}) {
Row() {
Text('1')
}.width('100%').height('50vp')
}.backgroundColor(Color.Red)
GridCol({ order: { xs:2, sm:2, md:6, lg:1} }) {
Row() {
Text('2')
}.width('100%').height('50vp')
}.backgroundColor(Color.Orange)
GridCol({ order: { xs:3, sm:3, md:1, lg:6} }) {
Row() {
Text('3')
}.width('100%').height('50vp')
}.backgroundColor(Color.Yellow)
GridCol({ order: { xs:4, sm:4, md:2, lg:5} }) {
Row() {
Text('4')
}.width('100%').height('50vp')
}.backgroundColor(Color.Green)
}
}
通过以上的功能特性就可以完成一套代码在多端设备上部署的能力了。