DEV Community

Cover image for HarmonyOS NEXT Development Case: Emoticon Searcher
zhongcx
zhongcx

Posted on

HarmonyOS NEXT Development Case: Emoticon Searcher

Image description

// Import necessary utility libraries for text decoding
import { util } from '@kit.ArkTS'
// Import BusinessError class for handling business logic errors
import { BusinessError } from '@kit.BasicServicesKit'
// Import inputMethod module for managing input method behavior
import { inputMethod } from '@kit.IMEKit'

// Define observable data model EmoticonBean representing single emoticon information
@ObservedV2
class EmoticonBean {
  // Style property with default empty string
  style: string = ""
  // Type property with default empty string
  type: string = ""
  // Emoticon symbol with default empty string
  emoticon: string = ""
  // Meaning property with default empty string
  meaning: string = ""

  // Constructor allowing property initialization during object creation
  constructor(style: string, type: string, emoticon: string, meaning: string) {
    this.style = style
    this.type = type
    this.emoticon = emoticon
    this.meaning = meaning
  }

  // Visibility state flag with @Trace decorator for change tracking
  @Trace isShown: boolean = true
}

// Define Index component as application entry using @Entry and @Component decorators
@Entry
@Component
struct Index {
  // State variable for search input content
  @State private textInput: string = ''
  // State variable for storing emoticon list
  @State private emoticonList: EmoticonBean[] = []

  // Style configuration properties
  private lineColor: string = "#e6e6e6"
  private titleBackground: string = "#f8f8f8"
  private textColor: string = "#333333"
  private basePadding: number = 4
  private lineWidth: number = 2
  private cellHeight: number = 50
  private weightRatio: number[] = [1, 1, 5, 4]
  private baseFontSize: number = 14

  // Method for splitting and highlighting search keywords
  private splitAndHighlight(item: EmoticonBean, keyword: string): string[] {
    let text = item.meaning
    if (!keyword) {
      item.isShown = true
      return [text]
    }
    let segments: string[] = [];
    let lastMatchEnd: number = 0;
    while (true) {
      const matchIndex = text.indexOf(keyword, lastMatchEnd);
      if (matchIndex === -1) {
        segments.push(text.slice(lastMatchEnd));
        break;
      } else {
        segments.push(text.slice(lastMatchEnd, matchIndex));
        segments.push(text.slice(matchIndex, matchIndex + keyword.length));
        lastMatchEnd = matchIndex + keyword.length;
      }
    }
    item.isShown = (segments.indexOf(keyword) != -1)
    return segments;
  }

