基于 Canvas 实现翻页转场动画鸿蒙示例代码

本文原创发布在华为开发者社区,更多鸿蒙场景化示例请见华为开发者联盟官网"行业实践与常见问题"专题页。

介绍

本示例通过自绘制渲染节点 RenderNode 和 @ohos.graphics.drawing 绘制模块,绘制书页翻页动画效果。可用于页面切换等场景。

基于 Canvas 实现翻页转场动画源码链接

效果预览

使用说明

点击右上角 "切换" 图标,切换到新页面。再次点击,切换回原页面。

实现思路

根据书页翻页动画的图形特征,在对原页面截图后,通过 drawing.Canvas 分别绘制原页面和翻页区域动画,通过定时器实现过渡动画。

1. 分析翻页图形特征

翻页时的图案可分为三个区域:绿色为原页面,记为 A;红色为原页面背部,记为 C;黄色为下一页内容,记为 B。

对图形进行数学化之后,可以得到点 c、d、b 是以点 e 为控制点的贝塞尔曲线上的点。同理,点 j、i、k 是以点 h 为控制点的贝塞尔曲线上的点。点 a 为翻角的顶点,线段 eh 是线段 af 的中垂线。

我们可以假设,线段 ce = 线段 ef * 0.5,点 p 为线段 cb 的中点,点 d 是线段 pe 的中点,点 b 则是线段 ae 与 cj 的交点。

由此,在已知点 a 和 点 f 坐标时,可通过计算得到其他点的坐标。

kotlin 复制代码
private calculate(): void {
  this.pointG.x = (this.pointA.x + this.pointF.x) / 2;
  this.pointG.y = (this.pointA.y + this.pointF.y) / 2;

  this.pointE.x = this.pointG.x -
    (this.pointF.y - this.pointG.y) * (this.pointF.y - this.pointG.y) / (this.pointF.x - this.pointG.x);
  this.pointE.y = this.pointF.y;

  this.pointH.x = this.pointF.x;
  this.pointH.y = this.pointG.y -
    (this.pointF.x - this.pointG.x) * (this.pointF.x - this.pointG.x) / (this.pointF.y - this.pointG.y);

  this.pointC.x = this.pointE.x - (this.pointF.x - this.pointE.x) / 2;
  this.pointC.y = this.pointF.y;

  this.pointJ.x = this.pointF.x;
  this.pointJ.y = this.pointH.y - (this.pointF.y - this.pointH.y) / 2;

  let slopeKcj = (this.pointJ.y - this.pointC.y) / (this.pointJ.x - this.pointC.x);
  let slopeKae = (this.pointA.y - this.pointE.y) / (this.pointA.x - this.pointE.x);
  let slopeKah = (this.pointA.y - this.pointH.y) / (this.pointA.x - this.pointH.x);

  this.pointB.x = (this.pointA.y - slopeKae * this.pointA.x + slopeKcj * this.pointJ.x - this.pointJ.y) / (slopeKcj - slopeKae);
  this.pointB.y = slopeKcj * (this.pointB.x - this.pointJ.x) + this.pointJ.y;

  this.pointK.x = (this.pointA.y - slopeKah * this.pointA.x + slopeKcj * this.pointJ.x - this.pointJ.y) / (slopeKcj - slopeKah);
  this.pointK.y = slopeKcj * (this.pointK.x - this.pointJ.x) + this.pointJ.y;

  this.pointD.x = this.pointE.x / 2 + (this.pointC.x + this.pointB.x) / 4;
  this.pointD.y = this.pointE.y / 2 + (this.pointC.y + this.pointB.y) / 4;

  this.pointI.x = this.pointH.x / 2 + (this.pointJ.x + this.pointK.x) / 4;
  this.pointI.y = this.pointH.y / 2 + (this.pointJ.y + this.pointK.y) / 4;
}

2. 绘制翻页图形

通过计算点,就可以绘制出 A 区域和 C 区域。由于 C 区域的路径曲线无法直接得到,我们可以大致绘制 C 区域轮廓,再通过叠加绘制 A 区域,得到两区域相交位置曲线。

