1. 介绍

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

2. 搭建HarmonyOS环境

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

  • 安装DevEco Studio,详情请参考下载和安装软件。
  • 设置DevEco Studio开发环境,DevEco Studio开发环境依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
    1. 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
    2. 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。

3. 基本步骤

  1. 创建自定义组件的类,并继承Component或其子类,添加构造方法。

    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);
    }
    

    4. 获取屏幕大小、中心点

    实现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;
}

5. 画外部圆盘

  1. 画外部圆盘的花瓣:通过调用Canvas的rotate()方法,将画布旋转指定角度。通过调用Canvas的save()和restore()方法,使画布保存最新的绘制状态。根据想要绘制的花瓣个数,改变旋转角度,循环画出花瓣效果。
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(); }
}

2.画外部圆盘:在指定的X、Y(0F, 0F)坐标处,画一个半径为centerX - padding的圆形(其实就是绘制一个红色的圆盘)。

paintOutCircle.setColor(ColorUtils.PAINT_OUT_CIRCLE);
canvas.drawCircle(0F, 0F, centerX - padding, paintOutCircle);

3.画外部圆盘边上的小圈圈和五角星:接下来一个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();
}

4.画五角星:通过计算获取到五角星的5个顶点位置(计算依据:五角星每个角的角度为36°,然后根据三角函数即可算出各个点的坐标),再使用Canvas、Path、Paint将5个顶点通过画线连接在一起,就完成了五角星的绘制。

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;
}

6. 画内部扇形抽奖区域

  1. 画抽奖区域扇形:使用RectFloat和Arc对象绘制弧,rect表示圆弧包围矩形的左上角和右下角的坐标,参数new Arc(startAngle, avgAngle, true)表示圆弧参数,例如起始角度、后掠角以及是否从圆弧的两个端点到其中心绘制直线。
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; }
}

2.画抽奖区域文字:利用Path,创建绘制路径,添加Arc,然后设置水平和垂直的偏移量。垂直偏移量radius / FIVE就是当前Arc朝着圆心移动的距离;水平偏移量,就是顺时针去旋转,水平偏移(Math.sin(avgAngle / CIRCLE * Math.PI) * radius) - measureWidth / TWO,是为了让文字在当前弧范围文字居中。最后,用path去绘制文本。

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; }
}

3.画抽奖区域文字对应图片:pixelMaps表示文字对应的图片ResourceId转换成PixelMap的数组,pixelMapHolderList表示将PixelMap转换成PixelMapHolder图片List,dst表示PixelMapHolder对象的左上角( -imageHeight / TWO,imageHeight / TWO)和右下角(centerX / THREE + imageWidth,centerX / THREE)的坐标。

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(); }
}

7. 画中心圆盘和指针

  1. 画中心圆盘大指针:通过Path ,确定要移动的三个点的坐标(-centerX / nine, 0F)、(centerX / nine, 0F)、(0F, -centerX / THREE),去绘制指针。
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);

2.画内部大圆和小圆:在圆盘圆心处,绘制两个半径分别为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);

3.画中心圆盘小指针:与步骤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);

4.画中心圆弧文字:通过Paint的getFontMetrics()方法,获取绘制字体的建议行距,然后根据建议行距去绘制文本。

Paint.FontMetrics fontMetrics = paintCenterText.getFontMetrics();
float textHeight = (float) Math.ceil(fontMetrics.leading - fontMetrics.ascent);
canvas.drawText(paintCenterText, "开始", 0F, textHeight / THREE);

8. 实现抽奖功能

  1. 实现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();
}

9. 最终实现效果

10. 代码示例

代码结构解读

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

  • 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. ability_main.xml布局文件。
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:height="match_parent" ohos:width="match_parent" ohos:orientation="vertical"> <Text ohos:id="$+id:text_helloworld" ohos:height="match_content" ohos:width="match_content" ohos:background_element="$graphic:background_ability_main" ohos:layout_alignment="horizontal_center" ohos:text="Hello World" ohos:text_size="50" />
</DirectionalLayout>

2.background_ability_main.xml样式文件

<?xml version="1.0" encoding="UTF-8" ?>
<shape xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:shape="rectangle"> <solid ohos:color="#FFFFFF"/>
</shape>

业务逻辑代码

  1. 新建LuckyCirclePanComponent类,实现自定义组件,绘制圆形抽奖转盘,并实现抽奖效果。
