4.9【HarmonyOS 鸿蒙开发】自定义组件 - 侥幸盘抽奖 (附带源码)
作者:韩茹
公司:程序咖(北京)科技有限公司
鸿蒙巴士专栏作家
一、我的项目介绍
当零碎提供的组件无奈满足设计需要时,您能够创立自定义组件,依据设计需要自定义组件的属性及响应事件,并绘制组件。自定义组件是在组件预留的两个自定义图层中实现绘制,通过 addDrawTask 办法增加绘制工作,最终与组件的其它图层合成在一起出现在界面中。
实现思路:
- 创立自定义组件的类,并继承 Component 或其子类,增加构造方法。
- 实现 Component.DrawTask 接口,在 onDraw 办法中进行绘制。
- 依据自定义组件须要实现的性能,去抉择实现相应的接口。例如可实现 Component.EstimateSizeListener 响应测量事件、Component.TouchEventListener 响应触摸事件、Component.ClickedListener 响应点击事件、Component.LongClickedListener 响应长按事件、Component.DoubleClickedListener 响应双击事件等。
- 本教程实现圆形抽奖转盘性能,要实现如下接口:
a) 须要实现获取屏幕宽高度、中心点坐标,所以实现 Component.EstimateSizeListener 接口,重写 onEstimateSize 办法。
b) 须要实现点击核心圆盘区域地位开始抽奖性能,所以实现 Component.TouchEventListener,重写 onTouchEvent 办法。
留神:应用自定义组件实现 Component.EstimateSizeListener 接口须要 HarmonyOS SDK 版本在 2.1.0.13 或以上。
二、我的项目展现
自定义圆形抽奖转盘示例工程的代码工程构造形容如下:
- customcomponent:LuckyCirclePanComponent 自定义圆形抽奖转盘组件类,绘制圆形抽奖转盘,并实现抽奖成果。
- slice:MainAbilitySlice 本示例教程起始页面,提供界面入口。
-
utils:工具类
- ColorUtils 色彩工具类,对绘制圆盘所需 RGB 色彩进行封装。
- LogUtils 日志打印类,对 HiLog 日志进行了封装。
- PixelMapUtils 图片工具类,次要是加载本地图片资源,通过本地图片资源的 resourceId,将图片转换成 PixelMap 类型。
- ToastUtils 弹窗工具类,抽奖完结后,弹出抽奖后果信息。
- MainAbility:主程序入口,DevEco Studio 生成,未增加逻辑,无需变更。
- MyApplication:DevEco Studio 主动生成,无需变更。
-
resources:寄存工程应用到的资源文件
- resources\base\element 中寄存 DevEco studio 主动生成的配置文件 string.json,无需变更。
- resources\base\graphic 中寄存页面款式文件,本示例教程通过自定义组件实现,没有定义页面款式,无需变更。
- resources\base\layout 中布局文件,本示例教程通过自定义组件实现,没有定义页面布局,无需变更。
- resources\base\media 下寄存图片资源,本示例教程应用了 5 张.png 图片,用于设置与奖品绝对应的图片,开发者可自行筹备;icon.png 由 DevEco Studio 主动生成,无需变更。
- config.json:配置文件。
三、实现步骤
1、创立一个包 customcomponent,创立自定义组件的类 LuckyCirclePanComponent,并继承 Component 或其子类,增加构造方法。
/**
* LuckyCirclePanComponent 类,实现自定义组件,绘制圆形抽奖转盘,并实现抽奖成果。*/
public class LuckyCirclePanComponent extends Component implements Component.DrawTask,
Component.EstimateSizeListener, Component.TouchEventListener {public LuckyCirclePanComponent(Context context) {super(context);
this.context = context;
// 初始化画笔
initPaint();
// 获取屏幕的宽高度、中心点坐标,调用 onEstimateSize 办法
setEstimateSizeListener(this);
// 增加绘制工作,调用 onDraw 办法
addDrawTask(this);
// 实现点击核心圆盘区域地位开始抽奖性能,调用 onTouchEvent 办法
setTouchEventListener(this);
}
}
2、实现 Component.DrawTask 接口,在 onDraw 办法中进行绘制。
@Override
public void onDraw(Component component, Canvas canvas) {
// 将画布沿 X、Y 轴平移指定间隔
canvas.translate(centerX, centerY);
// 画内部圆盘的花瓣
drawFlower(canvas);
// 画内部圆盘、小圈圈、五角星
drawOutCircleAndFive(canvas);
// 画外部扇形抽奖区域
drawInnerArc(canvas);
// 画外部扇形区域文字
drawArcText(canvas);
// 画外部扇形区域奖品对应的图片
drawImage(canvas);
// 画核心圆盘和指针
drawCenter(canvas);
}
3、获取屏幕大小、中心点
实现 Component.EstimateSizeListener 接口,重写 onEstimateSize 办法,获取屏幕的宽高度 width、height 及中心点坐标 centerX、centerY。
@Override
public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) {int componentWidth = EstimateSpec.getSize(widthEstimateConfig);
int componentHeight = EstimateSpec.getSize(heightEstimateConfig);
this.width = componentWidth;
this.height = componentHeight;
centerX = this.width / TWO;
centerY = this.height / TWO;
setEstimatedSize(EstimateSpec.getChildSizeWithMode(componentWidth, componentWidth, EstimateSpec.PRECISE),
EstimateSpec.getChildSizeWithMode(componentHeight, componentHeight, EstimateSpec.PRECISE)
);
return true;
}
4、画内部圆盘
A. 先画内部圆盘的花瓣:通过调用 Canvas 的 rotate() 办法,将画布旋转指定角度。通过调用 Canvas 的 save() 和 restore() 办法,使画布保留最新的绘制状态。依据想要绘制的花瓣个数,扭转旋转角度,循环画出花瓣成果。
/**
* 内部圆盘的花瓣
* @param canvas
*/
private void drawFlower(Canvas canvas) {
float beginAngle = startAngle + avgAngle;
float radius = centerX - padding;
for (int i = 0; i < COUNT; i++) {canvas.save();
canvas.rotate(beginAngle, 0F, 0F);
paintFlower.setColor(ColorUtils.PAINT_FLOWER_YELLOW);
canvas.drawCircle(-radius / TWO, radius / TWO, radius / TWO, paintFlower);
paintFlower.setColor(ColorUtils.PAINT_FLOWER_PINK);
canvas.drawCircle(-radius / TWO, radius / TWO, (radius - padding) / TWO, paintFlower);
beginAngle += avgAngle;
canvas.restore();}
}
B. 画内部圆盘:在指定的 X、Y(0F, 0F)坐标处,画一个半径为 centerX – padding 的圆形(其实就是绘制一个红色的圆盘)。
paintOutCircle.setColor(ColorUtils.PAINT_OUT_CIRCLE);
canvas.drawCircle(0F, 0F, centerX - padding, paintOutCircle);
C. 画内部圆盘边上的小圈圈和五角星:接下来一个 for 循环,且角度每次递增 (avgAngle / THREE),就是绘制圆环上的小圈圈和五角星了。因为是交替绘制五角星和小圈圈,所以用一个条件判断语句去绘制。
float beginAngle = startAngle + avgAngle / THREE;
for (int i = 0; i < COUNT * THREE; i++) {canvas.save();
canvas.rotate(beginAngle, 0F, 0F);
if (0 == i % TWO) {paintOutCircle.setColor(Color.WHITE);
canvas.drawCircle(centerX - padding - padding / TWO, 0F, vp2px(FIVE), paintOutCircle);
} else {paintFiveStart(canvas);
}
beginAngle += avgAngle / THREE;
canvas.restore();}
D. 画五角星:通过计算获取到五角星的 5 个顶点地位(计算根据:五角星每个角的角度为 36°,而后依据三角函数即可算出各个点的坐标),再应用 Canvas、Path、Paint 将 5 个顶点通过画线连贯在一起,就实现了五角星的绘制。
/**
* 画五角星
* @param canvas
*/
private void paintFiveStart(Canvas canvas) {
// 画五角星的 path
Path path = new Path();
float[] points = fivePoints(centerX - padding - padding / TWO, 0F, padding);
for (int i = 0; i < points.length - 1; i = i + TWO) {path.lineTo(points[i], points[i + 1]);
}
path.close();
canvas.drawPath(path, paintFive);
}
/**
* fivePoints 获取五角星的五个顶点
*
* @param pointXa 起始点 A 的 x 轴相对地位
* @param pointYa 起始点 A 的 y 轴相对地位
* @param sideLength 五角星的边长
* @return 五角星 5 个顶点坐标
*/
private static float[] fivePoints(float pointXa, float pointYa, float sideLength) {
final int eighteen = 18;
float pointXb = pointXa + sideLength / TWO;
double num = sideLength * Math.sin(Math.toRadians(eighteen));
float pointXc = (float) (pointXa + num);
float pointXd = (float) (pointXa - num);
float pointXe = pointXa - sideLength / TWO;
float pointYb = (float) (pointYa + Math.sqrt(Math.pow(pointXc - pointXd, TWO)
- Math.pow(sideLength / TWO, TWO)));
float pointYc = (float) (pointYa + Math.cos(Math.toRadians(eighteen)) * sideLength);
float pointYd = pointYc;
float pointYe = pointYb;
float[] points = new float[]{pointXa, pointYa, pointXd, pointYd, pointXb, pointYb,
pointXe, pointYe, pointXc, pointYc, pointXa, pointYa};
return points;
}
5、画外部扇形抽奖区域
A. 画抽奖区域扇形:应用 RectFloat 和 Arc 对象绘制弧,rect 示意圆弧突围矩形的左上角和右下角的坐标,参数 new Arc(startAngle, avgAngle, true) 示意圆弧参数,例如起始角度、后掠角以及是否从圆弧的两个端点到其核心绘制直线。
/**
* 画抽奖区域扇形
* @param canvas
*/
private void drawInnerArc(Canvas canvas) {float radius = Math.min(centerX, centerY) - padding * TWO;
RectFloat rect = new RectFloat(-radius, -radius, radius, radius);
for (int i = 0; i < COUNT; i++) {paintInnerArc.setColor(colors[i]);
canvas.drawArc(rect, new Arc(startAngle, avgAngle, true), paintInnerArc);
startAngle += avgAngle;
}
}
B. 画抽奖区域文字:利用 Path,创立绘制门路,增加 Arc,而后设置程度和垂直的偏移量。垂直偏移量 radius / FIVE 就是以后 Arc 朝着圆心挪动的间隔;程度偏移量,就是顺时针去旋转,程度偏移 (Math.sin(avgAngle / CIRCLE Math.PI) radius) – measureWidth / TWO,是为了让文字在以后弧范畴文字居中。最初,用 path 去绘制文本。
/**
* 画抽奖区域文字
* @param canvas
*/
private void drawArcText(Canvas canvas) {for (int i = 0; i < COUNT; i++) {
// 创立绘制门路
Path circlePath = new Path();
float radius = Math.min(centerX, centerY) - padding * TWO;
RectFloat rect = new RectFloat(-radius, -radius, radius, radius);
circlePath.addArc(rect, startAngle, avgAngle);
float measureWidth = paintArcText.measureText(textArrs[i]);
// 偏移量
float advance = (float) ((Math.sin(avgAngle / CIRCLE * Math.PI) * radius) - measureWidth / TWO);
canvas.drawTextOnPath(paintArcText, textArrs[i], circlePath, advance, radius / FIVE);
startAngle += avgAngle;
}
}
C. 画抽奖区域文字对应图片:pixelMaps 示意文字对应的图片 ResourceId 转换成 PixelMap 的数组,pixelMapHolderList 示意将 PixelMap 转换成 PixelMapHolder 图片 List,dst 示意 PixelMapHolder 对象的左上角(-imageHeight / TWO,imageHeight / TWO)和右下角(centerX / THREE + imageWidth,centerX / THREE)的坐标。
/**
* 画抽奖区域文字对应图片
* @param canvas
*/
private void drawImage(Canvas canvas) {
float beginAngle = startAngle + avgAngle / TWO;
for (int i = 0; i < COUNT; i++) {int imageWidth = pixelMaps[i].getImageInfo().size.width;
int imageHeight = pixelMaps[i].getImageInfo().size.height;
canvas.save();
canvas.rotate(beginAngle, 0F, 0F);
// 指定图片在屏幕上显示的区域
RectFloat dst = new RectFloat(centerX / THREE, -imageHeight / TWO,
centerX / THREE + imageWidth, imageHeight / TWO);
canvas.drawPixelMapHolderRect(pixelMapHolderList.get(i), dst, paintImage);
beginAngle += avgAngle;
canvas.restore();}
}
// 将 pixelMap 转换成 PixelMapHolder
private void pixelMapToPixelMapHolder() {pixelMapHolderList = new ArrayList<>(pixelMaps.length);
for (PixelMap pixelMap : pixelMaps) {pixelMapHolderList.add(new PixelMapHolder(pixelMap));
}
}
6、画核心圆盘和指针
A. 画核心圆盘大指针:通过 Path,确定要挪动的三个点的坐标 (-centerX / nine, 0F)、(centerX / nine, 0F)、(0F, -centerX / THREE),去绘制指针。
/**
* 画核心圆盘和指针
* @param canvas
*/
private void drawCenter(Canvas canvas) {
final int nine = 9;
final int seven = 7;
final int eighteen = 18;
// 画大指针
Path path = new Path();
path.moveTo(-centerX / nine, 0F);
path.lineTo(centerX / nine, 0F);
path.lineTo(0F, -centerX / THREE);
path.close();
canvas.drawPath(path, paintPointer);
}
B. 画外部大圆和小圆:在圆盘圆心处,绘制两个半径别离为 centerX / seven + padding / TWO、centerX / seven 的核心圆盘。
// 画外部大圆
paintCenterCircle.setColor(ColorUtils.PAINT_POINTER);
canvas.drawCircle(0F, 0F, centerX / seven + padding / TWO, paintCenterCircle);
// 画外部小圆
paintCenterCircle.setColor(Color.WHITE);
canvas.drawCircle(0F, 0F, centerX / seven, paintCenterCircle);
C. 画核心圆盘小指针:与步骤 1 中画核心圆盘大指针相似,通过 Path 去绘制核心圆盘小指针。
Path smallPath = new Path();
smallPath.moveTo(-centerX / eighteen, 0F);
smallPath.lineTo(centerX / eighteen, 0F);
smallPath.lineTo(0F, -centerX / THREE + padding / TWO);
smallPath.close();
canvas.drawPath(smallPath, paintSmallPoint);
D. 画核心圆弧文字:通过 Paint 的 getFontMetrics() 办法,获取绘制字体的倡议行距,而后依据倡议行距去绘制文本。
Paint.FontMetrics fontMetrics = paintCenterText.getFontMetrics();
float textHeight = (float) Math.ceil(fontMetrics.leading - fontMetrics.ascent);
canvas.drawText(paintCenterText, "开始", 0F, textHeight / THREE);
7、实现抽奖性能
A. 实现 Component.TouchEventListener 接口,重写 onTouchEvent 办法,获取屏幕上点击的坐标,当点击的范畴在核心圆盘区域时,圆形转盘开始转动抽奖。
@Override
public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
final int seven = 7;
switch (touchEvent.getAction()) {
case TouchEvent.PRIMARY_POINT_DOWN:
// 获取屏幕上点击的坐标
float floatX = touchEvent.getPointerPosition(touchEvent.getIndex()).getX();
float floatY = touchEvent.getPointerPosition(touchEvent.getIndex()).getY();
float radius = centerX / seven + padding / TWO;
boolean isScopeX = centerX - radius < floatX && centerX + radius > floatX;
boolean isScopeY = centerY - radius < floatY && centerY + radius > floatY;
if (isScopeX && isScopeY && !animatorVal.isRunning()) {startAnimator();
}
break;
case TouchEvent.PRIMARY_POINT_UP:
// 松开勾销
invalidate();
break;
default:
break;
}
return true;
}
2、圆形转盘开始转动抽奖:给转盘指定一个随机的转动角度 randomAngle,保障每次转动的角度是随机的(即每次抽到的奖品也是随机的),而后设置动画挪动的曲线类型,这里抽奖设置的是 Animator.CurveType.DECELERATE 示意动画疾速开始而后逐步加速的曲线。动画完结后,转盘进行转动(即抽奖完结),会弹出抽中的奖品提示信息。
/**
* 圆形转盘开始转动抽奖
*/
private void startAnimator() {
final int angle = 270;
startAngle = 0;
// 动画时长
final long animatorDuration = 4000L;
// 随机角度
int randomAngle = new SecureRandom().nextInt(CIRCLE);
animatorVal.setCurveType(Animator.CurveType.DECELERATE);
animatorVal.setDuration(animatorDuration);
animatorVal.setValueUpdateListener((AnimatorValue animatorValue, float value) -> {startAngle = value * (CIRCLE * FIVE - randomAngle + angle);
invalidate();});
stateChangedListener(animatorVal, randomAngle);
animatorVal.start();}
四、运行
附带源码
更多内容:
1、社区:鸿蒙巴士 https://www.harmonybus.net/
2、公众号:HarmonyBus
3、技术交换 QQ 群:714518656
4、视频课:https://www.chengxuka.com