鸿蒙UI开发——基于onTouch事件实现表情选择胶囊

1、背 景

有朋友留言说,抖音APP中,长按评论按钮触发的快捷表情选择胶囊动画比较好(效果如下图),希望使用鸿蒙ArkTs也实现一个类似的。

本文在鸿蒙ArkTs下也实现一个类似的效果,如下:

首先,核心交互流程与抖音APP保持一致,即:

    1. 长按一个按钮,我们可以在按钮旁边生成一个快捷表情选择胶囊;

    2. 手指可以快捷的在候选表情上滑动,在选中的表情上停留时,该表情会稍微放大;

动画细节上做了一些个人效果,例如:

  • 胶囊展示到界面时,候选表情做了一个类似IOS解锁时的类飞入效果;

  • 我们在表情胶囊上滑动选择时,整个表情胶囊不会随着动画效果的改变发生宽度抖动。

下面开始介绍如何实现,文末有源代码,有需要的同学自取。

2、问题分析

上述的交互效果其实难点并不在于动画效果,在于onTouch事件的管理,因为,我们在长按评论按钮时,此时系统将会分配事件流给评论按钮消费。

如果我们想在用户不松手移动到其他位置,让其他组件也生效,实现过程将稍微复杂一些。

本文的实现思路是:监听评论按钮的onTouch事件,在事件move过程中,实时获取该事件的发生坐标,基于坐标去判断坐落的表情包位置,从而控制焦点放大效果。

|-------------------------------------------------------|
| 📢📢注意 onTouch事件的调试千万要在真机或者模拟器中执行,在预览器中执行可能会出现非预期的问题。 |

❓我们怎么获取指定组件的坐标呢?

虽然我们通过onTouch事件可以知道用户手指的位置,那我们还有一个问题没解决,就是怎么知道各个表情包的坐标呢?

ArkTs为我们提供了一个API,根据组件ID获取组件实例对象, 通过组件实例对象将获取的坐标位置和大小同步返回给调用方,接口如下:

import { componentUtils } from '@kit.ArkUI';// 调用方式let modePosition:componentUtils.ComponentInfo = componentUtils.getRectangleById("id");

3、布 局

布局比较简单,在本文中,将整个表情胶囊用一个Row包裹,另外,评论图标与表情胶囊整体属于在一个Row中。示意图如下:

为了方便动态插拔,我们将图标资源集合使用一个数组来动态维护,资源类型定义与数组定义如下:​​​​​​​

interface CommentIconInfo {  source: Resource,  id: string;}const commentIcons: Array<CommentIconInfo> =  [    {      id: 'page_main_icon1',      source: $r('app.media.send_you_a_little_red_flower'),    },    {      id: 'page_main_icon2',      source: $r('app.media.powerful'),    },    {      id: 'page_main_icon3',      source: $r('app.media.send_heart'),    }, {    id: 'page_main_icon4',    source: $r('app.media.surprise'),  },  ]

布局代码如下:​​​​​​​