public class LuckyCirclePanComponent extends Component implements Component.DrawTask, Component.EstimateSizeListener, Component.TouchEventListener { private static final int CIRCLE = 360; private static final int TWO = 2; private static final int THREE = 3; private static final int FIVE = 5; // 盘块的个数 private static final int COUNT = 6; // 开始角度 private float startAngle = 0; // 平均每份的角度 private float avgAngle = CIRCLE / COUNT; // 边框间距 private int padding; // 宽度和高度 private float width; private float height; // 中心的X,Y坐标 private float centerX; private float centerY; // 画内部扇形的画笔 private Paint paintInnerArc; // 画内部扇形文字的画笔 private Paint paintArcText; // 外部圆弧的画笔 private Paint paintOutCircle; // 内部圆弧的画笔 private Paint paintCenterCircle; // 中间文字的画笔 private Paint paintCenterText; // 画转盘大指针的画笔 private Paint paintPointer; // 画转盘小指针的画笔 private Paint paintSmallPoint; // 画图片的画笔 private Paint paintImage; // 画外圆外面的花瓣 private Paint paintFlower; // 画五角星的画笔 private Paint paintFive; // 每个盘块的颜色 private Color[] colors = {ColorUtils.ARC_PINK, ColorUtils.ARC_YELLOW, ColorUtils.ARC_BLUE, ColorUtils.ARC_PINK, ColorUtils.ARC_YELLOW, ColorUtils.ARC_BLUE, }; // 抽奖的文字 private String[] textArrs = {"华为手表", "华为平板", "恭喜发财", "华为手机", "华为耳机", "恭喜发财"}; // 与文字对应的图片private PixelMap[] pixelMaps = {PixelMapUtils.createPixelMapByResId(ResourceTable.Media_icon, getContext()).get(),PixelMapUtils.createPixelMapByResId(ResourceTable.Media_icon, getContext()).get(),PixelMapUtils.createPixelMapByResId(ResourceTable.Media_icon, getContext()).get(),PixelMapUtils.createPixelMapByResId(ResourceTable.Media_icon, getContext()).get(),PixelMapUtils.createPixelMapByResId(ResourceTable.Media_icon, getContext()).get(),PixelMapUtils.createPixelMapByResId(ResourceTable.Media_icon, getContext()).get()};private List<PixelMapHolder> pixelMapHolderList; private Context context; // 动画 private AnimatorValue animatorVal = new AnimatorValue(); /** * constructor of LuckyCirclePanComponent * * @param context context */ public LuckyCirclePanComponent(Context context) { super(context); this.context = context; // 初始化画笔 initPaint(); // 获取屏幕的宽高度、中心点坐标,调用onEstimateSize方法 setEstimateSizeListener(this); // 添加绘制任务,调用onDraw方法 addDrawTask(this); // 实现点击中心圆盘区域位置开始抽奖功能,调用onTouchEvent方法 setTouchEventListener(this); } private void initPaint() { final int size = 20; padding = vp2px(size); pixelMapToPixelMapHolder(); paintInnerArc = new Paint(); paintInnerArc.setAntiAlias(true); paintInnerArc.setStyle(Paint.Style.FILL_STYLE); paintArcText = new Paint(); paintArcText.setAntiAlias(true); paintArcText.setTextSize(padding); paintArcText.setStyle(Paint.Style.FILL_STYLE); paintArcText.setColor(ColorUtils.TEXT); paintOutCircle = new Paint(); paintOutCircle.setAntiAlias(true); paintOutCircle.setStyle(Paint.Style.FILL_STYLE); paintCenterCircle = new Paint(); paintCenterCircle.setAntiAlias(true); paintCenterCircle.setStyle(Paint.Style.FILL_STYLE); paintPointer = new Paint(); paintPointer.setAntiAlias(true); paintPointer.setStyle(Paint.Style.FILL_STYLE); paintPointer.setColor(ColorUtils.PAINT_POINTER); paintCenterText = new Paint(); paintCenterText.setAntiAlias(true); paintCenterText.setTextSize(padding); paintCenterText.setStyle(Paint.Style.FILL_STYLE); paintCenterText.setColor(ColorUtils.TEXT); paintCenterText.setTextAlign(TextAlignment.CENTER); paintImage = new Paint(); paintImage.setAntiAlias(true); paintImage.setStrokeCap(Paint.StrokeCap.ROUND_CAP); paintImage.setStyle(Paint.Style.STROKE_STYLE); paintFlower = new Paint(); paintFlower.setAntiAlias(true); paintFlower.setStyle(Paint.Style.FILL_STYLE); paintFive = new Paint(); paintFive.setAntiAlias(true); paintFive.setStyle(Paint.Style.FILL_STYLE); paintFive.setColor(Color.YELLOW); paintSmallPoint = new Paint(); paintSmallPoint.setStyle(Paint.Style.FILL_STYLE); paintSmallPoint.setColor(Color.WHITE); } @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; } @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); } 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(); } } private void drawOutCircleAndFive(Canvas canvas) { paintOutCircle.setColor(ColorUtils.PAINT_OUT_CIRCLE); canvas.drawCircle(0F, 0F, centerX - padding, paintOutCircle); 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(); } } 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); } 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; } } 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; } } 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)); } } 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); // 画内部大圆 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); // 画小指针 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); // 画中心圆弧文字 Paint.FontMetrics fontMetrics = paintCenterText.getFontMetrics(); float textHeight = (float) Math.ceil(fontMetrics.leading - fontMetrics.ascent); canvas.drawText(paintCenterText, "开始", 0F, textHeight / THREE); } /** * vp2px 将vp转换成px * * @param size size * @return int */ public int vp2px(int size) { int density = getResourceManager().getDeviceCapability().screenDensity / DeviceCapability.SCREEN_MDPI; return size * density; } /** * 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; } @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; } 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(); } private void stateChangedListener(AnimatorValue animatorValue, int randomAngle) { final int four = 4; final int six = 6; animatorValue.setStateChangedListener(new Animator.StateChangedListener() { @Override public void onStart(Animator animator) { } @Override public void onStop(Animator animator) { } @Override public void onCancel(Animator animator) { } @Override public void onEnd(Animator animator) { if (randomAngle >= 0 && randomAngle < avgAngle) { ToastUtils.showTips(context, "恭喜您中了一块华为手表"); } else if (randomAngle >= avgAngle && randomAngle < TWO * avgAngle) { ToastUtils.showTips(context, "恭喜您中了一台华为平板"); } else if (randomAngle >= TWO * avgAngle && randomAngle < THREE * avgAngle) { ToastUtils.showTips(context, "sorry,您没有中奖"); } else if (randomAngle >= THREE * avgAngle && randomAngle < four * avgAngle) { ToastUtils.showTips(context, "恭喜您中了一部华为手机"); } else if (randomAngle >= four * avgAngle && randomAngle < FIVE * avgAngle) { ToastUtils.showTips(context, "恭喜您中了一副华为耳机"); } else if (randomAngle >= FIVE * avgAngle && randomAngle < six * avgAngle) { ToastUtils.showTips(context, "sorry,您没有中奖"); } else { invalidate(); } animator.release(); } @Override public void onPause(Animator animator) { } @Override public void onResume(Animator animator) { } }); }
}

