HarmonyOS custom lottery carousel development (ArkTS)

Introduction

This codelab is a custom lottery round turntable based on canvas components and explicit animation. Contains the following functions:

1. Use the Canvas component to draw the lottery round turntable.

2. Start the lottery function through explicit animation.

3. Pop up the prizes won through a customized pop-up window.

Related concepts

● Stack component: Stacking containers, sub-components are pushed onto the stack in order, and the latter sub-component covers the previous sub-component.

● Canvas: Canvas component, used for custom drawing graphics.

● CanvasRenderingContext2D object: Use RenderingContext to draw on the Canvas component. The drawing object can be a rectangle, text, picture, etc.

● Explicit animation: Provides the global animateTo explicit animation interface to specify the insertion of transition animations for state changes caused by closure code.

● Custom pop-up window: Display custom pop-up window through CustomDialogController class.

Complete example

gitee source code address

Source code download

Custom lottery wheel (ArkTS).zip

Environment setup

We first need to complete the establishment of the HarmonyOS development environment. Please refer to the following steps.

Software requirements

● DevEco Studio version: DevEco Studio 3.1 Release.

● HarmonyOS SDK version: API version 9.

Hardware requirements

● Device type: Huawei mobile phone or Huawei mobile phone device simulator running on DevEco Studio.

● HarmonyOS system: 3.1.0 Developer Release.

Environment setup

1. Install DevEco Studio. For details, please refer to Download and Install the Software.

2. Set up the DevEco Studio development environment. The DevEco Studio development environment depends on the network environment. You need to be connected to the network to ensure the normal use of the tool. You can configure the development environment according to the following two situations: If you can directly access the Internet, you only need to download it. HarmonyOS SDK operation.

a. If the network cannot directly access the Internet and needs to be accessed through a proxy server, please refer to Configuring the Development Environment.

3. Developers can refer to the following link to complete the relevant configuration for device debugging: Debugging using a real device

a. Use the simulator for debugging

Interpretation of code structure

This codelab only explains the core code. For the complete code, we will provide it in the source code download or gitee.

├──entry/src/main/ets // Code area</code><code>│ ├──common</code><code>│ │ ├──constants</code><code>│ │ │ ├──ColorConstants.ets // Color constant class</code><code>│ │ │ ├──CommonConstants.ets // Public constant class</code><code>│ │ │ └──StyleConstants.ets // Style constant class</code><code>│ │ └──utils</code><code>│ │ ├──CheckEmptyUtils.ets // Data empty checking tool class</code><code>│ │ └ ──Logger.ets // Log printing class</code><code>│ ├──entryability</code><code>│ │ └──EntryAbility.ts // Program entry class</code><code>│ ├──pages</code><code>│ │ └──CanvasPage.ets // Main interface </code><code>│ ├──view</code><code>│ │ └──PrizeDialog.ets // Winning information pop-up window class</code><code>│ └──viewmodel</code><code>│ ├──DrawModel.ets // Canvas related method class</code><code>│ ├── FillArcData.ets // Drawing arc data entity class</code><code>│ └──PrizeData.ets // Winning information entity class</code><code>└──entry/src/main/resources // Resource file directory</code>

Build the main interface

In this chapter, we will complete the development of the sample main interface, as shown in the figure:

Before drawing the lottery round wheel, you first need to obtain the width and height of the screen in the aboutToAppear method of CanvasPage.ets.

