鸿蒙媒体开发【拼图】拍照和图片

拼图

介绍

该示例通过@ohos.multimedia.image和@ohos.file.photoAccessHelper接口实现获取图片,以及图片裁剪分割的功能。

效果预览

使用说明:

  1. 使用预置相机拍照后启动应用,应用首页会读取设备内的图片文件并展示获取到的第一个图片,没有图片时图片位置显示空白;
  2. 点击开始按钮开始后,时间开始倒计时,在规定时间内未完成拼图则游戏结束。在游戏中,玩家点击重新开始进行游戏重置;
  3. 点击开始游戏后,玩家可以根据上方的大图,点击黄格周围的图片移动,点击后图片和黄格交换位置,最终拼成完整的图片;
  4. 不在游戏中时,玩家可以点击上方大图,选择自定义图片来进行拼图游戏。

具体实现

  • 游戏中图片裁剪分割的效果实现在ImageModel中,源码参考ImageModel:

    /*

    • Copyright (c) 2022 Huawei Device Co., Ltd.
    • Licensed under the Apache License, Version 2.0 (the "License");
    • you may not use this file except in compliance with the License.
    • You may obtain a copy of the License at
    • 复制代码
      http://www.apache.org/licenses/LICENSE-2.0
    • Unless required by applicable law or agreed to in writing, software
    • distributed under the License is distributed on an "AS IS" BASIS,
    • WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    • See the License for the specific language governing permissions and
    • limitations under the License.
      */

    import { image } from '@kit.ImageKit';
    import { photoAccessHelper } from '@kit.MediaLibraryKit';
    import { dataSharePredicates } from '@kit.ArkData';
    import { Context } from '@kit.AbilityKit';
    import { fileIo } from '@kit.CoreFileKit';
    import Logger from './Logger';
    import PictureItem from './PictureItem';
    import { CommonConstants as Common } from '../common/CommonConstants';

    const TAG = '[ImageModel]';

    export default class ImageModel {
    private phAccessHelper: photoAccessHelper.PhotoAccessHelper | null = null;

    复制代码
    constructor(context: Context) {
      this.phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
    }
    
    async getAllImg(): Promise<photoAccessHelper.PhotoAsset[]> {
      Logger.info('getAllImg');
      let photoList: Array<photoAccessHelper.PhotoAsset> = [];
      if (this.phAccessHelper === null) {
        Logger.info('phAccessHelper fail');
        return photoList;
      }
      let fileKeyType = photoAccessHelper.PhotoKeys.PHOTO_TYPE;
      let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();
      Logger.info(fileKeyType);
      let fetchOptions: photoAccessHelper.FetchOptions = {
        fetchColumns: [],
        predicates: predicates
      };
    
      try {
        let fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> =
          await this.phAccessHelper.getAssets(fetchOptions);
        if (fetchResult != undefined) {
          Logger.info('fetchResult success');
          let photoAsset: Array<photoAccessHelper.PhotoAsset> = await fetchResult.getAllObjects();
          if (photoAsset != undefined && photoAsset.length > 0) {
            for (let i = 0; i < photoAsset.length; i++) {
              if (photoAsset[i].photoType === 1) {
                photoList.push(photoAsset[i]);
              }
            }
          }
        }
      } catch (err) {
        Logger.error('getAssets failed, message = ', err);
      }
      Logger.info('photoList success');
      return photoList;
    }
    
    async splitPic(index: number): Promise<PictureItem[]> {
      let imagePixelMap: PictureItem[] = [];
      let imagesData: Array<photoAccessHelper.PhotoAsset> = await this.getAllImg();
      let imagePackerApi = image.createImagePacker();
      fileIo.open(imagesData[index].uri, fileIo.OpenMode.READ_ONLY).then(async (file: fileIo.File) => {
        let fd: number = file.fd;
        let imageSource = image.createImageSource(fd);
        let imageInfo = await imageSource.getImageInfo();
        Logger.info(TAG, `sizeImg createImageSource ${JSON.stringify(imageSource)}`);
        let height = imageInfo.size.height / Common.SPLIT_COUNT;
        for (let i = 0; i < Common.SPLIT_COUNT; i++) {
          for (let j = 0; j < Common.SPLIT_COUNT; j++) {
            let picItem: PictureItem;
            if (i === Common.SPLIT_COUNT - 1 && j === Common.SPLIT_COUNT - 1) {
              picItem = new PictureItem(Common.PICTURE_ITEM_NUMBER, {} as image.PixelMap);
              imagePixelMap.push(picItem);
            } else {
              Logger.info(TAG, `sizeImg x = ${imageInfo.size.width / Common.SPLIT_COUNT} y = ${height}`);
              let decodingOptions: image.DecodingOptions = {
                desiredRegion: {
                  size: {
                    height: height,
                    width: imageInfo.size.width / Common.SPLIT_COUNT
                  }, x: j * imageInfo.size.width / Common.SPLIT_COUNT, y: i * height
                }
              }
              imagePixelMap.push(
                new PictureItem(i * Common.SPLIT_COUNT + j, await imageSource.createPixelMap(decodingOptions)));
            }
          }
        }
        imagePackerApi.release();
        fileIo.closeSync(fd);
      })
      return imagePixelMap;
    }

    }

  • 获取本地图片:首先使用getPhotoAccessHelper获取相册管理模块实例,然后使用getAssets方法获取文件资源,最后使用getAllObjects获取检索结果中的所有文件资产方便展示;

  • 裁剪图片准备:裁剪图片需要使用@ohos.multimedia.image接口,裁剪前需要申请图片编辑权限,使用requestPermissionsFromUser申请,源码参考Index.ets;

    /*

    • Copyright (c) 2022-2023 Huawei Device Co., Ltd.
    • Licensed under the Apache License, Version 2.0 (the "License");
    • you may not use this file except in compliance with the License.
    • You may obtain a copy of the License at
    • 复制代码
      http://www.apache.org/licenses/LICENSE-2.0
    • Unless required by applicable law or agreed to in writing, software
    • distributed under the License is distributed on an "AS IS" BASIS,
    • WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    • See the License for the specific language governing permissions and
    • limitations under the License.
      */

    import { mediaquery } from '@kit.ArkUI';
    import { Permissions, abilityAccessCtrl } from '@kit.AbilityKit';
    import { emitter } from '@kit.BasicServicesKit';
    import { photoAccessHelper } from '@kit.MediaLibraryKit';
    import GameRules from '../model/GameRules';
    import ImagePicker from '../common/ImagePicker';
    import Logger from '../model/Logger';
    import PictureItem from '../model/PictureItem';
    import ImageModel from '../model/ImageModel';
    import { CommonConstants as Common } from '../common/CommonConstants';

    const PERMISSIONS: Array = [
    'ohos.permission.READ_MEDIA',
    'ohos.permission.WRITE_MEDIA',
    'ohos.permission.MEDIA_LOCATION',
    'ohos.permission.MANAGE_MISSIONS'
    ];

    @Entry
    @Component
    struct Index {
    @State numArray: PictureItem[] = [];
    @State imgData: Array<photoAccessHelper.PhotoAsset> = [];
    @State @Watch('onTimeOver') gameTime: number = Common.GAME_TIME;
    @State @Watch('onImageChange') index: number = Common.ZERO;
    @State isLand: boolean = false;
    @StorageLink('isGameStart') isGameStart: boolean = false;
    private listener = mediaquery.matchMediaSync('screen and (min-aspect-ratio: 1.5) or (orientation: landscape)');
    private ImageModel: ImageModel = new ImageModel(getContext(this));
    private game: GameRules = new GameRules();
    private timer: number = -1;
    private isRefresh: boolean = false;

    复制代码
    onLand = (mediaQueryResult: mediaquery.MediaQueryResult) => {
      Logger.info(`[eTSMediaQuery.Index]onLand: mediaQueryResult.matches=${mediaQueryResult.matches}`);
      this.isLand = mediaQueryResult.matches;
    };
    over = () => {
      Logger.info(Common.TAG, 'emitter on , eventID = 0');
      for (let i = Common.ZERO; i < Common.LOOP; i++) {
        this.numArray[i].index = i;
      }
      this.gameOver();
    };
    timeEnd = () => {
      Logger.info(Common.TAG, 'emitter on , eventID = 1');
      this.gameTime = Common.ZERO;
    };
    
    async aboutToAppear() {
      this.listener.on('change', this.onLand);
      await abilityAccessCtrl.createAtManager().requestPermissionsFromUser(getContext(this), PERMISSIONS);
      this.imgData = await this.ImageModel.getAllImg();
      Logger.info(Common.TAG, `images = ${this.imgData.length}`);
      this.numArray = await this.ImageModel.splitPic(this.index);
      // Test case. The game is simulated successfully.
      emitter.on({ eventId: Common.ZERO, priority: Common.ZERO }, this.over);
      // Test case. End of simulation time.
      emitter.on({ eventId: Common.ONE, priority: Common.ZERO }, this.timeEnd);
    }
    
    onTimeOver() {
      if (this.gameTime === Common.ZERO) {
        this.isGameStart = false;
        AlertDialog.show({ message: 'TimeOver' });
        clearInterval(this.timer);
      }
    }
    
    async onImageChange() {
      this.isRefresh = true;
      this.dialogController.close();
      this.numArray = [];
      this.numArray = await this.ImageModel.splitPic(this.index);
      this.init();
      this.isGameStart = false;
      this.isRefresh = false;
    }
    
    init() {
      this.gameTime = Common.GAME_TIME;
      clearInterval(this.timer);
    }
    
    gameOver() {
      let count = Common.ZERO;
      for (let i = Common.ZERO; i < Common.LOOP; i++) {
        if (this.numArray[i].index === i) {
          count++;
        } else {
          count = Common.ZERO;
          break;
        }
      }
      if (count === Common.LOOP) {
        this.isGameStart = false;
        AlertDialog.show({ message: $r('app.string.congratulations') });
        clearInterval(this.timer);
        this.gameTime = Common.GAME_TIME;
      }
    }
    
    start() {
      this.init();
      this.timer = setInterval(() => {
        this.gameTime--;
      }, Common.TIME_NUMBER)
    }
    
    dialogController: CustomDialogController = new CustomDialogController({
      builder: ImagePicker({
        imagesData: this.imgData,
        index: $index
      }),
      autoCancel: true,
      gridCount: Common.GRID_COUNT
    })
    
    @Builder
    ImageShow() {
      Image(this.imgData[this.index].uri)
        .id(Common.IMAGE_SHOW)
        .width(Common.IMAGE_WIDTH)
        .height($r('app.float.imageShow_height'))
        .objectFit(ImageFit.Fill)
        .onClick(async () => {
          if (this.isRefresh) {
            return;
          }
          this.imgData = await this.ImageModel.getAllImg();
          setTimeout(() => {
            this.dialogController.open();
          }, Common.TIME);
        })
    }
    
    @Builder
    ImageGrid(leftMargin: number, topMargin: number) {
      Grid() {
        ForEach(this.numArray, (item: PictureItem, index) => {
          GridItem() {
            Image(item.pixelMap)
              .width(Common.GRID_IMAGE_WIDTH)
              .objectFit(ImageFit.Fill)
              .height($r('app.float.grid_image_height'))
          }
          .id(`image${index}`)
          .backgroundColor(item.pixelMap === undefined ? $r('app.color.blank_picture_background') : $r('app.color.picture_background'))
          .onClick(() => {
            if (this.isRefresh) {
              return;
            }
            if (this.isGameStart) {
              this.isRefresh = true;
              this.numArray = this.game.gameInit(index, this.numArray);
              this.gameOver();
              this.isRefresh = false;
            }
          })
        }, (item: PictureItem) => JSON.stringify(item))
      }
      .id(Common.IMAGE_GRID)
      .columnsTemplate(Common.COLUMN_TEMPLATE)
      .columnsGap($r('app.float.gap'))
      .rowsGap($r('app.float.gap'))
      .width(Common.IMAGE_WIDTH)
      .height($r('app.float.grid_height'))
      .margin({ left: leftMargin, top: topMargin })
    }
    
    build() {
      Column() {
        Row() {
          Text(`Time:0${Math.floor(this.gameTime / Common.SECOND)}:${this.gameTime % Common.SECOND < Common.TEN
            ? Common.STRING_ZERO + this.gameTime % Common.SECOND : this.gameTime % Common.SECOND}`)
            .id(Common.TIME_STRING)
            .margin({ top: Common.MARGIN, bottom: Common.MARGIN })
        }
    
        if (this.imgData.length > Common.ZERO) {
          if (this.isLand) {
            Row() {
              this.ImageShow()
              this.ImageGrid(Common.TEN, Common.ZERO)
            }
            .margin({ top: Common.MARGIN })
          } else {
            Column() {
              this.ImageShow()
              this.ImageGrid(Common.ZERO, Common.FIVE)
            }
            .margin({ top: Common.MARGIN })
          }
        }
        Button($r('app.string.start'), { type: ButtonType.Capsule, stateEffect: true })
          .id(Common.START_BUTTON)
          .height($r('app.float.button_height'))
          .width(Common.FULL_WIDTH)
          .fontSize($r('app.float.button_font_size'))
          .margin({ top: Common.MARGIN })
          .backgroundColor(this.isGameStart ? $r('app.color.forbid') : $r('app.color.allow'))
          .enabled(!this.isGameStart)
          .onClick(() => {
            this.isGameStart = true;
            this.start();
            this.numArray = this.game.gameBegin(this.numArray);
          })
    
        Button($r('app.string.restart'), { type: ButtonType.Capsule, stateEffect: true })
          .id(Common.RESTART_BUTTON)
          .height($r('app.float.button_height'))
          .width(Common.FULL_WIDTH)
          .fontSize($r('app.float.button_font_size'))
          .margin({ top: Common.MARGIN })
          .backgroundColor(this.isGameStart ? $r('app.color.allow') : $r('app.color.forbid'))
          .enabled(this.isGameStart)
          .onClick(() => {
            this.isGameStart = true;
            this.start();
            this.numArray = this.game.gameBegin(this.numArray);
          })
      }
      .width(Common.FULL_WIDTH)
      .height(Common.FULL_HEIGHT)
      .padding({ left: Common.PERCENT, right: Common.PERCENT })
    }

    }

  • 图片编辑:首先使用createImagePacker创建ImagePacker实例,然后使用fs.open打开文件,调用createImageSource接口创建图片源实例方便操作图片,接下来使用getImageInfo方法获取图片大小便于分割,最后使用createPixelMap方法传入每一份的尺寸参数完成图片裁剪。

