HarmonyOS中的一次开发、多端部署,从UX设计角度.快速开发出适配多种类型设备的图片查看器的UI原型,我们基于图片浏览器的UI原型,继续深入介绍如何实现图片浏览器器的完整功能。

1 UX设计

本文将以图片浏览器为例,介绍如何使用自适应布局能力和响应式布局能力以适配不同尺寸的窗口,从而实现“一多”功能。

图片浏览器的UX设计从最初的设计阶段开始就拉通综合考虑到多设备适配。默认设备和平板电脑对应于小设备、中设备及大设备,本示例以这3类设备场景为例,介绍不同设备上的UX设计。一个典型的图片查看器的小设备、中设备及大设备的UX设计如图所:

小设备显示如图:

中设备显示如图:

大设备显示如图:

从图片查看器在各设备上的UX设计图中,可以观察到不同尺寸下的页面设计有较多相似的地方。各组成部分在不同设备下的显示位置如图所示:

从图可以看出小设备和大设备的UX差异性。小设备的操作栏位于底部,而大设备的操作栏则位于顶部右侧。

2 框架设计

图片查看器采用HammonyOS“一多”所推荐的三层工程结构,整个应用结构分为common、features、product三层。工程结构如图所示:

其中:

commons:公共特性目录,用于存放公共的类、工具等。

features:功能目录,用于存放业务功能,比如本文所要介绍的图片查看功能。

product:产品层目录,用于存放不同设备差异性的部分。

2.1 模块的依赖关系

推荐在common目录中存放基础公共代码,features目录中存放相对独立的功能模块代码,product目录中存放完全独立的产品代码。这样在product目录中的模块,可用依赖features和commons中的公共代码来实现功能,最大程度实现代码复用。配置依赖关系可以通过修改模块中的oh-package.json5文件。如下代码所示,通过修改default模块中的oh-package.json5文件,使其可以使用commons目录下的base模块,以及features目录下的pictureView模块。oh-package.json5文件的代码如下:

{
  "license": "",
  "devDependencies": {},
  "author": "",
  "name": "default",
  "description": "图片查看器.",
  "main": "",
  "version": "1.0.0",
  "dependencies": {
    "@ohos/commons": "file:../../commons/base",
    "@ohos/view": "file:../../features/pictureView"
  }
}

同样地,修改features模块中的oh-package.json5文件,使其可以使用commons目录下的base模块中的代码:

{
  "license": "",
  "devDependencies": {},
  "author": "",
  "name": "pictureview",
  "description": "图片查看功能.",
  "main": "Index.ets",
  "version": "1.0.0",
  "dependencies": {
    "@ohos/commons": "file:../../commons/base"
  }
}

修改ob-package.jon5文件后,请单击菜单栏的“Build -> Retbuild Projct”来执行模块的安装及工程的构建。

2.2 修改Module类型及其设备类型

通过修改每个模块中的配置文件modulejon5对模块进行配置,配置文件中各字段含义详见配置文件说明。

将detault模块的deviceTypes配置为["phone","tablet","2in1"],同时将其trype字段配置为entry。即default模块编译出的HAP能在手机、折叠屏手机和平板电脑上安装和运行,module.json5配置代码如下。

{
  "module": {
    "name": "default",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "DefaultAbility",
    "deviceTypes": [
      "phone",
      "tablet",
      "2in1"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "DefaultAbility",
        "srcEntry": "./ets/defaultability/DefaultAbility.ets",
        "description": "$string:PhoneAbility_desc",
        "icon": "$media:icon",
        "label": "$string:PhoneAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "minWindowWidth": 330,
        "minWindowHeight": 600,
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ]
  }
}

同样地,将pictureView模块和base模块的deviceTypes也要配置为["phone","tablet","2in1"]。pictureView模块module.json5文件代码如下:

{
  "module": {
    "name": "pictureView",
    "type": "har",
    "deviceTypes": [
      "phone",
      "tablet",
      "2in1"
    ]
  }
}

base模块module.json5文件代码如下:

{
  "module": {
    "name": "base",
    "type": "har",
    "deviceTypes": [
      "phone",
      "tablet",
      "2in1"
    ]
  }
}

3 pictureView模块实现

这里演示图片查看器的核心pictureView模块的实现。

3.1 实现顶部区域

顶部区域view/TopBar.ets代码实现如下:

import { BaseConstants, BreakpointConstants } from '@ohos/commons';
import PictureViewConstants, { ActionInterface } from '../constants/PictureViewConstants';

const TITLE: string = '图片浏览器';

/**
 * 顶部区域
 */
