HarmonyOS鸿蒙Next中这段代码中的遮罩为什么会导致主界面(预览区域)无法触摸,或者还有什么其他方法实现快门动画

HarmonyOS鸿蒙Next中这段代码中的遮罩为什么会导致主界面(预览区域)无法触摸,或者还有什么其他方法实现快门动画

const TAG: string = 'ModeComponent';

@Component
export struct ModeComponent {
  // 存储链接和监听页面状态变化
  @StorageLink('isOpenEditPage') @Watch('changePageState') isOpenEditPage: boolean = false;
  @State sceneMode: camera.SceneMode = camera.SceneMode.NORMAL_PHOTO;
  @State isRecording: boolean = false;
  @State showShutter: boolean = false; // 控制遮罩层显示


  // 页面状态改变时触发的函数
  changePageState(): void {
    if (this.isOpenEditPage) {
      this.onJumpClick();
    }
  }

  // 页面即将出现时执行的初始化函数
  aboutToAppear(): void {
    Logger.info(TAG, 'aboutToAppear');
    CameraService.setSavePictureCallback(this.handleSavePicture);
    // 确保组件初始化时遮罩层不显示
    this.showShutter = false;
  }

  // 处理保存图片的回调函数
  handleSavePicture = (photoAsset: photoAccessHelper.PhotoAsset): void => {
    Logger.info(TAG, 'handleSavePicture');
    this.setImageInfo(photoAsset);
    CameraService.saveCameraPhoto(photoAsset);
    AppStorage.set<boolean>('isOpenEditPage', true);
    Logger.info(TAG, 'setImageInfo end');
  }

  // 设置图片信息到全局上下文中
  setImageInfo(photoAsset: photoAccessHelper.PhotoAsset): void {
    Logger.info(TAG, 'setImageInfo');
    GlobalContext.get().setObject('photoAsset', photoAsset);
  }

