鸿蒙OS开发实战:【打造自己的搜索入口】

背景

几乎每家应用中都带有搜索功能,关于这个功能的页面不是特别复杂,但如果要追究其背后的一系列逻辑,可能是整个应用中最复杂的一个功能。今天主要实践目标,会抛开复杂的逻辑,尝试纯粹实现一个“搜索主页”,主要包含,输入框文字输入,热门词展示,热门帖子展示。全篇主要使用到的控件是TextInput, Flex, Swiper。为了贴近实战,文字输入过程中,也增加了联想词功能。整个示例将在模拟状态下完成,不做任何网络请求。

功能清单

  1. 输入框 - TextInput用法
  2. 按钮搜索词删除 - 触摸事件透传用法
  3. 搜索按钮 - 页面返回用法
  4. 联想词 - Span用法,if...else 渲染用法
  5. 历史搜索词 - 行数限制,排序
  6. 热门搜索词 - 换行布局,行为识别(打开链接,发起搜索)
  7. 热门帖子 - Swiper用法,Span用法

效果

Screenshot_20231219153940743.png

  

Screenshot_20231219152615992.png

布局结构

整体页面分为上下布局两大部分:

  1. 搜索栏
  2. 可滚动内容区域

开始前熟悉鸿蒙文档

鸿蒙OS开发更多内容↓点击HarmonyOS与OpenHarmony技术
鸿蒙技术文档《鸿蒙NEXT星河版开发学习文档》

搜狗高速浏览器截图20240326151344.png

搜索框

HarmonyOS 提供了Search控件, 这种样式不太满足今天要做的需求,所以我还是准备采用TextInput控件重新定制

0000000000011111111.20231116092649.47082174539092627589057824940312.gif

预期的搜索框需要包含基础的三个功能

  1. 文字输入
  2. 文字删除
  3. 提交已输入的文字(即:准备发起搜索)

这个样式的实现方式,我采用了左右布局,左布局采用叠加布局方式,翻译为代码,表现形式如下