@Preview
@Component
export struct TopBar {
  @StorageLink('currentBreakpoint') currentBp: string = BreakpointConstants.BREAKPOINT_MD;

  build() {
    Flex({
      direction: FlexDirection.Row,
      alignItems: ItemAlign.Center,
    }) {
      Column() {
        Flex({
          justifyContent: FlexAlign.SpaceBetween,
          direction: FlexDirection.Row,
          alignItems: ItemAlign.Stretch
        }) {
          Row() {
            Column() {
              Text(TITLE)
                .fontFamily(BaseConstants.FONT_FAMILY_MEDIUM)
                .fontSize(BaseConstants.FONT_SIZE_TWENTY)
                .fontWeight(BaseConstants.FONT_WEIGHT_FIVE)
            }
            .alignItems(HorizontalAlign.Start)
          }

          Row() {
            // 仅在大设备上显示操作按钮
            if (this.currentBp === BreakpointConstants.BREAKPOINT_LG) {
              ForEach(PictureViewConstants.ACTIONS, (item: ActionInterface) => {
                Image(item.icon)
                  .height(BaseConstants.DEFAULT_ICON_SIZE)
                  .width(BaseConstants.DEFAULT_ICON_SIZE)
                  .margin({ left: $r('app.float.detail_image_left') })
              }, (item: ActionInterface, index: number) => index + JSON.stringify(item))
            }
          }
        }
      }
    }
    .height($r('app.float.top_bar_height'))
    .margin({
      top: $r('app.float.top_bar_top'),
      bottom: $r('app.float.top_bar_bottom'),
      left: $r('app.float.top_bar_left'),
      right: $r('app.float.top_bar_right')
    })
  }
}

顶部区域主要实现两部分内容。左侧的应用标题及右侧的操作栏。其中,操作栏是通过currentBreakpoint来判断设备类型,从而决定是否需要显示。

当是大设备(BreakpointConstants.BREAKPOINT_LG)时,操作栏会显示。其他设备则不显示。

3.2 实现中部图片显示区域

中部图片显示区view/CenterPart.ets代码实现如下:

import { BreakpointConstants } from '@ohos/commons';
import { Adaptive } from '../viewmodel/Adaptive'

const FINGER_NUM: number = 2

/**
 * 中部图片显示区
 */
@Component
export struct CenterPart {
  @StorageLink('currentBreakpoint') currentBreakpoint: string = BreakpointConstants.BREAKPOINT_MD;
  @State scaleValue: number = 1;
  @State pinchValue: number = 1;
  @Link selectedPhoto: Resource;

  build() {
    Flex({ direction: FlexDirection.Column }) {
      Blank()
      Row() {
        Column() {
          // 绑定选中的图片
          Image(this.selectedPhoto)
            .autoResize(true)
        }
      }
      .height(Adaptive.PICTURE_HEIGHT(this.currentBreakpoint))
      .width(Adaptive.PICTURE_WIDTH(this.currentBreakpoint))
      .scale({ x: this.scaleValue, y: this.scaleValue, z: 1 })
      // 设置2指缩放
      .gesture(PinchGesture({ fingers: FINGER_NUM })
        .onActionUpdate((event: GestureEvent | undefined) => {
          if (event) {
            this.scaleValue = this.pinchValue * event.scale;
          }
        })
        .onActionEnd(() => {
          this.pinchValue = this.scaleValue;
        }))

      Blank()// 针对小设备设置
        .height(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_SM ? $r('app.float.center_blank_height_lg') :
          0)
    }
    .margin({
      top: $r('app.float.center_margin_top'),
      bottom: $r('app.float.center_margin_bottom')
    })
  }
}

中部图片显示区主要是用于显示图片预览列表所选中的图片。特别地,针对小设备还需要设置Blank组件的高度。

3.3 实现图片浏览列表

图片预览列表view/PreviewList.ets代码实现如下:

import { BreakpointConstants } from '@ohos/commons';
import PictureViewConstants from '../constants/PictureViewConstants';

const IMAGE_ASPECT_RATIO: number = 0.5;

/**
 * 图片浏览列表
 */
@Component
export struct PreviewList {
  @StorageLink('currentBreakpoint') currentBreakpoint: string = BreakpointConstants.BREAKPOINT_MD;
  @Link selectedPhoto: Resource;