  // 跳转到编辑页面的点击事件处理函数
  onJumpClick(): void {
    GlobalContext.get().setObject('sceneMode', this.sceneMode);
    router.pushUrl({ url: 'pages/EditPage' }, router.RouterMode.Single, (err) => {
      if (err) {
        Logger.error(TAG, `Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
        return;
      }
      Logger.info(TAG, 'Invoke pushUrl succeeded.');
    });
  }

  // 拍照动画函数
  async animateShutter() {
    this.showShutter = true;
    await new Promise<void>(resolve => setTimeout(resolve, 50)); // 显示黑色遮罩
    await new Promise<void>(resolve => setTimeout(resolve, 100)); // 保持一段时间
    this.showShutter = false; // 隐藏黑色遮罩
  }

  // 构建页面布局
  build() {
    Stack() { // 唯一的根节点
      // 相机预览区域(假设这里有相机预览组件)
      // CameraPreview() 组件应该在这里

      // 底部控制栏 - 独立于遮罩层
      Column() {
        // 第一行包含拍照和录像模式切换按钮
        Row({ space: Constants.COLUMN_SPACE_24 }) {
          Column() {
            Text($r('app.string.take_photos'))// 拍照文本
              .fontSize(Constants.FONT_SIZE_14)
              .fontColor(Color.White)
          }
          .width(Constants.CAPTURE_COLUMN_WIDTH)
          .backgroundColor(this.sceneMode === camera.SceneMode.NORMAL_PHOTO ? $r('app.color.theme_color') : '')
          .borderRadius(Constants.BORDER_RADIUS_14)
          .onClick(async () => {
            if (this.sceneMode === camera.SceneMode.NORMAL_PHOTO) {
              return;
            }
            this.sceneMode = camera.SceneMode.NORMAL_PHOTO;
            CameraService.setSceneMode(this.sceneMode);
            const cameraDeviceIndex = GlobalContext.get().getT('cameraDeviceIndex') as number;
            const surfaceId = GlobalContext.get().getT('xComponentSurfaceId') as string;
            await CameraService.initCamera(surfaceId, cameraDeviceIndex);
          })

          Column() {
            Text($r('app.string.videotape'))// 录像文本
              .fontSize(Constants.FONT_SIZE_14)
              .fontColor(Color.White)
          }
          .width(Constants.CAPTURE_COLUMN_WIDTH)
          .backgroundColor(this.sceneMode === camera.SceneMode.NORMAL_VIDEO ? $r('app.color.theme_color') : '')
          .borderRadius(Constants.BORDER_RADIUS_14)
          .onClick(async () => {
            if (this.sceneMode === camera.SceneMode.NORMAL_VIDEO) {
              return;
            }
            this.sceneMode = camera.SceneMode.NORMAL_VIDEO;
            CameraService.setSceneMode(this.sceneMode);
            const cameraDeviceIndex = GlobalContext.get().getT('cameraDeviceIndex') as number;
            const surfaceId = GlobalContext.get().getT('xComponentSurfaceId') as string;
            await CameraService.initCamera(surfaceId, cameraDeviceIndex);
          })
        }
        .height(Constants.CAPTURE_ROW_HEIGHT)
        .width(Constants.FULL_PERCENT)
        .justifyContent(FlexAlign.Center)
        .alignItems(VerticalAlign.Center)

        // 第二行包含拍照/录像按钮和切换前后摄像头的按钮
        Row() {
          Column() {
          }
          .width($r('app.string.200px'))

          // 照片视频按钮
          Column() {
            if (!this.isRecording) {
              Row() {
                Button() {
                  Text()
                    .width($r('app.string.120px'))
                    .height($r('app.string.120px'))
                    .borderRadius($r('app.string.40px'))
                    .backgroundColor(this.sceneMode === camera.SceneMode.NORMAL_VIDEO ?
                    $r('app.color.theme_color') : Color.White)
                }
                .border({
                  width: Constants.CAPTURE_BUTTON_BORDER_WIDTH,
                  color: $r('app.color.border_color'),
                  radius: Constants.CAPTURE_BUTTON_BORDER_RADIUS
                })
                .width($r('app.string.200px'))
                .height($r('app.string.200px'))
                .backgroundColor(Color.Black)
                .onClick(async () => {
                  if (this.sceneMode === camera.SceneMode.NORMAL_PHOTO) {
                    await this.animateShutter(); // 执行动画并在结束后移除遮罩
                    await CameraService.takePicture();
                  } else {
                    await CameraService.startVideo();
                    this.isRecording = true;
                  }
                })
              }
            } else {
              Row() {
                Button() {
                  Image($r('app.media.ic_camera_video_close'))
                    .size({ width: Constants.IMAGE_SIZE, height: Constants.IMAGE_SIZE })
                }
                .width($r('app.string.120px'))
                .height($r('app.string.120px'))
                .backgroundColor($r('app.color.theme_color'))
                .onClick(() => {
                  this.isRecording = !this.isRecording;
                  CameraService.stopVideo().then(() => {
                    this.isOpenEditPage = true;
                    Logger.info(TAG, 'stopVideo success');
                  })
                })
              }
              .width($r('app.string.200px'))
              .height($r('app.string.200px'))
              .borderRadius($r('app.string.60px'))
              .backgroundColor($r('app.color.theme_color'))
              .justifyContent(FlexAlign.SpaceAround)
            }
          }

          // 前后摄像头切换
          Column() {
            Row() {
              Button() {
                Image($r('app.media.switch_camera'))
                  .width($r('app.string.120px'))
                  .height($r('app.string.120px'))
              }
              .width($r('app.string.200px'))
              .height($r('app.string.200px'))
              .backgroundColor($r('app.color.flash_background_color'))
              .borderRadius($r('app.string.40px'))
              .onClick(async () => {
                let cameraDeviceIndex = GlobalContext.get().getT('cameraDeviceIndex') as number;
                let surfaceId = GlobalContext.get().getT('xComponentSurfaceId') as string;
                cameraDeviceIndex = cameraDeviceIndex ? 0 : 1;
                GlobalContext.get().setObject('cameraDeviceIndex', cameraDeviceIndex);
                await CameraService.initCamera(surfaceId, cameraDeviceIndex);
              })
            }
          }
        }
        .padding({
          left: Constants.CAPTURE_BUTTON_COLUMN_PADDING,
          right: Constants.CAPTURE_BUTTON_COLUMN_PADDING
        })
        .width(Constants.FULL_PERCENT)
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .height('auto') // 高度自适应
      .position({ x: 0, y: '85%' }) // 定位到底部

      // 拍照动画遮罩层 - 作为主Stack的子组件
      Stack() {
        Column() {}
        .width('100%')
        .height('100%')
        .backgroundColor(Color.Black)
      }
      .width('100%')
      .height('100%')
      .visibility(this.showShutter ? Visibility.Visible : Visibility.None)
      .backgroundColor(Color.Black)
    }
    .width('100%')
    .height('100%')
  }

  // 重新初始化相机的辅助函数
  async reinitializeCamera() {
    const cameraDeviceIndex = GlobalContext.get().getT('cameraDeviceIndex') as number;
    const surfaceId = GlobalContext.get().getT('xComponentSurfaceId') as string;
    await CameraService.initCamera(surfaceId, cameraDeviceIndex);
  }
}

更多关于HarmonyOS鸿蒙Next中这段代码中的遮罩为什么会导致主界面(预览区域)无法触摸,或者还有什么其他方法实现快门动画的实战教程也可以访问 https://www.itying.com/category-93-b0.html

4 回复

你好。

把CameraPreview上面会显示的组件view设置点击穿透即可。

参见文章:【HarmonyOS 5】View点击穿透,层叠View点击事件控制 | 华为开发者联盟

更多关于HarmonyOS鸿蒙Next中这段代码中的遮罩为什么会导致主界面(预览区域)无法触摸,或者还有什么其他方法实现快门动画的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


我也帮你问问:

还有什么其他方法实现快门动画?

在HarmonyOS Next中,遮罩导致主界面无法触摸通常是因为遮罩层占用了整个屏幕事件响应区域。使用绝对布局时,上层组件会拦截下层组件的触摸事件。要实现快门动画而不影响触摸,可以尝试以下方法:

  1. 使用Stack组件时设置hitTestBehavior为Transparent
  2. 将遮罩的触摸事件设置为穿透模式
  3. 改用Opacity组件控制透明度动画
  4. 使用Canvas绘制动画而非覆盖式遮罩

快门动画也可以考虑使用ArkUI的动画能力:

  • 使用属性动画改变组件透明度
  • 使用转场动画实现快门效果
  • 通过Keyframe动画控制多段式效果,

在HarmonyOS Next中,遮罩层导致主界面无法触摸的原因是Stack布局中后添加的组件会覆盖前面的组件,且默认会拦截触摸事件。针对快门动画的实现,建议以下优化方案:

  1. 使用透明度和动画替代全屏遮罩:
// 修改State定义
@State shutterOpacity: number = 0;

// 修改动画函数
async animateShutter() {
  this.shutterOpacity = 1;
  await new Promise<void>(resolve => setTimeout(resolve, 50));
  this.shutterOpacity = 0;
}

// 修改遮罩层代码
Stack() {
  Column() {}
  .width('100%')
  .height('100%')
  .backgroundColor(Color.Black)
  .opacity(this.shutterOpacity)
  .transition({ type: TransitionType.All, opacity: { duration: 100 } })
}
  1. 如果必须使用遮罩层,可以添加触摸穿透属性:
Stack() {
  Column() {}
  .width('100%')
  .height('100%')
  .backgroundColor(Color.Black)
  .hitTestBehavior(HitTestMode.Transparent) // 关键设置
}
  1. 另一种方案是使用专门的前景动画组件:
// 在build()中添加
if (this.showShutter) {
  Canvas(this.context)
    .width('100%')
    .height('100%')
    .onReady(() => {
      // 绘制闪光动画
    })
}

这些方法都能实现快门效果而不影响主界面交互,推荐使用第一种透明度动画方案,性能最优且实现简单。

回到顶部