[文章]通过一个圆形抽奖转盘演示HarmonyOS自定义组件的实现

阅读量0
0
4
1. 介绍      
当系统提供的组件无法满足设计需求时,您可以创建自定义组件,根据设计需求自定义组件的属性及响应事件,并绘制组件。自定义组件是在组件预留的两个自定义图层中实现绘制,通过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或以上。

本教程将通过以下内容为您展示如何使用自定义组件实现圆形抽奖转盘功能。

2. 搭建HarmonyOS环境
我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。

  • 安装DevEco Studio,详情请参考下载和安装软件。
  • 设置DevEco Studio开发环境,DevEco Studio开发环境依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
    • 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
    • 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
  • 开发者可以参考以下链接,完成设备调试的相关配置:
    • 使用真机进行调试
    • 使用模拟器进行调试
3. 基本步骤
  • 创建自定义组件的类,并继承Component或其子类,添加构造方法。
    1. public class LuckyCirclePanComponent extends Component implements Component.DrawTask,
    2.         Component.EstimateSizeListener, Component.TouchEventListener {
    3.         public LuckyCirclePanComponent(Context context) {
    4.                 super(context);
    5.                 this.context = context;
    6.                 // 初始化画笔
    7.                 initPaint();
    8.                 // 获取屏幕的宽高度、中心点坐标,调用onEstimateSize方法
    9.                 setEstimateSizeListener(this);
    10.                 // 添加绘制任务,调用onDraw方法
    11.                 addDrawTask(this);
    12.                 // 实现点击中心圆盘区域位置开始抽奖功能,调用onTouchEvent方法
    13.                 setTouchEventListener(this);
    14.         }
    15. }
    复制代码

  • 实现Component.DrawTask接口,在onDraw方法中进行绘制。
    1. [url=home.php?mod=space&uid=2735960]@Override[/url]
    2. public void onDraw(Component component, Canvas canvas) {
    3.         // 将画布沿X、Y轴平移指定距离
    4.         canvas.translate(centerX, centerY);
    5.         // 画外部圆盘的花瓣
    6.         drawFlower(canvas);
    7.         // 画外部圆盘、小圈圈、五角星
    8.         drawOutCircleAndFive(canvas);
    9.         // 画内部扇形抽奖区域
    10.         drawInnerArc(canvas);
    11.         // 画内部扇形区域文字
    12.         drawArcText(canvas);
    13.         // 画内部扇形区域奖品对应的图片
    14.         drawImage(canvas);
    15.         // 画中心圆盘和指针
    16.         drawCenter(canvas);
    17. }
    复制代码


4. 获取屏幕大小、中心点
实现Component.EstimateSizeListener接口,重写onEstimateSize方法,获取屏幕的宽高度width、height及中心点坐标centerX、centerY。
  1. @Override
  2. public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) {
  3.         int componentWidth = EstimateSpec.getSize(widthEstimateConfig);
  4.         int componentHeight = EstimateSpec.getSize(heightEstimateConfig);
  5.         this.width = componentWidth;
  6.         this.height = componentHeight;
  7.         centerX = this.width / TWO;
  8.         centerY = this.height / TWO;
  9.         setEstimatedSize(
  10.         
  11.         EstimateSpec.getChildSizeWithMode(componentWidth, componentWidth, EstimateSpec.PRECISE),
  12.         
  13.         EstimateSpec.getChildSizeWithMode(componentHeight, componentHeight, EstimateSpec.PRECISE)
  14.         );
  15.         return true;
  16. }
复制代码

5. 画外部圆盘
  • 画外部圆盘的花瓣:通过调用Canvas的rotate()方法,将画布旋转指定角度。通过调用Canvas的save()和restore()方法,使画布保存最新的绘制状态。根据想要绘制的花瓣个数,改变旋转角度,循环画出花瓣效果。
    1. private void drawFlower(Canvas canvas) {
    2.         float beginAngle = startAngle + avgAngle;
    3.         float radius = centerX - padding;
    4.         for (int i = 0; i < COUNT; i++) {
    5.                 canvas.save();
    6.                 canvas.rotate(beginAngle, 0F, 0F);
    7.                 paintFlower.setColor(ColorUtils.PAINT_FLOWER_YELLOW);
    8.                 canvas.drawCircle(-radius / TWO, radius / TWO, radius / TWO, paintFlower);

    9.                 paintFlower.setColor(ColorUtils.PAINT_FLOWER_PINK);
    10.                 canvas.drawCircle(-radius / TWO, radius / TWO, (radius - padding) / TWO, paintFlower);
    11.                 beginAngle += avgAngle;
    12.                 canvas.restore();
    13.         }
    14. }
    复制代码

  • 画外部圆盘:在指定的X、Y(0F, 0F)坐标处,画一个半径为centerX - padding的圆形(其实就是绘制一个红色的圆盘)。
    1. paintOutCircle.setColor(ColorUtils.PAINT_OUT_CIRCLE);
    2. canvas.drawCircle(0F, 0F, centerX - padding, paintOutCircle);
    复制代码

  • 画外部圆盘边上的小圈圈和五角星:接下来一个for循环,且角度每次递增(avgAngle / THREE),就是绘制圆环上的小圈圈和五角星了。因为是交替绘制五角星和小圈圈,所以用一个条件判断语句去绘制。
    1. float beginAngle = startAngle + avgAngle / THREE;
    2. for (int i = 0; i < COUNT * THREE; i++) {
    3.         canvas.save();
    4.         canvas.rotate(beginAngle, 0F, 0F);
    5.         if (0 == i % TWO) {
    6.                 paintOutCircle.setColor(Color.WHITE);
    7.                 canvas.drawCircle(centerX - padding - padding / TWO, 0F, vp2px(FIVE), paintOutCircle);
    8.         } else {
    9.                 paintFiveStart(canvas);
    10.         }
    11.         beginAngle += avgAngle / THREE;
    12.         canvas.restore();
    13. }
    复制代码

  • 画五角星:通过计算获取到五角星的5个顶点位置(计算依据:五角星每个角的角度为36°,然后根据三角函数即可算出各个点的坐标),再使用Canvas、Path、Paint将5个顶点通过画线连接在一起,就完成了五角星的绘制。
    1. private void paintFiveStart(Canvas canvas) {
    2.         // 画五角星的path
    3.         Path path = new Path();
    4.         float[] points = fivePoints(centerX - padding - padding / TWO, 0F, padding);
    5.         for (int i = 0; i < points.length - 1; i = i + TWO) {
    6.                 path.lineTo(points[i], points[i + 1]);
    7.         }
    8.         path.close();
    9.         canvas.drawPath(path, paintFive);
    10. }
    11. /**
    12. * fivePoints 获取五角星的五个顶点
    13. *
    14. * [url=home.php?mod=space&uid=3142012]@param[/url] pointXa 起始点A的x轴绝对位置
    15. * @param pointYa 起始点A的y轴绝对位置
    16. * @param sideLength 五角星的边长
    17. * [url=home.php?mod=space&uid=1141835]@Return[/url] 五角星5个顶点坐标
    18. */
    19. private static float[] fivePoints(float pointXa, float pointYa, float sideLength) {
    20.         final int eighteen = 18;
    21.         float pointXb = pointXa + sideLength / TWO;
    22.         double num = sideLength * Math.sin(Math.toRadians(eighteen));
    23.         float pointXc = (float) (pointXa + num);
    24.         float pointXd = (float) (pointXa - num);
    25.         float pointXe = pointXa - sideLength / TWO;
    26.         float pointYb = (float) (pointYa + Math.sqrt(Math.pow(pointXc - pointXd, TWO)
    27.                         - Math.pow(sideLength / TWO, TWO)));
    28.         float pointYc = (float) (pointYa + Math.cos(Math.toRadians(eighteen)) * sideLength);
    29.         float pointYd = pointYc;
    30.         float pointYe = pointYb;
    31.         float[] points = new float[]{pointXa, pointYa, pointXd, pointYd, pointXb, pointYb,
    32.                 pointXe, pointYe, pointXc, pointYc, pointXa, pointYa};
    33.         return points;
    34. }
    复制代码