  build() {
    List({ initialIndex: 1 }) {
      ForEach(PictureViewConstants.PICTURES, (item: Resource) => {
        ListItem() {
          Image(item)
            .height($r('app.float.list_image_height'))
            .aspectRatio(IMAGE_ASPECT_RATIO)
            .autoResize(true)
            .margin({ left: $r('app.float.list_image_margin_left') })
            .onClick(()=>{
              this.selectedPhoto = item;
            })
        }
      }, (item: Resource, index: number) => index + JSON.stringify(item))
    }
    .height($r('app.float.list_image_height'))
    .padding({
      top: $r('app.float.list_margin_top'),
      bottom: $r('app.float.list_margin_bottom')
    })
    .listDirection(Axis.Horizontal)
    .scrollSnapAlign(ScrollSnapAlign.CENTER)
    .scrollBar(BarState.Off)
  }
}

上述代码中,通过ForEach语句,将定义在PictureViewConstants.PICTURES里面的图片资源渲染为图片预览列表。

设置onClick()单击事件。当单击图片预览列表中的图片时,将该图片设置为已选中的图片,并通过@Link将选中的图片同步给CenterPart组件并在中部图片显示区进行显示。

3.4 实现底部区域操作栏

底部操作栏view/BottomBar.ets代码实现如下:

import PictureViewConstants, { ActionInterface } from '../constants/PictureViewConstants';
import { BaseConstants } from '@ohos/commons';

/**
 * 底部操作栏
 */
@Component
export struct BottomBar {
  build() {
    Flex({
      justifyContent: FlexAlign.Center,
      direction: FlexDirection.Row
    }) {
      ForEach(PictureViewConstants.ACTIONS, (item: ActionInterface) => {
        Column() {
          Image(item.icon)
            .height(BaseConstants.DEFAULT_ICON_SIZE)
            .width(BaseConstants.DEFAULT_ICON_SIZE)
          Text(item.icon_name)
            .fontFamily(BaseConstants.FONT_FAMILY_NORMAL)
            .fontSize(BaseConstants.FONT_SIZE_TEN)
            .fontWeight(BaseConstants.FONT_WEIGHT_FOUR)
            .padding({ top: $r('app.float.icon_padding_top') })
        }
        .width(PictureViewConstants.ICON_LIST_WIDTH)
      }, (item: ActionInterface, index: number) => index + JSON.stringify(item))
    }
    .height($r('app.float.icon_list_height'))
  }
}

上述代码中,通过ForEach语句,将定义在PictureViewConstants.ACTIONS里面的操作按钮渲染为操作栏。

需要注意的是,底部操作栏UI只在小设备和中设备中显示。因此,需要判断设备类型来决定是否示底部操作栏,pages/PictureViewIndex.ets代码如下:

import { TopBar } from '../view/TopBar';
import { CenterPart } from '../view/CenterPart';
import { BottomBar } from '../view/BottomBar';
import { PreviewList } from '../view/PreviewList';
import { BaseConstants, BreakpointConstants } from '@ohos/commons';
import { deviceInfo } from '@kit.BasicServicesKit';

/**
 * 预览器主页
 */
@Entry
@Preview
@Component
export struct PictureViewIndex {
  @StorageLink('currentBreakpoint') currentBreakpoint: string = BreakpointConstants.BREAKPOINT_MD;
  @State selectedPhoto: Resource = $r('app.media.photo');

  build() {
    Column() {
      Flex({
        direction: FlexDirection.Column,
        alignItems: ItemAlign.Center
      }) {
        // 顶部区域
        TopBar()

        // 中部图片显示区
        CenterPart({ selectedPhoto: this.selectedPhoto })
          .flexGrow(1)

        // 图片浏览列表
        PreviewList({ selectedPhoto: this.selectedPhoto })

        // 非大设备,则显示底部操作栏
        if (this.currentBreakpoint !== BreakpointConstants.BREAKPOINT_LG) {
          BottomBar()
        }
      }.padding({
        // 针对2in1设置
        top: deviceInfo.deviceType === BaseConstants.DEVICE_2IN1 ? $r('app.float.zero') :
        $r('app.float.device_padding_top'),

        // 针对非2in1设置
        bottom: deviceInfo.deviceType !== BaseConstants.DEVICE_2IN1 ? $r('app.float.tab_content_pb') :
        $r('app.float.zero')
      })
    }
    .height(BaseConstants.FULL_HEIGHT)
    .width(BaseConstants.FULL_WIDTH)
  }
}

3.5 尺寸适配

viewmodel/Adaptive.ets实现了尺寸适配,代码如下:

import { BaseConstants as Constants, BreakpointType } from '@ohos/commons';
import PictureViewConstants from '../constants/PictureViewConstants';
/**
 * 尺寸适配
 */
