DEV Community

Studotie Handwer
Studotie Handwer

Posted on

[Dev Notes] Word Segmentation and Smear-to-Select Feature

Preface

Recently, I've been working on an emoji search feature. Since it's a search, emojis need some recognizable attributes—so assigning each emoji a name field makes sense. But a user might have thousands of emojis—naming them one by one would be a nightmare. That’s why we need OCR for emojis. However, OCR isn’t always reliable, so we also need to allow users to manually adjust the names afterward.

But tapping to open an input box and then editing text is still too much friction. Inspired by Luo Yonghao’s “Big Bang” feature, selecting segmented text by smearing (dragging across it) offers a much better interaction experience.

Target Effect

Let’s look at the effect first:

image

Each segmented word is placed inside a box. Selected words are highlighted. Tapping or swiping lets you select/deselect them, with support for select all / deselect all.

Implementation Details

Refer to my previous post where I explain the implementation and logic behind swipe-to-select:

Previous Article Link (Huawei Developer Blog)

We’ll modify that implementation for our needs.

The biggest challenge here is dealing with the irregular layout of word blocks from left to right. For those familiar with web development, Flex layout’s flex-wrap property immediately comes to mind!

image

This official flex-wrap demo is exactly what we want.

Once the tech stack is set, we begin coding.


First, let’s extract and refine the swipe selection logic from the previous article into a basic library:

@ObservedV2
class GridSlideSelectInfo {
  ...
}
@ObservedV2
export class SlideToSelectGridViewModel {
  ...
}
Enter fullscreen mode Exit fullscreen mode

Next, we create a component framework. The parameters needed: words to hold all the segments, and selectedIndex to track which words are selected.

@ComponentV2
export struct WordDividerContent {
  @Param @Require words: string[]
  @Param @Require selectedIndex: Set<number>

  build() {}
}
Enter fullscreen mode Exit fullscreen mode

Quick note on how to do the word segmentation: if you have your own server, just request results from there. Otherwise, check out HarmonyOS AI’s textProcessing API:
HarmonyOS Word Segmentation Docs

textProcessing.getWordSegment(text)
  .then((res) => {
    this.aiDividedWords = res.map(val => val.word).filter(val => !val.includes('\n'))
  })
Enter fullscreen mode Exit fullscreen mode

This gives you aiDividedWords: string[].

Now, onto the UI.

First, use Flex layout with flex-wrap to handle word block layout:

build() {
  Column() {
    Scroll() {
      Flex({
        direction: FlexDirection.Row,
        wrap: FlexWrap.Wrap,
        ...
      }) {
        ForEach(this.words, (val: string, index) => {
          Text(val).selectedStyle(<selection logic>)
        })
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Custom style based on whether a word is selected:

@Extend(Text) function selectedStyle(selected: boolean) {
  .backgroundColor(selected ? ThemeManager.getBrand() : $r('sys.color.background_tertiary'))
  ...
}
Enter fullscreen mode Exit fullscreen mode

Add logic for click-to-select:

Text(val)
  .selectedStyle(this.judgeSelected(index))
  .onClick(() => {
    ...
  })
Enter fullscreen mode Exit fullscreen mode

Then we add area tracking and pan gesture handling:

@Local slideVM: SlideToSelectGridViewModel = new SlideToSelectGridViewModel()
private wordArea: Area[] = []

aboutToAppear(): void {
  this.wordArea = new Array(this.words.length)
}
Enter fullscreen mode Exit fullscreen mode

Add onAreaChange to each word block to track its area:

Text(val)
  .onAreaChange((_o, area) => { this.wordArea[index] = area })
Enter fullscreen mode Exit fullscreen mode

Track the full component’s area and bind gestures:

.onAreaChange((_, nv) => {
  this.slideVM.pageArea = nv
})
.gesture(PanGesture().onActionStart(...).onActionUpdate(...).onActionEnd(...))
Enter fullscreen mode Exit fullscreen mode

Now implement swipe-to-select logic, including a finder to locate which block a touch point is over:

private finder = (vx: number, vy: number) => {
  return this.wordArea.findIndex(val => this.slideVM.checkIsInArea(vx, vy, val))
}
Enter fullscreen mode Exit fullscreen mode

Update judgeSelected() to handle swipe states:

judgeSelected(index: number) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

Handle gesture callbacks:

this.slideVM.onSlideActionStart(...)
this.slideVM.onSlideActionMove(...)
this.slideVM.onSlideActionEnd(...)
Enter fullscreen mode Exit fullscreen mode

Gesture logic is encapsulated in the view model for cleanliness:

class SlideToSelectGridViewModel {
  public onSlideActionStart = ...
  public onSlideActionMove = ...
  public onSlideActionEnd = ...
}
Enter fullscreen mode Exit fullscreen mode

Finally, add two buttons for “Select All” and “Deselect All”:

Row() {
  ActionButton({ title: 'Deselect All', clickAction: () => { ... } })
  ActionButton({ title: 'Select All', clickAction: () => { ... } })
}
Enter fullscreen mode Exit fullscreen mode

And we’re done!

Top comments (0)