概要
在项目开发中,我们需要做出一个跟随用户手指拖动的摇杆,并且有侧重渲染颜色的实现。
思路流程
提示:在完成需求过程中,我们需要养成一个思维习惯。我们按照这个控件的功能特性,思考实现步骤
按照我们提出的这个控件功能特性,一般可以分三步完成:
- 分别绘制背景圆 和 移动的圆环
- 添加手势控制,控制圆环在背景圆内移动
- 添加渐变效果
技术细节
在开发过程中我选择了RelativeContainer相对布局容器,在实现布局上相对简单方便。
- RelativeContainer的使用参考如下,我写的一个小案例:
RelativeContainer(){
Text('xxx')
.width(xxx)
.height(xxx)
//指定设置在相对容器中子组件的对齐规则,仅当父容器为RelativeContainer时生效。
.alignRules({
//anchor为anchor:设置作为锚点的组件的id值。`__container__`是指外层容器。
//align:设置相对于锚点组件的对齐方式。
top: { anchor: `__container__`, align: VerticalAlign.Top },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
//组件的唯一标识,唯一性由使用者保证。
.id('text')
Image('app.media.xxx')
.alignRules({
//锚点的组件为Text
top: { anchor: 'text', align: VerticalAlign.Bottom },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
}
//容器属性
.width(...)
.height(...)
在这次开发中,我们会用到Canvas组件用于自定义绘制图形。
我们做图所示的摇杆,中间图式用canvas实现.其他部分用组件实现。
ui代码+手势代码如下所示:
RelativeContainer() {
//圆形方向盘
Column() {
}
.id('circle')
.height( 294)
.margin({
top: CommonConstants.SIZE_24,
bottom:CommonConstants.SIZE_32
})
.aspectRatio(CommonConstants.SIZE_1)
.borderRadius(147)
.alignRules({
top: { anchor: 'slider', align: VerticalAlign.Bottom },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
/*
* 方向盘参数:
* 大盘294 ---- r=147
*
* 控制器参数:
* 小盘96 ---- r=48
*
* X/Y偏移量的最大距离为 大圆半径-小圆半径
* Max:
* 小盘r=61
*
* */
//color显示
//拖拽方向
Canvas(this.context) {
}
.zIndex(2)
.onReady(() => {
//绘制侧重颜色
this.initCircle(this.context,'#4DFFFFFF',CommonConstants.SIZE_128)
})
.translate({ x: this.offsetX, y: this.offsetY, z: 0 })
.id('steering_wheel')
.alignRules({
center: { anchor: 'circle', align: VerticalAlign.Center },
middle: { anchor: 'circle', align: HorizontalAlign.Center }
})
.gesture(
PanGesture()
.onActionStart(() => {
})
.onActionUpdate((event: GestureEvent | undefined) => {
this.moving = true
if (event) {
this.maxLines = 0
let res = this.limitBorder(event.offsetX, event.offsetY, 109, 48)
this.offsetX = res[0]
this.offsetY = res[1]
this.maxLines = res[2]
//绘制侧重颜色
this.circle(this.context, '#4DFFFFFF',CommonConstants.SIZE_128, this.angle)
}
})
.onActionEnd((event: GestureEvent) => {
//恢复默认颜色
this.initCircle(this.context, '#4DFFFFFF',CommonConstants.SIZE_128)
this.moving = false
this.timeNum = 0
this.positionX = this.ori;
this.positionY = this.ori;
this.offsetX = this.ori
this.offsetY = this.ori
})
)
.aspectRatio(1)
.borderRadius(CommonConstants.SIZE_64)
.width(CommonConstants.SIZE_128 + 1)
//上箭头
Image($r("app.media.ic_rwheel_arrow_top"))
.width(CommonConstants.SIZE_32)
.height(CommonConstants.SIZE_24)
.alignRules({
bottom: { anchor: 'steering_wheel', align: VerticalAlign.Top },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.margin({ bottom: CommonConstants.SIZE_36 })
//左箭头
Image($r("app.media.ic_rwheel_arrow_left"))
.height(CommonConstants.SIZE_32)
.width(CommonConstants.SIZE_24)
.alignRules({
right: { anchor: 'steering_wheel', align: HorizontalAlign.Start },
center: { anchor: 'steering_wheel', align: VerticalAlign.Center }
})
.margin({ CommonConstants.SIZE_36 })
//右箭头
Image($r("app.media.ic_rwheel_arrow_left"))
.height(CommonConstants.SIZE_32)
.width(CommonConstants.SIZE_24)
.alignRules({
left: { anchor: 'steering_wheel', align: HorizontalAlign.End },
center: { anchor: 'steering_wheel', align: VerticalAlign.Center }
})
.margin({
left:CommonConstants.SIZE_64,
top: CommonConstants.IMAGE_SIZE_68
})
.rotate({
centerX: 0,
centerY: 0,
centerZ: 1,
angle: 180
})
//下箭头
Image($r("app.media.ic_rwheel_arrow_left"))
.height(CommonConstants.SIZE_32)
.width(CommonConstants.SIZE_24)
.alignRules({
top: { anchor: 'steering_wheel', align: VerticalAlign.Bottom },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.rotate({
centerX: 0,
centerY: 0,
centerZ: 1,
angle: -90
})
.margin({
top: CommonConstants.SIZE_64,
right: CommonConstants.SIZE_4
})
}
.width(CommonConstants.FULL_SIZE)
.height(CommonConstants.FULL_SIZE)
理解上面的代码,请看这里的解释。
- 在RelativeContainer容器中我们布置了所需UI静态,图式中间需绘制侧重颜色静态由canvas画布实现。
- 实现摇杆的拖动需要结合滑动手势方法PanGesture,通过滑动手势事件onActionStart(event: (event: GestureEvent) => void)、onActionUpdate(event: (event: GestureEvent) => void)、onActionEnd(event: (event: GestureEvent) => void) 结合translate 实现拖动及归中,同时为了小圆环以顺滑的状态在大圆环内移动我们需要用边界限制算法控制小圆的移动。
- 边界限制算法的实现是根据小圆可以拖动的最大距离,判断是否可以继续拖动。如果可以则返回当前偏移量,如果不行则根据偏移角度计算返回最大偏移位置。
// 边界限制算法
limitBorder(offsetX: number, offsetY: number, parentRadius: number, dotRadius: number): [number, number, number] {
// 边界限制算法
// 当前点与圆心的距离offsetX、offsetY
const distance = Math.sqrt(offsetX * offsetX + offsetY * offsetY);
// 最大允许距离(父圆半径 - 圆点半径)
const maxDistance = parentRadius - dotRadius;
const angle = Math.atan2(offsetY, offsetX);
this.angle = angle
if (distance > maxDistance) {
// 超出范围时,将坐标限制在边界
return [
maxDistance * Math.cos(angle),
maxDistance * Math.sin(angle),
maxDistance
];
} else {
// 未超出范围,直接返回
return [offsetX, offsetY, maxDistance];
}
}
- Canvas(context?: CanvasRenderingContext2D | DrawingRenderingContext)我们需要传入一个上下文,在我们的案例中使用了CanvasRenderingContext2D因为他有更强大的性能。
- 我们在onReady(event: () => void)事件中调用绘制方法
// 圆盘
circle(offContext: CanvasRenderingContext2D, color: string, width: number, angle: number) {
//斜边
const hypotenuse: number = Math.sqrt(2 * (width * width))
let position = this.positionPoints(angle, width, hypotenuse)
//清空画布重新绘制
offContext.reset()
offContext.translate(width / 2, width / 2)
offContext.beginPath()
offContext.strokeStyle = color
offContext.lineWidth = 22;
//arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void
//渐变
let grad = offContext.createLinearGradient(0, 0, position[2], position[3])
grad.addColorStop(0.1, '#4DFFFFFF')
grad.addColorStop(0.39, '#ff5e1bfa')
offContext.strokeStyle = grad
offContext.arc(0, 0, (width / 2) - 11, 0, Math.PI * 2)
offContext.stroke()
}
// 默认
initCircle(offContext: CanvasRenderingContext2D, color: string, width: number) {
//清空画布重新绘制
offContext.reset()
offContext.beginPath()
offContext.strokeStyle = color
offContext.lineWidth = 22;
offContext.arc(width / 2, width / 2, (width / 2) - 11, 0, Math.PI * 2)
offContext.stroke()
}
方法 | 作用 |
---|---|
arc | 绘制弧线路径 |
beginPath | 创建一个新的绘制路径 |
stroke | 根据指定的路径,进行边框绘制操作 |
reset | 清空画布重新绘制 |
- 在使用画布绘制时我们需要注意,因为要呈现动态的效果。我们需要在第一步清空画布内容,然后开始新路径绘制。
- 用画布绘制圆环的思路是改变绘制的lineWidth,以达到绘制圆环的目的。通过更改圆环颜色达到渐变效果。
- 注意:无论是绘制什么形状调用相应方法后,需调用offContext.stroke()才能呈现在画布上。
- 通过CanvasGradient进行颜色渐变绘制,通过addColorStop设置渐变断点值,包括偏移和颜色。我们需要根据用户拖动返回的偏移量计算圆环渐变色起始点。
小结
本文仅呈现的部分代码已经可以达到百分之八十的复现。依据边界限制算法大家可以尝试一下补全渐变渲染的代码~大家有问题可以和我在评论区多多交流~