//一. (左布局)输入框 + (右布局)搜索按钮
Row() {Stack() {// 输入框TextInput()// 放大镜图片 + 删除按钮图片 Row() {Image(放大镜图片)if (this.currentInputBoxContent.length != 0) {Image(删除按钮图片)}}//搜索按钮 / 返回按钮Text(this.searchButtonText)
}

这里的Stack布局方式,实际中会引发一个问题:点击TextInput控件时非常不灵敏,实际情况是“放大镜图片+删除按钮图解片”Row布局,消耗了输入框的触摸事件。 解决这个问题,可以使用系统提供的hitTestBehavior(HitTestMode.None)这个接口,这个接口的参数提供了4种响应触摸事件的功能

所以解决此问题只需要添加完这个接口即可恢复正常触摸事件:见代码中的 NOTE:父组件不消耗触摸事件

//一. (左布局)输入框 + (右布局)搜索按钮
Row() {Stack() {// 输入框TextInput()// 放大镜图片 + 删除按钮图片 Row() {Image(放大镜图片)if (this.currentInputBoxContent.length != 0) {Image(删除按钮图片)}.hitTestBehavior(HitTestMode.None) // NOTE:父组件不消耗触摸事件}//搜索按钮 / 返回按钮Text(this.searchButtonText)
}

由于采用的是Stack叠加布局方式,所以要解决的第二个问题是如何将Row布局两边对齐Stack即,处于TextInput控件的两端,根据[Row容器内子元素在水平方向上的排列]可知,在Row布局上添加justifyContent(FlexAlign.SpaceBetween)这句代码即可。

官方指导示意图:

0000000000011111111.20231211142810.46416226973241546619287224558714.png

变更后的代码

//一. (左布局)输入框 + (右布局)搜索按钮
Row() {Stack() {// 输入框TextInput()// 放大镜图片 + 删除按钮图片 Row() {Image(放大镜图片)if (this.currentInputBoxContent.length != 0) {Image(删除按钮图片)}.hitTestBehavior(HitTestMode.None) // NOTE:父组件不消耗触摸事件.justifyContent(FlexAlign.SpaceBetween) // NOTE: 两端对齐}//搜索按钮 / 返回按钮Text(this.searchButtonText)
}

TextInput的构造函数参数说明

  • placeholder: 俗称:按提示,提示词,引导词
  • text: 输入框已输入的文字内容

TextInput的属性方法onChange

用来监听最新已输入的文字

  • 这个方法中,我们可以通过判断内容长度,来设置控制中的搜索按钮文字,如果有内容,按钮文案将变为"搜索",反之,按钮文案变为“取消”,即点击之后将关闭当前页面; 同时请求远端联想词的功能也是在这里触发,注意:本篇文章中的联想词仅仅是本地模拟的数据,没有进行网络请求,也没有模拟网络延时加载,在真实场景中一定要注意用户的操作行为应该中断本次联想词的网络请求(即使网络请求已经发出去,回来之后,也要扔掉拿到的联想词数据)。

TextInput的属性方法enterKeyType

这个用来修改软件盘上的回车键文字提示,这个设置的值为EnterKeyType.Search,所以在中文模式下,你会发现键盘上的文字为:搜索

TextInput({ placeholder: '热词搜索', text: this.currentInputBoxContent }).height('40vp').fontSize('20fp').enterKeyType(EnterKeyType.Search).placeholderColor(Color.Grey).placeholderFont({ size: '14vp', weight: 400 }).width('100%').padding({ left: '35vp', right: '35vp' }).borderStyle(BorderStyle.Solid).borderWidth('1vp').borderColor(Color.Red).onChange((currentContent) => {this.currentInputBoxContent = currentContentif (this.currentInputBoxContent.length != 0) {this.searchButtonText = '搜索'this.showThinkWord = truethis.simulatorThinkWord()} else {this.searchButtonText = '取消'this.showThinkWord = false}}).onSubmit((enterKey: EnterKeyType) => {this.submitData(new HistoryWordModel(0, this.currentInputBoxContent));})

至此,一个完整的输入框已完美的完成布局。

历史搜索词

一个搜索的新手产品,在讲解这部分需求时,会使用简短的话术:把搜索过的内容显示出来。

实际情况是比较严谨复杂的,最多多展示多少行? 每个历史词最多展示多少个字符? 要不要识别词性?......`, 针对这些严格的逻辑,研发人员需要优先解决动态布局的问题,剩下的仅仅是堆积代码。

在Android系统中,针对这种布局场景,需要代码动态实现,即采用Java方式布局,不幸的是HarmonyOS 中没有这个说法。

解决方案:

给历史词变量添加 @State 修饰,根据视图高度动态计算行数,然后动态删除多余关键词记录

注意:@State 修饰的Array无法对sort方法生效,结合场景描述即:最新搜索的关键词,都要排在第一个位置,所以每发起一次搜索,都要对Array类型的变量进行一次排序,由于@State的限制,我们需要在中间中转一次。

既然已经知道问题,那么先看一下布局代码,然后继续完成需求

首先,对动态布局的需求来讲,HarmonyOS中,貌似只能用Flex容器来解决,因为它不仅可以包含子组件,也有自动换行功能,所这里我采用的是Flex容器,如果你要更好的方案,欢迎留言交流

通过视图高度动态计算行数,可以依赖onAreaChange接口,在其回调中,通过每次的新值结构体(即Area),获取当前布局高度,然后除以第一次获取到的高度,这样即可完成行数的测算

关于Flex自动换行功能,这个要依赖于一个参数wrap: FlexWrap.Wrap

if (this.historyWords.length != 0) {Row() {Text('历史搜索').fontSize('20fp').fontWeight(FontWeight.Bold)Image($r('app.media.ic_public_delete')).width('20vp').height('20vp').onClick(() => {this.dialogController.open()})}.width('100%').margin({ top: '20vp' }).padding({ left: '10vp', right: '10vp' }).justifyContent(FlexAlign.SpaceBetween)Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {ForEach(this.historyWords, (item: HistoryWordModel, index) => {Text(item.word).fontSize(15).margin(5).fontColor('#5d5d5d').maxLines(1).backgroundColor('#f6f6f6').padding({ left: 20, right: 20, top: 5, bottom: 5 }).borderRadius('30vp').textOverflow({ overflow: TextOverflow.Ellipsis }).onClick(()=>{this.submitData(item);})})}.width('100%').margin({ top: '12vp' }).onAreaChange((oldValue: Area, newValue: Area) => {let newHeight = newValue.height as number//全局声明一个历史词单行高度变量,初始值设置为0,一旦产生历史词,将行高设置为此值//后续将以此值为标准来计算历史词行数if(this.currentHistoryHeight == 0){this.currentHistoryHeight = newHeight}//这里仅仅取整this.currentLineNumbs = newHeight / this.currentHistoryHeight//MAX_LINES 代表最大行数if (this.currentLineNumbs >= MAX_LINES) {//删除一个历史词,由于historyWords添加了@State修饰,所以数据发生变化后,页面会刷新//页面刷新后,又会重新触发此方法this.historyWords = this.historyWords.slice(0, this.historyWords.length-1)}})}

刚刚提到过一个问题,@State 修饰的Array变量是无法进行排序的。应对这个问题,可以在中间中转一下,即声明一个局部Array,先将历史记录赋值给它,让这个局部Array参与sort,然后清空@State修饰的Array变量,最终将局部Array赋值给@State修饰的Array变量,描述有点繁琐,直接看代码。

只要发起搜索行为,都会使用到此方法, 另外注意阅读代码注释有NOTE的文字

submitData(wordModel: HistoryWordModel) {if (wordModel.word.length != 0) {//标识本次搜索的关键词是否存在let exist: boolean = false//如果搜索关键词存在,记录其位置,如果发现其已经是第一个位置,则不进行排序刷新动作let existIndex: number = -1//判断搜索关键是否存在于历史搜索词列表中this.historyWords.forEach((item, index) => {if(item.word === wordModel.word){//如果本次搜索关键词已经处于历史搜索词的第一个位置,不做删除动作if(index != 0){//如果存在,先删除历史词列表中的这个关键词this.historyWords.splice(index, 1)}exist = trueexistIndex = index}});//本次搜索关键词在历史搜索词列表中处于第一个位置,因此不做任何额外处理//NOTE:真实场景中,这里除了重置状态,应该发起网络请求if(existIndex == 0){console.log('不需要刷新页面')this.currentInputBoxContent = ''this.searchButtonText = '取消'this.showThinkWord = falsereturn}if(!exist){//如果本次搜索关键词在历史词列表中不存在,则将其加入其中wordModel.index = this.historyWordIndex++this.historyWords.push(wordModel)} else {//如果本次搜索关键词已存在于历史词列表中,将其对应的下标加1,因为后续会用下表排序//下标越大代表离发生过的搜索行为离当前越近this.historyWordIndex++this.historyWords.push(new HistoryWordModel(this.historyWordIndex, wordModel.word, wordModel.link))}//NOTE:这个就是中转排序的起始代码let Test: Array<HistoryWordModel> = []this.historyWords.forEach((item, index) => {Test.push(item)})Test.sort((a:HistoryWordModel, b:HistoryWordModel) => {return b.index - a.index})this.historyWords.length = 0Test.forEach((item, index) => {this.historyWords.push(item)})//NOTE:这个就是中转排序的结束代码this.currentInputBoxContent = ''this.searchButtonText = '取消'this.showThinkWord = false} else {Prompt.showToast({message: '请输入关键词',bottom: px2vp(this.toastBottom)})}
}

至此,历史记录也实现完成。

联想词实现

在已有的搜索场景中,我们都知道,当发起联想词时,历史搜索记录,热词等等,均不会出现在当前屏幕中,为了实现此种效果,我采用了Stack控件叠加覆盖机制和if...else渲染机制,最终实现完成之后,发现没必要使用Stack控件,因为用了if...else布局后,像当于会动态挂载和卸载试图。

联想词实现还会碰到的一个问题:高亮关键词, 按照HarmonyOS 的布局机制,一切布局都应该提前计算好,全量布局多种场景样式,以if...else机制为基础,完整最终的业务场景效果。

那么,如何提前计算好数据呢?高亮词在数据结构上我们可以分为三段:前,中,后。如何理解呢?比如搜索关键词“1”,那我的联想词无非就几种情况123,213,1,231,那么,我声明三个变量s, m, e, 分别代表前,中,后,此时你会发现三个变量是完全可以覆盖所有的匹配场景的。这种方式暂且命名为:分割,“分割”后,在最终展示时,由于需要高亮文字,所以,我们还需要知晓已“分割”的文字中,到底哪一段应该高亮,基于此种考虑,需要额外声明高亮下标,参考“前,中,后”,将下标分别定义为0,1,2

具体实现,看代码吧

Stack() {//联想词需要展示时if (this.showThinkWord) {Column() {//遍历联想词ForEach(this.thinkWords, (item: ThinkWordModel, index) => {//NOTE: Span控件可以实现文字分段多种样式Text() {//判断一条联想词数据的“前”if (item.wordStart && item.wordStart.length != 0) {Span(item.wordStart).fontSize(18).fontColor(item.highLightIndex == 0 ? item.highLightColor : item.normalColor)}//判断一条联想词数据的“中”if (item.wordMid && item.wordMid.length != 0) {Span(item.wordMid).fontSize(18).fontColor(item.highLightIndex == 1 ? item.highLightColor : item.normalColor)}//判断一条联想词数据的“后”if (item.wordEnd && item.wordEnd.length != 0) {Span(item.wordEnd).fontSize(18).fontColor(item.highLightIndex == 2 ? item.highLightColor : item.normalColor)}}......})}......} else {// 没有联想词时,系统机制会讲联想词视图卸载掉,即if中的视图会完全从视图节点中拿掉Column() {//二. 搜索历史if (this.historyWords.length != 0) {......}//三. 热门搜索Text('热门搜索')......Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {ForEach(this.hotWords, (item: HotWordsModel, index) => {Text(item.word)......})}//四. 热门帖子Text('热门帖子')Swiper(this.swiperController) {LazyForEach(this.data, (item: string, index: number) => {......}, item => item)}}}}

热门帖子实现

在整个搜索主页中,这个功能可能算比较简单的,在Scroll控件中放置Swiper控件,然后按照官方文档,循环塞入数据,整个效果即可实现。 这个里边用到了Span,由于我们在联想词实现时已经实践过了Span, 这里就不再描述。

NOTE:为了迎合需求,将滑动指示隐藏掉,indicator(false)false代表隐藏滑动指示

//四. 热门帖子
Text('热门帖子').fontSize('20fp').width('100%').fontWeight(FontWeight.Bold).margin({ left: '10vp', top: '20vp' })Swiper(this.swiperController) {//data仅仅是为了循环,数据总个数是3  LazyForEach(this.data, (item: string, index: number) => {//每一页Swiper内容视图,通过 @Builder 修饰的方法进行一次封装if (index == 0) {this.swiperList(this.hotTopicList1)} else if (index == 1) {this.swiperList(this.hotTopicList2)} else if (index == 2) {this.swiperList(this.hotTopicList3)}}, item => item)}
.padding({ bottom: '50vp' })
.displayMode(SwiperDisplayMode.AutoLinear)
.margin({ top: '12vp' })
.cachedCount(2)
.index(1)
.indicator(false)
.loop(true)
.itemSpace(0)
.curve(Curve.Linear)

完整代码

主页面代码 SearchUI.ets

import common from '@ohos.app.ability.common';
import Prompt from '@system.prompt';
import router from '@ohos.router';
import dataPreferences from '@ohos.data.preferences';import { CommonConstants } from '../../common/CommonConstants';
import HotWordsModel from '../../viewmodel/HotWordsModel';
import mediaquery from '@ohos.mediaquery';
import ThinkWordModel from '../../viewmodel/ThinkWordModel';
import HistoryWordModel from '../../viewmodel/HistoryWordModel';const MAX_LINES: number = 3;@Entry
@Component
struct SearchUIIndex {private hotTopicList1: Array<string> = ['四种醉驾可从宽处理','冰面摔倒至腹腔出血','董宇辉复播','朱一龙拍戏受伤送医','音乐节求婚观众退票','周杰伦新歌歌名','用好“改革开放”这关键一招','男子冬钓失联 遗体在冰缝中被发现','女孩用科目三跳绳 获省级比赛第1名','美丽乡村 幸福生活',]private hotTopicList2: Array<string> = ['醉驾轻微可不起诉','狄龙被驱逐','劳荣枝希望还清花呗','周海媚告别仪式完成','董宇辉兼任副总裁','小米智能锁自动开门','李家超:基本法第23条明年内实施','山东两幼师出租房内遇害','南京同曦老总大闹裁判休息室','女子出车祸鲨鱼夹插入后脑勺','官方辟谣南京过江隧道连环追尾','上海地铁开启疯狂动物城模式',]private hotTopicList3: Array<string> = ['朱丹好友起诉朱丹','"中年大叔"自拍刷屏','西方臻选回应被封号','草莓价格大跳水','库里三分球8中0','国足开启亚洲杯备战',]private currentHistoryHeight: number = 0@State toastBottom: number = 0;@State currentInputBoxContent: string = ''private controller = new TextInputController()private hotWords: Array<HotWordsModel> = []@State historyWords: Array<HistoryWordModel> = []@State inputBoxFocus: boolean = false;@State hotwordLines: number = 0@State searchButtonText: string = '取消'private swiperController: SwiperController = new SwiperController()private data: MyDataSource = new MyDataSource([])private currentLineNumbs: number = 0private context = getContext(this) as common.UIAbilityContext;@State screenDirection: number = this.context.config.direction@State showThinkWord: boolean = false@State thinkWords: Array<ThinkWordModel> = []// 当设备横屏时条件成立listener = mediaquery.matchMediaSync('(orientation: landscape)');dialogController: CustomDialogController = new CustomDialogController({builder: CustomDialogExample({historyWords: $historyWords,title: '确认全部删除?',cancel: this.onCancel,confirm: this.onAccept,}),alignment: DialogAlignment.Default, // 可设置dialog的对齐方式,设定显示在底部或中间等,默认为底部显示})onCancel() {}onAccept() {console.log('当前数组长度:' + this.historyWords.length)}configureParamsByScreenDirection() {if (this.screenDirection == 0) {this.toastBottom = (AppStorage.Get(CommonConstants.ScreenHeight) as number) / 2} else {this.toastBottom = (AppStorage.Get(CommonConstants.ScreenWidth) as number) / 2}}DATASOURCE: string[] = ['联想词测试','测试联想词','全城寻找测试在哪里','找不到人','哈尔滨的啤酒好喝','HarmonyOS版权归属华为']simulatorThinkWord() {this.thinkWords = []this.DATASOURCE.forEach((value: string, index: number) => {let s: string = ''let m: string = ''let e: string = ''let hIndex: number = -1let position = value.indexOf(this.currentInputBoxContent)if (position != -1) {if (position == 0) {s = value.substr(0, this.currentInputBoxContent.length)} else {s = value.substr(0, position)}if (s.length < value.length) {position = value.substr(s.length).indexOf(this.currentInputBoxContent)if (position == -1) {m = value.substr(s.length)} else {m = value.substr(s.length, this.currentInputBoxContent.length)}if (s.length + m.length < value.length) {e = value.substr(s.length + m.length)}}if (s === this.currentInputBoxContent) {hIndex = 0} else if (m === this.currentInputBoxContent) {hIndex = 1} else if (e === this.currentInputBoxContent) {hIndex = 2}this.thinkWords.push(new ThinkWordModel('#000000', '#ff0000', hIndex, s, m, e))}})}onPortrait(mediaQueryResult) {if (mediaQueryResult.matches) {//横屏this.screenDirection = 1} else {//竖屏this.screenDirection = 0}setTimeout(() => {this.configureParamsByScreenDirection()}, 300)}aboutToAppear() {this.searchButtonText = '取消'let list = []for (var i = 1; i <= 3; i++) {list.push(i.toString());}this.data = new MyDataSource(list)this.hotWords.push(new HotWordsModel('HarmonyOS', '#E84026', 'https://developer.harmonyos.com/'))this.hotWords.push(new HotWordsModel('实名认证', '#5d5d5d'))this.hotWords.push(new HotWordsModel('HMS Core', '#5d5d5d'))this.hotWords.push(new HotWordsModel('Serverless', '#5d5d5d'))this.hotWords.push(new HotWordsModel('生态市场', '#5d5d5d'))this.hotWords.push(new HotWordsModel('应用上架', '#5d5d5d'))this.hotWords.push(new HotWordsModel('仓颉', '#5d5d5d'))this.hotWords.push(new HotWordsModel('HUAWEI HiAI', '#5d5d5d'))this.hotWords.push(new HotWordsModel('表盘', '#5d5d5d'))this.hotWords.push(new HotWordsModel('推送', '#5d5d5d'))this.hotWords.push(new HotWordsModel('主题', '#5d5d5d'))this.hotWords.push(new HotWordsModel('公测', '#5d5d5d'))let portraitFunc = this.onPortrait.bind(this)this.listener.on('change', portraitFunc)this.toastBottom = (AppStorage.Get(CommonConstants.ScreenHeight) as number) / 2dataPreferences.getPreferences(getContext(this), 'HistoryWord', (err, preferences) => {if (err) {console.error(`Failed to get preferences. Code:${err.code},message:${err.message}`);return;}console.info('Succeeded in getting preferences.');// 进行相关数据操作})}historyWordIndex: number = 1submitData(wordModel: HistoryWordModel) {if (wordModel.word.length != 0) {let exist: boolean = falselet existIndex: number = -1this.historyWords.forEach((item, index) => {if(item.word === wordModel.word){if(index != 0){this.historyWords.splice(index, 1)}exist = trueexistIndex = index}});if(existIndex == 0){console.log('不需要刷新页面')this.currentInputBoxContent = ''this.searchButtonText = '取消'this.showThinkWord = falsereturn}if(!exist){wordModel.index = this.historyWordIndex++this.historyWords.push(wordModel)} else {this.historyWordIndex++this.historyWords.push(new HistoryWordModel(this.historyWordIndex, wordModel.word, wordModel.link))}let Test: Array<HistoryWordModel> = []this.historyWords.forEach((item, index) => {Test.push(item)})Test.sort((a:HistoryWordModel, b:HistoryWordModel) => {return b.index - a.index})this.historyWords.length = 0Test.forEach((item, index) => {this.historyWords.push(item)})this.currentInputBoxContent = ''this.searchButtonText = '取消'this.showThinkWord = false} else {Prompt.showToast({message: '请输入关键词',bottom: px2vp(this.toastBottom)})}}build() {Column() {//一. 输入框 + 搜索按钮Row() {Stack() {TextInput({ placeholder: '热词搜索', controller: this.controller, text: this.currentInputBoxContent }).height('40vp').fontSize('20fp').enterKeyType(EnterKeyType.Search).placeholderColor(Color.Grey).placeholderFont({ size: '14vp', weight: 400 }).width('100%').padding({ left: '35vp', right: '35vp' }).borderStyle(BorderStyle.Solid).borderWidth('1vp').borderColor(Color.Red).onChange((currentContent) => {this.currentInputBoxContent = currentContentif (this.currentInputBoxContent.length != 0) {this.searchButtonText = '搜索'this.showThinkWord = truethis.simulatorThinkWord()} else {this.searchButtonText = '取消'this.showThinkWord = false}}).onSubmit((enterKey: EnterKeyType) => {this.submitData(new HistoryWordModel(0, this.currentInputBoxContent));})Row() {Image($r('app.media.ic_public_input_search')).width('20vp').height('20vp')if (this.currentInputBoxContent.length != 0) {Image($r('app.media.ic_public_cancel_filled')).width('20vp').height('20vp').onClick(() => {this.currentInputBoxContent = ''})}}.width('100%').hitTestBehavior(HitTestMode.None).justifyContent(FlexAlign.SpaceBetween).padding({ left: '10vp', right: '10vp' })}.alignContent(Alignment.Start).width('83%')Text(this.searchButtonText).fontSize('15fp').borderRadius('10vp').padding('5vp').backgroundColor(Color.Red).fontColor(Color.White).width('15%').textAlign(TextAlign.Center).onClick(() => {if ('搜索' === this.searchButtonText) {this.submitData(new HistoryWordModel(0, this.currentInputBoxContent));} else {if ("1" === router.getLength()) {this.context.terminateSelf()} else {router.back()}}}).stateStyles({focused: {.backgroundColor(Color.Orange)},pressed: {.backgroundColor(Color.Orange)},normal: {.backgroundColor(Color.Red)}})}.justifyContent(FlexAlign.SpaceBetween).padding({ left: '10vp', right: '10vp' }).width('100%')Scroll() {Stack() {if (this.showThinkWord) {Column() {ForEach(this.thinkWords, (item: ThinkWordModel, index) => {Text() {if (item.wordStart && item.wordStart.length != 0) {Span(item.wordStart).fontSize(18).fontColor(item.highLightIndex == 0 ? item.highLightColor : item.normalColor)}if (item.wordMid && item.wordMid.length != 0) {Span(item.wordMid).fontSize(18).fontColor(item.highLightIndex == 1 ? item.highLightColor : item.normalColor)}if (item.wordEnd && item.wordEnd.length != 0) {Span(item.wordEnd).fontSize(18).fontColor(item.highLightIndex == 2 ? item.highLightColor : item.normalColor)}}.width('100%').height(50).textAlign(TextAlign.Center).fontSize(18).textAlign(TextAlign.Start).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })Divider().width('100%').height(1).color(Color.Grey)})}.width('100%').height('100%').padding({ left: '12vp', right: '12vp' }).backgroundColor(Color.White)} else {Column() {//二. 搜索历史if (this.historyWords.length != 0) {Row() {Text('历史搜索').fontSize('20fp').fontWeight(FontWeight.Bold)Image($r('app.media.ic_public_delete')).width('20vp').height('20vp').onClick(() => {this.dialogController.open()})}.width('100%').margin({ top: '20vp' }).padding({ left: '10vp', right: '10vp' }).justifyContent(FlexAlign.SpaceBetween)Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {ForEach(this.historyWords, (item: HistoryWordModel, index) => {Text(item.word).fontSize(15).margin(5).fontColor('#5d5d5d').maxLines(1).backgroundColor('#f6f6f6').padding({ left: 20, right: 20, top: 5, bottom: 5 }).borderRadius('30vp').textOverflow({ overflow: TextOverflow.Ellipsis }).onClick(()=>{this.submitData(item);})})}.width('100%').margin({ top: '12vp' }).onAreaChange((oldValue: Area, newValue: Area) => {let newHeight = newValue.height as numberif(this.currentHistoryHeight == 0){this.currentHistoryHeight = newHeight}this.currentLineNumbs = newHeight / this.currentHistoryHeightconsole.log('当前行数: ' + this.currentLineNumbs)if (this.currentLineNumbs >= MAX_LINES) {this.historyWords = this.historyWords.slice(0, this.historyWords.length-1)}})}//三. 热门搜索Text('热门搜索').fontSize('20fp').width('100%').fontWeight(FontWeight.Bold).margin({ left: '10vp', top: '20vp' })Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {ForEach(this.hotWords, (item: HotWordsModel, index) => {Text(item.word).fontSize(15).margin(5).fontColor(item.wordColor).backgroundColor('#f6f6f6').padding({ left: 20, right: 20, top: 5, bottom: 5 }).borderRadius('30vp').onClick(() => {if (this.hotWords[index].wordLink && this.hotWords[index].wordLink.length != 0) {router.pushUrl({ url: 'custompages/WebView', params: {"targetUrl": this.hotWords[index].wordLink,} }).then(() => {console.info('Succeeded in jumping to the second page.')}).catch((error) => {console.log(error)})} else if(this.hotWords[index].word){this.submitData(new HistoryWordModel(0, this.hotWords[index].word));}})})}.width('100%').margin({ top: '12vp' }).onAreaChange((oldValue: Area, newValue: Area) => {console.log('热词高度:' + newValue.height + '')})//四. 热门帖子Text('热门帖子').fontSize('20fp').width('100%').fontWeight(FontWeight.Bold).margin({ left: '10vp', top: '20vp' })Swiper(this.swiperController) {LazyForEach(this.data, (item: string, index: number) => {if (index == 0) {this.swiperList(this.hotTopicList1)} else if (index == 1) {this.swiperList(this.hotTopicList2)} else if (index == 2) {this.swiperList(this.hotTopicList3)}}, item => item)}.padding({ bottom: '50vp' }).displayMode(SwiperDisplayMode.AutoLinear).margin({ top: '12vp' }).cachedCount(2).index(1).indicator(false).loop(true).itemSpace(0).curve(Curve.Linear)}}}}.scrollBar(BarState.Off)}.padding({ top: px2vp(AppStorage.Get(CommonConstants.StatusBarHeight)) })}@Builder swiperList(data: string[]){Column() {ForEach(data, (da, i) => {if(i == 0){Text(){Span((i+1)+'. ').fontColor('#E84026').fontSize(20)Span(da).fontColor('#5d5d5d').fontSize(18)}.width('100%').height(50)} else if(i == 1){Text(){Span((i+1)+'. ').fontColor('#ED6F21').fontSize(20)Span(da).fontColor('#5d5d5d').fontSize(18)}.width('100%').height(50)} else if(i == 2){Text(){Span((i+1)+'. ').fontColor('#F9A01E').fontSize(20)Span(da).fontColor('#5d5d5d').fontSize(18)}.width('100%').height(50)} else {Text((i + 1) + '. '+ da).fontColor('#5d5d5d').width('100%').height(50).textAlign(TextAlign.Center).fontSize(18).textAlign(TextAlign.Start)}if (i != this.hotTopicList1.length - 1) {Divider().width('100%').vertical(false)}})}.borderRadius('10vp').margin({ left: '10vp', right: '10vp', bottom: '25vp' }).backgroundColor('#f6f6f6').padding('10vp')}
}@CustomDialog
struct CustomDialogExample {controller: CustomDialogControllertitle: string = ''@Link historyWords: Array<string>cancel: () => voidconfirm: () => voidbuild() {Column() {Text(this.title).fontSize(20).margin({ top: 10, bottom: 10 })Flex({ justifyContent: FlexAlign.SpaceAround }) {Button('取消').onClick(() => {this.controller.close()this.cancel()}).backgroundColor(0xffffff).fontColor(Color.Black)Button('确认').onClick(() => {this.controller.close()this.confirm()this.historyWords = []}).backgroundColor(0xffffff).fontColor(Color.Red)}.margin({ bottom: 10 })}}
}class MyDataSource implements IDataSource {private list: number[] = []private listener: DataChangeListenerconstructor(list: number[]) {this.list = list}totalCount(): number {return this.list.length}getData(index: number): any {return this.list[index]}registerDataChangeListener(listener: DataChangeListener): void {this.listener = listener}unregisterDataChangeListener() {}
}

历史词数据结构 HistoryWordModel.ets

export default class HistoryWordModel {public index: numberpublic link: stringpublic word: stringconstructor(index, word, link?) {this.index = indexthis.link = linkthis.word = word}
}

热词数据结构 HotWordModel.ets

export default class HotWordModel {public word: string //词语public wordColor: string //文字颜色public wordLink?: string //文字超链接constructor(word, wordColor, wordLink?) {this.word = wordthis.wordColor = wordColorthis.wordLink = wordLink}
}

联想词数据结构 ThinkWordModel.ets

export default class ThinkWordModel {public normalColor: stringpublic highLightColor: stringpublic wordStart: string //词语public wordMid: string //文字颜色public wordEnd: string //文字超链接public highLightIndex: numberconstructor(normalColor: string, highLightColor: string, highLightIndex: number,wordStart?: string, wordMid?: string,wordEnd?: string) {this.normalColor = normalColorthis.highLightColor = highLightColorthis.highLightIndex = highLightIndexthis.wordStart = wordStartthis.wordMid = wordMidthis.wordEnd = wordEnd}}

总结

  1. 对于Android&iOS开发者来讲,在HarmonyOS中实现动态布局,还是非常容易陷入之前的开发思路中
  2. 新的平台,熟悉API很重要

鸿蒙最值得程序员入行

为什么这么说?市场是决定人力需求的,数据说话最管用:

1、鸿蒙其全栈自研,头部大厂商都陆续加入合作开发鸿蒙原生应用——人才需求上涨

2、鸿蒙作为新系统、新技术,而现在市面上技术人才少——高薪招聘开启

3、鸿蒙1+8+N生态,不仅只有应用开发;还有车载、数码、智能家居、家电等——就业范围广

4、纯血鸿蒙,目前没有多少人熟悉。都处于0基础同一起跑线——无行业内卷

开发者最需要什么?岗位多、薪资高、不内卷、行业竞争低。而当下的鸿蒙恰恰符合要求。

那么这么好的鸿蒙岗位,应聘要求都很高吧?其实不然鸿蒙作为新出的独立系统,其源头上大家都处于同一水平线上,一开始的技术要求都不会很高,毕竟面试官也是刚起步学习。招聘要求示例:

从信息看出,几乎应职要求是对标有开发经验的人群。可以说鸿蒙对开发者非常友好,尽管上面没提鸿蒙要求,但是面试都会筛选具有鸿蒙开发技能的人。我们程序员都知道学习开发技术,最先是从语言学起,鸿蒙语言有TS、ArkTS等语法,那么除了这些基础知识之外,其核心技术点有那些呢?下面就用一张整理出的鸿蒙学习路线图表示:

从上面的OpenHarmony技术梳理来看,鸿蒙的学习内容也是很多的。现在全网的鸿蒙学习文档也是非常的少,下面推荐一些:完整内容可在头像页保存,或这qr23.cn/AKFP8k甲助力

内容包含:《鸿蒙NEXT星河版开发学习文档》

  • ArkTS
  • 声明式ArkUI
  • 多媒体
  • 通信问题
  • 系统移植
  • 系统裁剪
  • FW层的原理
  • 各种开发调试工具
  • 智能设备开发
  • 分布式开发等等。

这些就是对往后开发者的分享,希望大家多多点赞关注喔!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/779117.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

初识Node.js与内置模块

能够知道什么是 Node.js能够知道 Node.js 可以做什么能够说出 Node.js 中的 JavaScript 的组成部分能够使用 fs 模块读写操作文件能够使用 path 模块处理路径能够使用 http 模块写一个基本的 web 服务器 一.初识Node.js 1.浏览器中的 JavaScript 的组成部分 2.Node.js 简介 …

MySQL创建表:练习题

练习题&#xff1a; 创建一个名为"students"的数据库&#xff0c;并切换到该数据库。 在"students"数据库中创建一个名为"grades"的表&#xff0c;包含以下字段&#xff1a; id: 整数类型 name: 字符串类型&#xff0c;学生姓名 subject: 字符串…

postgis已有表插入外部表数据带空间字段和主键

1、postgis已有表 其中gid是主键字段,geom是几何字段 2、待插入表的数据(分三种情况) (1)通过坐标将写入几何类型字段 INSERT INTO test (gid, geom, mc,lng,lat) SELECT (SELECT COALESCE(MAX(gid)<

微服务demo(二)nacos服务注册与集中配置

环境&#xff1a;nacos1.3.0 一、服务注册 1、pom&#xff1a; 移步spring官网https://spring.io&#xff0c;查看集成Nacos所需依赖 找到对应版本点击进入查看集成说明 然后再里面找到集成配置样例&#xff0c;这里只截一张&#xff0c;其他集成内容继续向下找 我的&#x…

There is no getter for property named ‘deleted‘

实体类在继承BaseEntity的时候,由于没填写deleted参数名导致mybatis报错 这时候要么改application.yml里的mybatis参数&#x1f447; 要么就将BaseEntity基类的delete上加个existfalse&#x1f447;(推荐)

【ctf.show】--- md5

ctf.show_web9 1.用 dirsearch 扫目录&#xff1a;python dirsearch.py -u 网址 -e php 发现 robots.txt 2.访问 robots.txt 文件 发现 index.phps 3.访问 index.phps 发现源码 <?php $flag""; $password$_POST[password]; if(strlen…

Ventoy装机

文章目录 Ventoy安装操作系统问题U盘无法识别问题BIOS设置图片 Ventoy安装操作系统问题 当前使用的m.2&#xff08;nvm&#xff09;可以使用在台式机上。 "verification failed sercury violation"这个问题似乎与使用Ventoy创建启动盘并在启用了Secure Boot&#x…

C++——vector类及其模拟实现

前言&#xff1a;前边我们进行的string类的方法及其模拟实现的讲解。这篇文章将继续进行C的另一个常用类——vector。 一.什么是vector vector和string一样&#xff0c;隶属于C中STL标准模板库中的一个自定义数据类型&#xff0c;实际上就是线性表。两者之间有着很多相似&…

从关键词到上下文:GPT 如何重新定义 SEO 策略

如何利用GPT技术革新SEO内容创建&#xff1f; 新的 SEO 格局 探索 SEO 的快速变化&#xff0c;重点关注从以关键字为中心的策略到更深入地了解用户意图和上下文的转变。 GPT 简介及其对内容创建、用户参与和搜索引擎优化 (SEO) 的革命性影响。 了解 GPT&#xff1a;技术范式转…

【数据结构刷题专题】—— 二分查找

二分查找 二分查找模板题&#xff1a;704. 二分查找 二分查找前提&#xff1a; 有序数组数组中无重复元素 左闭右闭&#xff1a; class Solution { public:int search(vector<int>& nums, int target) {int left 0;int right nums.size() - 1;while (left <…

网络爬虫框架Scrapy的入门使用

Scrapy的入门使用 Scrapy概述引擎&#xff08;Engine&#xff09;调度器&#xff08;Scheduler&#xff09;下载器&#xff08;Downloader&#xff09;SpiderItem Pipeline 基本使用安装scrapy创建项目定义Item数据模型对象创建爬虫(Spider)管道pipeline来保存数据启动爬虫 其他…

消费盲返:新型返利模式引领购物新潮流

消费盲返&#xff0c;一种引领潮流的新型消费返利模式&#xff0c;其核心在于&#xff1a;消费者在平台选购商品后&#xff0c;不仅能享受优惠价格&#xff0c;更有机会获得后续订单的部分利润作为额外奖励。这种创新的返利机制&#xff0c;既提升了消费者的购物体验&#xff0…

AUTOSAR关于内存栈的分层及描述

首先关于关于内存栈的分层&#xff1a;如下图所示&#xff0c;Nvm靠近RTE的&#xff1b;MemIf居中&#xff0c;EA和FEE被包含其中。 其次关于这三层的缩写&#xff1a;可以看到EEPROM的模拟和EEPROM的抽象层。 我们可以看到 大概的数据流&#xff1a; 和大致的结构分层作用&am…

使用argocd作为cd流程

一、前言 讲述关于argocd在cicd流程中的使用&#xff0c;ci这里使用gitlabjenkins流水线的方式&#xff0c;jenkins用于拉代码打包、构建镜像、变更yaml文件的镜像、推送变更的yaml文件到gitlab的gitops仓库中&#xff0c;最后再有argocd实现cd流程&#xff0c; 二、使用 关于…

​网络安全相关证书资料​——OSCP、CISP-PTE

网络安全相关证书有哪些&#xff1f;——就实战型看&#xff0c;OSCP、CISP-PTE &#xff08;国家注册渗透测试工程师&#xff09;最好。 网络安全相关证书有哪些&#xff1f; 网络安全相关证书有哪些呢&#xff1f;了解一下&#xff01; 1. CISP &#xff08;国家注册信息安全…

全国产数据采集卡定制,24位八通道以太网数据采集卡 labview 100K采样

XM702是一款以太网型高速数据采集卡&#xff0c;具有8通 道真差分输入&#xff0c;24位分辨率&#xff0c;单通道最高采样率100ksps八通 道同步共计800ksps、精密前置增益放大、集成IEPE/ICP硬件 支持的特点。本产品采用了多个高精度24位ADC单元及配合本 公司多年积累开发的前置…

性能小钢炮,美学艺术品!佰维PD2000移动固态硬盘新品即将上市

开春时节&#xff0c;想必大家已经准备踏上春游旅程。每到一个景点&#xff0c;或许总想记录精彩的瞬间&#xff0c;留存优美的景色。但由于手机存储容量有限&#xff0c;很多时候我们拍不了多少照片&#xff0c;手机就开始提示存储空间不足。而iCloud昂贵的云存储价格也让人望…

2023年后端面试总结

备注&#xff1a;这篇文章是我在2023年年初在自己的网站上写的&#xff0c;最近在迁移技术文章&#xff0c;我感觉这个也是和咱程序员相关&#xff0c;所以今天就决定把它迁移过来。 .......................................................................分割线..........…

机器学习概论—增强学习

机器学习概论—增强学习 强化学习(Reinforcement Learning, RL)或者说是增强学习,是机器学习的一个领域,旨在使智能体通过与环境的交互学习如何做出决策,它是关于在特定情况下采取适当的行动来最大化奖励。它被各种软件和机器用来寻找在特定情况下应采取的最佳行为或路径…

【单调栈】力扣84.柱状图中最大的矩形

上篇文章我们介绍了使用 无重复值 单调栈代码解决 含有重复值 的问题&#xff0c;在文章的最后&#xff0c;留下了一道考察相同思想的题目&#xff0c;今天我们来看看如何套路解决该题。 &#xff08;还没看过前几篇介绍的小伙伴赶快关注&#xff0c;在 「单调栈」 集合里查看…