2.在MainAbilitySlice类中调用自定义圆形抽奖转盘组件类,显示圆形抽奖转盘。

public class MainAbilitySlice extends AbilitySlice { private DirectionalLayout myLayout = new DirectionalLayout(this); @Override public void onStart(Intent intent) { super.onStart(intent); DirectionalLayout.LayoutConfig config = new DirectionalLayout.LayoutConfig( DirectionalLayout.LayoutConfig.MATCH_PARENT, DirectionalLayout.LayoutConfig.MATCH_PARENT); LuckyCirclePanComponent luckyCirclePanComponent = new LuckyCirclePanComponent(this); luckyCirclePanComponent.setLayoutConfig(config); myLayout.addComponent(luckyCirclePanComponent); super.setUIContent(myLayout); } @Override public void onActive() { super.onActive(); } @Override public void onForeground(Intent intent) { super.onForeground(intent); }
}

3.新建ColorUtils类,对绘制圆盘所需RGB颜色进行封装。

public class ColorUtils { /** * arc pink color */ public static final Color ARC_PINK = new Color(Color.rgb(255, 163, 174)); /** * arc yellow Color */ public static final Color ARC_YELLOW = new Color(Color.rgb(255, 222, 78)); /** * arc blue color */ public static final Color ARC_BLUE = new Color(Color.rgb(118, 226, 219)); /** * text color */ public static final Color TEXT = new Color(Color.rgb(234, 134, 164)); /** * paint pointer Color */ public static final Color PAINT_POINTER = new Color(Color.rgb(246, 200, 216)); /** * paint flower yellow Color */ public static final Color PAINT_FLOWER_YELLOW = new Color(Color.rgb(243, 180, 104)); /** * paint flower pink Color */ public static final Color PAINT_FLOWER_PINK = new Color(Color.rgb(229, 136, 185)); /** * paint out circle color */ public static final Color PAINT_OUT_CIRCLE = new Color(Color.rgb(237, 109, 86)); private ColorUtils() { }
}

