如何使用自定义组件实现圆形抽奖转盘

奶盖
发布于 2021-6-21 11:24
浏览
1收藏

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方法。

说明
使用自定义组件实现Component.EstimateSizeListener接口需要HarmonyOS SDK版本在2.1.0.13或以上。

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

2. 基本步骤

  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); 
  	} 
  }
  1. 实现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. 画外部圆盘

  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(); 
  	} 
  }
  1. 画外部圆盘:在指定的X、Y(0F, 0F)坐标处,画一个半径为centerX - padding的圆形(其实就是绘制一个红色的圆盘)。
  paintOutCircle.setColor(ColorUtils.PAINT_OUT_CIRCLE); 
  canvas.drawCircle(0F, 0F, centerX - padding, paintOutCircle);
  1. 画外部圆盘边上的小圈圈和五角星:接下来一个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(); 
  }
  1. 画五角星:通过计算获取到五角星的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; 
  }

5. 画内部扇形抽奖区域

  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; 
  	} 
  }
  1. 画抽奖区域文字:利用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; 
   } 
}
  1. 画抽奖区域文字对应图片: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(); 
  	} 
  }

6. 画中心圆盘和指针

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

  1. 画中心圆盘小指针:与步骤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);
  1. 画中心圆弧文字:通过Paint的getFontMetrics()方法,获取绘制字体的建议行距,然后根据建议行距去绘制文本。
  Paint.FontMetrics fontMetrics = paintCenterText.getFontMetrics(); 
  float textHeight = (float) Math.ceil(fontMetrics.leading - fontMetrics.ascent); 
  canvas.drawText(paintCenterText, "开始", 0F, textHeight / THREE);

7. 实现抽奖功能

  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; 
}
  1. 圆形转盘开始转动抽奖:给转盘指定一个随机的转动角度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(); 
  }

8. 最终实现效果

如何使用自定义组件实现圆形抽奖转盘-鸿蒙开发者社区

如何使用自定义组件实现圆形抽奖转盘-鸿蒙开发者社区

9. 代码示例

代码结构解读

为了方便您的学习,我们提供了自定义圆形抽奖转盘示例工程的代码,代码的工程结构描述如下:
如何使用自定义组件实现圆形抽奖转盘-鸿蒙开发者社区

  • 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>
  1. 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_watch, getContext()).get(), 
           PixelMapUtils.createPixelMapByResId(ResourceTable.Media_tablet, getContext()).get(), 
           PixelMapUtils.createPixelMapByResId(ResourceTable.Media_thanks, getContext()).get(), 
           PixelMapUtils.createPixelMapByResId(ResourceTable.Media_phone, getContext()).get(), 
           PixelMapUtils.createPixelMapByResId(ResourceTable.Media_headset, getContext()).get(), 
           PixelMapUtils.createPixelMapByResId(ResourceTable.Media_thanks, 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) { 
           } 
       }); 
   } 
}
  1. 在MainAbilitySlice类中调用自定义圆形抽奖转盘组件类,显示圆形抽奖转盘。
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() { 
   } 
}

  1. 新建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() { 
      } 
  }
  1. 新建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(); 
          } 
      } 
  }
  1. 新建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); 
      } 
  }

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

10. 恭喜您

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

已于2021-6-21 11:24:28修改
2
收藏 1
回复
举报
回复
    相关推荐