4.9【HarmonyOS鸿蒙开发】自定义组件-幸运盘抽奖

4.9【HarmonyOS鸿蒙开发】自定义组件-幸运盘抽奖(附带源码)

作者:韩茹
公司:程序咖(北京)科技有限公司
鸿蒙巴士专栏作家
一、项目介绍
当系统提供的组件无法满足设计需求时,您可以创建自定义组件,根据设计需求自定义组件的属性及响应事件,并绘制组件。自定义组件是在组件预留的两个自定义图层中实现绘制,通过addDrawTask方法添加绘制任务,最终与组件的其它图层合成在一起呈现在界面中。
实现思路:
  1. 创建自定义组件的类,并继承Component或其子类,添加构造方法。
  2. 实现Component.DrawTask接口,在onDraw方法中进行绘制。
  3. 根据自定义组件需要完成的功能,去选择实现相应的接口。例如可实现Component.EstimateSizeListener响应测量事件、Component.TouchEventListener响应触摸事件、Component.ClickedListener响应点击事件、Component.LongClickedListener响应长按事件、Component.DoubleClickedListener响应双击事件等。
  4. 本教程实现圆形抽奖转盘功能,要实现如下接口:
    a) 需要实现获取屏幕宽高度、中心点坐标,所以实现Component.EstimateSizeListener接口,重写onEstimateSize方法。
    b) 需要实现点击中心圆盘区域位置开始抽奖功能,所以实现Component.TouchEventListener,重写onTouchEvent方法。
注意:使用自定义组件实现Component.EstimateSizeListener接口需要HarmonyOS SDK版本在2.1.0.13或以上。
二、项目展示
自定义圆形抽奖转盘示例工程的代码工程结构描述如下:
4.9【HarmonyOS鸿蒙开发】自定义组件-幸运盘抽奖
文章图片

  • 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、获取屏幕大小、中心点
【4.9【HarmonyOS鸿蒙开发】自定义组件-幸运盘抽奖】实现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

    推荐阅读