4.新建PixelMapUtils类,加载本地图片资源,通过本地图片资源的resourceId,将图片转换成PixelMap类型。

public class PixelMapUtils { private static final String TAG = "PixelMapUtils"; private PixelMapUtils() { } private static byte[] readResource(Resource resource) { final int bufferSize = 1024; final int ioEnd = -1; byte[] byteArray; byte[] buffers = new byte[bufferSize]; try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { while (true) { int readLen = resource.read(buffers, 0, bufferSize); if (readLen == ioEnd) { LogUtils.error(TAG, "readResource finish"); byteArray = output.toByteArray(); break; } output.write(buffers, 0, readLen); } } catch (IOException e) { LogUtils.debug(TAG, "readResource failed " + e.getLocalizedMessage()); return new byte[0]; } LogUtils.info(TAG, "readResource len: " + byteArray.length); return byteArray; } /** * Creates a {@code PixelMap} object based on the image resource ID. * * This method only loads local image resources. If the image file does not exist or the loading fails, * {@code null} is returned. * * @param resourceId Indicates the image resource ID. * @param slice Indicates the slice. * @return Returns the image. */ public static Optional<PixelMap> createPixelMapByResId(int resourceId, Context slice) { final float rotateDegrees = 90F; ResourceManager manager = slice.getResourceManager(); if (manager == null) { return Optional.empty(); } try (Resource resource = manager.getResource(resourceId)) { if (resource == null) { return Optional.empty(); } ImageSource.SourceOptions srcOpts = new ImageSource.SourceOptions(); srcOpts.formatHint = "image/png"; ImageSource imageSource = ImageSource.create(readResource(resource), srcOpts); if (imageSource == null) { return Optional.empty(); } ImageSource.DecodingOptions decodingOpts = new ImageSource.DecodingOptions(); decodingOpts.desiredSize = new Size(0, 0); decodingOpts.desiredRegion = new Rect(0, 0, 0, 0); decodingOpts.desiredPixelFormat = PixelFormat.ARGB_8888; decodingOpts.rotateDegrees = rotateDegrees; return Optional.of(imageSource.createPixelmap(decodingOpts)); } catch (NotExistException | IOException e) { return Optional.empty(); } }
}

5.新建LogUtils类,对HiLog日志进行封装。

public class LogUtils { private static final String TAG_LOG = "LogUtil"; private static final HiLogLabel LABEL_LOG = new HiLogLabel(0, 0, LogUtils.TAG_LOG); private static final String LOG_FORMAT = "%{public}s: %{public}s"; private LogUtils() { } /** * Print debug log * * @param tag log tag * @param msg log message */ public static void debug(String tag, String msg) { HiLog.debug(LABEL_LOG, LOG_FORMAT, tag, msg); } /** * Print info log * * @param tag log tag * @param msg log message */ public static void info(String tag, String msg) { HiLog.info(LABEL_LOG, LOG_FORMAT, tag, msg); } /** * Print warn log * * @param tag log tag * @param msg log message */ public static void warn(String tag, String msg) { HiLog.warn(LABEL_LOG, LOG_FORMAT, tag, msg); } /** * Print error log * * @param tag log tag * @param msg log message */ public static void error(String tag, String msg) { HiLog.error(LABEL_LOG, LOG_FORMAT, tag, msg); }
}

6.新建ToastUtils类,对ToastDialg提示对话框进行封装。

import ohos.agp.window.dialog.ToastDialog;
import ohos.app.Context;public class ToastUtils {public static void showTips(Context context,String info){new ToastDialog(context).setText(info).show();}
}

