【Journey of HarmonyOS Next】Developing with ArkTS (2) – > UI Development III



This content originally appeared on DEV Community and was authored by ZHZL-m

Image description

1 -> Draw a graph

The drawing capability is mainly supported by the drawing components provided by the framework, and supports the SVG standard drawing commands.

1.1 -> Draw basic geometry

The drawing component encapsulates some common basic geometry, such as rectangular Rect, circular Circle, Ellipse, etc., saving developers the process of route calculation.

In the food ingredient list on the FoodDetail page, a circular icon is added to the name of each ingredient as an ingredient label.

  1. Create a Circle component with a circular icon in front of each ingredient as a label. Set the diameter of the circle to 6vp. Modify the IngredientItem method in the ContentTable component of the FoodDetail page to add a Circle before the ingredient name.
// FoodDetail.ets
@Component
struct ContentTable {
  private foodItem: FoodData

  @Builder IngredientItem(title:string, colorValue: string, name: string, value: string) {
    Flex() {
      Text(title)
        .fontSize(17.4)
        .fontWeight(FontWeight.Bold)
        .layoutWeight(1)
      Flex({ alignItems: ItemAlign.Center }) {
        Circle({width: 6, height: 6})
          .margin({right: 12})
          .fill(colorValue)
        Text(name)
          .fontSize(17.4)
          .flexGrow(1)
        Text(value)
          .fontSize(17.4)
      }
      .layoutWeight(2)
    }
  }

  build() {
    ......
  }
}
  1. The label color of each ingredient is different, so we call IngredientItem in the build method to fill each circle with a different color.
// FoodDetail.ets
@Component
struct ContentTable {
  private foodItem: FoodData

  @Builder IngredientItem(title:string, colorValue: string, name: string, value: string) {
    Flex() {
      Text(title)
        .fontSize(17.4)
        .fontWeight(FontWeight.Bold)
        .layoutWeight(1)
      Flex({ alignItems: ItemAlign.Center }) {
        Circle({width: 6, height: 6})
          .margin({right: 12})
          .fill(colorValue)
        Text(name)
          .fontSize(17.4)
          .flexGrow(1)
        Text(value)
          .fontSize(17.4)
      }
      .layoutWeight(2)
    }
  }

  build() {
    Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
      this.IngredientItem('Calories', '#FFf54040', 'Calories', this.foodItem.calories + 'kcal')
      this.IngredientItem('Nutrition', '#FFcccccc', 'Protein', this.foodItem.protein + 'g')
      this.IngredientItem(' ', '#FFf5d640', 'Fat', this.foodItem.fat + 'g')
      this.IngredientItem(' ', '#FF9e9eff', 'Carbohydrates', this.foodItem.carbohydrates + 'g')
      this.IngredientItem(' ', '#FF53f540', 'VitaminC', this.foodItem.vitaminC + 'mg')
    }
    .height(280)
    .padding({ top: 30, right: 30, left: 30 })
  }
}

1.2 -> Draw custom geometry

In addition to drawing the basic geometry, developers can also use the Path component to draw custom routes, and the following is the following to draw the logo pattern of the application.

  1. Create a new page in the pages folder Logo.ets.

Image description

  1. Delete the template code from Logo.ets and create a Logo Component.
@Entry
@Component
struct Logo {
  build() {

  }
}
  1. Create a Flex component as the root node, set the width and height to 100%, set its alignment in both the major axis direction and the cross axis direction to Center, and create the Shape component as a Flex subassembly.

The Shape component is the parent component of all draw components. If you need to combine multiple draw components into a whole, you need to create a Shape as its parent.

The size of the logo we want to draw is 630px*630px. The declarative UI paradigm supports the setting of multiple length units, and in the previous sections, we directly used number as a parameter, that is, the default length unit vp, virtual pixel unit. VP is related to device resolution and screen density. For example, if the resolution of the device is 1176 * 2400, the screen base density (resolution) is 3, and VP = PX / resolution, the screen width of the device is 392VP.

However, the drawing component adopts the svg standard, and the default is px as the unit, in order to facilitate unification, in this part of the drawing logo, the px unit is uniformly adopted. The declarative UI framework also supports px units, the input parameter type is string, and the width is set to 630px, i.e., 210vp, and the setting method is width(‘630px’) or width(210).

@Entry
@Component
struct Logo {
  build() {
    Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      Shape() {

      }
      .height('630px')
      .width('630px')
    }
    .width('100%')
    .height('100%')
  }
}
  1. Fill the page with a gradient. The offset angle is set to a linear gradient with an offset angle of 180deg, and a three-stage gradient of #BDE895 –>95DE7F –> #7AB967, and its intervals are [0, 0.1], (0.1, 0.6), (0.6, 1).