build() {  Column() {    Row() {      Row() {        ForEach(this.commentIcons, (item: CommentIconInfo) => {          Image(item.source)            .id(item.id)            .width(this.selectedId === item.id ? this.selectedSize : this.normalSize)            .height(this.selectedId === item.id ? this.selectedSize : this.normalSize)            .padding({ left: this.iconMarginLeft })            .animation({ duration: this.animDuration, curve: curves.springMotion() })            .visibility(this.showCommentIconPanel ? Visibility.Visible : Visibility.None)        }, (item: CommentIconInfo) => {          return `${item.id}` // 复写id生成器        })      }      .visibility(this.showCommentIconPanel ? Visibility.Visible : Visibility.None)      .backgroundColor(Color.Pink)      .width(this.showCommentIconPanel ? this.getRowWidth() : 10)      .borderRadius(this.selectedId ? 40 : 20)      .padding({ left: this.paddingHoriz, right: this.paddingHoriz })      .animation({ duration: this.animDuration, curve: curves.springMotion() })      SymbolGlyph($r('sys.symbol.ellipsis_message_fill'))        .fontSize(60)        .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR)        .fontColor([Color.Green])        .margin({ left: 10 })        .onTouch(this.onTouchEvent)    }    .justifyContent(FlexAlign.End)    .width('100%')  }  .padding(20)  .height('100%')}

4、onTouch事件处理

我们在onTouch事件中需要做两个核心事情:

  1. 控制表情胶囊的显示/隐藏

  2. 控制用户手指指向的表情包,并聚焦放大显示

代码如下:​​​​​​​

private onTouchEvent = (event: TouchEvent) => {  if (!event) {    return;  }  // 手指按下0.5s后弹出表情选择面板  if (event.type === TouchType.Down) {    this.lastTouchDown = Date.now();  } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {    this.showCommentIconPanel = false; // 松手后,取消显示表情面板(具体消失逻辑可以根据业务需求调整)  } else if (!this.showCommentIconPanel) {    this.showCommentIconPanel = (Date.now() - this.lastTouchDown) > 500;  }  this.checkCommentIcons(event);}// 判断用户手指是否选中了某个表情包private checkCommentIcons = (event: TouchEvent) => {  const touchInfo = event.touches[0];  this.selectedId = '';  if (!touchInfo) {    return;  }  const windowX = Math.ceil(touchInfo.windowX); // 获取用户手指x坐标  const context = this.getUIContext();  // 检测用户手指x坐标可以命中的表情  this.commentIcons.forEach(iconInfo => {    if (event.type === TouchType.Up) {      return;    }    const compInfo = componentUtils.getRectangleById(iconInfo.id);    const x = Math.ceil(context.px2vp(compInfo.windowOffset.x));    const width = Math.ceil(context.px2vp(compInfo.size.width));    if (windowX >= x && windowX <= x + width) {      this.selectedId = iconInfo.id; // 范围中有表情被选中    }  });  this.commentIcons = [...this.commentIcons]; // 低成本刷新数组(浅拷贝)}

5、源代码

示例效果如下:

下方的源代码替换了11、15、19、22行的图片资源后,可以正常运行。代码如下:​​​​​​​

import { componentUtils } from '@kit.ArkUI';import { curves } from '@kit.ArkUI';interface CommentIconInfo {  source: Resource,  id: string;}const commentIcons: Array<CommentIconInfo> =  [    {      id: 'page_main_icon1',      source: $r('app.media.send_you_a_little_red_flower'),    },    {      id: 'page_main_icon2',      source: $r('app.media.powerful'),    },    {      id: 'page_main_icon3',      source: $r('app.media.send_heart'),    }, {    id: 'page_main_icon4',    source: $r('app.media.surprise'),  },  ]@Entry@Componentstruct Index {  @State selectedSize: number = 60;  @State normalSize: number = 35;  @State showCommentIconPanel: boolean = false;  @State commentIcons: Array<CommentIconInfo> = commentIcons;  @State selectedId: string = '';  // 一些本地使用的量  lastTouchDown: number = 0;  iconMarginLeft: number = 5;  paddingHoriz: number = 10; // 左右padding  animDuration: number = 500;  private onTouchEvent = (event: TouchEvent) => {    if (!event) {      return;    }    // 手指按下0.5s后弹出表情选择面板    if (event.type === TouchType.Down) {      this.lastTouchDown = Date.now();    } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {      this.showCommentIconPanel = false; // 松手后,取消显示表情面板(具体消失逻辑可以根据业务需求调整)    } else if (!this.showCommentIconPanel) {      this.showCommentIconPanel = (Date.now() - this.lastTouchDown) > 500;    }    this.checkCommentIcons(event);  }  private checkCommentIcons = (event: TouchEvent) => {    const touchInfo = event.touches[0];    this.selectedId = '';    if (!touchInfo) {      return;    }    const windowX = Math.ceil(touchInfo.windowX); // 获取用户手指x坐标    const context = this.getUIContext();    // 检测用户手指x坐标可以命中的表情    this.commentIcons.forEach(iconInfo => {      if (event.type === TouchType.Up) {        return;      }      const compInfo = componentUtils.getRectangleById(iconInfo.id);      const x = Math.ceil(context.px2vp(compInfo.windowOffset.x));      const width = Math.ceil(context.px2vp(compInfo.size.width));      if (windowX >= x && windowX <= x + width) {        this.selectedId = iconInfo.id; // 范围中有表情被选中      }    });    this.commentIcons = [...this.commentIcons]; // 低成本刷新数组(浅拷贝)  }  build() {    Column() {      Row() {        Row() {          ForEach(this.commentIcons, (item: CommentIconInfo) => {            Image(item.source)              .id(item.id)              .width(this.selectedId === item.id ? this.selectedSize : this.normalSize)              .height(this.selectedId === item.id ? this.selectedSize : this.normalSize)              .padding({ left: this.iconMarginLeft })              .animation({ duration: this.animDuration, curve: curves.springMotion() })              .visibility(this.showCommentIconPanel ? Visibility.Visible : Visibility.None)          }, (item: CommentIconInfo) => {            return `${item.id}` // 复写id生成器          })        }        .visibility(this.showCommentIconPanel ? Visibility.Visible : Visibility.None)        .backgroundColor(Color.Pink)        .width(this.showCommentIconPanel ? this.getRowWidth() : 10)        .borderRadius(this.selectedId ? 40 : 20)        .padding({ left: this.paddingHoriz, right: this.paddingHoriz })        .animation({ duration: this.animDuration, curve: curves.springMotion() })        SymbolGlyph($r('sys.symbol.ellipsis_message_fill'))          .fontSize(60)          .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR)          .fontColor([Color.Green])          .margin({ left: 10 })          .onTouch(this.onTouchEvent)      }      .justifyContent(FlexAlign.End)      .width('100%')    }    .padding(20)    .height('100%')  }  private getRowWidth() { // 防止抖动    const baseWidth =      this.paddingHoriz + this.paddingHoriz + this.commentIcons.length * this.normalSize +        (this.commentIcons.length - 1) * this.iconMarginLeft;    if (this.selectedId) {      return baseWidth + this.selectedSize - this.normalSize;    }    return baseWidth;  }}

项目源代码地址:

https://gitee.com/lantingshuxu/harmony-class-room-demos/tree/feat%2FmagicComment/
相关推荐
程序猿阿伟8 分钟前
《AI赋能鸿蒙Next,打造极致沉浸感游戏》
人工智能·游戏·harmonyos
guo_zhen_qian1 小时前
HarmonyOS命令行工具
华为·harmonyos
氦客2 小时前
Android Compose 显示底部对话框 (ModalBottomSheet),实现类似BottomSheetDialog的效果
android·dialog·ui·compose·modal·bottomsheet·底部对话框
PieroPc7 小时前
特制一个自己的UI库,只用CSS、图标、emoji图 第二版
前端·css·ui
HarmonyOS_SDK8 小时前
意图框架习惯推荐方案,为用户提供个性化内容分发
harmonyos
行十万里人生10 小时前
从 SQL 语句到数据库操作
数据库·sql·华为od·华为·oracle·华为云·harmonyos
对自己不够狠12 小时前
HarMonyOS使用Tab构建页签
前端·华为·harmonyos
恋猫de小郭12 小时前
深入 Flutter 和 Compose 在 UI 渲染刷新时 Diff 实现对比
flutter·ui
慧集通-让软件连接更简单!12 小时前
慧集通(DataLinkX)iPaaS集成平台-系统管理之UI库管理、流程模板
ui·api·erp·系统集成·连接器·集成平台
时光凉忆13 小时前
鸿蒙开发 - 自定义组件 和 组件通信的方法
harmonyos·鸿蒙开发