【如此之白】OpenHarmony ArkUI实现Web API Drag拖拽效果 原创 精华
大家经常浏览网页,是不是发现鼠标按住图片拖动后,可以拖出一个虚影。
对于WEB前端,Web API提供drag接口,很简单就可以实现一个这样的效果。
对于OpenHarmony应用开发的小伙伴们,我们也可以利用onTouch实现一个这样的效果。
拖拽效果展示
完整代码查看:
https://gitee.com/hytyj_hamstermie/tian-open-document.git
ArkUI实现Web API Drag拖拽效果/demo/page.ets
实现思路
实现思路很简单。
我们设置一个模拟块
,这个模拟块
是永远叠在最上面的。
模拟块
一开始是隐藏的。
当用户触摸到绑定touch事件的一个固定块
后,这个模拟块将拷贝该固定块
的所有样式、内容,并获取该固定块
的位置。
显示模拟块
,并覆盖在固定块
上。
在触摸事件持续过程中,模拟块
跟随手指触摸坐标进行自身的位移更新。
在触摸事件结束后,模拟块
消失。
开发环境
- IDE: DevEco Studio 3.0beta2 Build Version 3.0.0.800
- SDK Version: 8
- supportSystem “standard”
- 系统版本:OpenHarmony 3.1beta
实践
效果如图:
定义块样式
首先,我们先定义出固定块的样式。使用@Extend装饰器,我们可以更快地复制块样式。
使用@Extend装饰器
IDE中使用@Extend装饰器会报错Duplicate function implementation.
但OpenHarmony是支持@Extend装饰器的。推测可能属于IDE问题,这里推荐使用typescript忽略。
// 单行忽略
// @ts-ignore
// 忽略全文
// @ts-nocheck
// 取消忽略全文
// @ts-check
请慎用@ts-nocheck,之前因为方便,直接使用了忽略全文,导致调试时,很多语法问题没有修改回来。
我们给所有@Extend
上面标注上// @ts-ignore
- 块样式
// @ts-ignore
@Extend(Flex) function blockStyle (opacity: number, backgroundColor: Color, width: number, height: number) {
.width(width)
.height(height)
.opacity(opacity)
.backgroundColor(backgroundColor)
}
- 块内文字样式
// @ts-ignore
@Extend(Text) function blockTextStyle (color: Color, size: number) {
.fontColor(color)
.fontSize(size)
.align(Alignment.Center)
}
注意:
- @Extend装饰器方法是无法定义默认参数的
- @Extend装饰器不能在struct中使用
定义固定块数据
@State装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的build方法进行UI刷新。
//定义块组数据
@State blockInfos: any[] = [
{
text: "A",
textsize: 30,
color: "#ffffff",
bgcolor: "#336699",
opacity: 1
},
{
text: "B",
textsize: 30,
color: "#ffffff",
bgcolor: "#339961",
opacity: 1
},
{
text: "C",
textsize: 30,
color: "#ffffff",
bgcolor: "#929933",
opacity: 1
},
{
text: "D",
textsize: 30,
color: "#ffffff",
bgcolor: "#995033",
opacity: 1
}
]
简单块样式相关数据:
- 文字内容
- 文字大小
- 文字颜色
- 背景颜色
- 透明度
定义渲染固定块方法
@Builder装饰器必须在struct内使用
@Builder装饰的方法可以定义默认参数
把主要传入参数放在最前:
- 文字内容
- 背景颜色
- 透明度
- touch事件方法
//可以设置默认参数
@Builder ItemBlock(
text:string,
bgcolor: Color,
opacity: number,
touchevent: any,
textcolor: Color = Color.White,
textsize: number = 30,
width: number = 90,
height: number = 90,
) {
Flex({
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Center
}) {
Text(text).blockTextStyle(textcolor, textsize)
}
.blockStyle(opacity, bgcolor, width, height)
.onTouch(touchevent)
}
定义渲染模拟块方法
定义模拟块相关状态数据
//位移坐标x
@State x: number = 0
//位移坐标y
@State y: number = 0
//是否显示
@State flayerShow: boolean = false
//模拟块宽度
@State flayerWidth: number = 0
//模拟块高度
@State flayerHeight: number = 0
//模拟块文字内容
@State flayerText: string = ""
//模拟块文字大小
@State flayerTextSize: number = 30
//模拟块文字颜色
@State flayerTextColor: Color = Color.White
//模拟块背景颜色
@State flayerBgColor: Color = Color.Black
实现
@Builder DragBlock(
text:string = this.flayerText,
bgcolor: Color = this.flayerBgColor,
textcolor: Color = this.flayerTextColor,
textsize: number = this.flayerTextSize,
width: number = this.flayerWidth,
height: number = this.flayerHeight,
opacity: number = 1
) {
Flex({
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Center
}) {
Text(this.flayerText).blockTextStyle(textcolor, textsize)
}
.blockStyle(opacity, bgcolor, width, height)
.position({
x: this.x,
y: this.y
})
}
布局
//堆叠容器
Stack() {
//固定块
Flex({
direction: FlexDirection.Column,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.Center
}) {
Grid() {
// OpenHarmony 3.1beta已修复ForEach返回index失效问题
ForEach(this.blockInfos, (info, index) => {
GridItem() {
Flex({
direction: FlexDirection.Column,
alignItems: ItemAlign.Center,
justifyContent: FlexAlign.SpaceAround
}) {
this.ItemBlock(
info.text,
info.bgcolor,
info.opacity,
(e: TouchEvent) => {
//注意这里,就是实现拖拽的重点
})
}
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr')
.backgroundColor("#dddddd")
.margin(20)
.height(300)
}
.backgroundColor("#f8f8f8")
.width("100%")
.height("100%")
//模拟块,叠在最上层,通过flayerShow控制其展示
if (this.flayerShow) {
this.DragBlock()
}
}
.width('100%')
.height('100%')
了解onTouch
参考HarmonyOS触摸事件:https://developer.harmonyos.com/cn/docs/documentation/doc-references/ts-universal-events-touch-0000001158261221
OpenHarmony区别
OpenHarmony对比HarmonyOS的API文档,在TouchEvent中多返回了一个target对象。
target携带了当前绑定触摸组件的基本信息,使我们实现一些能力更加便利。
TouchEvent信息示例:
{
"timestamp":345459939477,
"target":{
"area":{
"pos":{"x":0,"y":30},
"globalPos":{"x":88.33333333333333,"y":520},
"width":90,
"height":90
}
},
"touches":[{"type":0,"id":1,"screenX":126,"screenY":571.5,"x":37.66666793823242,"y":51.5}],
"type":0,
"changedTouches":[{"type":0,"id":1,"screenX":126,"screenY":571.5,"x":37.66666793823242,"y":51.5}]
}
触摸事件
名称 | 是否冒泡 | 功能描述 |
---|---|---|
onTouch(callback: (event?: TouchEvent) => void) | 是 | 触摸动作触发该方法调用,event参数见TouchEvent介绍。 |
TouchEvent对象说明
属性
属性名称 | 类型 | 描述 |
---|---|---|
type | TouchType | 触摸事件的类型。 |
touches | Array<TouchObject> | 全部手指信息。 |
changedTouches | Array<TouchObject> | 当前发生变化的手指信息。 |
timestamp | number | 事件时间戳。 |
target | TargetObject | 触发组件的信息,仅TouchType.Down时有信息 |
TouchObject对象说明
属性名称 | 类型 | 描述 |
---|---|---|
type | TouchType | 触摸事件的类型。 |
id | number | 手指唯一标识符。 |
screenX | number | 触摸点相对于设备屏幕左边沿的X坐标。 |
screenY | number | 触摸点相对于设备屏幕上边沿的Y坐标。 |
x | number | 触摸点相对于被触摸元素左边沿的X坐标。 |
y | number | 触摸点相对于被触摸元素上边沿的Y坐标。 |
TouchType枚举说明
名称 | 描述 |
---|---|
Down | 手指按下时触发。 |
Up | 手指抬起时触发。 |
Move | 手指按压态在屏幕上移动时触发。 |
Cancel | 触摸事件取消时触发。 |
TargetObject对象说明
属性名称 | 类型 | 描述 |
---|---|---|
area | AreaObject | 触摸到的组件区域信息 |
AreaObject对象说明
属性名称 | 类型 | 描述 |
---|---|---|
pos | 坐标对象{x,y} | 未知 |
globalPos | 坐标对象{x,y} | 组件相对于设备屏幕左上边沿的坐标 |
width | number | 组件宽度信息 |
height | number | 组件高度信息 |
实现onTouch方法
现在就让我们来实现最重要的onTouch方法啦
一个拖拽的实现会有三个阶段:
- 手指按下,对应TouchType.Down
- 手指拖动,对应TouchType.Move
- 手指松开,对应TouchType.Up
if (e.type === TouchType.Down) {
//手指按下
} else if (e.type === TouchType.Move) {
//手指拖动
} else if (e.type === TouchType.Up) {
//手指松开
}
手指按下
在IDE中,获取到的坐标,组件的高宽,返回的数据类型并不是API文档中写的number
而是一个Length
类型。
Length
是一个长度类型。
名称 | 类型定义 | 描述 |
---|---|---|
Length | string | number | 用于描述尺寸单位,输入为number类型时,使用vp单位;输入为string类型时,需要显式指定像素单位,如’10px’,也可设置百分比字符串,如’100%'。 |
由于之前我们定义的数据类型为number
,所以这里我们可以使用as
类型断言,将当前数据类型断言为number
。
if (e.type === TouchType.Down) {
//拷贝块样式
let areainfo = e.target.area
//断言数据类型
this.flayerWidth = areainfo.width as number
this.flayerHeight = areainfo.height as number
this.flayerText = info.text
this.flayerTextSize = info.textsize
this.flayerTextColor = info.color
this.flayerBgColor = info.bgcolor
//获取初始坐标
//断言数据类型
this.x = areainfo.globalPos.x as number
this.y = areainfo.globalPos.y as number
//拷贝完成,展示模拟块
this.flayerShow = true
//将的对应块中的透明度数据设置为0.2
this.blockInfos[index].opacity = 0.2
}
手指拖动
将模拟块的坐标不断更新为手指的触摸坐标。
我们希望拖动的是块的中心点,那么就要将坐标做一个偏移处理。偏移量就是:块的宽度/2,块的高度/2。
else if (e.type === TouchType.Move) {
this.x = e.touches[0].screenX - this.flayerWidth / 2
this.y = e.touches[0].screenY - this.flayerHeight / 2
}
手指松开
手指松开后这个拖拽操作也终止了,那么就应该让这个模拟块消失了。
else if (e.type === TouchType.Up) {
this.flayerShow = false
this.blockInfos[index].opacity = 1
}
这样一个简单的拖拽就完成了,打开Previewer看看效果吧。
问题
虽然在模拟器上能很好运行,但是在开发板上出现了问题:
应用运行在开发板上时,TouchEvent内的所有值,都比应得值小了1倍,在进行拖动时,会发现位置有很大偏移。
这个问题已提交issue,希望OpenHarmony能够解决吧。
另外,由于缺少GPU支持,在开发板上会很卡。
开发板上查看
想要在开发板(3516/rk3568)上查看的小伙伴,可以将touchEvent返回数值*2就可以看到正常的效果。
if (e.type === TouchType.Down) {
let areainfo = e.target.area
//返回的高宽信息*2
this.flayerWidth = areainfo.width as number * 2
this.flayerHeight = areainfo.height as number * 2
this.flayerText = info.text
this.flayerTextSize = info.textsize
this.flayerTextColor = info.color
this.flayerBgColor = info.bgcolor
this.flayerShow = true
//返回的坐标*2
this.x = areainfo.globalPos.x as number * 2
this.y = areainfo.globalPos.y as number * 2
this.blockInfos[index].opacity = 0.2
}else if (e.type === TouchType.Move) {
this.x = e.touches[0].screenX * 2 - this.flayerWidth / 2
this.y = e.touches[0].screenY * 2 - this.flayerHeight / 2
}
又一个程序员女神。不错的文章,谢谢分享!