一.鸿蒙各个屏幕尺寸适配
我们需要对手机、平板等适配时,可以使用其中的“断点”概念完成适配。例如使用GridRow和GridCol等。
我们先定义断点处的大小,例如下面的YYSBreakPoints数组,包含两个元素,那么就将设备分为了三个区间。
1.1 我们先定义好如下的适配类。
export class UIConstants {
static XS = "xs"
static SM = "sm"
static MD = "md"
static LG = "lg"
static XL = "xl"
static XXL = "xxl"
static YYSBreakPoints: string[] = ["800vp", "1600vp"]
}
1.2 我们在实际的编码处将如下的代码加入,那么它就会监听到或者说检测到当前设备的大小,从而触发我们编写的逻辑,其中的“this.deviceWidth”当然是我们当前类中具有状态的值,从而改变布局尺寸。
@Builder
GridFitSize() {
GridRow({
breakpoints: { value: UIConstants.YYSBreakPoints },
}) {
GridCol().height(1).width(1)
}.onBreakpointChange((breakpoints: string) => {
if (breakpoints == UIConstants.XS || breakpoints == UIConstants.SM) {
/// 小设备.
this.deviceWidth = 屏幕宽度 / 3
} else if (breakpoints == UIConstants.LG) {
/// 大设备.
this.deviceWidth = 屏幕宽度 / 7.5
} else {
/// 超大设备.
this.deviceWidth = 屏幕宽度 / 11.5
}
}).height(1).width(1)
}
二.获取组件在设备中相对于屏幕的x坐标
其实获取到组件的相关大小尺寸x,y的坐标都是可以的,我这里只是做一个示范,因为这里有坑.
2.1 我们首先要为我们要获取相关尺寸信息的组件设置一个唯一的id,这个id为组件的通用属性.我们先设置好了,才能通过此id获取到组件的尺寸信息.
2.2 然后通过id获取到componentUtils.ComponentInfo
let position: componentUtils.ComponentInfo =
this.getUIContext()
.getComponentUtils()
.getRectangleById('你事先设置好的组件id')
2.3 最后我们获取x坐标,screenOffset属性为相对于屏幕的坐标信息,它还有localOffset是相对于父视图组件的等。这里有个坑,原因不明,它所拿到的坐标好像并不是最真实的,需要我们进行一个转化。代码如下:
let result:number = position.screenOffset.x * (this.deviceWidth / position.size.width)
我们上面在第二歩获取到的position拿到后,我们通过2.3进行转化,this.deviceWidth是我们的这个组件宽度(假设我们写死了这个组件的宽度),除以position.size.width后在乘以position.screenOffset.x才能拿到真正的相对于手机屏幕的x轴坐标.
三.关于视图的布局经验.
3.1 如果你一直使用Row\Column等线性布局,使用'100%'可能会超出去,那么我们可以这样写:
.width('calc(100% - 32vp)')
使用calc进行计算,这会比较方便,经常用到。
3.2 热区设置:关于热区的操作鸿蒙提供了比较方便的API,这是我在其他移动端技术栈中都没有遇到过的,可能会比较方便,因为我并没有实践。
responseRegion(value: Array | Rectangle)
它后面是一个数组,可以设置好几个热区,但是从官方的解释中,它并没有说可以扩大热区。
扩大热区的另外一个办法是为组件设置padding,但是你要算好它的宽和高,与padding相互配合,然后去实现它的onClick方法,这是比较常见的,在其他技术栈中也适用的方法。
3.3 .layoutWeight(1) 它也比较重要,从官方文档中,你可能完全读不出来它在实际开发中的大用。它被常用于在线性布局中,比如Row\Colum中,.layoutWeight(1)这样设置为1后,它可以让它所设置的组件,在主轴方向上占满剩余的空间,具体的其他使用可以查看官方文档
3.4 再啰嗦一条总结:flexGrow(0) : 用于弹性布局,占用剩余空间,1表示占满,设置为0表示保留自身大小。layoutWeight: 用于线性布局,占用剩余空间,1表示占满
四.关于鸿蒙MVVM的实践
鸿蒙与Flutter不太一样,它不能在一个viewModel中进行状态管理,只能在viewModel处理完逻辑后将数据回调到@Component或者@Entry中,然后在它们中进行状态管理才行.要将一些逻辑放在viewModel中我实践中出来就两个方式
1.重写class的构造方法,以回调函数作为此构造方法的参数,给到viewModel的回调方法属性中。
2.不通过构造函数,直接在viewModel中定义回调函数属性,在视图层给回调函数给值。
type HaveDataCallback = (hasData: boolean) => void;
/// 有多个参数的话,可以使用Class或者interface整合起来.
declare interface PersonParam {
address?: string | null,
hasDataCallback?: HaveDataCallback
}
export class PersonPageViewModel{
public getNameCallback?: (userName: string) => void
public refreshDataCallback?: () => void
hasDataCallback?: HaveDataCallback
constructor(personParam?: PersonParam,) {
/// 方式一:通过重写constructor构造函数实现,个人觉得不够优雅.
this.hasDataCallback = personParam?.hasDataCallback
}
}
方式二:直接在component中给值(比较优雅)
@Entry
@Component
export struct PersonPage{
/// 这里我们假设此时PersonPageViewModel并没有重写constructor构造方法,好让语法通过.
private viewModel: PersonPageViewModel = new PersonPageViewModel()
async aboutToAppear(): Promise<void> {
/// 顺带演示如何获取路由参数
let params: ESObject = router.getParams() as ESObject
if (!params) {
return
}
await this.viewModel.getPersonInfo(params)
/// 配置viewModel的路由回调,关键就在此处。
this.configViewModelCallback()
}
configViewModelCallback(){
this.viewModel.refreshDataCallback = ()=>{
/// 配置viewModel中的refreshDataCallback回调
}
this.viewModel.getNameCallback = (userName: string) => {
/// 初始化viewModel中的getNameCallback回调.
}
}
}
五.组件化与单例内部类
鸿蒙中的组件化是通过动态共享包来实现的,也就是官方文档中的“分包”概念,项目中的不同开发者依据自己的开发模块创建不同的动态共享包,然后进行任务开发。但是!!这里有个坑,比如有了单例内部类,按照道理说在项目中此单例只会初始化一次,但是在分包时,比如上一个包使用了这个单例内部类,然后路由到另外一个分包时,此单例会被重新创建,导致数据的变更,解决方法是使用AppStorage对此单例进行存储,这样在不同分包中进行路由跳转时,就能够保证此单例还是这个对象,并没有被重新创建。代码如下:
export class DataInfoManger {
private static instance: DataInfoManger | null
/// 私有化,防止外部调用.
private constructor() {}
/// 获取单例 DataInfoManger
/// 如果不使用AppStorage的话,在跨包路由的时候可能会发生重建.
static getInstance(): DataInfoManger {
if (DataInfoManger.instance == null) {
DataInfoManger.instance = AppStorage.get<DataInfoManger>('DataInfoManger') as DataInfoManger;
if (!DataInfoManger.instance) {
DataInfoManger.instance = new DataInfoManger();
AppStorage.setOrCreate('DataInfoManger', DataInfoManger.instance);
}
}
return DataInfoManger.instance
}
}
六.@Builder按值传递问题
在官方的解释中,我们知道这个装饰器默认是按照值传递的,所以你没有注意到这一点的话,在平时的开发中你会发现,我的逻辑和值都是正确的,为什么将值传递进@Builder修饰的方法中,为什么UI并没有变化,因为它是按照值传递的,并不会引起UI的刷新。 这是官方的文档截图:
只有按照引用的方式传递值,才会触发动态渲染刷新,切记,切记!
/// 1.按照引用传递,好像也可以使用interface,但是一般用class
class ItemUpdateData {
title: string = '';
isSelected: boolean = false;
index: number = 0
}
/// 顺带演示一下如何定义一个回调函数类型.
export type ItemDataCallBack = (type:string) => void
/// 2.此时我们调用这个setupItem的自定义构建函数时,传人的ItemUpdateData对象才会引起UI的刷新.
@Builder
setupItem(itemData: ItemUpdateData) {
Column(){
xxxxxx
}
}
七.实现一个图片从上到下按照一条直线一直运动的动画
我的思路是:不使用诸如贝赛儿曲线等,通过创建一个AnimatorResult对象,不断的改变一个Image组件的margin的top属性来达到效果,示范代码如下:
/// 1.定义一个私有的AnimatorResult对象
private imageAnimator: AnimatorResult | undefined = undefined
/// 2.定义图片的外间距,并作为状态管理.
@State imageMarginTop: number = 0
/// 3.组件出现前就创建AnimatorResult对象
aboutToAppear(): void {
this.createImageAnimation()
}
build(){
Stack() {
/// xxxx 其他布局代码
/// 4.要做动画的Image组件
Image($r('app.media.your_header'))
.width(10).height(16.25)
.margin({ left: 0, top: this.imageMarginTop })
}.align(Alignment.Top)
.onAppear(() => {
/// 5.在组件的onAppear生命周期中,调用执行动画的API
this.imageAnimator?.play()
})
}
/// 6.创建动画AnimatorResult对象
createImageAnimation() {
/// 属性具体意思可以去官方文档查阅
this.imageAnimator = Animator.create({
duration: 2000,
easing: "linear",
delay: 0,///动画延时播放时长
fill: "backwards",/// 动画将在animation-delay期间应用第一个关键帧中定义的值。即执行完毕再从头开始.
direction: "normal",
iterations: -1,/// 动画播放次数。设置为0时不播放,设置为-1时无限次播放。
begin: 0,
end: 50
})
this.imageAnimator!.onFrame = (value: number) => {
/// 在动画执行期间不断改变Image组件的margin的top属性
this.imageMarginTop = value
}
}
八.布局回调.
只有在@entry中,才会有onPageShow !!! 我发现很多人以为@component也会走onPageShow生命周期回调,其实只有在struct被@entry修饰时,才会走onPageShow回调,也就是布局完成的回调。如果说本struct只被@component修饰,是不会走的,那会走什么呢?当完成布局时,会走布局回调!
/// 监听页面渲染.
private layoutListener: inspector.ComponentObserver =
this.getUIContext().getUIInspector().createComponentObserver('layoutListener')
/// 页面出现进行监听
aboutToAppear(): void {
this.layoutListener.on('layout', this.layoutFinish.bind(this))
}
/// 页面消失取消监听
aboutToDisappear(): void {
this.layoutListener.off('layout', this.layoutFinish.bind(this))
}
/// 完成执行此函数.
layoutFinish(){
}
九:方法调用
方法调用有什么好说的?这可能是我遇到的一个问题吧,是一个小细节,它与js和dart语言有点不一样,所以当时开发的时候导致了我的错误。
/// 1.比如说我定义了一个变量,注意它是联合类型的.
private name?:string | null = null
/// 2.我再定义一个方法
useName(name:string){
}
/// 3.我在调用这个方法的时候,我这样写
if (this.name != null){
this.useName(this.name)
}
上面这样写程序会直接崩溃,系统报入参类型不正确。即使你在外面判断了this.name是否为空,这都不起作用。
一个解决办法是,在if条件判空之后,使用感叹号强制解包.另外一个办法是把我们声明的属性类型改为此方法相同的类型.
十.LazyForEach的刷新
这里并无代码演示,只是提醒一下。我们都知道它是懒加载,可以加载一个数组等内容。但是,我们需要注意的是它的Key,也就是它后面的一个参数。它的参数与构建UI的参数是一致的,系统默认是有值的,它让我们返回一个字符串,当这个字符串值没有改变的时候,我们构建的Item的UI是不会刷新的,但是当我们使用状态管理的一些变量作为返回值时,只要此返回的Key值发生改变,那么对应的Item的UI也会刷新。
好了,集齐10条,收工!