.linearGradient(
  {
    angle: 180,
    colors: [['#BDE895', 0.1], ["#95DE7F", 0.6],  ["#7AB967", 1]]
})

Image description

@Entry
@Component
struct Logo {
  build() {
    Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      Shape() {

      }
      .height('630px')
      .width('630px')
    }
    .width('100%')
    .height('100%')
    .linearGradient(
    {
      angle: 180,
      colors: [['#BDE895', 0.1], ["#95DE7F", 0.6],  ["#7AB967", 1]]
    })
  }
}
  1. Draw the first route Path and set its draw commands.
Path()
  .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')

The PATH drawing command uses the SVG standard, and the above command can be broken down as:

M162 128.7

Move the stroke to the Moveto coordinate point (162, 128.7).

A222 222 0 0 1 100.8 374.4

Draw an elliptical arc with a radius of rx, ry is 222, x-axis-rotation is 0, large-arc-flag is 0, that is, a small radian angle, sweep-flag is 1, that is, draw an arc counterclockwise, and lowercase a is a relative position, that is, the end coordinate is (162 + 100.8 = 262.8, 128.7 + 374.4 = 503.1).

H198

Draw a horizontal line to 198, i.e. draw a horizontal line from (262.8, 503.1) to (198, 503.1).

A36 36 0 0 3 -36 -36

Draw an elliptical arc with the same meaning as above, ending at (198 – 36 = 162, 503.1 – 36 = 467.1).

V128.7

Draw a vertical line to 128.7, i.e. draw a vertical line from (162, 467.1) to (162, 128.7).

with

closepath
The fill color is white.
.fill(Color.White)

@Entry
@Component
struct Logo {
  build() {
    Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      Shape() {
        Path()
          .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
          .fill(Color.White)
      }
      .height('630px')
      .width('630px')
    }
    .width('100%')
    .height('100%')
    .linearGradient(
      {
        angle: 180,
        colors: [['#BDE895', 0.1], ["#95DE7F", 0.6],  ["#7AB967", 1]]
      })
  }
}
  1. Draw a second Path inside the Shape component. The background color of the second Path is a gradient color, but the fill of the gradient color is its overall box, so you need to clip it and cut it into the Shape, that is, it is cropped according to the shape of the Shape.
Path()
  .commands('M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z')
  .fill('none')
  .linearGradient(
  {
    angle: 30,
    colors: [["#C4FFA0", 0],  ["#ffffff", 1]]
  })
  .clip(new Path().commands('M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'))

The Path drawing command is relatively long, and it can be called as a member variable of the component through this.

@Entry
@Component
struct Logo {
  private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
  build() {
    ......
        Path()
          .commands(this.pathCommands1)
          .fill('none')
          .linearGradient(
          {
            angle: 30,
            colors: [["#C4FFA0", 0],  ["#ffffff", 1]]
          })
          .clip(new Path().commands(this.pathCommands1))
     ......
  }
}
  1. Draw a second Path inside the Shape component.
@Entry
@Component
struct Logo {
  private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
  private pathCommands2:string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z'
  build() {
    Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      Shape() {
        Path()
          .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
          .fill(Color.White)

        Path()
          .commands(this.pathCommands1)
          .fill('none')
          .linearGradient(
          {
            angle: 30,
            colors: [["#C4FFA0", 0],  ["#ffffff", 1]]
          })
          .clip(new Path().commands(this.pathCommands1))

        Path()
          .commands(this.pathCommands2)
          .fill('none')
          .linearGradient(
          {
            angle: 50,
            colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4],  ["#ffffff", 0.7]]
          })
          .clip(new Path().commands(this.pathCommands2))
      }
      .height('630px')
      .width('630px')
    }
    .width('100%')
    .height('100%')
    .linearGradient(
      {
        angle: 180,
        colors: [['#BDE895', 0.1], ["#95DE7F", 0.6],  ["#7AB967", 1]]
      })
  }
}

Finish drawing the app logo. Shape combines three Path components to create an artistic leaf with svg commands, symbolizing a green and healthy way of eating.

  1. Add the app’s title and slogan.
@Entry
@Component
struct Logo {
  private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
  private pathCommands2:string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z'
  build() {
    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      Shape() {
        Path()
          .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
          .fill(Color.White)

        Path()
          .commands(this.pathCommands1)
          .fill('none')
          .linearGradient(
          {
            angle: 30,
            colors: [["#C4FFA0", 0],  ["#ffffff", 1]]
          })
          .clip(new Path().commands(this.pathCommands1))

        Path()
          .commands(this.pathCommands2)
          .fill('none')
          .linearGradient(
          {
            angle: 50,
            colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4],  ["#ffffff", 0.7]]
          })
          .clip(new Path().commands(this.pathCommands2))
      }
      .height('630px')
      .width('630px')

      Text('Healthy Diet')
        .fontSize(26)
        .fontColor(Color.White)
        .margin({ top:300 })

      Text('Healthy life comes from a balanced diet')
        .fontSize(17)
        .fontColor(Color.White)
        .margin({ top:4 })
    }
    .width('100%')
    .height('100%')
    .linearGradient(
      {
        angle: 180,
        colors: [['#BDE895', 0.1], ["#95DE7F", 0.6],  ["#7AB967", 1]]
      })
  }
}