export class Adaptive {
  static PICTURE_HEIGHT = (currentBreakpoint: string): string => {
    return new BreakpointType(
      PictureViewConstants.PICTURE_HEIGHT_SM,
      PictureViewConstants.PICTURE_HEIGHT_MD,
      PictureViewConstants.PICTURE_HEIGHT_LG,
    ).GetValue(currentBreakpoint);
  };
  static PICTURE_WIDTH = (currentBreakpoint: string): string => {
    return new BreakpointType(
      PictureViewConstants.PICTURE_WIDTH_SM,
      PictureViewConstants.PICTURE_WIDTH_MD,
      PictureViewConstants.PICTURE_WIDTH_LG,
    ).GetValue(currentBreakpoint);
  };
}

3.6 常亮和接口

pictureView模块实现提供的常量和接口定义在constants/PictureViewConstants.ets文件中,代码如下:

/**
 * 常量
 */
export interface ActionInterface {
  icon: Resource
  icon_name: string
}

export default class PictureViewConstants {
  /**
   * Picture size.
   */
  static PICTURE_HEIGHT_SM = '88%';
  static PICTURE_HEIGHT_MD = '100%';
  static PICTURE_HEIGHT_LG = '100%';
  static PICTURE_WIDTH_SM = '100%';
  static PICTURE_WIDTH_MD = '84.5%';
  static PICTURE_WIDTH_LG = '46.9%';
  static readonly ICON_LIST_WIDTH = "18%";
  /**
   * Actions.
   */
  static ACTIONS: ActionInterface[] = [
    {
      icon: $r('app.media.ic_public_share'), icon_name: "分享"
    },
    {
      icon: $r('app.media.ic_public_favor'), icon_name: "收藏"
    },
    {
      icon: $r("app.media.ic_gallery_public_details_4"), icon_name: "编辑"
    },
    {
      icon: $r("app.media.ic_gallery_public_details_5"), icon_name: "删除"
    },
    {
      icon: $r('app.media.ic_public_more'), icon_name: "更多"
    }
  ];
  /**
   * Pictures.
   */
  static readonly PICTURES: Resource[] = [
    $r('app.media.photo1'),
    $r('app.media.photo2'),
    $r('app.media.photo3'),
    $r('app.media.photo4'),
    $r('app.media.photo5'),
    $r('app.media.photo6'),
    $r('app.media.photo7'),
    $r('app.media.photo8'),
    $r('app.media.photo9'),
    $r('app.media.photo1'),
    $r('app.media.photo2'),
    $r('app.media.photo3'),
    $r('app.media.photo4'),
    $r('app.media.photo5'),
    $r('app.media.photo6'),
    $r('app.media.photo7'),
    $r('app.media.photo8'),
    $r('app.media.photo9'),
    $r('app.media.photo1'),
    $r('app.media.photo2'),
    $r('app.media.photo3'),
    $r('app.media.photo4'),
    $r('app.media.photo5'),
    $r('app.media.photo6'),
    $r('app.media.photo7'),
    $r('app.media.photo8'),
    $r('app.media.photo9'),
    $r('app.media.photo1'),
    $r('app.media.photo2'),
    $r('app.media.photo3'),
    $r('app.media.photo4'),
    $r('app.media.photo5'),
    $r('app.media.photo6'),
    $r('app.media.photo7'),
    $r('app.media.photo8'),
    $r('app.media.photo9'),
    $r('app.media.photo1'),
    $r('app.media.photo2'),
    $r('app.media.photo3'),
    $r('app.media.photo4'),
    $r('app.media.photo5'),
    $r('app.media.photo6'),
    $r('app.media.photo7'),
    $r('app.media.photo8'),
    $r('app.media.photo9'),
    $r('app.media.photo1'),
    $r('app.media.photo2'),
    $r('app.media.photo3'),
    $r('app.media.photo4'),
    $r('app.media.photo5'),
    $r('app.media.photo6'),
    $r('app.media.photo7'),
    $r('app.media.photo8'),
    $r('app.media.photo9')
  ];
}

4 base模块实现

这里介绍base模块核心代码的实现。

base模块主要是提供应用的常量和工具类。

4.1 基础常量类

constants/BaseConstants.ets是基础常量类,定义了尺寸、样式、设备类型等,代码如下:

/**
 * 基础常量
 */
