HarmonyOS一次开发多端部署三巨头之界面级一多开发

界面级一多开发

  • 引言
    • [1. 布局能力](#1. 布局能力)
      • [1.1 自适应布局](#1.1 自适应布局)
        • [1.1.1 拉伸能力](#1.1.1 拉伸能力)
        • [1.1.2 均分能力](#1.1.2 均分能力)
        • [1.1.3 占比能力](#1.1.3 占比能力)
        • [1.1.4 缩放能力](#1.1.4 缩放能力)
        • 1.1.5延伸能力
        • [1.1.6 隐藏能力](#1.1.6 隐藏能力)
        • [1.1.7 折行能力](#1.1.7 折行能力)
      • [1.2 响应式布局](#1.2 响应式布局)
        • [1.2.1 断点和媒体查询](#1.2.1 断点和媒体查询)
        • [1.2.2 栅格布局](#1.2.2 栅格布局)
    • [2. 视觉风格](#2. 视觉风格)
      • [2.1 分层参数](#2.1 分层参数)
      • [2.2 自定义资源](#2.2 自定义资源)
    • [3. 交互归一](#3. 交互归一)
    • [4. IDE多设备预览](#4. IDE多设备预览)

引言

一次开发多端部署
定义 :一套代码工程,一次开发上架,多端按需部署
目标 :支撑开发者快速高效的开发多终端设备上的应用

为了实现一多开发的定义与目标,我们需要解决三个问题

  1. 页面如何适配:不同设备间的屏幕尺寸,色彩风格等存在差异。
  2. 功能如何兼容:不同设备的系统能力有差异,如智能穿戴设备,是否具有定位能力、智慧屏是否具有摄像头等。
  3. 工程如何组织:如何实现同一套代码,同时能部署到多种不同的设备上。

今天为大家带来的是界面级一多开发的具体实现。

1. 布局能力

  1. 自适应布局(Adaptive Layout) :元素可以根据相对关系自动变化以适应外部容器变化的布局能力。当前开发框架提炼了其七种自适应布局能力,这些布局也可以独立使用,也可多种布局叠加使用。
  2. 响应式布局(ResponsiveLayout):元素可以根据特定的特征(如窗口宽度、屏幕方向等)触发变化以适应外部容器变化的布局能力,响应式布局依据断点、媒体查询、栅格等能力实现

1.1 自适应布局

1.1.1 拉伸能力

拉伸能力是指容器组件尺寸发生变化时,增加或减少的空间全部分配给容器组件内指定区域。本例中,页面有中间的内容区以及两侧留白区组成

TypeScript 复制代码
Row() {
//通过flexGrow和flexShrink属性,将多余的的空间全部分配给图片,将不足的控件全部分配给两侧空白区域
    Row().width(150)
    .flexGrow(0).flexShrink(1)
    Image($r("app.media.image")).width(400)
    .flexGrow(1).flexShrink(0)
    Row().width(150)
    .flexGrow(0).flexShrink(1)
}
1.1.2 均分能力

均分能力是指容器组件发生变化时,增加或减少的空间均匀分配给容器组件内所有空白区域。本例中,父容器尺寸变化过程中,图标及文字的尺寸不变,图标间的间距及图标离左右边缘的的距离同时均等改变。

TypeScript 复制代码
Column() {
    Row() {
        Foreach(this.list,(item: number) => {..})
    }.width('100%')
    //均匀分配父容器主轴方向的剩余空间
    .justifyContent(FlexAlign.spaceEvenly)
    //同上Row
    Row() {..}
}.width(this.rate * 100 + '%')
1.1.3 占比能力

占比能力是指子组件的宽高按照预设比例,随父容器发生变化。本例中,简单的播放器控制栏,其中"上一首"、"播放/暂停"、"下一首"的layoutWeight属性都设置为1,因此他们按照比例"1:1:1"的比例均分父容器主轴方向上的空间。

TypeScript 复制代码
Row() {
    Column() {..}
    .layoutWeight(1)//设置子组件在父容器主轴上的布局权重
    Column() {..}
    .layoutWeight(1)//设置子组件在父容器主轴上的布局权重
    Column() {..}
    .layoutWeight(1)//设置子组件在父容器主轴上的布局权重
}
.width(this.rate * 100 + "%")
1.1.4 缩放能力

缩放能力是指子组件的宽高按照预设的比例,随容器组件发生变化,且变化过程中子组件的宽高比不变。例如本例中,Column组件随其Flex父组件尺寸变化而缩放的过程中,始终保持预设的宽高比,其中的图片也始终显示正常

TypeScript 复制代码
Column() {
    Column() {
        Image($r('app.media.image'))
        .width('100%').height('100%')
    }
    .aspectRatio(1)//固定高宽比
}
.height(this.sliderHeight)
.width(this.sliderWidth)
1.1.5延伸能力

延伸能力是指容器组件内的子组件,按照其在列表的先后顺序,随容器组件变化显示或隐藏。它可以根据显示区域的尺寸,显示不同数量的元素。本例中,当父容器尺寸发生变化时,页面中显示的图标数量随之发生改变。

TypeScript 复制代码
Row({ space: 10 }) {
    //通过List组件实现隐藏能力
    List( {space:10 }){...}
    .listDirection(Axis.Horizontal)
    .width('100%')
}
.width(this.rate * 100 + '%')
1.1.6 隐藏能力

隐藏能力是指容器组件内的子组件,按照其预设的显示优先级,随容器组件尺寸变化显示或隐藏,其中相同显示优先级的子组件同时显示或隐藏。本例中,下面五个按键设置了不同的优先级,父组件宽度变化后,优先显示高优先级的元素。

TypeScript 复制代码
Row() {
    Image($r('app.media.favorite'))
    .displayPriority(1)//布局优先级
    Image($r('app.media.down'))
    .displayPriority(2)//布局优先级
    Image($r('app.media.pause'))
    .displayPriority(3)//布局优先级
    Image($r('app.media.next'))
    .displayPriority(2)//布局优先级
    Image($r('app.media.list'))
    .displayPriority(1)//布局优先级
}
.width(this.rate * 100 + '%')
1.1.7 折行能力

折行能力是指容器组件发生尺寸发生变化,当布局方向尺寸不足以显示完整内容时自动换行。它常用于横竖屏适配或默认设备向平板切换的场景。本例中,当父容器尺寸发生变化时,其中的内容做自适应换行。

TypeScript 复制代码
Column() {
//通过Flex组件warp参数实现自适应执行
    Flex({
        warp: FlexWarp,
        direction: FlexDirection.Row
    }) {
        ForEach(this.imageList), (item: Resource) => {
            Image(item).width(183).height(138)
            })
    }
    .width(this.rate * 100 + '%')
}

1.2 响应式布局

1.2.1 断点和媒体查询

断点 :将窗口宽度划分为不同的范围(即断点),监听窗口尺寸变化,当断点改变时同步调整页面布局。
注意 :断点支持自定义,取值范围可修改,下表是常用的四个断点范围

媒体查询:媒体查询提供丰富的媒体特征监听能力,可以监听应用显示应用区域变化、横竖屏、深浅色、设备类型等

TypeScript 复制代码
import { mediaquery } from "@kit.ArkUI";

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 enum BreakpointTypeEnum {
  SM = 'sm',
  MD = 'md',
  LG = 'lg',
  XL = 'xl'
}

export class BreakpointSystem {
  private currentBreakpoint: string = "md";
  private breakpoints: Breakpoint[] = [
    { name: 'sm', size: 320 },
    { name: 'md', size: 600 },
    { name: 'lg', size: 840 },
    { name: 'xl', size: 1500 }
  ];

  private updateCurrentBreakpoint(breakpoint: string) {
    if (this.currentBreakpoint !== breakpoint) {
      this.currentBreakpoint = breakpoint;
      AppStorage.setOrCreate<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)';
      }
      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');
      }
    })
  }
}

以上是媒体查询和断点结合的工具类,目的是为了记录屏幕尺寸的变化,而我们需要使用这个工具类时,可以进行如下操作

TypeScript 复制代码
//先引入工具类
import { BreakpointSystem } from '@ohos/utils';

struct Index {
..
//给BreakpointSystem实例化
@StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD;
..
//在页面的生命周期中注册监听和取消监听
aboutToAppear() {
  this.breakpointSystem.register();
}

aboutToDisappear() {
  this.breakpointSystem.unregister();
}
build() {
1.2.2 栅格布局

根据设备的水平宽度,将不同的屏幕尺寸划分为不同数量的栅格,来实现屏幕的自适应。且栅格和栅格之间可以设置一个间距,本例中为12vp

  • 可以调节布局占栅格的数量(设置参数cpan)、偏移量(设置参数offset),来实现栅格的适配。
  • 可以修改断点的取值范围,支持启用最多六个断点(设置breakpoints和value参数)。
TypeScript 复制代码
@Entry
@Component
struct LoginPage {
    aStorageProp('currentDeviceSize')currentDeviceSize: string = CommonConstants.SM;
    build(){
        GridRow({
            columns:{sm:4,md:8,lg:12 },
            gutter:{x:'12vp'}) {
            Gridcol({
                span:sm:4,md:6,lg:8
                offset:{sm:0,md:1,lg:2} {
                Column(){
                    // Title component
                    LoginTitle()
                    //Bottom component
                    LoginBottom()
                }
            }
        }
        .backgroundColor($r(#F1F3F5))
    }
    onPageShow(){
        MultipleDevicesutils.register();
    }
}

总结

  • 通过设置GridCol的span属性分配组件所占栅格列数
  • 通过设置GridCol的offset、GridRow的gutter等属性改变间距实现最佳效果

2. 视觉风格

2.1 分层参数

为了保证各组件有相同风格的默认样式,或者为了保证HarmonyOS系统应用有统一的风格。UX定义了一套系统资源,预置在系统中,开发者可以直接使用

使用了分层参数后,系统选择了深色模式,字体和背景也能自适应

TypeScript 复制代码
@Entry
@Component
struck Index {
    build() {
        Row() {
            Column() {
                Text('分层参数')
                .fontColor($r('sys.color.ohos_id_color_primary'))
                .fontSize($r('sys.float.ohos_id_text_size_headline3'))
            }
        }
        .backgroundColor($r('sys.color.ohos_id_color_background'))
    }
}

2.2 自定义资源

开发者可以在resources目录中通过限定词目录来定义不同设备状态的资源,资源可以按照"key-value"的形式自定义。应用在运行态选择使用某资源时,系统会根据设备状态优先从相匹配的目录中寻找资源。

3. 交互归一

对于不同类型的智能设备,用户可能有不同的交互方式,如通过触摸屏、鼠标、触控板等。针对不同来自不同输入设备的相同输入,通过交互归一提供给开发者统一的API。交互归一后开发者无需关注当前设备和输入设备类型,只需在交互归一事件接口中做逻辑响应即可。

以缩放交互为例,通过多指触控的张合来完成缩放动作,在多设备场景下,缩放交互会出现多种不同的操作输入方式,如表所示。在开发接口上,这些缩放操作都统一为PinchGesture的API事件。

TypeScript 复制代码
Image()
.scale({ x: this.scaleValue, y: this.scaleValue, z: 1 })
.gesture(
    //双指捏合触发该手势事件
    PinchGesture({ fingers: 2 })
    .onActionStart((event?: GestureEvent)=>{})
    .onActionUpdate((event?: GestureEvent)=>{
        this.scaleValue = this.pinchValue * event.scale
    })
    .onActionEnd(()=>{
        this.pinchValue = this.scaleValue
    })
)

4. IDE多设备预览

IDE预览器支持多设备预览,在平常代码的书写中可以提供开发者更好的预览体验。

相关推荐
枫叶丹41 小时前
【HarmonyOS之旅】HarmonyOS开发基础知识(三)
华为od·华为·华为云·harmonyos
一个处女座的程序猿O(∩_∩)O1 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink4 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者6 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
SoraLuna6 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
向前看-6 小时前
验证码机制
前端·后端
燃先生._.7 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
工业甲酰苯胺8 小时前
分布式系统架构:服务容错
数据库·架构
高山我梦口香糖8 小时前
[react]searchParams转普通对象
开发语言·前端·javascript