DEV Community

Cover image for HarmonyOS Development: Customize a Contact Template
程序员一鸣
程序员一鸣

Posted on

HarmonyOS Development: Customize a Contact Template

Foreword 

this article is based on Api13 

on the right is the list of letters, and on the left is the list corresponding to the letters. This effect is common in address books, such as WeChat address books, and also common in mobile phone contacts, as shown in the following figure: 

Image description

the letters in the vertical column on the right can be clicked or slid with gestures, while the list on the left needs to automatically switch the information under the corresponding letters with gestures. How can this effect be realized? I remember when I was working in Android, the letter List on the right was drawn with Canvas, which is convenient to measure the current sliding letters according to gestures. In fact, Hongmeng can also follow this idea, but we can also directly use the List component to do it. 

Let's look at the final implementation effect first, which is basically satisfied. The list on the left can absorb the ceiling, the list on the right can slide and click, and the list can be switched in linkage. 

Image description

The dynamic effects are as follows: 

Image description

the main contents of this paper are as follows: 

1, realize the letter list display

2. Slide and click on the letter list

3. Display the left contact list

4. Realize the linkage between letters and contact list

5, source code

6. Relevant summary


first, to achieve the letter list display 

to show the list of letters, we must first have letter Data. We can integrate the letters from A to Z into an array of letters. Here is A simple method. Using the String.fromCharCode method, we can easily traverse the letters and save them one by one. The code is as follows:

 for (let i = 65; i <= 90; i++) {
      this.letterList.push(String.fromCharCode(i))
    }
Enter fullscreen mode Exit fullscreen mode

With the data source, we can use the List component to display. First of all, we need to know that the effect to be achieved is to display the letter List on the right and vertically centered. Therefore, here, we do not need to set the height of the List component to make it Adaptive. On the outer layer, we can use the RelativeContainer relative layout, so that we can use the attribute alignRules to place the position. 

Right and centered vertically:


alignRules({
        right: { anchor: "__container__", align: HorizontalAlign.End },
        center: { anchor: "__container__", align: VerticalAlign.Center },
      })
Enter fullscreen mode Exit fullscreen mode

the List is loaded as follows:

List({ space: 5 }) {
        ForEach(this.letterList, (item: string, index: number) => {
          ListItem() {
            Text(item)
              .fontColor("#666666")
              .fontSize(14)
          }
        })
      }
      .width(30)
      .backgroundColor("#f2f6f9")
      .margin({ right: 10 })
      .padding({ top: 10, bottom: 10 })
      .borderRadius(30)
      .alignListItem(ListItemAlign.Center)
      .alignRules({
        right: { anchor: "__container__", align: HorizontalAlign.End },
        center: { anchor: "__container__", align: VerticalAlign.Center },
      })
Enter fullscreen mode Exit fullscreen mode

through the above code, we have realized that the letter list is displayed on the right and vertically centered. 

Image description

Second, the realization of the letter list sliding and clicking 

after the letter list is displayed, we need to implement the following functions: 

  1. Each letter can be triggered by clicking. 

  2. Each letter can trigger sliding. 

  3. When touching its own letters, change its own color and text size. 

  4. Touch the current letter and display the current letter in the middle of the screen. 

The above functions are the basic settings of the letter list, and the subsequent linkage with the Left List also needs to be driven by the above events. Therefore, these steps are very important. 

At the beginning, the idea was to set click and move for each letter component, that is, Text component. Later, it was found that a single setting could not affect other components. Therefore, it was still necessary to set the List component on the outer layer. As for click and slide, gestures could actually be used to handle it.

How to determine the currently touched letter based on the gesture? In fact, it is very simple, because we are A vertical list and only judge the Y coordinate. First, we can obtain the Y coordinate of each letter. When the Y value of the gesture touch is greater than or less than A certain value, it is proved that we touch this letter, for example, the letter B, and the Y coordinate obtained is 20, then the Y coordinate of the touch is greater than the Y coordinate of the letter A and less than the Y coordinate of the letter B, is this the B letter area? 

The pseudocode is as follows:

 

let A字母Y坐标 = 10
let B字母Y坐标 = 20
if(moveY>A字母Y坐标&&moveY<B字母Y坐标){
  //B字母
}
Enter fullscreen mode Exit fullscreen mode

step 1, get the Y coordinate of each letter 

first define a map collection that stores the positions of all letters:

 

private letterPositionMap: HashMap<string, number> = new HashMap()
Enter fullscreen mode Exit fullscreen mode

then, store the Y coordinate of each letter, which can be obtained by onAreaChange method. Here, you can directly set each item, that is, ListItem.

 

