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或以上。

二、我的项目展现

自定义圆形抽奖转盘示例工程的代码工程构造形容如下:

  • 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