6. 画内部扇形抽奖区域
  • 画抽奖区域扇形:使用RectFloat和Arc对象绘制弧,rect表示圆弧包围矩形的左上角和右下角的坐标,参数new Arc(startAngle, avgAngle, true)表示圆弧参数,例如起始角度、后掠角以及是否从圆弧的两个端点到其中心绘制直线。
    1. private void drawInnerArc(Canvas canvas) {
    2.         float radius = Math.min(centerX, centerY) - padding * TWO;
    3.         RectFloat rect = new RectFloat(-radius, -radius, radius, radius);
    4.         for (int i = 0; i < COUNT; i++) {
    5.                 paintInnerArc.setColor(colors[i]);
    6.                 canvas.drawArc(rect, new Arc(startAngle, avgAngle, true), paintInnerArc);
    7.                 startAngle += avgAngle;
    8.         }
    9. }
    复制代码

  • 画抽奖区域文字:利用Path,创建绘制路径,添加Arc,然后设置水平和垂直的偏移量。垂直偏移量radius / FIVE就是当前Arc朝着圆心移动的距离;水平偏移量,就是顺时针去旋转,水平偏移(Math.sin(avgAngle / CIRCLE * Math.PI) * radius) - measureWidth / TWO,是为了让文字在当前弧范围文字居中。最后,用path去绘制文本。
    1. private void drawArcText(Canvas canvas) {
    2.         for (int i = 0; i < COUNT; i++) {
    3.                 // 创建绘制路径
    4.                 Path circlePath = new Path();
    5.                 float radius = Math.min(centerX, centerY) - padding * TWO;
    6.                 RectFloat rect = new RectFloat(-radius, -radius, radius, radius);
    7.                 circlePath.addArc(rect, startAngle, avgAngle);
    8.                 float measureWidth = paintArcText.measureText(textArrs[i]);
    9.                 // 偏移量
    10.                 float advance = (float) ((Math.sin(avgAngle / CIRCLE * Math.PI) * radius) - measureWidth / TWO);
    11.                 canvas.drawTextOnPath(paintArcText, textArrs[i], circlePath, advance, radius / FIVE);
    12.                 startAngle += avgAngle;
    13.         }
    14. }
    复制代码

  • 画抽奖区域文字对应图片:pixelMaps表示文字对应的图片ResourceId转换成PixelMap的数组,pixelMapHolderList表示将PixelMap转换成PixelMapHolder图片List,dst表示PixelMapHolder对象的左上角( -imageHeight / TWO,imageHeight / TWO)和右下角(centerX / THREE + imageWidth,centerX / THREE)的坐标。
    1. private void drawImage(Canvas canvas) {
    2.         float beginAngle = startAngle + avgAngle / TWO;
    3.         for (int i = 0; i < COUNT; i++) {
    4.                 int imageWidth = pixelMaps[i].getImageInfo().size.width;
    5.                 int imageHeight = pixelMaps[i].getImageInfo().size.height;
    6.                 canvas.save();
    7.                 canvas.rotate(beginAngle, 0F, 0F);
    8.                 // 指定图片在屏幕上显示的区域
    9.                 RectFloat dst = new RectFloat(centerX / THREE, -imageHeight / TWO,
    10.                                 centerX / THREE + imageWidth, imageHeight / TWO);
    11.                 canvas.drawPixelMapHolderRect(pixelMapHolderList.get(i), dst, paintImage);
    12.                 beginAngle += avgAngle;
    13.                 canvas.restore();
    14.         }
    15. }
    复制代码