.onAreaChange((_: Area, newValue: Area) => {
            this.letterPositionMap.set(item, Number(newValue.position.y) + Number(newValue.height))
          })
Enter fullscreen mode Exit fullscreen mode

The second step is to implement the onTouch method, listening for gestures to press and move


 

.onTouch((event) => {
        switch (event.type) {
          case TouchType.Down://手势按下
          case TouchType.Move://手势抬起
            this.changeStatus(event.touches[0].y);
            break
        }
      })
Enter fullscreen mode Exit fullscreen mode

the third step is to calculate and obtain the letter index.


 

  changeStatus(moveY: number) {
    const positions = this.letterList.map(letter => this.letterPositionMap.get(letter))
    let index = 0
    for (let i = 0; i < positions.length; i++) {
      if (moveY < positions[i]) {
        break
      }
      index = i + 1
    }

    //index就是触摸的每个字母的索引

  }
}
Enter fullscreen mode Exit fullscreen mode

Through the above steps, we have obtained the index of each letter. Next, we can change the color and size of the letter. Of course, we need to use decorators to modify the color and size of the letter, so we cannot write it to death. The processing method can be either an object or an array. Here we simply use an array to represent it. 

Change Style 

define an array of text colors and sizes:

@State textFontSizeArray: Length[] = []
@State textFontColorArray: ResourceColor[] = []
Enter fullscreen mode Exit fullscreen mode

set default values:

aboutToAppear(): void {
    for (let i = 65; i <= 90; i++) {
      this.letterList.push(String.fromCharCode(i))
      this.textFontSizeArray.push(14)
      this.textFontColorArray.push("#666666")
    }
  }
Enter fullscreen mode Exit fullscreen mode

the UI component is modified to define an array:

 Text(item)
              .fontColor(this.textFontColorArray[index])
              .fontSize(this.textFontSizeArray[index])
Enter fullscreen mode Exit fullscreen mode

define the style change method:

the purpose of this method is to clear all styles and then set the new style for the entry at the specified index.

  latterChange(index: number) {
    this.textFontSizeArray.forEach((_, index) => {
      this.textFontSizeArray[index] = 14
      this.textFontColorArray[index] = "#666666"
    })
    this.textFontSizeArray[index] = 16
    this.textFontColorArray[index] = "#222222"
  }
Enter fullscreen mode Exit fullscreen mode

After calling in the changeStatus method, we can see that the text style has changed, and then we can achieve the effect of displaying letters in the middle. 

First, define a component used to display letters, make it in the middle position, and hide it by default. letterSelect is the defined letter used for display and needs to be decorated with decorators.

 

Text(this.letterSelect)
        .visibility(this.letterSelect == undefined ? Visibility.None : Visibility.Visible)
        .backgroundColor("#f2f6f9")
        .fontColor("#222222")
        .fontWeight(FontWeight.Bold)
        .fontSize(18)
        .textAlign(TextAlign.Center)
        .width(50)
        .height(50)
        .borderRadius(50)
        .alignRules({
          middle: { anchor: "__container__", align: HorizontalAlign.Center },
          center: { anchor: "__container__", align: VerticalAlign.Center },
        })
Enter fullscreen mode Exit fullscreen mode

In the changeStatus method, dynamically change the value of letterSelect.

 

this.letterSelect = this.letterList[index]
Enter fullscreen mode Exit fullscreen mode

It should be noted that when the gesture is lifted, it needs to be hidden. This point needs to be noted. In order to be able to hide without appearing so abrupt, it can be delayed.

 

case TouchType.Up: //手势抬起
            clearTimeout(this.timeoutId)
            this.timeoutId = setTimeout(() => {
              this.letterSelect = undefined
            }, 500)
            break
Enter fullscreen mode Exit fullscreen mode

We are looking at the effect achieved: 

Image description

three, to achieve the left Contact List Display 

the contact person on the left is relatively simple, is it a list that can be used for the top operation, or is it to prepare the data first, because the grouping operation is involved, so the definition of the data is slightly different from that of the common one. 

1 Definition of data 

define an interface for grouping display:

 

interface ListData {
  title: string;
  projects: string[];
}
Enter fullscreen mode Exit fullscreen mode

