1、背 景
有朋友留言說,抖音APP中,長按評論按鈕觸發的快捷表情選擇膠囊動畫比較好(效果如下圖),希望使用鴻蒙ArkTs也實現一個類似的。
本文在鴻蒙ArkTs下也實現一個類似的效果,如下:
首先,核心交互流程與抖音APP保持一致,即:
-
-
長按一個按鈕,我們可以在按鈕旁邊生成一個快捷表情選擇膠囊;
-
手指可以快捷的在候選表情上滑動,在選中的表情上停留時,該表情會稍微放大;
-
動畫細節上做了一些個人效果,例如:
-
膠囊展示到界面時,候選表情做了一個類似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事件中需要做兩個核心事情:
-
控制表情膠囊的顯示/隱藏
-
控制用戶手指指向的表情包,并聚焦放大顯示
代碼如下:???????
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
@Component
struct 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/