  // Lifecycle method for loading data when component appears
  aboutToAppear() {
    getContext().resourceManager.getRawFileContent("emoticons.json", (err: BusinessError, data) => {
      if (err) {
        console.error('getRawFileContent error: ' + JSON.stringify(err))
        return
      }
      let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true })
      let jsonString = textDecoder.decodeToString(data, { stream: false })
      let jsonObjectArray: object[] = JSON.parse(jsonString)
      for (let i = 0; i < jsonObjectArray.length; i++) {
        let item = jsonObjectArray[i]
        this.emoticonList.push(new EmoticonBean(item['s'], item['t'], item['e'], item['m']))
      }
      try {
        console.info(`this.emoticonList:${JSON.stringify(this.emoticonList, null, '\u00a0\u00a0')}`)
      } catch (err) {
        console.error('parse error: ' + JSON.stringify(err))
      }
    })
  }

  // Build method for UI construction
  build() {
    Column({ space: 0 }) {
      Search({ value: $$this.textInput })
        .margin(this.basePadding)
        .fontFeature("\"ss01\" on")

      Column() {
        Row() {
          Text('Style')
            .height('100%')
            .layoutWeight(this.weightRatio[0])
            .textAlign(TextAlign.Center)
            .fontSize(this.baseFontSize)
            .fontWeight(600)
            .fontColor(this.textColor)
          Line().height('100%').width(this.lineWidth).backgroundColor(this.lineColor)
          Text('Type')
            .height('100%')
            .layoutWeight(this.weightRatio[1])
            .textAlign(TextAlign.Center)
            .fontSize(this.baseFontSize)
            .fontWeight(600)
            .fontColor(this.textColor)
          Line().height('100%').width(this.lineWidth).backgroundColor(this.lineColor)
          Text('Emoticon')
            .height('100%')
            .layoutWeight(this.weightRatio[2])
            .textAlign(TextAlign.Center)
            .fontSize(this.baseFontSize)
            .fontWeight(600)
            .fontColor(this.textColor)
          Line().height('100%').width(this.lineWidth).backgroundColor(this.lineColor)
          Text('Meaning')
            .height('100%')
            .layoutWeight(this.weightRatio[3])
            .textAlign(TextAlign.Center)
            .fontSize(this.baseFontSize)
            .fontWeight(600)
            .fontColor(this.textColor)
        }.height(this.cellHeight).borderWidth(this.lineWidth).borderColor(this.lineColor)
        .backgroundColor(this.titleBackground)
      }.width(`100%`).padding({ left: this.basePadding, right: this.basePadding })

      Scroll() {
        Column() {
          ForEach(this.emoticonList, (item: EmoticonBean) => {
            Row() {
              Text(item.style)
                .height('100%')
                .layoutWeight(this.weightRatio[0])
                .textAlign(TextAlign.Center)
                .fontSize(this.baseFontSize)
                .fontColor(this.textColor)
              Line().height('100%').width(this.lineWidth).backgroundColor(this.lineColor)
              Text(item.type)
                .height('100%')
                .layoutWeight(this.weightRatio[1])
                .textAlign(TextAlign.Center)
                .fontSize(this.baseFontSize)
                .fontColor(this.textColor)
              Line().height('100%').width(this.lineWidth).backgroundColor(this.lineColor)
              Text(item.emoticon)
                .height('100%')
                .layoutWeight(this.weightRatio[2])
                .textAlign(TextAlign.Center)
                .fontSize(this.baseFontSize)
                .fontColor(this.textColor)
                .copyOption(CopyOptions.LocalDevice)
              Line().height('100%').width(this.lineWidth).backgroundColor(this.lineColor)
              Text() {
                ForEach(this.splitAndHighlight(item, this.textInput), (segment: string, index: number) => {
                  ContainerSpan() {
                    Span(segment)
                      .fontColor(segment === this.textInput ? Color.White : Color.Black)
                      .onClick(() => {
                        console.info(`Highlighted text clicked: ${segment}`);
                        console.info(`Click index: ${index}`);
                      });
                  }.textBackgroundStyle({
                    color: segment === this.textInput ? Color.Red : Color.Transparent
                  });
                });
              }
              .height('100%')
              .layoutWeight(this.weightRatio[3])
              .textAlign(TextAlign.Center)
              .fontSize(this.baseFontSize)
              .fontColor(this.textColor)
              .padding({ left: this.basePadding, right: this.basePadding })
            }
            .height(this.cellHeight)
            .borderWidth({ left: this.lineWidth, right: this.lineWidth, bottom: this.lineWidth })
            .borderColor(this.lineColor)
            .visibility(item.isShown ? Visibility.Visible : Visibility.None)
          })
        }.width(`100%`).padding({ left: this.basePadding, right: this.basePadding })
      }.width('100%').layoutWeight(1).align(Alignment.Top)
      .onTouch((event) => {
        if (event.type == TouchType.Down) {
          inputMethod.getController().stopInputSession()
        }
      })
    }.width('100%').height('100%').backgroundColor(Color.White);
  }
}
Enter fullscreen mode Exit fullscreen mode

Technical Features

  1. Reactive Data Model

    The @ObservedV2 decorator enables automatic UI updates when data changes, while @Trace tracks property-level modifications.

  2. Dynamic Search Highlighting

    The splitAndHighlight method implements real-time keyword matching with visual feedback using color differentiation.

  3. Optimized User Experience

    • Automatic keyboard dismissal on blank area touch
    • Copy-to-clipboard support for emoticons
    • Responsive layout with weighted columns
  4. Performance Considerations

    • Virtualized list rendering through ForEach and Scroll
    • JSON data lazy loading
    • Visibility control instead of conditional rendering
  5. Error Handling

    Comprehensive error catching mechanisms for both file operations and JSON parsing.

Implementation Notes

  1. Data Loading

    Emoticon data is loaded from emoticons.json during aboutToAppear lifecycle phase, using ArkTS's resource management system.

  2. UI Structure

    • Search component at top
    • Fixed header row
    • Scrollable content area
    • Four-column layout with proportional weights
  3. Accessibility

    • Clear visual hierarchy
    • Touch-friendly dimensions
    • High contrast color scheme

This implementation demonstrates HarmonyOS NEXT's capabilities in building responsive, data-driven applications with clean architecture and optimal performance characteristics.

Top comments (0)