define the test data, which needs to be set according to the interface.

 

 private listData: ListData[] = [
      {
        title: 'A',
        projects: ['安大同学', '安少同学', '安1同学', '安2同学', '安3同学']
      },
      {
        title: 'B',
        projects: ['包1同学', '包2同学', '包3同学', '包4同学', '包5同学', '包6同学', '包7同学', '包8同学', '包9同学']
      },
      {
        title: 'C',
        projects: ['蔡1同学', '蔡2同学', '蔡3同学', '蔡4同学', '蔡5同学', '蔡6同学', '蔡7同学', '蔡8同学', '蔡9同学']
      },
      {
        title: 'D',
        projects: ['杜1同学', '杜2同学', '杜3同学', '杜4同学', '杜5同学', '杜6同学']
      },
      {
        title: 'F',
        projects: ['范1同学', '范2同学', '范3同学', '范4同学', '范5同学', '范6同学', '范7同学']
      },
      {
        title: 'L',
        projects: ['李大同学', '李2同学', '李3同学', '李4同学', '李5同学', '李6同学']
      },
      {
        title: 'M',
        projects: ['马大同学', '马2同学', '马3同学', '马4同学', '马5同学', '马6同学']
      }
    ]
Enter fullscreen mode Exit fullscreen mode

2. Set the List component 

because it is a group display, the listtemgroup component is used here.

 

List({ space: 10 }) {
        ForEach(this.listData, (item: ListData) => {
          ListItemGroup({ header: this.itemHead(item.title) }) {
            ForEach(item.projects, (project: string) => {
              ListItem() {
                Text(project)
                  .width('100%')
                  .height(50)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
              }
            }, (item: string) => item)
          }
          .divider({ strokeWidth: 1, color: "#f2f6f9" }) // 每行之间的分界线
        })
      }
      .width('100%')
      .height("100%")
      .sticky(StickyStyle.Header)
      .scrollBar(BarState.Off)
Enter fullscreen mode Exit fullscreen mode

itemHead component:


  @Builder
  itemHead(text: string) {
    Text(text)
      .fontSize(16)
      .backgroundColor("#f2f6f9")
      .width('100%')
      .height(40)
      .fontWeight(FontWeight.Bold)
      .padding({ left: 10 })
  }
Enter fullscreen mode Exit fullscreen mode

four, to achieve the letter and contact list linkage 

in the front, we have basically realized all the functions. At present, there is only one linkage. What we need to know is that the linkage is two-way, that is to say, sliding the letter list, the information list on the left also needs to be scrolled, sliding the information list, and the letter list on the right also needs to be changed in style.

1. Setting up the scroller 

here we use scroller to locate the List component. First, we define a scroller.

 

  private scroller: Scroller = new Scroller()
Enter fullscreen mode Exit fullscreen mode

Then set the information list on the left:

 

List({ space: 10, scroller: this.scroller }) 
Enter fullscreen mode Exit fullscreen mode

2. Right linkage 

also in the previous gesture method, the scroller performs positioning.

 

   const scrollIndex = this.listData.findIndex(item => item.title === this.letterSelect)
    if (scrollIndex !== -1) {
      this.scroller.scrollToIndex(scrollIndex)
    }
Enter fullscreen mode Exit fullscreen mode

3. Left linkage 

the left-side linkage is relatively simple. It is nothing more than listening to the sliding events of the left-side list and then changing the style of the right-side list according to the index.

 

    .onScrollIndex((start: number) => {
        let title = this.listData[start].title
        let latterPosition = this.letterList.indexOf(title)
        this.latterChange(latterPosition)
      })
Enter fullscreen mode Exit fullscreen mode

V. Source Code 

the function is very simple, all the code is as follows:

 

import { HashMap } from '@kit.ArkTS'

interface ListData {
  title: string;
  projects: string[];
}

@Entry
@Component
struct Index {
  private letterList: string[] = []
  private letterPositionMap: HashMap<string, number> = new HashMap()
  @State textFontSizeArray: Length[] = []
  @State textFontColorArray: ResourceColor[] = []
  @State letterSelect: string | undefined = undefined //当前选中的字母
  private timeoutId: number = 0
  private scroller: Scroller = new Scroller()
  private listData: ListData[] = [
    {
      title: 'A',
      projects: ['安大同学', '安少同学', '安1同学', '安2同学', '安3同学']
    },
    {
      title: 'B',
      projects: ['包1同学', '包2同学', '包3同学', '包4同学', '包5同学', '包6同学', '包7同学', '包8同学', '包9同学']
    },
    {
      title: 'C',
      projects: ['蔡1同学', '蔡2同学', '蔡3同学', '蔡4同学', '蔡5同学', '蔡6同学', '蔡7同学', '蔡8同学', '蔡9同学']
    },
    {
      title: 'D',
      projects: ['杜1同学', '杜2同学', '杜3同学', '杜4同学', '杜5同学', '杜6同学']
    },
    {
      title: 'F',
      projects: ['范1同学', '范2同学', '范3同学', '范4同学', '范5同学', '范6同学', '范7同学']
    },
    {
      title: 'L',
      projects: ['李大同学', '李2同学', '李3同学', '李4同学', '李5同学', '李6同学']
    },
    {
      title: 'M',
      projects: ['马大同学', '马2同学', '马3同学', '马4同学', '马5同学', '马6同学']
    }
  ]