【鸿蒙】HarMonyOS的自定义组件之抽奖大转盘相关推荐

  1. 微信小程序独家秘笈之抽奖大转盘

    代码地址如下: http://www.demodashi.com/demo/14209.html 一.前期准备工作 软件环境:微信开发者工具 官方下载地址:https://mp.weixin.qq.c ...

  2. css3抽奖转盘,从零制作CSS3抽奖大转盘

    今天的任务是做一个纯CSS3的还算比较漂亮的抽奖大转盘,也就是下图效果. 我只说思路和重要的CSS3代码. Paste_Image.png 外盘 外盘是指有彩灯的深橙色圆环,以及圆环的外边线. 外盘设 ...

  3. 用c语言写抽奖大转盘,iOS抽奖大转盘的二种实现方法

    有个朋友需要写个抽奖大转盘的功能,就让我帮忙写了下.我用了2种方法实现了效果,在这里和大家一起分享下. 一.一键转动大转盘 我一开始拿到手的是一堆的图片,然后自己花了点时间,搭建出美工要求的UI,接下 ...

  4. javaScript实现抽奖大转盘(一)

    今天试了试自己写个抽奖大转盘. 先是借了两张别人的图片: 下面是布局部分: <div class="round"><div class="box&quo ...

  5. 抽奖大转盘-React-移动端

    抽奖大转盘-React-移动端 react安装 修改项目结构 配置路由 删除一些不必要的文件 大转盘 整理代码结构和一些静态资源 书写静态页面 移动端px-rem转换 静态页面 静态页面样式 抽奖大转 ...

  6. 优秀课程案例:使用Scratch制作一个抽奖大转盘方法二!

    点击上面微信号关注我关注我哟每天坚持早上9:00左右推送文章,争取做到日更,喜欢的可以设置星标,并分享点赞我们的文章,非常感谢大家的支持,您的点击的在看就是我们的动力! 昨天我们分享了一个抽奖大转盘: ...

  7. Redis 抽奖大转盘的实战示例

    本文主要介绍了Redis 抽奖大转盘的实战示例,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下.编程学习资料点击领取 目录 1. 项目介绍 2. 项目演示 3. 表结 ...

  8. jQuery实现简单抽奖大转盘

    上效果!!! 初始页面效果 点击开始后效果 上代码 <!DOCTYPE html> <html lang="en"> <head><met ...

  9. vue幸运抽奖大转盘的丑绝实现

    自己通过canvas+vue(vue不是必备的可以)实现的一个抽奖转盘,重点在实现逻辑,所以样式丑绝. 基本效果图,中间指针可以替换为图片 数据格式 转盘的分块由传入的数组长度确定,分为4,6,8块还 ...

最新文章

  1. windows中的常用Dos命令
  2. 多线程:线程安全?如何实现?
  3. async与await封装ajax请求
  4. 数据结构 最长公共子序列问题
  5. 昆仑通态复制的程序可以用吗_昆仑通态专题(七):MCGS组态软件的设备窗口...
  6. 线上分享|云和恩墨大讲堂201902:MySQL基础之体系结构
  7. [机器学习]-K近邻-最简单的入门实战例子
  8. java在线反编译class文件
  9. 通过银行卡号获取银行名称和银行图标的ICON
  10. linux rz sz putty,PuTTY xshell rz sz命令实现上传下载到windows的方法
  11. python实现pearson相关性检验
  12. 我的叔叔精通计算机英语翻译,人教小学英语精通版 三年级下册Unit3 课文翻译...
  13. 初中生学计算机编程的好处,为什么初中生更加适合学习计算机编程?
  14. java匿名内部类,什么是匿名内部类,如何定义匿名内部类,如何使用匿名内部类?
  15. HTML 与 CSS
  16. 天翼云联想云坚果云我应该选择哪一个呢?
  17. jQuery轮播图之上下轮播
  18. 未明学院:中国最有钱大学top榜单出炉,你的学校排第几?
  19. 金蝶K3案例教程应付账款前台操作
  20. 自动驾驶公交车第 1 部分:车辆运营技术要求

热门文章

  1. 怎么批量给PDF加水印?
  2. 遨博机器人aubo_robot 包编译问题及解决方法
  3. BBS社区运营,需要什么专业知识?
  4. Simulink代码生成:生成ASAP2文件
  5. 2021-03-24---------绩优股指数和垃圾股指数-------------120日均线-----
  6. 如何使用Python画QQ图
  7. sql语句查询 近7天,三十天数据
  8. wot的游戏引擎很牛吧_使用WOT享受更安全的Web浏览
  9. [leetCode]327. 区间和的个数
  10. VS中使用QT,多国语言翻译问题