本案例介绍像素单位的基本知识与像素单位转换API的使用。通过像素转换案例,向开发者讲解了如何使用像素单位设置组件的尺寸、字体的大小以及不同像素单位之间的转换方法。主要功能包括:
- 展示了不同像素单位的使用。
- 展示了像素单位转换相关API的使用。
一、学习像素单位
ArkUI为开发者提供4种像素单位,框架采用vp为基准数据单位。
名称 | 描述 |
px | 屏幕物理像素单位。 |
vp | 屏幕密度相关像素,根据屏幕像素密度转换为屏幕物理像素,当数值不带单位时,默认单位vp。 说明: vp与px的比例与屏幕像素密度有关。 |
fp | 字体像素,与vp类似适用屏幕密度变化,随系统字体大小设置变化。 |
lpx | 视窗逻辑像素单位,lpx单位为实际屏幕宽度与逻辑宽度(通过 designWidth 配置)的比值,designWidth默认值为720。当 designWidth 为 720 时,在实际宽度为 1440 物理像素的屏幕上,1lpx为2px大小。 |
像素单位转换
提供其他单位与px单位互相转换的方法。
接口 | 描述 |
vp2px(value : number) : number | 将vp单位的数值转换为以px为单位的数值。 说明: 默认使用当前UI实例所在屏幕的虚拟像素比进行转换,UI实例未创建时,使用默认屏幕的虚拟像素比进行转换。 |
px2vp(value : number) : number | 将px单位的数值转换为以vp为单位的数值。 说明: 默认使用当前UI实例所在屏幕的虚拟像素比进行转换,UI实例未创建时,使用默认屏幕的虚拟像素比进行转换。 |
fp2px(value : number) : number | 将fp单位的数值转换为以px为单位的数值。 |
px2fp(value : number) : number | 将px单位的数值转换为以fp为单位的数值。 |
lpx2px(value : number) : number | 将lpx单位的数值转换为以px为单位的数值。 |
px2lpx(value : number) : number | 将px单位的数值转换为以lpx为单位的数值。 |
示例
说明
直接使用vp2px/px2vp/fp2px/px2fp/lpx2px/px2lpx可能存在UI上下文不明确的问题,建议使用getUIContext获取UIContext实例,再使用UIContext下的vp2px/px2vp/fp2px/px2fp/lpx2px/px2lpx调用绑定实例的接口。
PixelConversionBasic
// ets/pages/Index.ets@Entry
@Component
struct Example {build() {Column() {Column() {Column() {Text("width(220)").width(220).height(40).backgroundColor(0x6435c9).textAlign(TextAlign.Center).fontColor(Color.White).fontSize('12vp')}.margin(5)Column() {Text("width('220px')").width('220px').height(40).backgroundColor(0x6435c9).textAlign(TextAlign.Center).fontColor(Color.White)}.margin(5)Column() {Text("width('220vp')").width('220vp').height(40).backgroundColor(0x6435c9).textAlign(TextAlign.Center).fontColor(Color.White).fontSize('12vp')}.margin(5)Column() {Text("width('220lpx') designWidth:720").width('220lpx').height(40).backgroundColor(0x6435c9).textAlign(TextAlign.Center).fontColor(Color.White).fontSize('12vp')}.margin(5)Column() {Text("width(vp2px(220) + 'px')")// 建议使用this.getUIContext().vp2px().width(vp2px(220) + 'px').height(40).backgroundColor(0x6435c9).textAlign(TextAlign.Center).fontColor(Color.White).fontSize('12vp')}.margin(5)Column() {Text("fontSize('12fp')").width(220).height(40).backgroundColor(0x6435c9).textAlign(TextAlign.Center).fontColor(Color.White).fontSize('12fp')}.margin(5)Column() {Text("width(px2vp(220))")// 建议使用this.getUIContext().px2vp().width(px2vp(220)).height(40).backgroundColor(0x6435c9).textAlign(TextAlign.Center).fontColor(Color.White).fontSize('12fp')}.margin(5)}.width('100%')}}
}
二、学习组件导航 (Navigation)
Navigation是路由导航的根视图容器,一般作为页面(@Entry)的根容器,包括单栏(Stack)、分栏(Split)和自适应(Auto)三种显示模式。Navigation组件适用于模块内和跨模块的路由切换,通过组件级路由能力实现更加自然流畅的转场体验,并提供多种标题栏样式来呈现更好的标题和内容联动效果。一次开发、多端部署场景下,Navigation组件能够自动适配窗口显示大小,在窗口较大的场景下自动切换分栏展示效果。
Navigation组件主要包含导航页(NavBar)和子页(NavDestination)。导航页由标题栏(Titlebar,包含菜单栏menu)、内容区(Navigation子组件)和工具栏(Toolbar)组成,其中导航页可以通过hideNavBar属性进行隐藏,导航页不存在页面栈中,导航页和子页,以及子页之间可以通过路由操作进行切换。
在API Version 9上,Navigation需要配合NavRouter组件实现页面路由。从API Version 10开始,更推荐使用NavPathStack实现页面路由。
设置页面显示模式
Navigation组件通过mode属性设置页面的显示模式。
- 自适应模式
Navigation组件默认为自适应模式,此时mode属性为NavigationMode.Auto。自适应模式下,当页面宽度大于等于一定阈值( API version 9及以前:520vp,API version 10及以后:600vp )时,Navigation组件采用分栏模式,反之采用单栏模式。
Navigation() {// ...
}
.mode(NavigationMode.Auto)
- 单页面模式
图1 单页面布局示意图
将mode属性设置为NavigationMode.Stack,Navigation组件即可设置为单页面显示模式。
Navigation() {// ...
}
.mode(NavigationMode.Stack)
- 分栏模式
图2 分栏布局示意图
将mode属性设置为NavigationMode.Split,Navigation组件即可设置为分栏显示模式。
@Entry
@Component
struct NavigationExample {@State TooTmp: ToolbarItem = {'value': "func", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {}}@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack()private arr: number[] = [1, 2, 3];@BuilderPageMap(name: string) {if (name === "NavDestinationTitle1") {pageOneTmp()} else if (name === "NavDestinationTitle2") {pageTwoTmp()} else if (name === "NavDestinationTitle3") {pageThreeTmp()}}build() {Column() {Navigation(this.pageInfos) {TextInput({ placeholder: 'search...' }).width("90%").height(40).backgroundColor('#FFFFFF')List({ space: 12 }) {ForEach(this.arr, (item:number) => {ListItem() {Text("NavRouter" + item).width("100%").height(72).backgroundColor('#FFFFFF').borderRadius(24).fontSize(16).fontWeight(500).textAlign(TextAlign.Center).onClick(()=>{this.pageInfos.pushPath({ name: "NavDestinationTitle" + item})})}}, (item:number) => item.toString())}.width("90%").margin({ top: 12 })}.title("主标题").mode(NavigationMode.Split).navDestination(this.PageMap).menus([{value: "", icon: "./image/ic_public_search.svg", action: ()=> {}},{value: "", icon: "./image/ic_public_add.svg", action: ()=> {}},{value: "", icon: "./image/ic_public_add.svg", action: ()=> {}},{value: "", icon: "./image/ic_public_add.svg", action: ()=> {}},{value: "", icon: "./image/ic_public_add.svg", action: ()=> {}}]).toolbarConfiguration([this.TooTmp, this.TooTmp, this.TooTmp])}.height('100%').width('100%').backgroundColor('#F1F3F5')}
}// PageOne.ets
@Component
export struct pageOneTmp {@Consume('pageInfos') pageInfos: NavPathStack;build() {NavDestination() {Column() {Text("NavDestinationContent1")}.width('100%').height('100%')}.title("NavDestinationTitle1").onBackPressed(() => {const popDestinationInfo = this.pageInfos.pop() // 弹出路由栈栈顶元素console.log('pop' + '返回值' + JSON.stringify(popDestinationInfo))return true})}
}// PageTwo.ets
@Component
export struct pageTwoTmp {@Consume('pageInfos') pageInfos: NavPathStack;build() {NavDestination() {Column() {Text("NavDestinationContent2")}.width('100%').height('100%')}.title("NavDestinationTitle2").onBackPressed(() => {const popDestinationInfo = this.pageInfos.pop() // 弹出路由栈栈顶元素console.log('pop' + '返回值' + JSON.stringify(popDestinationInfo))return true})}
}// PageThree.ets
@Component
export struct pageThreeTmp {@Consume('pageInfos') pageInfos: NavPathStack;build() {NavDestination() {Column() {Text("NavDestinationContent3")}.width('100%').height('100%')}.title("NavDestinationTitle3").onBackPressed(() => {const popDestinationInfo = this.pageInfos.pop() // 弹出路由栈栈顶元素console.log('pop' + '返回值' + JSON.stringify(popDestinationInfo))return true})}
}
设置标题栏模式
标题栏在界面顶部,用于呈现界面名称和操作入口,Navigation组件通过titleMode属性设置标题栏模式。
- Mini模式
普通型标题栏,用于一级页面不需要突出标题的场景。
图3 Mini模式标题栏
Navigation() {// ...
}
.titleMode(NavigationTitleMode.Mini)
- Full模式
强调型标题栏,用于一级页面需要突出标题的场景。
图4 Full模式标题栏
Navigation() {// ...
}
.titleMode(NavigationTitleMode.Full)
设置菜单栏
菜单栏位于Navigation组件的右上角,开发者可以通过menus属性进行设置。menus支持Array<NavigationMenuItem>和CustomBuilder两种参数类型。使用Array<NavigationMenuItem>类型时,竖屏最多支持显示3个图标,横屏最多支持显示5个图标,多余的图标会被放入自动生成的更多图标。
图5 设置了3个图标的菜单栏
let TooTmp: NavigationMenuItem = {'value': "", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {}}
Navigation() {// ...
}
.menus([TooTmp,TooTmp,TooTmp])
图片也可以引用resources中的资源。
let TooTmp: NavigationMenuItem = {'value': "", 'icon': "resources/base/media/ic_public_highlights.svg", 'action': ()=> {}}
Navigation() {// ...
}
.menus([TooTmp,TooTmp,TooTmp])
图6 设置了4个图标的菜单栏
let TooTmp: NavigationMenuItem = {'value': "", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {}}
Navigation() {// ...
}
.menus([TooTmp,TooTmp,TooTmp,TooTmp])
设置工具栏
工具栏位于Navigation组件的底部,开发者可以通过toolbarConfiguration属性进行设置。
图7 工具栏
let TooTmp: ToolbarItem = {'value': "func", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {}}
let TooBar: ToolbarItem[] = [TooTmp,TooTmp,TooTmp]
Navigation() {// ...
}
.toolbarConfiguration(TooBar)
路由操作
Navigation路由相关的操作都是基于页面栈NavPathStack提供的方法进行,每个Navigation都需要创建并传入一个NavPathStack对象,用于管理页面。主要涉及页面跳转、页面返回、页面替换、页面删除、参数获取、路由拦截等功能。
从API version 12开始,页面栈允许被继承。开发者可以在派生类中自定义属性和方法,也可以重写父类的方法。派生类对象可以替代基类NavPathStack对象使用。
@Entry
@Component
struct Index {// 创建一个页面栈对象并传入NavigationpageStack: NavPathStack = new NavPathStack()build() {Navigation(this.pageStack) {}.title('Main')}
}
页面跳转
NavPathStack通过Push相关的接口去实现页面跳转的功能,主要分为以下三类:
- 普通跳转,通过页面的name去跳转,并可以携带param。
this.pageStack.pushPath({ name: "PageOne", param: "PageOne Param" })
this.pageStack.pushPathByName("PageOne", "PageOne Param")
- 带返回回调的跳转,跳转时添加onPop回调,能在页面出栈时获取返回信息,并进行处理。
this.pageStack.pushPathByName('PageOne', "PageOne Param", (popInfo) => {console.log('Pop page name is: ' + popInfo.info.name + ', result: ' + JSON.stringify(popInfo.result))
});
- 带错误码的跳转,跳转结束会触发异步回调,返回错误码信息。
this.pageStack.pushDestinationByName('PageOne', "PageOne Param").catch((error: BusinessError) => {console.error(`Push destination failed, error code = ${error.code}, error.message = ${error.message}.`);}).then(() => {console.info('Push destination succeed.');});
页面返回
NavPathStack通过Pop相关接口去实现页面返回功能。
// 返回到上一页
this.pageStack.pop()
// 返回到上一个PageOne页面
this.pageStack.popToName("PageOne")
// 返回到索引为1的页面
this.pageStack.popToIndex(1)
// 返回到根首页(清除栈中所有页面)
this.pageStack.clear()
页面替换
NavPathStack通过Replace相关接口去实现页面替换功能。
// 将栈顶页面替换为PageOne
this.pageStack.replacePath({ name: "PageOne", param: "PageOne Param" })
this.pageStack.replacePathByName("PageOne", "PageOne Param")
页面删除
NavPathStack通过Remove相关接口去实现删除页面栈中特定页面的功能。
// 删除栈中name为PageOne的所有页面
this.pageStack.removeByName("PageOne")
// 删除指定索引的页面
this.pageStack.removeByIndexes([1,3,5])
参数获取
NavPathStack通过Get相关接口去获取页面的一些参数。
// 获取栈中所有页面name集合
this.pageStack.getAllPathName()
// 获取索引为1的页面参数
this.pageStack.getParamByIndex(1)
// 获取PageOne页面的参数
this.pageStack.getParamByName("PageOne")
// 获取PageOne页面的索引集合
this.pageStack.getIndexByName("PageOne")
路由拦截
NavPathStack提供了setInterception方法,用于设置Navigation页面跳转拦截回调。该方法需要传入一个NavigationInterception对象,该对象包含三个回调函数:
名称 | 描述 |
willShow | 页面跳转前回调,允许操作栈,在当前跳转生效。 |
didShow | 页面跳转后回调,在该回调中操作栈会在下一次跳转生效。 |
modeChange | Navigation单双栏显示状态发生变更时触发该回调。 |
说明
无论是哪个回调,在进入回调时页面栈都已经发生了变化。
开发者可以在willShow回调中通过修改路由栈来实现路由拦截重定向的能力。
this.pageStack.setInterception({willShow: (from: NavDestinationContext | "navBar", to: NavDestinationContext | "navBar",operation: NavigationOperation, animated: boolean) => {if (typeof to === "string") {console.log("target page is navigation home page.");return;}// 将跳转到PageTwo的路由重定向到PageOnelet target: NavDestinationContext = to as NavDestinationContext;if (target.pathInfo.name === 'PageTwo') {target.pathStack.pop();target.pathStack.pushPathByName('PageOne', null);}}
})
子页面
NavDestination是Navigation子页面的根容器,用于承载子页面的一些特殊属性以及生命周期等。NavDestination可以设置独立的标题栏和菜单栏等属性,使用方法与Navigation相同。NavDestination也可以通过mode属性设置不同的显示类型,用于满足不同页面的诉求。
页面显示类型
- 标准类型
NavDestination组件默认为标准类型,此时mode属性为NavDestinationMode.STANDARD。标准类型的NavDestination的生命周期跟随其在NavPathStack页面栈中的位置变化而改变。
- 弹窗类型
NavDestination设置mode为NavDestinationMode.DIALOG弹窗类型,此时整个NavDestination默认透明显示。弹窗类型的NavDestination显示和消失时不会影响下层标准类型的NavDestination的显示和生命周期,两者可以同时显示。
// Dialog NavDestination
@Entry
@Component
struct Index {@Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack()@BuilderPagesMap(name: string) {if (name == 'DialogPage') {DialogPage()}}build() {Navigation(this.pageStack) {Button('Push DialogPage').margin(20).width('80%').onClick(() => {this.pageStack.pushPathByName('DialogPage', '');})}.mode(NavigationMode.Stack).title('Main').navDestination(this.PagesMap)}
}@Component
export struct DialogPage {
@Consume('NavPathStack') pageStack: NavPathStack;build() {NavDestination() {Stack({ alignContent: Alignment.Center }) {Column() {Text("Dialog NavDestination").fontSize(20).margin({ bottom: 100 })Button("Close").onClick(() => {this.pageStack.pop()}).width('30%')}.justifyContent(FlexAlign.Center).backgroundColor(Color.White).borderRadius(10).height('30%').width('80%')}.height("100%").width('100%')}.backgroundColor('rgba(0,0,0,0.5)').hideTitleBar(true).mode(NavDestinationMode.DIALOG)
}
}
页面生命周期
Navigation作为路由容器,其生命周期承载在NavDestination组件上,以组件事件的形式开放。
其生命周期大致可分为三类,自定义组件生命周期、通用组件生命周期和自有生命周期。其中,aboutToAppear和aboutToDisappear是自定义组件的生命周期(NavDestination外层包含的自定义组件),OnAppear和OnDisappear是组件的通用生命周期。剩下的六个生命周期为NavDestination独有。
生命周期时序如下图所示:
- aboutToAppear:在创建自定义组件后,执行其build()函数之前执行(NavDestination创建之前),允许在该方法中改变状态变量,更改将在后续执行build()函数中生效。
- onWillAppear:NavDestination创建后,挂载到组件树之前执行,在该方法中更改状态变量会在当前帧显示生效。
- onAppear:通用生命周期事件,NavDestination组件挂载到组件树时执行。
- onWillShow:NavDestination组件布局显示之前执行,此时页面不可见(应用切换到前台不会触发)。
- onShown:NavDestination组件布局显示之后执行,此时页面已完成布局。
- onWillHide:NavDestination组件触发隐藏之前执行(应用切换到后台不会触发)。
- onHidden:NavDestination组件触发隐藏后执行(非栈顶页面push进栈,栈顶页面pop出栈或应用切换到后台)。
- onWillDisappear:NavDestination组件即将销毁之前执行,如果有转场动画,会在动画前触发(栈顶页面pop出栈)。
- onDisappear:通用生命周期事件,NavDestination组件从组件树上卸载销毁时执行。
- aboutToDisappear:自定义组件析构销毁之前执行,不允许在该方法中改变状态变量。
页面监听和查询
为了方便组件跟页面解耦,在NavDestination子页面内部的自定义组件可以通过全局方法监听或查询到页面的一些状态信息。
- 页面信息查询
自定义组件提供queryNavDestinationInfo方法,可以在NavDestination内部查询到当前所属页面的信息,返回值为NavDestinationInfo,若查询不到则返回undefined。
import { uiObserver } from '@kit.ArkUI';// NavDestination内的自定义组件
@Component
struct MyComponent {navDesInfo: uiObserver.NavDestinationInfo | undefinedaboutToAppear(): void {this.navDesInfo = this.queryNavDestinationInfo();}build() {Column() {Text("所属页面Name: " + this.navDesInfo?.name)}.width('100%').height('100%')}
}
- 页面状态监听
通过observer.on('navDestinationUpdate')提供的注册接口可以注册NavDestination生命周期变化的监听,使用方式如下:
uiObserver.on('navDestinationUpdate', (info) => {console.info('NavDestination state update', JSON.stringify(info));
});
也可以注册页面切换的状态回调,能在页面发生路由切换的时候拿到对应的页面信息NavDestinationSwitchInfo,并且提供了UIAbilityContext和UIContext不同范围的监听:
// 在UIAbility中使用
import { UIContext, uiObserver } from '@kit.ArkUI';// callBackFunc 是开发者定义的监听回调函数
function callBackFunc(info: uiObserver.NavDestinationSwitchInfo) {}
uiObserver.on('navDestinationSwitch', this.context, callBackFunc);// 可以通过窗口的getUIContext()方法获取对应的UIContent
uiContext: UIContext | null = null;
uiObserver.on('navDestinationSwitch', this.uiContext, callBackFunc);
页面转场
Navigation默认提供了页面切换的转场动画,通过页面栈操作时,会触发不同的转场效果(Dialog类型的页面默认无转场动画),Navigation也提供了关闭系统转场、自定义转场以及共享元素转场的能力。
三、案例页面展示
四、案例目录文件结构
五、项目资源初始化
color.json
{"color": [{"name": "start_window_background","value": "#FFFFFF"},{"name": "page_background","value": "#F1F3F5"},{"name": "item_background","value": "#FFFFFF"},{"name": "blue_background","value": "#007DFF"},{"name": "title_font","value": "#182431"},{"name": "font_background","value": "#FAFAFA"},{"name": "notice_font","value": "#FF7500"}]
}
float.json
{"float": [{"name": "btn_height","value": "40vp"},{"name": "value_height","value": "28vp"},{"name": "item_padding","value": "12vp"},{"name": "subtitle_margin_top","value": "2vp"},{"name": "blue_margin_top","value": "4vp"},{"name": "title_margin_top","value": "6vp"},{"name": "desc_margin_top","value": "8vp"},{"name": "item_margin_top","value": "18vp"},{"name": "desc_line_height","value": "20fp"},{"name": "subtitle_line_height","value": "16fp"},{"name": "name_font_size","value": "16fp"},{"name": "title_font_size","value": "14fp"},{"name": "subtitle_font_size","value": "12fp"},{"name": "large_font_size","value": "24fp"},{"name": "item_border_radius","value": "24vp"},{"name": "notice_border_radius","value": "12vp"},{"name": "value_border_radius","value": "4vp"},{"name": "label_opacity","value": "0.6"}]
}
string.json(英文)
{"string": [{"name": "module_desc","value": "module description"},{"name": "EntryAbility_desc","value": "description"},{"name": "EntryAbility_label","value": "label"},{"name": "pixel_introduce","value": "Pixel Introduce"},{"name": "pixel_conversion","value": "Pixel Conversion"},{"name": "HarmonyHeiTi","value": "HarmonyHeiTi"},{"name": "HarmonyHeiTi_Medium","value": "HarmonyHeiTi-Medium"},{"name": "font_desc","value": "This is a text for %dfp"},{"name": "notice","value": "For the conversion between lpx and px, set designWidth based on the site requirements."},{"name": "px_unit","value": "Physical pixel unit of the screen."},{"name": "vp_unit","value": "Screen density-related pixels, which are converted to screen physical pixels based on the screen pixel density."},{"name": "lpx_unit","value": "Logical pixel unit of the window. The unit of lpx is the ratio of the actual screen width to the logical width (configured by designWidth)."},{"name": "fp_unit","value": "Font pixel, similar to vp, varies with the system font size setting."},{"name": "vp_desc","value": "On a device with a pixel density of 160 dpi, 1vp = 1px, corresponding physical screen pixels = (Screen pixel density/160) px."},{"name": "fp_desc","value": "By default, the value is the same as that of vp, that is, 1vp=1fp. If the system font is manually adjusted, scale indicates the scaling ratio. The font size after setting (unit: fp) equals the font size before setting x scale."}]
}
string.json(中文)
{"string": [{"name": "module_desc","value": "模块描述"},{"name": "EntryAbility_desc","value": "description"},{"name": "EntryAbility_label","value": "像素转换"},{"name": "pixel_introduce","value": "像素介绍"},{"name": "pixel_conversion","value": "像素转换"},{"name": "HarmonyHeiTi","value": "HarmonyHeiTi"},{"name": "HarmonyHeiTi_Medium","value": "HarmonyHeiTi-Medium"},{"name": "font_desc","value": "这是一段为%dfp的文字"},{"name": "notice","value": "lpx与px之间的转换,需要根据实际情况设置designWidth"},{"name": "px_unit","value": "屏幕物理像素单位。"},{"name": "vp_unit","value": "屏幕密度相关像素,根据屏幕像素密度转换为屏幕物理像素。"},{"name": "lpx_unit","value": "视窗逻辑像素单位,lpx单位为实际屏幕宽度与逻辑宽度(通过designWidth配置)的比值。"},{"name": "fp_unit","value": "字体像素,与vp类似,随系统字体大小设置变化。"},{"name": "vp_desc","value": "像素密度为160dpi的设备上1vp=1px,1vp对应的物理屏幕像素=(屏幕像素密度/160)px "},{"name": "fp_desc","value": "默认情况下与vp相同,即1vp=1fp,如果用户手动调整了系统字体,scale为缩放比例,设置后的字体大小(单位fp) = 设置前的字体大小 * scale"}]
}
常量
// ets/common/constants/Constants.etsexport default class Constants {static readonly FULL_PERCENT: string = '100%'static readonly PIXEL_CONVERSION: string = '像素转换'static readonly PIXEL_INTRODUCTION: string = '像素介绍'static readonly INTRODUCTION_PAGE_URL: string = 'pages/IntroductionPage'static readonly CONVERSION_PAGE_URL: string = 'pages/ConversionPage'static readonly INDEX_PAGE_TAG: string = 'IndexPage push error 'static readonly TITLE_FONT_WEIGHT: number = 500static readonly LABEL_FONT_WEIGHT: number = 400static readonly COLUMN_SPACE: number = 24static readonly ITEM_PADDING: number = 12static readonly LARGE_FONT_SIZE: number = 24static readonly SMALL_FONT_SIZE: number = 14static readonly VP_SIZE: number = 60static readonly PIXEL_WIDTH: number = 200
}
Logger
// ets/common/utils/Logger.etsimport { hilog } from '@kit.PerformanceAnalysisKit'const LOGGER_PREFIX: string = 'Pixel Conversion'class Logger {private domain: numberprivate prefix: stringprivate format: string = '%{public}s, %{public}s'constructor(prefix: string = '', domain: number = 0xFF00) {this.prefix = prefixthis.domain = domain}debug(...args: string[]): void {hilog.debug(this.domain, this.prefix, this.format, args)}info(...args: string[]): void {hilog.info(this.domain, this.prefix, this.format, args)}warn(...args: string[]): void {hilog.warn(this.domain, this.prefix, this.format, args)}error(...args: string[]): void {hilog.error(this.domain, this.prefix, this.format, args)}
}export default new Logger(LOGGER_PREFIX)
六、页面
// ets/pages/Index.etsimport { router } from '@kit.ArkUI'
import Constants from '../common/constants/Constants'
import Logger from '../common/utils/Logger'@Entry
@Component
struct IndexPage {jumpPage(url: string) {router.pushUrl({ url }).catch((error: Error) => {Logger.error(Constants.INDEX_PAGE_TAG, JSON.stringify(error));});}build() {Column({ space: Constants.COLUMN_SPACE }) {Button($r('app.string.pixel_introduce')).height($r('app.float.btn_height')).width(Constants.FULL_PERCENT).backgroundColor($r('app.color.blue_background')).onClick((): void => this.jumpPage(Constants.INTRODUCTION_PAGE_URL))Button($r('app.string.pixel_conversion')).height($r('app.float.btn_height')).width(Constants.FULL_PERCENT).backgroundColor($r('app.color.blue_background')).onClick((): void => this.jumpPage(Constants.CONVERSION_PAGE_URL))}.backgroundColor($r('app.color.page_background')).justifyContent(FlexAlign.Center).padding(Constants.COLUMN_SPACE).width(Constants.FULL_PERCENT).height(Constants.FULL_PERCENT)}
}
七、像素介绍页面
// ets/viewmodel/IntroductionItem.etsexport default class IntroductionItem {name: stringtitle: Resource | stringsubTitle: Resource | stringvalue: stringsmallFontSize: numberlargeFontSize: numberconstructor(name?: string, title?: Resource | string, subTitle?: Resource | string, value?: string,smallFontSize?: number, largeFontSize?: number) {this.name = name ? name : ''this.title = title ? title : ''this.subTitle = subTitle ? subTitle : ''this.value = value ? value : ''this.smallFontSize = smallFontSize ? smallFontSize : 0this.largeFontSize = largeFontSize ? largeFontSize : 0}
}
// ets/viewmodel/InstroductionViewModel.etsimport Constants from '../common/constants/Constants'
import IntroductionItem from './IntroductionItem'class IntroductionViewModel {getIntroductionList() {let introductionItems = INTRODUCE_LISTreturn introductionItems;}
}const INTRODUCE_LIST: IntroductionItem[] = [new IntroductionItem('px', $r('app.string.px_unit'), '', Constants.PIXEL_WIDTH + 'px', 0, 0),new IntroductionItem('vp', $r('app.string.vp_unit'), $r('app.string.vp_desc'), Constants.PIXEL_WIDTH + 'vp', 0, 0),new IntroductionItem('lpx', $r('app.string.lpx_unit'), '', Constants.PIXEL_WIDTH + 'lpx', 0, 0),new IntroductionItem('fp', $r('app.string.fp_unit'), $r('app.string.fp_desc'), '', Constants.SMALL_FONT_SIZE,Constants.LARGE_FONT_SIZE)
]let introductionViewModel = new IntroductionViewModel()export default introductionViewModel as IntroductionViewModel
// ets/views/IntroductionComponent.etsimport Constants from '../common/constants/Constants'
import IntroductionItem from '../viewmodel/IntroductionItem'@Extend(Text) function titleTextStyle() {.fontColor($r('app.color.title_font')).fontFamily($r('app.string.HarmonyHeiTi_Medium')).fontWeight(Constants.TITLE_FONT_WEIGHT)
}@Component
export default struct IntroduceItemComponent {model: IntroductionItem = new IntroductionItem()build() {Column() {Text(this.model.name).titleTextStyle().fontSize($r('app.float.name_font_size'))Text(this.model.title).titleTextStyle().fontSize($r('app.float.title_font_size')).fontFamily($r('app.string.HarmonyHeiTi')).lineHeight($r('app.float.desc_line_height')).margin({ top: $r('app.float.desc_margin_top') }).fontWeight(Constants.LABEL_FONT_WEIGHT)if (this.model.subTitle) {Text(this.model.subTitle).titleTextStyle().opacity($r('app.float.label_opacity')).lineHeight($r('app.float.subtitle_line_height')).fontSize($r('app.float.subtitle_font_size')).fontFamily($r('app.string.HarmonyHeiTi')).margin({ top: $r('app.float.subtitle_margin_top') }).fontWeight(Constants.LABEL_FONT_WEIGHT)}if (this.model.value.length > 0) {Text(this.model.value).titleTextStyle().fontColor($r('app.color.item_background')).fontSize($r('app.float.name_font_size')).textAlign(TextAlign.Center).backgroundColor($r('app.color.blue_background')).height($r('app.float.value_height')).width(this.model.value).borderRadius($r('app.float.value_border_radius')).margin({ top: $r('app.float.item_padding') })} else {Column() {Text($r('app.string.font_desc', this.model.smallFontSize)).titleTextStyle().fontSize(this.model.smallFontSize)Text($r('app.string.font_desc', this.model.largeFontSize)).titleTextStyle().fontSize(this.model.largeFontSize).margin({ top: $r('app.float.title_margin_top') })}.alignItems(HorizontalAlign.Start).backgroundColor($r('app.color.font_background')).width(Constants.FULL_PERCENT).borderRadius($r('app.float.notice_border_radius')).padding($r('app.float.item_padding')).margin({ top: $r('app.float.item_padding') })}}.alignItems(HorizontalAlign.Start).width(Constants.FULL_PERCENT).padding($r('app.float.item_padding')).borderRadius($r('app.float.item_border_radius')).backgroundColor($r('app.color.item_background'))}
}
// ets/pages/IntroductionPage.etsimport Constants from '../common/constants/Constants'
import IntroductionItem from '../viewmodel/IntroductionItem'
import IntroduceItemComponent from '../views/IntroductionItemComponent'
import IntroductionViewModel from '../viewmodel/IntroductionViewModel'@Entry
@Component
struct IntroductionPage {build() {Column() {Navigation() {List({ space: Constants.ITEM_PADDING }) {ForEach(IntroductionViewModel.getIntroductionList(), (item: IntroductionItem) => {ListItem() {IntroduceItemComponent({ model: item })}.padding({left: $r('app.float.item_padding'),right: $r('app.float.item_padding')})})}.width(Constants.FULL_PERCENT).height(Constants.FULL_PERCENT)}.titleMode(NavigationTitleMode.Mini).title(Constants.PIXEL_INTRODUCTION)}.backgroundColor($r('app.color.page_background')).width(Constants.FULL_PERCENT).height(Constants.FULL_PERCENT)}
}
八、像素转换页面
// ets/viewmodel/ConversionItem.etsexport default class ConversionItem {title: stringsubTitle: stringvalue: number = 0conversionTitle: stringconversionSubTitle: stringconversionValue: numbernotice?: Resource | stringconstructor(title?: string, subTitle?: string, value?: number, conversionTitle?: string, conversionSubTitle?: string,conversionValue?: number, notice?: Resource | string) {this.title = title ? title : ''this.subTitle = subTitle ? subTitle : ''this.value = value ? value : 0this.conversionTitle = conversionTitle ? conversionTitle : ''this.conversionSubTitle = conversionSubTitle ? conversionSubTitle : ''this.conversionValue = conversionValue ? conversionValue : 0this.notice = notice ? notice : ''}
}
// ets/viewmodel/ConversionViewModel.etsimport Constants from '../common/constants/Constants'
import ConversionItem from './ConversionItem'class ConversionViewModel {getConversionList() {let conversionItems = CONVERSION_LIST;return conversionItems;}
}export const CONVERSION_LIST: ConversionItem[] = [new ConversionItem('vp > px', `vp2px(${Constants.VP_SIZE})`, vp2px(Constants.VP_SIZE), 'px > vp',`px2vp(${Constants.VP_SIZE})`, px2vp(Constants.VP_SIZE)),new ConversionItem('fp > px',`fp2px(${Constants.VP_SIZE})`,fp2px(Constants.VP_SIZE),'px > fp',`px2fp(${Constants.VP_SIZE})`,px2fp(Constants.VP_SIZE)),new ConversionItem('lpx > px',`lpx2px(${Constants.VP_SIZE})`,lpx2px(Constants.VP_SIZE),'px > lpx',`px2lpx(${Constants.VP_SIZE})`,px2lpx(Constants.VP_SIZE),$r('app.string.notice'))
]let conversionViewModel = new ConversionViewModel()export default conversionViewModel as ConversionViewModel
// ets/views/ConversionItemComponent.etsimport Constants from '../common/constants/Constants'
import ConversionItem from '../viewmodel/ConversionItem'@Extend(Text) function descTextStyle() {.fontColor($r('app.color.title_font')).fontSize($r('app.float.title_font_size')).fontFamily($r('app.string.HarmonyHeiTi')).lineHeight($r('app.float.desc_line_height')).fontWeight(Constants.LABEL_FONT_WEIGHT).margin({ top: $r('app.float.desc_margin_top') })
}@Extend(Text) function titleTextStyle() {.fontColor($r('app.color.title_font')).fontSize($r('app.float.name_font_size')).fontFamily($r('app.string.HarmonyHeiTi_Medium')).fontWeight(Constants.TITLE_FONT_WEIGHT)
}@Styles function blueStyle() {.backgroundColor($r('app.color.blue_background')).height($r('app.float.value_height')).borderRadius($r('app.float.value_border_radius')).margin({ top: $r('app.float.blue_margin_top') })
}@Component
export default struct ConversionItemComponent {model: ConversionItem = new ConversionItem()build() {Column() {Text(this.model.title).titleTextStyle().margin({ top: $r('app.float.title_margin_top') })Text(this.model.subTitle).descTextStyle().opacity($r('app.float.label_opacity'))Row().blueStyle().width(this.model.value)Text(this.model.conversionTitle).titleTextStyle().margin({ top: $r('app.float.item_margin_top') })Text(this.model.conversionSubTitle).descTextStyle().opacity($r('app.float.label_opacity'))Row().blueStyle().width(this.model.conversionValue)if (this.model.notice) {Text(this.model.notice).descTextStyle().fontColor($r('app.color.notice_font'))}}.alignItems(HorizontalAlign.Start).width(Constants.FULL_PERCENT).padding($r('app.float.item_padding')).borderRadius($r('app.float.item_border_radius')).backgroundColor($r('app.color.item_background'))}
}
// ets/pages/ConversionPage.etsimport Constants from '../common/constants/Constants'
import ConversionItemComponent from '../views/ConversionItemComponent'
import ConversionItem from '../viewmodel/ConversionItem'
import ConversionViewModel from '../viewmodel/ConversionViewModel'@Entry
@Component
struct ConversionPage {build() {Column() {Navigation() {List({ space: Constants.ITEM_PADDING }) {ForEach(ConversionViewModel.getConversionList(), (item: ConversionItem) => {ListItem() {ConversionItemComponent({ model: item })}.padding({left: $r('app.float.item_padding'),right: $r('app.float.item_padding')})})}.width(Constants.FULL_PERCENT).height(Constants.FULL_PERCENT)}.titleMode(NavigationTitleMode.Mini).title(Constants.PIXEL_CONVERSION)}.backgroundColor($r('app.color.page_background')).width(Constants.FULL_PERCENT).height(Constants.FULL_PERCENT)}
}