  aboutToAppear(): void {
    for (let i = 65; i <= 90; i++) {
      this.letterList.push(String.fromCharCode(i))
      this.textFontSizeArray.push(14)
      this.textFontColorArray.push("#666666")
    }
  }

  @Builder
  itemHead(text: string) {
    Text(text)
      .fontSize(16)
      .backgroundColor("#f2f6f9")
      .width('100%')
      .height(40)
      .fontWeight(FontWeight.Bold)
      .padding({ left: 10 })
  }

  build() {
    RelativeContainer() {

      List({ space: 10, scroller: this.scroller }) {
        ForEach(this.listData, (item: ListData) => {
          ListItemGroup({ header: this.itemHead(item.title) }) {
            ForEach(item.projects, (project: string) => {
              ListItem() {
                Text(project)
                  .width('100%')
                  .height(50)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
              }
            }, (item: string) => item)
          }
          .divider({ strokeWidth: 1, color: "#f2f6f9" })
        })
      }
      .width('100%')
      .height("100%")
      .sticky(StickyStyle.Header)
      .scrollBar(BarState.Off)
      .onScrollIndex((start: number) => {
        let title = this.listData[start].title
        let latterPosition = this.letterList.indexOf(title)
        this.latterChange(latterPosition)
      })


      List({ space: 5 }) {
        ForEach(this.letterList, (item: string, index: number) => {
          ListItem() {
            Text(item)
              .fontColor(this.textFontColorArray[index])
              .fontSize(this.textFontSizeArray[index])
          }.onAreaChange((_: Area, newValue: Area) => {
            this.letterPositionMap.set(item, Number(newValue.position.y) + Number(newValue.height))
          })
        })
      }
      .width(30)
      .backgroundColor("#f2f6f9")
      .margin({ right: 10 })
      .padding({ top: 10, bottom: 10 })
      .borderRadius(30)
      .alignListItem(ListItemAlign.Center)
      .alignRules({
        right: { anchor: "__container__", align: HorizontalAlign.End },
        center: { anchor: "__container__", align: VerticalAlign.Center },
      })
      .onTouch((event) => {
        switch (event.type) {
          case TouchType.Down: //手势按下
          case TouchType.Move: //手势抬起
            this.changeStatus(event.touches[0].y);
            break
          case TouchType.Up: //手势抬起
            clearTimeout(this.timeoutId)
            this.timeoutId = setTimeout(() => {
              this.letterSelect = undefined
            }, 500)
            break
        }
      })

      Text(this.letterSelect)
        .visibility(this.letterSelect == undefined ? Visibility.None : Visibility.Visible)
        .backgroundColor("#f2f6f9")
        .fontColor("#222222")
        .fontWeight(FontWeight.Bold)
        .fontSize(18)
        .textAlign(TextAlign.Center)
        .width(50)
        .height(50)
        .borderRadius(50)
        .alignRules({
          middle: { anchor: "__container__", align: HorizontalAlign.Center },
          center: { anchor: "__container__", align: VerticalAlign.Center },
        })

    }
    .width("100%")
    .height("100%")
  }

  changeStatus(moveY: number) {
    const positions = this.letterList.map(letter => this.letterPositionMap.get(letter))
    let index = 0
    for (let i = 0; i < positions.length; i++) {
      if (moveY < positions[i]) {
        break
      }
      index = i + 1
    }

    //index就是触摸的每个字母的索引

    this.letterSelect = this.letterList[index]

    const scrollIndex = this.listData.findIndex(item => item.title === this.letterSelect)
    if (scrollIndex !== -1) {
      this.scroller.scrollToIndex(scrollIndex)
    }

    this.latterChange(index)
  }

  latterChange(index: number) {
    this.textFontSizeArray.forEach((_, index) => {
      this.textFontSizeArray[index] = 14
      this.textFontColorArray[index] = "#666666"
    })
    this.textFontSizeArray[index] = 16
    this.textFontColorArray[index] = "#222222"
  }
}
Enter fullscreen mode Exit fullscreen mode

VI. Relevant Summary 

the way of implementation is not static. You can also implement it through Canvas custom drawing. It is basically the same. It is necessary to confirm the position of the current touch letter, then change the style and link the left and right lists. 

This article label: HarmonyOS/ArkUI/contact list actual combat

Top comments (0)