鸿蒙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/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/65908.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Node.js——http 模块(二)

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

研华 PCI-1751 驱动更新导LabVIEW致程序异常

问题描述&#xff1a; 某 LabVIEW 程序长期运行正常&#xff0c;但在使用研华 PCI-1751 数据采集卡运行一段时间后&#xff0c;程序开始出现不正常的行为。具体过程如下&#xff1a; 初始问题&#xff1a; 更换新的 PCI-1751 板卡后&#xff0c;驱动程序被更新&#xff0c;但程…

接上篇基于Alertmanager 配置钉钉告警

Alertmanager 是一个用于处理和管理 Prometheus 警报的开源工具。它负责接收来自 Prometheus 服务器的警报&#xff0c;进行去重、分组、静默、抑制等操作&#xff0c;并通过电子邮件、PagerDuty、Slack 等多种渠道发送通知。 主要功能 去重&#xff1a;合并相同或相似的警报&…

网络原理(三)—— 传输层 之 UDP 和 TCP协议

传输层 在传输层两大关键的协议就是UDP和TCP协议了&#xff0c;除此之外&#xff0c;还有别的传输层协议&#xff0c;本文章将介绍UDP和TCP协议&#xff0c;重点介绍TCP协议。 首先回顾TCP和UDP 的特点&#xff1a; UDP&#xff1a;不可靠传输&#xff0c;面向数据包&#xf…

针对服务器磁盘爆满,MySql数据库始终无法启动,怎么解决

&#xff08;点击即可进入聊天助手&#xff09; 很多站长在运营网站的过程当中都会遇到一个问题,就是网站突然无法打开,数据一直无法启动 无论是强制重启还是,删除网站内的所有应用,数据库一直无法启动 这个时候,就需要常见的运维手段了,需要对服务器后台各个资源,进行逐一排查…

高性能现代PHP全栈框架 Spiral

概述 Spiral Framework 诞生于现实世界的软件开发项目是一个现代 PHP 框架&#xff0c;旨在为更快、更清洁、更卓越的软件开发提供动力。 特性 高性能 由于其设计以及复杂精密的应用服务器&#xff0c;Spiral Framework框架在不影响代码质量以及与常用库的兼容性的情况下&a…

【面试题】Spring/SpringBoot部分[2025/1/6 ~ 2025/1/12]

Spring/SpringBoot部分[2025/1/6 ~ 2025/1/12] 1. 说说 Spring 启动过程&#xff1f;2. 说说 Springboot 的启动流程&#xff1f;3. 你了解的 Spring 都用到哪些设计模式&#xff1f;4. Spring 有哪几种事务传播行为?5. SpringBoot 是如何实现自动配置的&#xff1f;6. Spring…

【机器学习:十八、更高级的神经网络概念】

1. 梯度下降法的改进&#xff1a;Adam算法 1.1 Adam算法简介 Adam&#xff08;Adaptive Moment Estimation&#xff09;是一种优化算法&#xff0c;结合了动量梯度下降和 RMSProp 的优点&#xff0c;在处理稀疏梯度和高维空间优化时表现尤为出色。其核心在于动态调整每个参数…

计算机网络之---VPN与隧道协议

VPN与隧道协议 VPN&#xff08;虚拟专用网络&#xff09;和隧道协议是现代网络安全技术的重要组成部分&#xff0c;它们主要用于在不安全的公共网络&#xff08;如互联网&#xff09;上建立一个安全的私密网络连接。VPN通过加密通信和认证机制&#xff0c;确保数据的隐私性和完…

【STM32-学习笔记-6-】DMA

文章目录 DMAⅠ、DMA框图Ⅱ、DMA基本结构Ⅲ、不同外设的DMA请求Ⅳ、DMA函数Ⅴ、DMA_InitTypeDef结构体参数①、DMA_PeripheralBaseAddr②、DMA_PeripheralDataSize③、DMA_PeripheralInc④、DMA_MemoryBaseAddr⑤、DMA_MemoryDataSize⑥、DMA_MemoryInc⑦、DMA_DIR⑧、DMA_Buff…