7. 画中心圆盘和指针
  • 画中心圆盘大指针:通过Path ,确定要移动的三个点的坐标(-centerX / nine, 0F)、(centerX / nine, 0F)、(0F, -centerX / THREE),去绘制指针。
    1. Path path = new Path();
    2. path.moveTo(-centerX / nine, 0F);
    3. path.lineTo(centerX / nine, 0F);
    4. path.lineTo(0F, -centerX / THREE);
    5. path.close();
    6. canvas.drawPath(path, paintPointer);
    复制代码

  • 画内部大圆和小圆:在圆盘圆心处,绘制两个半径分别为centerX / seven + padding / TWO、centerX / seven的中心圆盘。
    1. // 画内部大圆
    2. paintCenterCircle.setColor(ColorUtils.PAINT_POINTER);
    3. canvas.drawCircle(0F, 0F, centerX / seven + padding / TWO, paintCenterCircle);
    4. // 画内部小圆
    5. paintCenterCircle.setColor(Color.WHITE);
    6. canvas.drawCircle(0F, 0F, centerX / seven, paintCenterCircle);
    复制代码

  • 画中心圆盘小指针:与步骤1中画中心圆盘大指针类似,通过Path去绘制中心圆盘小指针。
    1. Path smallPath = new Path();
    2. smallPath.moveTo(-centerX / eighteen, 0F);
    3. smallPath.lineTo(centerX / eighteen, 0F);
    4. smallPath.lineTo(0F, -centerX / THREE + padding / TWO);
    5. smallPath.close();
    6. canvas.drawPath(smallPath, paintSmallPoint);
    复制代码

  • 画中心圆弧文字:通过Paint的getFontMetrics()方法,获取绘制字体的建议行距,然后根据建议行距去绘制文本。
    1. Paint.FontMetrics fontMetrics = paintCenterText.getFontMetrics();
    2. float textHeight = (float) Math.ceil(fontMetrics.leading - fontMetrics.ascent);
    3. canvas.drawText(paintCenterText, "开始", 0F, textHeight / THREE);
    复制代码


8. 实现抽奖功能
  • 实现Component.TouchEventListener接口,重写onTouchEvent方法,获取屏幕上点击的坐标,当点击的范围在中心圆盘区域时,圆形转盘开始转动抽奖。
    1. @Override
    2. public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
    3.         final int seven = 7;
    4.         switch (touchEvent.getAction()) {
    5.                 case TouchEvent.PRIMARY_POINT_DOWN:
    6.                         // 获取屏幕上点击的坐标
    7.                         float floatX = touchEvent.getPointerPosition(touchEvent.getIndex()).getX();
    8.                         float floatY = touchEvent.getPointerPosition(touchEvent.getIndex()).getY();
    9.                         float radius = centerX / seven + padding / TWO;
    10.                         boolean isScopeX = centerX - radius < floatX && centerX + radius > floatX;
    11.                         boolean isScopeY = centerY - radius < floatY && centerY + radius > floatY;
    12.                         if (isScopeX && isScopeY && !animatorVal.isRunning()) {
    13.                                 startAnimator();
    14.                         }
    15.                         break;
    16.                 case TouchEvent.PRIMARY_POINT_UP:
    17.                         // 松开取消
    18.                         invalidate();
    19.                         break;
    20.                 default:
    21.                         break;
    22.         }
    23.         return true;
    24. }
    复制代码

  • 圆形转盘开始转动抽奖:给转盘指定一个随机的转动角度randomAngle,保证每次转动的角度是随机的(即每次抽到的奖品也是随机的),然后设置动画移动的曲线类型,这里抽奖设置的是Animator.CurveType.DECELERATE表示动画快速开始然后逐渐减速的曲线。动画结束后,转盘停止转动(即抽奖结束),会弹出抽中的奖品提示信息。
    1. private void startAnimator() {
    2.         final int angle = 270;
    3.         startAngle = 0;
    4.         // 动画时长
    5.         final long animatorDuration = 4000L;
    6.         // 随机角度
    7.         int randomAngle = new SecureRandom().nextInt(CIRCLE);
    8.         animatorVal.setCurveType(Animator.CurveType.DECELERATE);
    9.         animatorVal.setDuration(animatorDuration);
    10.         animatorVal.setValueUpdateListener((AnimatorValue animatorValue, float value) -> {
    11.                 startAngle = value * (CIRCLE * FIVE - randomAngle + angle);
    12.                 invalidate();
    13.         });
    14.         stateChangedListener(animatorVal, randomAngle);
    15.         animatorVal.start();
    16. }
    复制代码


9. 最终实现效果      
         
   

10. 代码示例      
代码结构解读
为了方便您的学习,我们提供了自定义圆形抽奖转盘示例工程的代码,代码的工程结构描述如下:

  • customcomponent:LuckyCirclePanComponent自定义圆形抽奖转盘组件类,绘制圆形抽奖转盘,并实现抽奖效果。
  • slice:MainAbilitySlice本示例教程起始页面,提供界面入口。
  • utils:工具类
    • ColorUtils颜色工具类,对绘制圆盘所需RGB颜色进行封装。
    • LogUtils日志打印类,对HiLog日志进行了封装。
    • PixelMapUtils图片工具类,主要是加载本地图片资源,通过本地图片资源的resourceId,将图片转换成PixelMap类型。
    • ToastUtils弹窗工具类,抽奖结束后,弹出抽奖结果信息。
  • MainAbility:主程序入口,DevEco Studio生成,未添加逻辑,无需变更。
  • MyApplication:DevEco Studio自动生成,无需变更。
  • resources:存放工程使用到的资源文件
    • resourcesbaseelement中存放DevEco studio自动生成的配置文件string.json,无需变更。
    • resourcesbasegraphic中存放页面样式文件,本示例教程通过自定义组件完成,没有定义页面样式,无需变更。
    • resourcesbaselayout中布局文件,本示例教程通过自定义组件完成,没有定义页面布局,无需变更。
    • resourcesbasemedia下存放图片资源,本示例教程使用了5张.png图片,用于设置与奖品相对应的图片,开发者可自行准备;icon.png由DevEco Studio自动生成,无需变更。
  • config.json:配置文件。