// CanvasPage.ets</code><code>// Get context</code><code>let context = getContext(this);</code>
<code>aboutToAppear() {<!-- --></code><code> // Get the width and height of the screen</code><code> window.getLastWindow(context)</code><code> .then ((windowClass: window.Window) => {<!-- --></code><code> let windowProperties = windowClass.getWindowProperties();</code><code> this.screenWidth = px2vp(windowProperties.windowRect .width);</code><code> this.screenHeight = px2vp(windowProperties.windowRect.height);</code><code> })</code><code> .catch((error: Error) => {<!-- --></code><code> Logger.error('Failed to obtain the window size. Cause: ' + JSON.stringify(error));</code><code> }) </code><code>}</code>

Add the Canvas component to the CanvasPage.ets layout interface and draw in the onReady method.

// CanvasPage.ets</code><code>private settings: RenderingContextSettings = new RenderingContextSettings(true);</code><code>private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);</code>
<code>Stack({ alignContent: Alignment.Center }) {<!-- --></code><code> Canvas(this.canvasContext)</code><code> ...</code><code> .onReady(() => {<!-- --></code><code> // Draw through the draw method</code><code> this.drawModel.draw(this.canvasContext, this.screenWidth , this.screenHeight);</code><code> })</code>
<code> //Start lottery drawing</code><code> Image($r('app.media.ic_center'))</code><code> ...</code><code>}</code><code>...</code>

In DrawModel.ets, draw the custom circular lottery wheel step by step through the draw method.

// DrawModel.ets</code><code>// Draw the lottery round turntable</code><code>draw(canvasContext: CanvasRenderingContext2D, screenWidth: number, screenHeight: number) {<!-- -- ></code><code> if (CheckEmptyUtils.isEmptyObj(canvasContext)) {<!-- --></code><code> Logger.error('[DrawModel][draw] canvasContext is empty.' );</code><code> return;</code><code> }</code><code> this.canvasContext= canvasContext;</code><code> this.screenWidth = screenWidth;</code><code> this.canvasContext.clearRect(0, 0, this.screenWidth, screenHeight);</code><code> // Translate the canvas by a specified distance along the X and Y axes</code><code> this.canvasContext.translate (this.screenWidth / CommonConstants.TWO,</code><code> screenHeight / CommonConstants.TWO);</code><code> // Draw the petals of the outer disk</code><code> this.drawFlower() ;</code><code> // Draw outer disks and small circles</code><code> this.drawOutCircle();</code><code> // Draw inner disks</code><code> this.drawInnerCircle();</code><code> // Draw the internal fan-shaped lottery area</code><code> this.drawInnerArc();</code><code> // Draw the text in the internal fan-shaped area</code> code><code> this.drawArcText();</code><code> // Draw the picture corresponding to the prize in the internal sector area</code><code> this.drawImage();</code><code> this. canvasContext.translate(-this.screenWidth / CommonConstants.TWO,</code><code> -screenHeight / CommonConstants.TWO);</code><code>}</code>

Draw the outer disk

Draw the petals of the outer disk: Rotate the canvas by a specified angle by calling the rotate() method. Then by calling the save() and restore() methods,

Causes the canvas to save the latest drawing state. According to the number of petals you want to draw, change the rotation angle and draw the petal effect in a loop.

// DrawModel.ets</code><code>// Draw the petals of the outer disk</code><code>drawFlower() {<!-- --></code><code> let beginAngle = this.startAngle + this.avgAngle;</code><code> const pointY = this.screenWidth * CommonConstants.FLOWER_POINT_Y_RATIOS;</code><code> const radius = this.screenWidth * CommonConstants.FLOWER_RADIUS_RATIOS;</code><code> const innerRadius = this.screenWidth * CommonConstants.FLOWER_INNER_RATIOS;</code><code> for (let i = 0; i < CommonConstants.COUNT; i + + ) {<!-- --></code><code> this.canvasContext?.save();</code><code> this.canvasContext?.rotate(beginAngle * Math.PI / CommonConstants.HALF_CIRCLE);</code><code> this.fillArc(new FillArcData( 0, -pointY, radius, 0, Math.PI * CommonConstants.TWO),</code><code> ColorConstants.FLOWER_OUT_COLOR);</code>
<code> this.fillArc(new FillArcData(0, -pointY, innerRadius, 0, Math.PI * CommonConstants.TWO),</code><code> ColorConstants.FLOWER_INNER_COLOR);</code><code> beginAngle + = this.avgAngle;</code><code> this.canvasContext?.restore();</code><code> }</code><code>}</code>
<code>// Draw arc method</code><code>fillArc(fillArcData: FillArcData, fillColor: string) {<!-- --></code><code> if (CheckEmptyUtils.isEmptyObj(fillArcData) | | CheckEmptyUtils.isEmptyStr(fillColor)) {<!-- --></code><code> Logger.error('[DrawModel][fillArc] fillArcData or fillColor is empty.');</code><code> return;</code><code> }</code><code> if (this.canvasContext !== undefined) {<!-- --></code><code> this.canvasContext.beginPath( );</code><code> this.canvasContext.fillStyle = fillColor;</code><code> this.canvasContext.arc(fillArcData.x, fillArcData.y, fillArcData.radius,</code><code> fillArcData .startAngle, fillArcData.endAngle);</code><code> this.canvasContext.fill();</code><code> }</code><code>}</code>

Draw an outer disk and a small circle on the edge of the disk: at the specified X, Y (0, 0) coordinates, draw a circle with a radius of this.screenWidth * CommonConstants.OUT_CIRCLE_RATIOS. Next, a for loop is used, and the angle is incremented each time CommonConstants.CIRCLE / CommonConstants.SMALL_CIRCLE_COUNT to draw small circles on the ring.

// DrawModel.ets</code><code>drawOutCircle() {<!-- --></code><code> // Draw the outer disk</code><code> this.fillArc( new FillArcData(0, 0, this.screenWidth * CommonConstants.OUT_CIRCLE_RATIOS, 0,</code><code> Math.PI * CommonConstants.TWO), ColorConstants.OUT_CIRCLE_COLOR);</code>
<code> let beginAngle = this.startAngle;</code><code> // Draw a small circle</code><code> for (let i = 0; i < CommonConstants.SMALL_CIRCLE_COUNT; i + + ) {<!- - --></code><code> this.canvasContext?.save();</code><code> this.canvasContext?.rotate(beginAngle * Math.PI / CommonConstants.HALF_CIRCLE);</code><code> this.fillArc(new FillArcData(this.screenWidth * CommonConstants.SMALL_CIRCLE_RATIOS, 0,</code><code> CommonConstants.SMALL_CIRCLE_RADIUS, 0, Math.PI * CommonConstants.TWO),</code><code> ColorConstants. WHITE_COLOR);</code><code> beginAngle = beginAngle + CommonConstants.CIRCLE / CommonConstants.SMALL_CIRCLE_COUNT;</code><code> this.canvasContext?.restore();</code><code> }</code> <code>}</code>

Draw an internal fan-shaped lottery area

Draw the inner disk and the inner fan-shaped draw area: use the fillArc method to draw the inner disk. Through a for loop, the angle increments this.avgAngle each time. Then keep changing the starting angle of the arc this.startAngle * Math.PI / CommonConstants.HALF_CIRCLE and the ending angle of the arc (this.startAngle + this.avgAngle) * Math.PI / CommonConstants.HALF_CIRCLE. Finally, use the fill() method to fill the path.

// DrawModel.ets</code><code>// Draw the inner circle</code><code>drawInnerCircle() {<!-- --></code><code> this.fillArc( new FillArcData(0, 0, this.screenWidth * CommonConstants.INNER_CIRCLE_RATIOS, 0,</code><code> Math.PI * CommonConstants.TWO), ColorConstants.INNER_CIRCLE_COLOR);</code><code> this.fillArc(new FillArcData(0, 0, this.screenWidth * CommonConstants.INNER_WHITE_CIRCLE_RATIOS, 0,</code><code> Math.PI * CommonConstants.TWO), ColorConstants.WHITE_COLOR);</code><code>}</code>
<code>//Draw the internal fan-shaped lottery area</code><code>drawInnerArc() {<!-- --></code><code> // Color collection</code><code> let colors = [ </code><code> ColorConstants.ARC_PINK_COLOR, ColorConstants.ARC_YELLOW_COLOR,</code><code> ColorConstants.ARC_GREEN_COLOR, ColorConstants.ARC_PINK_COLOR,</code><code> ColorConstants.ARC_YELLOW_COLOR, ColorConstants.ARC_GREEN_COLOR</code><code> ];</code><code> let radius = this.screenWidth * CommonConstants.INNER_ARC_RATIOS;</code><code> for (let i = 0; i < CommonConstants.COUNT; i + + ) {<!-- --></code><code> this.fillArc(new FillArcData(0, 0, radius, this.startAngle * Math.PI / CommonConstants.HALF_CIRCLE,</code><code> (this.startAngle + this.avgAngle ) * Math.PI / CommonConstants.HALF_CIRCLE), colors[i]);</code><code> this.canvasContext?.lineTo(0, 0);</code><code> this.canvasContext?.fill( );</code><code> this.startAngle + = this.avgAngle;</code><code> }</code><code>}

Draw the text in the internal lottery area: use a for loop to draw each group of text through the drawCircularText() method. The drawCircularText() method receives three parameters, which are strings, starting radians and ending radians. Before drawing text, you need to set the arc angleDecrement occupied by each letter, and then set the horizontal and vertical offsets. The vertical offset circleText.y – Math.sin(angle) * radius is the distance moved towards the center of the circle; the horizontal offset circleText.x + Math.cos(angle) * radius is to center the text within the current arc range. . Finally, use the fillText method to draw the text.

// DrawModel.ets</code><code>// Draw text in the internal fan-shaped area</code><code>drawArcText() {<!-- --></code><code> if (this .canvasContext !== undefined) {<!-- --></code><code> this.canvasContext.textAlign = CommonConstants.TEXT_ALIGN;</code><code> this.canvasContext.textBaseline = CommonConstants.TEXT_BASE_LINE;</code><code> this.canvasContext.fillStyle = ColorConstants.TEXT_COLOR;</code><code> this.canvasContext.font = StyleConstants.ARC_TEXT_SIZE + CommonConstants.CANVAS_FONT;</code><code> }</code><code> // Collection of text arrays to be drawn</code><code> let textArrays = [</code><code> $r('app.string.text_smile'),</code><code> $ r('app.string.text_hamburger'),</code><code> $r('app.string.text_cake'),</code><code> $r('app.string. text_smile'),</code><code> $r('app.string.text_beer'),</code><code> $r('app.string.text_watermelon')</code> <code> ];</code><code> let arcTextStartAngle = CommonConstants.ARC_START_ANGLE;</code><code> let arcTextEndAngle = CommonConstants.ARC_END_ANGLE;</code><code> for (let i = 0; i < CommonConstants .COUNT; i + + ) {<!-- --></code><code> this.drawCircularText(this.getResourceString(textArrays[i]),</code><code> (this.startAngle + arcTextStartAngle) * Math.PI / CommonConstants.HALF_CIRCLE,</code><code> (this.startAngle + arcTextEndAngle) * Math.PI / CommonConstants.HALF_CIRCLE);</code><code> this.startAngle + = this.avgAngle;</code><code> }</code><code>}</code>
<code>//Draw arc text</code><code>drawCircularText(textString: string, startAngle: number, endAngle: number) {<!-- --></code><code> if (CheckEmptyUtils.isEmptyStr (textString)) {<!-- --></code><code> Logger.error('[DrawModel][drawCircularText] textString is empty.');</code><code> return;</code><code> }</code><code> class CircleText {<!-- --></code><code> x: number = 0;</code><code> y: number = 0;</code><code> radius: number = 0;</code><code> };</code><code> let circleText: CircleText = {<!-- --></code><code> x: 0,</code><code> y: 0,</code><code> radius: this.screenWidth * CommonConstants.INNER_ARC_RATIOS</code><code> };</code><code> // Radius of the circle </code><code> let radius = circleText.radius - circleText.radius / CommonConstants.COUNT;</code><code> // The arc occupied by each letter</code><code> let angleDecrement = (startAngle - endAngle) / (textString.length - 1);</code><code> let angle = startAngle;</code><code> let index = 0;</code><code> let character: string;</code>
<code> while (index < textString.length) {<!-- --></code><code> character = textString.charAt(index);</code><code> this.canvasContext?.save() ;</code><code> this.canvasContext?.beginPath();</code><code> this.canvasContext?.translate(circleText.x + Math.cos(angle) * radius,</code><code> circleText.y - Math.sin(angle) * radius);</code><code> this.canvasContext?.rotate(Math.PI / CommonConstants.TWO - angle);</code><code> this.canvasContext ?.fillText(character, 0, 0);</code><code> angle -= angleDecrement;</code><code> index + + ;</code><code> this.canvasContext?.restore(); </code><code> }</code><code>}</code>

Draw an image corresponding to the text in the internal lottery area: Use the drawImage method to draw an image corresponding to the text in the lottery area. This method receives five parameters, namely the image resource, the X and Y axis coordinates of the upper left corner of the drawing area, and the width and height of the drawing area.

// DrawModel.ets</code><code>// Draw pictures corresponding to the text in the internal fan-shaped area</code><code>drawImage() {<!-- --></code><code> let beginAngle = this.startAngle;</code><code> let imageSrc = [</code><code> CommonConstants.WATERMELON_IMAGE_URL, CommonConstants.BEER_IMAGE_URL,</code><code> CommonConstants.SMILE_IMAGE_URL, CommonConstants.CAKE_IMAGE_URL,</code><code> code><code> CommonConstants.HAMBURG_IMAGE_URL, CommonConstants.SMILE_IMAGE_URL</code><code> ];</code><code> for (let i = 0; i < CommonConstants.COUNT; i + + ) {<!-- --></code><code> let image = new ImageBitmap(imageSrc[i]);</code><code> this.canvasContext?.save();</code><code> this.canvasContext?. rotate(beginAngle * Math.PI / CommonConstants.HALF_CIRCLE);</code><code> this.canvasContext?.drawImage(image, this.screenWidth * CommonConstants.IMAGE_DX_RATIOS,</code><code> this.screenWidth * CommonConstants. IMAGE_DY_RATIOS, CommonConstants.IMAGE_SIZE,</code><code> CommonConstants.IMAGE_SIZE);</code><code> beginAngle + = this.avgAngle;</code><code> this.canvasContext?.restore();</code> code><code> }</code><code>}</code>

Implement lottery function

Add the rotate attribute in the Canvas component of CanvasPage.ets, and add the click event onClick in the Image component. Click the “Start Lottery” picture, and the round wheel will start spinning to draw the prize.

// CanvasPage.ets</code><code>Stack({ alignContent: Alignment.Center }) {<!-- --></code><code> Canvas(this.canvasContext)</code> <code> ...</code><code> .onReady(() => {<!-- --></code><code> this.drawModel.draw(this.canvasContext, this.screenWidth, this .screenHeight);</code><code> })</code><code> .rotate({<!-- --></code><code> x: 0,</code><code> y : 0,</code><code> z: 1,</code><code> angle: this.rotateDegree,</code><code> centerX: this.screenWidth / CommonConstants.TWO,</code><code> centerY: this.screenHeight / CommonConstants.TWO</code><code> })</code>
<code> //Start lottery drawing</code><code> Image($r('app.media.ic_center'))</code><code> ...</code><code> .enabled (this.enableFlag)</code><code> .onClick(() => {<!-- --></code><code> this.enableFlag = !this.enableFlag;</code><code> // Start the lottery</code><code> this.startAnimator();</code><code> })</code><code>}</code><code>...</code>

The circular turntable starts to rotate and draw a lottery: assign a random rotation angle randomAngle to the turntable to ensure that the angle of each rotation is random, that is, the prizes drawn each time are also random. After the animation ends, the turntable stops rotating, the lottery ends, and the information about the prizes won pops up.

// CanvasPage.ets</code><code>dialogController: CustomDialogController = new CustomDialogController({<!-- --></code><code> builder: PrizeDialog({<!-- --></code><code> prizeData: $prizeData,</code><code> enableFlag: $enableFlag</code><code> }),</code><code> autoCancel: false</code><code>} );</code>
<code>//Start the lottery</code><code>startAnimator() {<!-- --></code><code> let randomAngle = Math.round(Math.random() * CommonConstants.CIRCLE); </code><code> // Get winning information</code><code> this.prizeData = this.drawModel.showPrizeData(randomAngle);</code>
<code> animateTo({<!-- --></code><code> duration: CommonConstants.DURATION,</code><code> curve: Curve.Ease,</code><code> delay: 0, </code><code> iterations: 1,</code><code> playMode: PlayMode.Normal,</code><code> onFinish: () => {<!-- --></code><code> this.rotateDegree = CommonConstants.ANGLE - randomAngle;</code><code> // Open a custom pop-up window and pop up lottery information</code><code> this.dialogController.open();</code><code> }</code><code> }, () => {<!-- --></code><code> this.rotateDegree = CommonConstants.CIRCLE * CommonConstants.FIVE + </code><code> CommonConstants.ANGLE - randomAngle;</code><code> })</code><code>}</code>

Pop up the prize information in the draw: After the draw is over, the text and picture information in the draw will pop up, which is implemented through a customized pop-up window.

// PrizeDialog.ets</code><code>@CustomDialog</code><code>export default struct PrizeDialog {<!-- --></code><code> @Link prizeData: PrizeData;</code><code> @Link enableFlag: boolean;</code><code> private controller?: CustomDialogController;</code>
<code> build() {<!-- --></code><code> Column() {<!-- --></code><code> Image(this.prizeData.imageSrc !== undefined ? this.prizeData.imageSrc : '')</code><code> ...</code>
<code> Text(this.prizeData.message)</code><code> ...</code>
<code> Text($r('app.string.text_confirm'))</code><code> ...</code><code> .onClick(() => {<!-- -- ></code><code> // Close the custom pop-up window </code><code> this.controller?.close();</code><code> this.enableFlag = !this.enableFlag;</code><code> })</code><code> }</code><code> ...</code><code> }</code><code>}</code>

Summary

You have completed this Codelab and learned the following knowledge points:

1. Use the canvas component Canvas to draw the lottery round turntable.

2. Use explicit animation to start the lottery function.

3. Use a custom pop-up window to pop up the drawn prizes.