SQL Server中可以通过扩展事件来自动抓取阻塞

在SQL Server中可以通过扩展事件来自动抓取阻塞&#xff0c;以下是详细流程&#xff1a; 开启阻塞跟踪配置&#xff1a; • 执行以下SQL语句来启用相关配置&#xff1a; EXEC sp_configureshow advanced options, 1; RECONFIGURE; EXEC sp_configure blocked process thresh…

DNS解析域名简记

域名通常是由: 权威域名.顶级域名.根域名组成的。 从左往右&#xff0c;级别依次升高&#xff0c;这和外国人从小范围到大范围的说话习惯相关。&#xff08;我们自己是更习惯先说大范围再说小范围&#xff0c;如XX省XX市XX区XX路&#xff09; DNS解析域名时&#xff0c;会先查…

【爬虫】单个网站链接爬取文献数据:标题、摘要、作者等信息

源码链接&#xff1a; https://github.com/Niceeggplant/Single—Site-Crawler.git 一、项目概述 从指定网页中提取文章关键信息的工具。通过输入文章的 URL&#xff0c;程序将自动抓取网页内容 二、技术选型与原理 requests 库&#xff1a;这是 Python 中用于发送 HTTP 请求…

关于扫描模型 拓扑 和 传递贴图工作流笔记

关于MAYA拓扑和传递贴图的操作笔记 一、拓扑低模: 1、拓扑工作区位置: 1、准备出 目标 高模。 (高模的状态如上 ↑ )。 2、打开顶点吸附,和建模工具区,选择四边形绘制. 2、拓扑快捷键使…

解决无法远程管理Windows Server服务器核心安装

问题 有时&#xff0c;人们会为了节省运算资源&#xff0c;例如运行Hyper-V虚拟机&#xff0c;而选择Windows Server核心安装&#xff0c;即无图形化界面。这时&#xff0c;我们就只能通过Powershell命令对其进行操控&#xff0c;或为了获得图形化界面而使用远程服务器管理工具…

SQL HAVING 子句深入解析

SQL HAVING 子句深入解析 介绍 SQL&#xff08;Structured Query Language&#xff09;是一种用于管理关系数据库管理系统的标准编程语言。在SQL中&#xff0c;HAVING子句是与GROUP BY子句一起使用的&#xff0c;用于筛选分组后的数据。它根据聚合函数的结果对组进行条件过滤…

【计算机网络】lab7 TCP协议

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;计算机网络_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 实验目的…

JavaRestClient 客户端初始化+索引库操作

1. 介绍 ES官方提供了各种不同语言的客户端&#xff0c;用来操作ES。这些客户端的本质就是组装DSL语句&#xff0c;通过http请求发送给ES。 Elasticsearch目前最新版本是8.0&#xff0c;其java客户端有很大变化。不过大多数企业使用的还是8以下版本 2. 客户端初始化 在elastic…

【JVM-2.2】使用JConsole监控和管理Java应用程序:从入门到精通

在Java应用程序的开发和运维过程中&#xff0c;监控和管理应用程序的性能和资源使用情况是非常重要的。JConsole是Java Development Kit&#xff08;JDK&#xff09;自带的一款图形化监控工具&#xff0c;它可以帮助开发者实时监控Java应用程序的内存、线程、类加载以及垃圾回收…

基于html5实现音乐录音播放动画源码

源码介绍 基于html5实现音乐录音播放动画源码是一款类似Shazam的UI&#xff0c;点击按钮后&#xff0c;会变成为一个监听按钮。旁边会有音符飞入这个监听按钮&#xff0c;最后转换成一个音乐播放器。 效果预览 源码获取 基于html5实现音乐录音播放动画源码