// 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);
}
}
Technical Features
Reactive Data Model
The@ObservedV2
decorator enables automatic UI updates when data changes, while@Trace
tracks property-level modifications.Dynamic Search Highlighting
ThesplitAndHighlight
method implements real-time keyword matching with visual feedback using color differentiation.-
Optimized User Experience
- Automatic keyboard dismissal on blank area touch
- Copy-to-clipboard support for emoticons
- Responsive layout with weighted columns
-
Performance Considerations
- Virtualized list rendering through
ForEach
andScroll
- JSON data lazy loading
- Visibility control instead of conditional rendering
- Virtualized list rendering through
Error Handling
Comprehensive error catching mechanisms for both file operations and JSON parsing.
Implementation Notes
Data Loading
Emoticon data is loaded fromemoticons.json
duringaboutToAppear
lifecycle phase, using ArkTS's resource management system.-
UI Structure
- Search component at top
- Fixed header row
- Scrollable content area
- Four-column layout with proportional weights
-
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)