2 -> Add animation effects

2.1 -> animateTo implement splash screen animations

Declarative UI Paradigm component animations include property animations and animateTo explicit animations:

Attribute Animation: Animate changes in the common properties of a component.
Explicit animation: You can animate the change of the component from state A to state B, including the addition and deletion of styles, position information, and nodes, etc., so that developers do not need to pay attention to the change process, but only need to specify the state of the start and end points. animateTo also provides a callback interface for the playback state, which is an enhancement and encapsulation of attribute animations.
The animation effect of the splash screen is the animation of the zooming out and zooming in effect of the logo icon, and the animation is finished to jump to the food classification list page. Next, let’s use animateTo to achieve a splash screen effect for the splash page animation.

  1. Animation effects play automatically. The expected effect of the splash screen animation is that after entering the Logo page, the animateTo animation effect will automatically start playing, which can be achieved with the help of the callback interface of the component’s hidden event. Call the onAppear method of Shape to set its explicit animation.
Shape() {
  ...
}
.onAppear(() => {
   animateTo()
})
  1. Create member variables for opacity and scale values and decorate them with decorator @State. Indicates that it is stateful data, i.e., a change triggers a refresh of the page.
@Entry
@Component
struct Logo {
  @State private opacityValue: number = 0
  @State private scaleValue: number = 0
  build() {
    Shape() {
      ...
    }
   .scale({ x: this.scaleValue, y: this.scaleValue })
   .opacity(this.opacityValue)
   .onAppear(() => {
     animateTo()
    })
  }
}
  1. Set the animation curve of animateTo. The acceleration curve of the logo is slow first and then fast, using Bezier curve cubicBezier, cubicBezier (0.4, 0, 1, 1).

To use the interpolation calculations in the Animation Capability interface, you need to import the curves module first.

import Curves from '@ohos.curves'

@ohos.curves module provides linear curves. The initialization function of Linear, step, cubicBezier, and spring interpolation curves can be used to create an interpolation curve object based on the input parameters.

@Entry
@Component
struct Logo {
  @State private opacityValue: number = 0
  @State private scaleValue: number = 0
  private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)

  build() {
    Shape() {
      ...
    }
   .scale({ x: this.scaleValue, y: this.scaleValue })
   .opacity(this.opacityValue)
   .onAppear(() => {
     animateTo({
        curve: this.curve1
     })
    })
  }
}
  1. Set the animation duration to 1s, delay 0.1s to start playback, and set the closure function to display the animation event, that is, the starting point state to the end state is transparency opacityValue and the size scaleValue is from 0 to 1 to achieve the fade-out and magnification effect of the logo.
@Entry
@Component
struct Logo {
  @State private opacityValue: number = 0
  @State private scaleValue: number = 0
  private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)

  build() {
    Shape() {
      ...
    }
   .scale({ x: this.scaleValue, y: this.scaleValue })
   .opacity(this.opacityValue)
   .onAppear(() => {
     animateTo({
      duration: 1000, 
      curve: this.curve1, 
      delay: 100, 
     }, () => {
       this.opacityValue = 1
       this.scaleValue = 1
      })
    })
  }
}
  1. Freeze frame for 1 second after the splash screen is played, and enter the FoodCategoryList page. Set the onFinish callback API of animateTo, call the setTimeout API of the timer for 1 second, and then call router.replace to display the FoodCategoryList page.
import router from '@ohos.router'

@Entry
@Component
struct Logo {
  @State private opacityValue: number = 0
  @State private scaleValue: number = 0
  private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)

  build() {
    Shape() {
      ...
    }
   .scale({ x: this.scaleValue, y: this.scaleValue })
   .opacity(this.opacityValue)
   .onAppear(() => {

     animateTo({
      duration: 1000,
       curve: this.curve1, 
       delay: 100, 
       onFinish: () => {
         setTimeout(() => {
           router.replace({ url: "pages/FoodCategoryList" })
         }, 1000);
       }
     }, () => {
       this.opacityValue = 1
       this.scaleValue = 1
      })
    })
  }
}

The overall code is as follows.


import Curves from '@ohos.curves'
import router from '@ohos.router'