export class BaseConstants {
  /**
   * Component size.
   */
  static readonly FULL_HEIGHT: string = '100%';
  static readonly FULL_WIDTH: string = '100%';
  /**
   * Text property.
   */
  static readonly FONT_FAMILY_NORMAL: Resource = $r('app.float.font_family_normal');
  static readonly FONT_FAMILY_MEDIUM: Resource = $r('app.float.font_family_medium');
  static readonly FONT_WEIGHT_FIVE: number = 500;
  static readonly FONT_WEIGHT_FOUR: number = 400;
  static readonly FONT_SIZE_TEN: Resource = $r('app.float.font_size_ten');
  static readonly FONT_SIZE_TWENTY: Resource = $r('app.float.font_size_twenty');
  /**
   * Default icon size.
   */
  static readonly DEFAULT_ICON_SIZE: number = 24;
  /**
   * Device 2in1.
   */
  static readonly DEVICE_2IN1: string = '2in1';
}

4.2 设备类型常量

constants/BreakpointConstants.ets是设备类型常量类,定义了尺寸、样式、设备类型等,代码如下:

/**
 * 设备类型常量
 */
export class BreakpointConstants {
  /**
   * 小设备
   */
  static readonly BREAKPOINT_SM: string = 'sm';
  /**
   * 中设备
   */
  static readonly BREAKPOINT_MD: string = 'md';
  /**
   * 大设备
   */
  static readonly BREAKPOINT_LG: string = 'lg';
  /**
   * 屏幕宽度范围
   */
  static readonly BREAKPOINT_SCOPE: number[] = [0, 320, 600, 840];
}

4.3 设备尺寸类型

utils/BreakpointType.ets是设备尺寸类型类,定义了应用中所支持的所要设备尺寸,代码如下:

import { BreakpointConstants } from '../constants/BreakpointConstants';

/**
 * 设备尺寸类型
 */
export class BreakpointType<T> {
  sm: T;
  md: T;
  lg: T;

  constructor(sm: T, md: T, lg: T) {
    this.sm = sm;
    this.md = md;
    this.lg = lg;
  }

  GetValue(currentBreakpoint: string): T {
    if (currentBreakpoint === BreakpointConstants.BREAKPOINT_MD) {
      return this.md;
    } else if (currentBreakpoint === BreakpointConstants.BREAKPOINT_LG) {
      return this.lg;
    } else {
      return this.sm;
    }
  }
}

5 default 模块实现

default模块实现独立的产品代码。本应用可支持在小设备(比如手机)、中设备(比如折叠屏机)、大设备(比如平板电脑)上运行。

5.1图片查看器主页

图片查看器主页pages/Index.ets代码如下:

import { PictureViewIndex } from '@ohos/view';
/**
 * 图片查看器主页
 */
@Entry
@Component
struct Index {
  build() {
    Column() {
      PictureViewIndex()
    }
  }
}

可以看到,图片查看器主页并没有太多代码,是直接依赖于pictureVicwIndex模块的实现。

5.2 计算设备的类型

设备的宽度并不是一成不变的,比如折叠屏手机,在折叠状态属于小设备,而在展开状态下则是中设备,为此以下代码实现了当前设备类型的计算。

为了能够动态计算设备类型,在defaultability/DefaultAbility.ets的onWindowStageCreate()函数中添加如下代码,根据窗口的宽带来计算设备类型:

import { BreakpointConstants } from '@ohos/commons/Index';

export default class PhoneAbility extends UIAbility {
  ...
  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

    // 获取窗口对象
    windowStage.getMainWindow((err: BusinessError<void>, data) => {
      let windowObj: window.Window = data;

      // 计算设备的尺寸
      this.updateBreakpoint(windowObj.getWindowProperties().windowRect.width);
      windowObj.on('windowSizeChange', (windowSize: window.Size) => {
        this.updateBreakpoint(windowSize.width);
      })

      if (err.code) {
        hilog.info(0x0000, 'testTag', '%{public}s', 'getMainWindow failed');
        return;
      }
    })

    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
    });
  }

  // 变更设备类型
  private updateBreakpoint(windowWidth: number) :void{
    let windowWidthVp = windowWidth / display.getDefaultDisplaySync().densityPixels;
    let curBp: string = '';
    if (windowWidthVp < BreakpointConstants.BREAKPOINT_SCOPE[2]) {
      curBp = BreakpointConstants.BREAKPOINT_SM;
    } else if (windowWidthVp < BreakpointConstants.BREAKPOINT_SCOPE[3]) {
      curBp = BreakpointConstants.BREAKPOINT_MD;
    } else {
      curBp = BreakpointConstants.BREAKPOINT_LG;
    }
    AppStorage.setOrCreate('currentBreakpoint', curBp);
  }
  ...
}

6 小结

本文介绍了如何实现图片查看器的完整功能,并且实现了“一次开发,多端部署”的特征。图片查看器的开发内容涉及顶部区域、中部图片显示区、图片预览列表、底部区域操作栏、尺寸适配等。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