以上就是本篇文章所带来的鸿蒙开发中一小部分技术讲解;想要学习完整的鸿蒙全栈技术。可以在结尾找我可全部拿到!

下面是鸿蒙的完整学习路线 ,展示如下:

除此之外,根据这个学习鸿蒙全栈学习路线,也附带一整套完整的学习【文档+视频】,内容包含如下:

内容包含了:(ArkTS、ArkUI、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、鸿蒙南向开发、鸿蒙项目实战)等技术知识点。帮助大家在学习鸿蒙路上快速成长!

鸿蒙【北向应用开发+南向系统层开发】文档

鸿蒙【基础+实战项目】视频

鸿蒙面经

为了避免大家在学习过程中产生更多的时间成本,对比我把以上内容全部放在了↓↓↓想要的可以自拿喔!谢谢大家观看!

相关推荐
非优秀程序员4 小时前
题记|《互联网产品恩仇录》
后端·程序员·创业
贾伟康8 小时前
HarmonyOS API 26 DevEco Code 需求到 ArkTS 骨架的提示词分层
harmonyos·arkts·ai coding·deveco code·工程提效
威哥爱编程9 小时前
模拟面试智能体开发实战 02:回答等半分钟,谁还愿意练?把服务端和流式输出搭起来
python·agent·harmonyos
威哥爱编程10 小时前
模拟面试智能体开发实战 01:面试挂了都不知道挂在哪?用 Python 给自己造一个随叫随到的 AI 面试官
python·agent·harmonyos
易师傅11 小时前
技术面试已死?AI 时代,真正拉开程序员差距的是什么?
前端·后端·程序员
贾伟康14 小时前
HarmonyOS API 26 HMAF Skill 可解释任务计划与失败回退
harmonyos·arkts·智能体·skill·hmaf
贾伟康14 小时前
HarmonyOS API 26 HMAF 2.0 意图解析的可解释动作图
harmonyos·arkts·智能体·a2a·hmaf
Hilaku16 小时前
你们公司有 35 岁以上的前端吗?
前端·javascript·程序员
AI前端工程狮16 小时前
服务端已经死了,客户端却报"超时"
人工智能·程序员
KevinChaos16 小时前
阳台小花园开发手记 03|HarmonyOS 7.0 互动卡片:叶片伸出边框仍与花盆对齐
华为·harmonyos