@Entry
@Component
struct Logo {
  @State private opacityValue: number = 0
  @State private scaleValue: number = 0
  private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)
  private pathCommands1: string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
  private pathCommands2: string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z'

  build() {
    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      Shape() {
        Path()
          .commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
          .fill(Color.White)
        Path()
          .commands(this.pathCommands1)
          .fill('none')
          .linearGradient(
          {
            angle: 30,
            colors: [["#C4FFA0", 0], ["#ffffff", 1]]
          })
          .clip(new Path().commands(this.pathCommands1))

        Path()
          .commands(this.pathCommands2)
          .fill('none')
          .linearGradient(
          {
            angle: 50,
            colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]]
          })
          .clip(new Path().commands(this.pathCommands2))
      }
      .height('630px')
      .width('630px')
      .scale({ x: this.scaleValue, y: this.scaleValue })
      .opacity(this.opacityValue)
      .onAppear(() => {
        animateTo({
          duration: 1000,
          curve: this.curve1,
          delay: 100,
          onFinish: () => {
            setTimeout(() => {
              router.replace({ url: "pages/FoodCategoryList" })
            }, 1000);
          }
        }, () => {
          this.opacityValue = 1
          this.scaleValue = 1
        })
      })

      Text('Healthy Diet')
        .fontSize(26)
        .fontColor(Color.White)
        .margin({ top: 300 })

      Text('Healthy life comes from a balanced diet')
        .fontSize(17)
        .fontColor(Color.White)
        .margin({ top: 4 })
    }
    .width('100%')
    .height('100%')
    .linearGradient(
      {
        angle: 180,
        colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
      })
  }
}

2.2 -> Page transitions

Shared element transition between the food category list page and the food details page, i.e. after clicking on the FoodListItem/FoodGridItem, the food thumbnail will zoom in and jump to the larger image of the food detail page.

  1. Set the sharedTransition method for the Image component of the FoodListItem and FoodGridItem. The transition id is foodItem.id, the transition animation duration is 1s, the delay is 0.1s, and the change curve is Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), and the curves module needs to be introduced.

Sharing transitions carries the properties of the current element that are set, so create a Row component that acts as the parent component of the Image and set the background color on the Row.

Set autoResize to false on the Image component of the FoodListItem, because the image component will adjust the size of the image source according to the final display area by default to optimize the image rendering performance. In transitions, the image is reloaded as it zooms in, so autoResize is set to false for smooth transitions.

// FoodList.ets
import Curves from '@ohos.curves'

@Component
struct FoodListItem {
  private foodItem: FoodData
  build() {
    Navigator({ target: 'pages/FoodDetail' }) {
      Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
        Row() {
          Image(this.foodItem.image)
            .objectFit(ImageFit.Contain)
            .autoResize(false)
            .height(40)
            .width(40)
            .sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 })
        }

        .margin({ right: 16 })
        Text(this.foodItem.name)
          .fontSize(14)
          .flexGrow(1)
        Text(this.foodItem.calories + ' kcal')
          .fontSize(14)
      }
      .height(64)
    }
    .params({ foodData: this.foodItem })
    .margin({ right: 24, left:32 })
  }
}

@Component
struct FoodGridItem {
  private foodItem: FoodData
  build() {
    Column() {
      Row() {
        Image(this.foodItem.image)
          .objectFit(ImageFit.Contain)
          .autoResize(false)
          .height(152)
          .width('100%')
          .sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 })
      }
      Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
        Text(this.foodItem.name)
          .fontSize(14)
          .flexGrow(1)
          .padding({ left: 8 })
        Text(this.foodItem.calories + 'kcal')
          .fontSize(14)
          .margin({ right: 6 })
      }
      .height(32)
      .width('100%')
      .backgroundColor('#FFe5e5e5')
    }
    .height(184)
    .width('100%')
    .onClick(() => {
      router.push({ url: 'pages/FoodDetail', params: { foodId: this.foodItem } })
    })
  }
}
  1. Set the sharedTransition method for the Image component of the FoodImageDisplay of the FoodDetail page. The setup method is the same as above.
import Curves from '@ohos.curves'

@Component
struct FoodImageDisplay {
  private foodItem: FoodData
  build() {
    Stack({ alignContent: Alignment.BottomStart }) {
      Image(this.foodItem.image)
        .objectFit(ImageFit.Contain)
        .sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 })
      Text(this.foodItem.name)
        .fontSize(26)
        .fontWeight(500)
        .margin({ left: 26, bottom: 17.4 })
    }
    .height(357)  
  }
}

3 -> Descriptions of common components

Components are the core of building pages, and each component implements independent visual and interactive functional units through simple encapsulation of data and methods. Components are independent of each other, ready to use, and can be reused where the requirements are the same.

The following table shows the components currently available for the declarative development paradigm.

Image description


This content originally appeared on DEV Community and was authored by ZHZL-m