编写布局与样式
  • ability_main.xml布局文件。
    1. <?xml version="1.0" encoding="utf-8"?>
    2. <DirectionalLayout
    3.         xmlns:ohos="http://schemas.huawei.com/res/ohos"
    4.         ohos:height="match_parent"
    5.         ohos:width="match_parent"
    6.         ohos:orientation="vertical">
    7.     <Text
    8.             ohos:id="$+id:text_helloworld"
    9.             ohos:height="match_content"
    10.             ohos:width="match_content"
    11.             ohos:background_element="$graphic:background_ability_main"
    12.             ohos:layout_alignment="horizontal_center"
    13.             ohos:text="Hello World"
    14.             ohos:text_size="50"
    15.     />
    16. </DirectionalLayout>
    复制代码

  • background_ability_main.xml样式文件。
    1. <?xml version="1.0" encoding="UTF-8" ?>
    2. <shape xmlns:ohos="http://schemas.huawei.com/res/ohos"
    3.        ohos:shape="rectangle">
    4.     <solid
    5.             ohos:color="#FFFFFF"/>
    6. </shape>
    复制代码

业务逻辑代码
  • 新建LuckyCirclePanComponent类,实现自定义组件,绘制圆形抽奖转盘,并实现抽奖效果。
    1. public class LuckyCirclePanComponent extends Component implements Component.DrawTask,
    2.         Component.EstimateSizeListener, Component.TouchEventListener {
    3.     private static final int CIRCLE = 360;
    4.     private static final int TWO = 2;
    5.     private static final int THREE = 3;
    6.     private static final int FIVE = 5;

    7.     // 盘块的个数
    8.     private static final int COUNT = 6;

    9.     // 开始角度
    10.     private float startAngle = 0;

    11.     // 平均每份的角度
    12.     private float avgAngle = CIRCLE / COUNT;

    13.     // 边框间距
    14.     private int padding;

    15.     // 宽度和高度
    16.     private float width;
    17.     private float height;

    18.     // 中心的X,Y坐标
    19.     private float centerX;
    20.     private float centerY;

    21.     // 画内部扇形的画笔
    22.     private Paint paintInnerArc;

    23.     // 画内部扇形文字的画笔
    24.     private Paint paintArcText;

    25.     // 外部圆弧的画笔
    26.     private Paint paintOutCircle;

    27.     // 内部圆弧的画笔
    28.     private Paint paintCenterCircle;

    29.     // 中间文字的画笔
    30.     private Paint paintCenterText;

    31.     // 画转盘大指针的画笔
    32.     private Paint paintPointer;

    33.     // 画转盘小指针的画笔
    34.     private Paint paintSmallPoint;

    35.     // 画图片的画笔
    36.     private Paint paintImage;

    37.     // 画外圆外面的花瓣
    38.     private Paint paintFlower;

    39.     // 画五角星的画笔
    40.     private Paint paintFive;

    41.     // 每个盘块的颜色
    42.     private Color[] colors = {ColorUtils.ARC_PINK, ColorUtils.ARC_YELLOW, ColorUtils.ARC_BLUE,
    43.         ColorUtils.ARC_PINK, ColorUtils.ARC_YELLOW, ColorUtils.ARC_BLUE, };

    44.     // 抽奖的文字
    45.     private String[] textArrs = {"华为手表", "华为平板", "恭喜发财", "华为手机", "华为耳机", "恭喜发财"};

    46.     // 与文字对应的图片
    47.     private PixelMap[] pixelMaps = {PixelMapUtils.createPixelMapByResId(ResourceTable.Media_watch, getContext()).get(),
    48.             PixelMapUtils.createPixelMapByResId(ResourceTable.Media_tablet, getContext()).get(),
    49.             PixelMapUtils.createPixelMapByResId(ResourceTable.Media_thanks, getContext()).get(),
    50.             PixelMapUtils.createPixelMapByResId(ResourceTable.Media_phone, getContext()).get(),
    51.             PixelMapUtils.createPixelMapByResId(ResourceTable.Media_headset, getContext()).get(),
    52.             PixelMapUtils.createPixelMapByResId(ResourceTable.Media_thanks, getContext()).get()};

    53.     private List<PixelMapHolder> pixelMapHolderList;
    54.     private Context context;

    55.     // 动画
    56.     private AnimatorValue animatorVal = new AnimatorValue();

    57.     /**
    58.      * constructor of LuckyCirclePanComponent
    59.      *
    60.      * @param context context
    61.      */
    62.     public LuckyCirclePanComponent(Context context) {
    63.         super(context);
    64.         this.context = context;
    65.         // 初始化画笔
    66.         initPaint();
    67.         // 获取屏幕的宽高度、中心点坐标,调用onEstimateSize方法
    68.         setEstimateSizeListener(this);
    69.         // 添加绘制任务,调用onDraw方法
    70.         addDrawTask(this);
    71.         // 实现点击中心圆盘区域位置开始抽奖功能,调用onTouchEvent方法
    72.         setTouchEventListener(this);
    73.     }

    74.     private void initPaint() {
    75.         final int size = 20;
    76.         padding = vp2px(size);

    77.         pixelMapToPixelMapHolder();

    78.         paintInnerArc = new Paint();
    79.         paintInnerArc.setAntiAlias(true);
    80.         paintInnerArc.setStyle(Paint.Style.FILL_STYLE);

    81.         paintArcText = new Paint();
    82.         paintArcText.setAntiAlias(true);
    83.         paintArcText.setTextSize(padding);
    84.         paintArcText.setStyle(Paint.Style.FILL_STYLE);
    85.         paintArcText.setColor(ColorUtils.TEXT);

    86.         paintOutCircle = new Paint();
    87.         paintOutCircle.setAntiAlias(true);
    88.         paintOutCircle.setStyle(Paint.Style.FILL_STYLE);

    89.         paintCenterCircle = new Paint();
    90.         paintCenterCircle.setAntiAlias(true);
    91.         paintCenterCircle.setStyle(Paint.Style.FILL_STYLE);

    92.         paintPointer = new Paint();
    93.         paintPointer.setAntiAlias(true);
    94.         paintPointer.setStyle(Paint.Style.FILL_STYLE);
    95.         paintPointer.setColor(ColorUtils.PAINT_POINTER);

    96.         paintCenterText = new Paint();
    97.         paintCenterText.setAntiAlias(true);
    98.         paintCenterText.setTextSize(padding);
    99.         paintCenterText.setStyle(Paint.Style.FILL_STYLE);
    100.         paintCenterText.setColor(ColorUtils.TEXT);
    101.         paintCenterText.setTextAlign(TextAlignment.CENTER);

    102.         paintImage = new Paint();
    103.         paintImage.setAntiAlias(true);
    104.         paintImage.setStrokeCap(Paint.StrokeCap.ROUND_CAP);
    105.         paintImage.setStyle(Paint.Style.STROKE_STYLE);

    106.         paintFlower = new Paint();
    107.         paintFlower.setAntiAlias(true);
    108.         paintFlower.setStyle(Paint.Style.FILL_STYLE);

    109.         paintFive = new Paint();
    110.         paintFive.setAntiAlias(true);
    111.         paintFive.setStyle(Paint.Style.FILL_STYLE);
    112.         paintFive.setColor(Color.YELLOW);

    113.         paintSmallPoint = new Paint();
    114.         paintSmallPoint.setStyle(Paint.Style.FILL_STYLE);
    115.         paintSmallPoint.setColor(Color.WHITE);
    116.     }

    117.     @Override
    118.     public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) {
    119.         int componentWidth = EstimateSpec.getSize(widthEstimateConfig);
    120.         int componentHeight = EstimateSpec.getSize(heightEstimateConfig);
    121.         this.width = componentWidth;
    122.         this.height = componentHeight;
    123.         centerX = this.width / TWO;
    124.         centerY = this.height / TWO;
    125.         setEstimatedSize(
    126.                 EstimateSpec.getChildSizeWithMode(componentWidth, componentWidth, EstimateSpec.PRECISE),
    127.                 EstimateSpec.getChildSizeWithMode(componentHeight, componentHeight, EstimateSpec.PRECISE)
    128.         );
    129.         return true;
    130.     }

    131.     @Override
    132.     public void onDraw(Component component, Canvas canvas) {
    133.         // 将画布沿X、Y轴平移指定距离
    134.         canvas.translate(centerX, centerY);
    135.         // 画外部圆盘的花瓣
    136.         drawFlower(canvas);
    137.         // 画外部圆盘、小圈圈、五角星
    138.         drawOutCircleAndFive(canvas);
    139.         // 画内部扇形抽奖区域
    140.         drawInnerArc(canvas);
    141.         // 画内部扇形区域文字
    142.         drawArcText(canvas);
    143.         // 画内部扇形区域奖品对应的图片
    144.         drawImage(canvas);
    145.         // 画中心圆盘和指针
    146.         drawCenter(canvas);
    147.     }

    148.     private void drawFlower(Canvas canvas) {
    149.         float beginAngle = startAngle + avgAngle;
    150.         float radius = centerX - padding;
    151.         for (int i = 0; i < COUNT; i++) {
    152.             canvas.save();
    153.             canvas.rotate(beginAngle, 0F, 0F);
    154.             paintFlower.setColor(ColorUtils.PAINT_FLOWER_YELLOW);
    155.             canvas.drawCircle(-radius / TWO, radius / TWO, radius / TWO, paintFlower);

    156.             paintFlower.setColor(ColorUtils.PAINT_FLOWER_PINK);
    157.             canvas.drawCircle(-radius / TWO, radius / TWO, (radius - padding) / TWO, paintFlower);
    158.             beginAngle += avgAngle;
    159.             canvas.restore();
    160.         }
    161.     }

    162.     private void drawOutCircleAndFive(Canvas canvas) {
    163.         paintOutCircle.setColor(ColorUtils.PAINT_OUT_CIRCLE);
    164.         canvas.drawCircle(0F, 0F, centerX - padding, paintOutCircle);
    165.         float beginAngle = startAngle + avgAngle / THREE;
    166.         for (int i = 0; i < COUNT * THREE; i++) {
    167.             canvas.save();
    168.             canvas.rotate(beginAngle, 0F, 0F);
    169.             if (0 == i % TWO) {
    170.                 paintOutCircle.setColor(Color.WHITE);
    171.                 canvas.drawCircle(centerX - padding - padding / TWO, 0F, vp2px(FIVE), paintOutCircle);
    172.             } else {
    173.                 paintFiveStart(canvas);
    174.             }
    175.             beginAngle += avgAngle / THREE;
    176.             canvas.restore();
    177.         }
    178.     }

    179.     private void paintFiveStart(Canvas canvas) {
    180.         // 画五角星的path
    181.         Path path = new Path();
    182.         float[] points = fivePoints(centerX - padding - padding / TWO, 0F, padding);
    183.         for (int i = 0; i < points.length - 1; i = i + TWO) {
    184.             path.lineTo(points[i], points[i + 1]);
    185.         }
    186.         path.close();
    187.         canvas.drawPath(path, paintFive);
    188.     }

    189.     private void drawInnerArc(Canvas canvas) {
    190.         float radius = Math.min(centerX, centerY) - padding * TWO;
    191.         RectFloat rect = new RectFloat(-radius, -radius, radius, radius);
    192.         for (int i = 0; i < COUNT; i++) {
    193.             paintInnerArc.setColor(colors[i]);
    194.             canvas.drawArc(rect, new Arc(startAngle, avgAngle, true), paintInnerArc);
    195.             startAngle += avgAngle;
    196.         }
    197.     }

    198.     private void drawArcText(Canvas canvas) {
    199.         for (int i = 0; i < COUNT; i++) {
    200.             // 创建绘制路径
    201.             Path circlePath = new Path();
    202.             float radius = Math.min(centerX, centerY) - padding * TWO;
    203.             RectFloat rect = new RectFloat(-radius, -radius, radius, radius);
    204.             circlePath.addArc(rect, startAngle, avgAngle);
    205.             float measureWidth = paintArcText.measureText(textArrs[i]);
    206.             // 偏移量
    207.             float advance = (float) ((Math.sin(avgAngle / CIRCLE * Math.PI) * radius) - measureWidth / TWO);
    208.             canvas.drawTextOnPath(paintArcText, textArrs[i], circlePath, advance, radius / FIVE);
    209.             startAngle += avgAngle;
    210.         }
    211.     }

    212.     private void drawImage(Canvas canvas) {
    213.         float beginAngle = startAngle + avgAngle / TWO;
    214.         for (int i = 0; i < COUNT; i++) {
    215.             int imageWidth = pixelMaps[i].getImageInfo().size.width;
    216.             int imageHeight = pixelMaps[i].getImageInfo().size.height;
    217.             canvas.save();
    218.             canvas.rotate(beginAngle, 0F, 0F);
    219.             // 指定图片在屏幕上显示的区域
    220.             RectFloat dst = new RectFloat(centerX / THREE, -imageHeight / TWO,
    221.                     centerX / THREE + imageWidth, imageHeight / TWO);
    222.             canvas.drawPixelMapHolderRect(pixelMapHolderList.get(i), dst, paintImage);
    223.             beginAngle += avgAngle;
    224.             canvas.restore();
    225.         }
    226.     }

    227.     // 将pixelMap转换成PixelMapHolder
    228.     private void pixelMapToPixelMapHolder() {
    229.         pixelMapHolderList = new ArrayList<>(pixelMaps.length);
    230.         for (PixelMap pixelMap : pixelMaps) {
    231.             pixelMapHolderList.add(new PixelMapHolder(pixelMap));
    232.         }
    233.     }

    234.     private void drawCenter(Canvas canvas) {
    235.         final int nine = 9;
    236.         final int seven = 7;
    237.         final int eighteen = 18;
    238.         // 画大指针
    239.         Path path = new Path();
    240.         path.moveTo(-centerX / nine, 0F);
    241.         path.lineTo(centerX / nine, 0F);
    242.         path.lineTo(0F, -centerX / THREE);
    243.         path.close();
    244.         canvas.drawPath(path, paintPointer);

    245.         // 画内部大圆
    246.         paintCenterCircle.setColor(ColorUtils.PAINT_POINTER);
    247.         canvas.drawCircle(0F, 0F, centerX / seven + padding / TWO, paintCenterCircle);
    248.         // 画内部小圆
    249.         paintCenterCircle.setColor(Color.WHITE);
    250.         canvas.drawCircle(0F, 0F, centerX / seven, paintCenterCircle);

    251.         // 画小指针
    252.         Path smallPath = new Path();
    253.         smallPath.moveTo(-centerX / eighteen, 0F);
    254.         smallPath.lineTo(centerX / eighteen, 0F);
    255.         smallPath.lineTo(0F, -centerX / THREE + padding / TWO);
    256.         smallPath.close();
    257.         canvas.drawPath(smallPath, paintSmallPoint);

    258.         // 画中心圆弧文字
    259.         Paint.FontMetrics fontMetrics = paintCenterText.getFontMetrics();
    260.         float textHeight = (float) Math.ceil(fontMetrics.leading - fontMetrics.ascent);
    261.         canvas.drawText(paintCenterText, "开始", 0F, textHeight / THREE);
    262.     }

    263.     /**
    264.      * vp2px 将vp转换成px
    265.      *
    266.      * @param size size
    267.      * @return int
    268.      */
    269.     public int vp2px(int size) {
    270.         int density = getResourceManager().getDeviceCapability().screenDensity / DeviceCapability.SCREEN_MDPI;
    271.         return size * density;
    272.     }

    273.     /**
    274.      * fivePoints 获取五角星的五个顶点
    275.      *
    276.      * @param pointXa 起始点A的x轴绝对位置
    277.      * @param pointYa 起始点A的y轴绝对位置
    278.      * @param sideLength 五角星的边长
    279.      * @return 五角星5个顶点坐标
    280.      */
    281.     private static float[] fivePoints(float pointXa, float pointYa, float sideLength) {
    282.         final int eighteen = 18;
    283.         float pointXb = pointXa + sideLength / TWO;
    284.         double num = sideLength * Math.sin(Math.toRadians(eighteen));
    285.         float pointXc = (float) (pointXa + num);
    286.         float pointXd = (float) (pointXa - num);
    287.         float pointXe = pointXa - sideLength / TWO;
    288.         float pointYb = (float) (pointYa + Math.sqrt(Math.pow(pointXc - pointXd, TWO)
    289.                 - Math.pow(sideLength / TWO, TWO)));
    290.         float pointYc = (float) (pointYa + Math.cos(Math.toRadians(eighteen)) * sideLength);
    291.         float pointYd = pointYc;
    292.         float pointYe = pointYb;
    293.         float[] points = new float[]{pointXa, pointYa, pointXd, pointYd, pointXb, pointYb,
    294.             pointXe, pointYe, pointXc, pointYc, pointXa, pointYa};
    295.         return points;
    296.     }

    297.     @Override
    298.     public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
    299.         final int seven = 7;
    300.         switch (touchEvent.getAction()) {
    301.             case TouchEvent.PRIMARY_POINT_DOWN:
    302.                 // 获取屏幕上点击的坐标
    303.                 float floatX = touchEvent.getPointerPosition(touchEvent.getIndex()).getX();
    304.                 float floatY = touchEvent.getPointerPosition(touchEvent.getIndex()).getY();
    305.                 float radius = centerX / seven + padding / TWO;
    306.                 boolean isScopeX = centerX - radius < floatX && centerX + radius > floatX;
    307.                 boolean isScopeY = centerY - radius < floatY && centerY + radius > floatY;
    308.                 if (isScopeX && isScopeY && !animatorVal.isRunning()) {
    309.                     startAnimator();
    310.                 }
    311.                 break;
    312.             case TouchEvent.PRIMARY_POINT_UP:
    313.                 // 松开取消
    314.                 invalidate();
    315.                 break;
    316.             default:
    317.                 break;
    318.         }
    319.         return true;
    320.     }

    321.     private void startAnimator() {
    322.         final int angle = 270;
    323.         startAngle = 0;
    324.         // 动画时长
    325.         final long animatorDuration = 4000L;
    326.         // 随机角度
    327.         int randomAngle = new SecureRandom().nextInt(CIRCLE);
    328.         animatorVal.setCurveType(Animator.CurveType.DECELERATE);
    329.         animatorVal.setDuration(animatorDuration);
    330.         animatorVal.setValueUpdateListener((AnimatorValue animatorValue, float value) -> {
    331.             startAngle = value * (CIRCLE * FIVE - randomAngle + angle);
    332.             invalidate();
    333.         });
    334.         stateChangedListener(animatorVal, randomAngle);
    335.         animatorVal.start();
    336.     }

    337.     private void stateChangedListener(AnimatorValue animatorValue, int randomAngle) {
    338.         final int four = 4;
    339.         final int six = 6;
    340.         animatorValue.setStateChangedListener(new Animator.StateChangedListener() {
    341.             @Override
    342.             public void onStart(Animator animator) {
    343.             }

    344.             @Override
    345.             public void onStop(Animator animator) {
    346.             }

    347.             @Override
    348.             public void onCancel(Animator animator) {
    349.             }

    350.             @Override
    351.             public void onEnd(Animator animator) {
    352.                 if (randomAngle >= 0 && randomAngle < avgAngle) {
    353.                     ToastUtils.showTips(context, "恭喜您中了一块华为手表");
    354.                 } else if (randomAngle >= avgAngle && randomAngle < TWO * avgAngle) {
    355.                     ToastUtils.showTips(context, "恭喜您中了一台华为平板");
    356.                 } else if (randomAngle >= TWO * avgAngle && randomAngle < THREE * avgAngle) {
    357.                     ToastUtils.showTips(context, "sorry,您没有中奖");
    358.                 } else if (randomAngle >= THREE * avgAngle && randomAngle < four * avgAngle) {
    359.                     ToastUtils.showTips(context, "恭喜您中了一部华为手机");
    360.                 } else if (randomAngle >= four * avgAngle && randomAngle < FIVE * avgAngle) {
    361.                     ToastUtils.showTips(context, "恭喜您中了一副华为耳机");
    362.                 } else if (randomAngle >= FIVE * avgAngle && randomAngle < six * avgAngle) {
    363.                     ToastUtils.showTips(context, "sorry,您没有中奖");
    364.                 } else {
    365.                     invalidate();
    366.                 }
    367.                 animator.release();
    368.             }

    369.             @Override
    370.             public void onPause(Animator animator) {
    371.             }

    372.             @Override
    373.             public void onResume(Animator animator) {
    374.             }
    375.         });
    376.     }
    377. }
    复制代码

  • 在MainAbilitySlice类中调用自定义圆形抽奖转盘组件类,显示圆形抽奖转盘。
    1. public class MainAbilitySlice extends AbilitySlice {
    2.     private DirectionalLayout myLayout = new DirectionalLayout(this);

    3.     @Override
    4.     public void onStart(Intent intent) {
    5.         super.onStart(intent);
    6.         DirectionalLayout.LayoutConfig config = new DirectionalLayout.LayoutConfig(
    7.                 DirectionalLayout.LayoutConfig.MATCH_PARENT, DirectionalLayout.LayoutConfig.MATCH_PARENT);
    8.         LuckyCirclePanComponent luckyCirclePanComponent = new LuckyCirclePanComponent(this);
    9.         luckyCirclePanComponent.setLayoutConfig(config);
    10.         myLayout.addComponent(luckyCirclePanComponent);
    11.         super.setUIContent(myLayout);
    12.     }

    13.     @Override
    14.     public void onActive() {
    15.         super.onActive();
    16.     }

    17.     @Override
    18.     public void onForeground(Intent intent) {
    19.         super.onForeground(intent);
    20.     }
    21. }
    复制代码

  • 新建ColorUtils类,对绘制圆盘所需RGB颜色进行封装。
    1. public class ColorUtils {
    2.     /**
    3.      * arc pink color
    4.      */
    5.     public static final Color ARC_PINK = new Color(Color.rgb(255, 163, 174));

    6.     /**
    7.      * arc yellow Color
    8.      */
    9.     public static final Color ARC_YELLOW = new Color(Color.rgb(255, 222, 78));

    10.     /**
    11.      * arc blue color
    12.      */
    13.     public static final Color ARC_BLUE = new Color(Color.rgb(118, 226, 219));

    14.     /**
    15.      * text color
    16.      */
    17.     public static final Color TEXT = new Color(Color.rgb(234, 134, 164));

    18.     /**
    19.      * paint pointer Color
    20.      */
    21.     public static final Color PAINT_POINTER = new Color(Color.rgb(246, 200, 216));

    22.     /**
    23.      * paint flower yellow Color
    24.      */
    25.     public static final Color PAINT_FLOWER_YELLOW = new Color(Color.rgb(243, 180, 104));

    26.     /**
    27.      * paint flower pink Color
    28.      */
    29.     public static final Color PAINT_FLOWER_PINK = new Color(Color.rgb(229, 136, 185));

    30.     /**
    31.      * paint out circle color
    32.      */
    33.     public static final Color PAINT_OUT_CIRCLE = new Color(Color.rgb(237, 109, 86));

    34.     private ColorUtils() {
    35.     }
    36. }
    复制代码

  • 新建PixelMapUtils类,加载本地图片资源,通过本地图片资源的resourceId,将图片转换成PixelMap类型。
    1. public class PixelMapUtils {
    2.     private static final String TAG = "PixelMapUtils";

    3.     private PixelMapUtils() {
    4.     }

    5.     private static byte[] readResource(Resource resource) {
    6.         final int bufferSize = 1024;
    7.         final int ioEnd = -1;
    8.         byte[] byteArray;
    9.         byte[] buffers = new byte[bufferSize];
    10.         try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
    11.             while (true) {
    12.                 int readLen = resource.read(buffers, 0, bufferSize);
    13.                 if (readLen == ioEnd) {
    14.                     LogUtils.error(TAG, "readResource finish");
    15.                     byteArray = output.toByteArray();
    16.                     break;
    17.                 }
    18.                 output.write(buffers, 0, readLen);
    19.             }
    20.         } catch (IOException e) {
    21.             LogUtils.debug(TAG, "readResource failed " + e.getLocalizedMessage());
    22.             return new byte[0];
    23.         }
    24.         LogUtils.info(TAG, "readResource len: " + byteArray.length);
    25.         return byteArray;
    26.     }

    27.     /**
    28.      * Creates a {@code PixelMap} object based on the image resource ID.
    29.      *
    30.      * This method only loads local image resources. If the image file does not exist or the loading fails,
    31.      * {@code null} is returned.
    32.      *
    33.      * @param resourceId Indicates the image resource ID.
    34.      * @param slice Indicates the slice.
    35.      * @return Returns the image.
    36.      */
    37.     public static Optional<PixelMap> createPixelMapByResId(int resourceId, Context slice) {
    38.         final float rotateDegrees = 90F;
    39.         ResourceManager manager = slice.getResourceManager();
    40.         if (manager == null) {
    41.             return Optional.empty();
    42.         }
    43.         try (Resource resource = manager.getResource(resourceId)) {
    44.             if (resource == null) {
    45.                 return Optional.empty();
    46.             }
    47.             ImageSource.SourceOptions srcOpts = new ImageSource.SourceOptions();
    48.             srcOpts.formatHint = "image/png";
    49.             ImageSource imageSource = ImageSource.create(readResource(resource), srcOpts);
    50.             if (imageSource == null) {
    51.                 return Optional.empty();
    52.             }
    53.             ImageSource.DecodingOptions decodingOpts = new ImageSource.DecodingOptions();
    54.             decodingOpts.desiredSize = new Size(0, 0);
    55.             decodingOpts.desiredRegion = new Rect(0, 0, 0, 0);
    56.             decodingOpts.desiredPixelFormat = PixelFormat.ARGB_8888;
    57.             decodingOpts.rotateDegrees = rotateDegrees;

    58.             return Optional.of(imageSource.createPixelmap(decodingOpts));
    59.         } catch (NotExistException | IOException e) {
    60.             return Optional.empty();
    61.         }
    62.     }
    63. }
    复制代码

  • 新建LogUtils类,对HiLog日志进行封装。
    1. public class LogUtils {
    2.     private static final String TAG_LOG = "LogUtil";

    3.     private static final HiLogLabel LABEL_LOG = new HiLogLabel(0, 0, LogUtils.TAG_LOG);

    4.     private static final String LOG_FORMAT = "%{public}s: %{public}s";

    5.     private LogUtils() {
    6.     }

    7.     /**
    8.      * Print debug log
    9.      *
    10.      * @param tag log tag
    11.      * @param msg log message
    12.      */
    13.     public static void debug(String tag, String msg) {
    14.         HiLog.debug(LABEL_LOG, LOG_FORMAT, tag, msg);
    15.     }

    16.     /**
    17.      * Print info log
    18.      *
    19.      * @param tag log tag
    20.      * @param msg log message
    21.      */
    22.     public static void info(String tag, String msg) {
    23.         HiLog.info(LABEL_LOG, LOG_FORMAT, tag, msg);
    24.     }

    25.     /**
    26.      * Print warn log
    27.      *
    28.      * @param tag log tag
    29.      * @param msg log message
    30.      */
    31.     public static void warn(String tag, String msg) {
    32.         HiLog.warn(LABEL_LOG, LOG_FORMAT, tag, msg);
    33.     }

    34.     /**
    35.      * Print error log
    36.      *
    37.      * @param tag log tag
    38.      * @param msg log message
    39.      */
    40.     public static void error(String tag, String msg) {
    41.         HiLog.error(LABEL_LOG, LOG_FORMAT, tag, msg);
    42.     }
    43. }
    复制代码

  • 新建ToastUtils类,抽奖结束后,弹出中奖提示信息。

    1. public final class ToastUtils {
    2.     private static final int TEXT_SIZE = 48;
    3.     private static final int LEFT_PADDING = 30;
    4.     private static final int TOP_PADDING = 20;
    5.     private static final int RIGHT_PADDING = 30;
    6.     private static final int BOTTOM_PADDING = 20;
    7.     private static final int RGB_COLOR = 0x666666FF;
    8.     private static final int CORNER_RADIUS = 15;
    9.     private static final int DURATION = 4000;

    10.     private ToastUtils() {
    11.     }

    12.     /**
    13.      * Show tips
    14.      *
    15.      * @param context ability slice
    16.      * @param msg show msg
    17.      */
    18.     public static void showTips(Context context, String msg) {
    19.         Text text = new Text(context);
    20.         text.setWidth(MATCH_CONTENT);
    21.         text.setHeight(MATCH_CONTENT);
    22.         text.setTextSize(TEXT_SIZE);
    23.         text.setText(msg);
    24.         text.setPadding(LEFT_PADDING, TOP_PADDING, RIGHT_PADDING, BOTTOM_PADDING);
    25.         text.setMultipleLine(true);
    26.         text.setTextColor(Color.WHITE);
    27.         text.setTextAlignment(TextAlignment.CENTER);
    28.         DirectionalLayout.LayoutConfig config = new DirectionalLayout.LayoutConfig();
    29.         config.alignment = LayoutAlignment.CENTER;
    30.         text.setLayoutConfig(config);

    31.         ShapeElement style = new ShapeElement();
    32.         style.setShape(ShapeElement.RECTANGLE);
    33.         style.setRgbColor(new RgbColor(RGB_COLOR));
    34.         style.setCornerRadius(CORNER_RADIUS);
    35.         text.setBackground(style);

    36.         ToastDialog toastDialog = new ToastDialog(context);
    37.         toastDialog.setSize(MATCH_PARENT, MATCH_CONTENT);
    38.         toastDialog.setDuration(DURATION);
    39.         toastDialog.setAutoClosable(true);
    40.         toastDialog.setTransparent(true);

    41.         toastDialog.setAlignment(LayoutAlignment.CENTER);
    42.         toastDialog.setComponent(text);
    43.         toastDialog.show();
    44.     }
    45. }
    复制代码

说明
以上代码示例仅供参考使用,产品化的代码需要考虑数据校验和国际化。

11. 恭喜你      
通过本教程的学习,你已学会了如何使用自定义组件实现圆形抽奖转盘。
   

回帖

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。 侵权投诉
链接复制成功,分享给好友