A 区域需要绘制原页面,通过 componentSnapshot.getSync 获取组件截图 pixelMap,再绘制到 Canvas 上。需要确保原组件长度足够覆盖住下面的组件,否则动画时会遮挡不住下面的组件。启用沉浸式状态栏时,需要额外长度截图以补足状态栏安全区偏移。

ini 复制代码
async draw(context: DrawContext) {
  const canvas = context.canvas;

  let pointF = new PathPoint(viewWidth, 0);
  let pointA = new PathPoint(this.posX, this.posY);
  this.pathInfo.updatePoint(pointA, pointF);

  let pathCBrush = new drawing.Brush();
  pathCBrush.setColor({
    alpha: 255,
    red: 225,
    green: 225,
    blue: 220
  });

  let pathC = this.pathInfo.getPathC();
  canvas.attachBrush(pathCBrush);
  canvas.drawPath(pathC);

  let pathA = this.pathInfo.getPathA(viewWidth, viewHeight);
  canvas.clipPath(pathA, drawing.ClipOp.INTERSECT, true);
  canvas.drawImage(this.pixelMap, 0, 0);
}

需要注意的是,如果开启沉浸式状态栏效果,则需要对画布位置进行调节。

kotlin 复制代码
makeNode(uiContext: UIContext): FrameNode {
  this.rootNode = new FrameNode(uiContext);
  if (this.rootNode === null) {
    return this.rootNode;
  }

  const renderNode = this.rootNode.getRenderNode();
  if (renderNode !== null) {
    renderNode.frame = {
      x: 0,
      y: -px2vp(statusBarHeight), // 如果设置了沉浸式状态栏需要在这里进行偏移,否则直接设置 0
      width: px2vp(viewWidth),
      height: px2vp(viewHeight)
    };
    renderNode.pivot = { x: 50, y: 50 };
  }
  return this.rootNode;
}

3. 动画转场

因为使用 Canvas 自绘制效果,需要通过定时器来实时计算各点的位置,实现动画过渡

kotlin 复制代码
// 开始翻页转场
startFlipSwitch() {
  this.showFlipAnimation = true;
  this.positionX = this.windowWidth;
  this.positionY = 0;
  this.progress = 0;
  this.pixelMap = componentSnapshot.getSync('page', { waitUntilRenderFinished: true });
  this.paperNodeController.newRectNode(this.positionX, this.positionY, this.pixelMap);

  // 渲染存在延迟,防止出现截图闪烁,加一个延时
  setTimeout(() => {
    this.switchPage = !this.switchPage;
  }, 50);

  // 通过定时器来控制动画
  this.timerId = setInterval(() => {
    this.updateAnimationPos();
  }, 20);
}

// 更新 A 点坐标
updateAnimationPos() {
  this.progress++;
  this.positionY = this.progress * 10;
  this.positionX = this.windowWidth * (1 - this.progress / 30);
  this.paperNodeController.newRectNode(this.positionX, this.positionY, this.pixelMap);

  if (this.progress >= 100) {
    clearInterval(this.timerId);
    this.progress = 0;
    this.showFlipAnimation = false;
  }
}
相关推荐
工一木子36 分钟前
URL时间戳参数深度解析:缓存破坏与前端优化的前世今生
前端·缓存
半点寒12W2 小时前
微信小程序实现路由拦截的方法
前端
某公司摸鱼前端3 小时前
uniapp socket 封装 (可拿去直接用)
前端·javascript·websocket·uni-app
要加油哦~3 小时前
vue | 插件 | 移动文件的插件 —— move-file-cli 插件 的安装与使用
前端·javascript·vue.js
小林学习编程3 小时前
Springboot + vue + uni-app小程序web端全套家具商场
前端·vue.js·spring boot
柳鲲鹏3 小时前
WINDOWS最快布署WEB服务器:apache2
服务器·前端·windows
weixin-a153003083164 小时前
【playwright篇】教程(十七)[html元素知识]
java·前端·html
ai小鬼头5 小时前
AIStarter最新版怎么卸载AI项目?一键删除操作指南(附路径设置技巧)
前端·后端·github
一只叫煤球的猫5 小时前
普通程序员,从开发到管理岗,为什么我越升职越痛苦?
前端·后端·全栈
vvilkim5 小时前
Electron 自动更新机制详解:实现无缝应用升级
前端·javascript·electron