鸿蒙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 = currentContent
    if (this.currentInputBoxContent.length != 0) {
      this.searchButtonText = '搜索'
      this.showThinkWord = true
      this.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 = true
           existIndex = index
         }
    });

    //本次搜索关键词在历史搜索词列表中处于第一个位置,因此不做任何额外处理
    //NOTE:真实场景中,这里除了重置状态,应该发起网络请求
    if(existIndex == 0){
      console.log('不需要刷新页面')
      this.currentInputBoxContent = ''
      this.searchButtonText = '取消'
      this.showThinkWord = false
      return
    }

    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 = 0

    Test.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 = 0
  private 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 = -1

      let 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) / 2

    dataPreferences.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 = 1

  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 = true
             existIndex = index
           }
      });

      if(existIndex == 0){
        console.log('不需要刷新页面')
        this.currentInputBoxContent = ''
        this.searchButtonText = '取消'
        this.showThinkWord = false
        return
      }

      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 = 0

      Test.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 = currentContent
              if (this.currentInputBoxContent.length != 0) {
                this.searchButtonText = '搜索'
                this.showThinkWord = true
                this.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 number

                  if(this.currentHistoryHeight == 0){
                     this.currentHistoryHeight = newHeight
                  }

                  this.currentLineNumbs = newHeight / this.currentHistoryHeight

                  console.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: CustomDialogController
  title: string = ''
  @Link historyWords: Array<string>
  cancel: () => void
  confirm: () => void

  build() {
    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: DataChangeListener

  constructor(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: number
  public link: string
  public word: string

  constructor(index, word, link?) {
    this.index = index
    this.link = link
    this.word = word
  }
}

热词数据结构 HotWordModel.ets

export default class HotWordModel {
  public word: string //词语
  public wordColor: string //文字颜色
  public wordLink?: string //文字超链接

  constructor(word, wordColor, wordLink?) {
    this.word = word
    this.wordColor = wordColor
    this.wordLink = wordLink
  }
}

联想词数据结构 ThinkWordModel.ets

export default class ThinkWordModel {
   public normalColor: string
   public highLightColor: string
   public wordStart: string //词语
   public wordMid: string //文字颜色
   public wordEnd: string //文字超链接
   public highLightIndex: number

   constructor(normalColor: string, highLightColor: string, highLightIndex: number,wordStart?: string, wordMid?: string,
               wordEnd?: string) {
      this.normalColor = normalColor
      this.highLightColor = highLightColor
      this.highLightIndex = highLightIndex
      this.wordStart = wordStart
      this.wordMid = wordMid
      this.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层的原理
  • 各种开发调试工具
  • 智能设备开发
  • 分布式开发等等。

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32CubeIDE基础学习-USART串口通信实验(中断方式)

STM32CubeIDE基础学习-USART串口通信实验&#xff08;中断方式&#xff09; 文章目录 STM32CubeIDE基础学习-USART串口通信实验&#xff08;中断方式&#xff09;前言第1章 硬件介绍第2章 工程配置2.1 工程外设配置部分2.2 生成工程代码部分 第3章 代码编写第4章 实验现象总结 …

3D数据格式导出工具HOOPS Publish如何生成高质量3D PDF?

在当今数字化时代&#xff0c;从建筑设计到制造业&#xff0c;从医学领域到电子游戏开发&#xff0c;3D技术已经成为了不可或缺的一部分。在这个进程中&#xff0c;将3D模型导出为3D PDF格式具有重要的意义。同时&#xff0c;HOOPS Publish作为一个领先的解决方案&#xff0c;为…

Python算法学习

一、排序 排序算法是指将一组数据按照某种规则重新排列&#xff0c;使得数据呈现出递增或递减的顺序。常见的排序算法包括冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序等。 1.冒泡排序 解释&#xff1a; 冒泡排序通过不断交换相邻两个元素的位置&#xff0c;使…

仓库规划(plan)

明天就要考试了&#xff0c;但是我正处于一点都不想学的状态 高考前我也是这样的 逆天 代码如下&#xff1a; #include<vector> #include<cstdio> using namespace std; int n, m; struct Node{int id;vector<int> d;bool operator<(const Node &t…

LInux|命令行参数|环境变量

LInux|命令行参数|环境变量 命令行参数main的参数之argc&#xff0c;argv几个小知识<font color#0099ff size 5 face"黑体">1.子进程默认能看到并访问父进程的数据<font color#4b0082 size 5 face"黑体">2.命令行创建的程序父进程都是bash 环…

命名空间【C++】(超详细)

文章目录 命名空间的概念命名空间的定义命名空间定义的位置作用域每一个命名空间都是一个独立的域作用域符&#xff1a;&#xff1a; 编译器找一个变量/函数等的定义&#xff0c;寻找域的顺序为什么要有命名空间&#xff1f;1.解决库与程序员定义的同名的重定义问题2.解决程序员…

ESP32使用SPIFFS时提示:E (21) SPIFFS: mount failed, -10025

因为是首次使用SPIFFS系统&#xff0c;需要格式化分区 在初始化时加入如下代码&#xff1a; if (!SPIFFS.begin()){// 初始化失败时处理Serial.println("SPIFFS-An error occurred while mounting SPIFFS");// 格式化SPIFFS分区if (SPIFFS.format()){// 格式化成功S…

OSPF基本原理和概念

文章目录 背景知识OSPF协议概述&#xff1a;OSPF区域的表示OSPF 骨干区域 –区域0OSPF 非骨干区域 -非0区域OSPF的五种区域类型OSPF工作原理OSPF 的报文类型OSPF邻居表中的七个状态 总结 背景知识 一台路由设备如何获取其他网段的路由&#xff0c;并加入到路由表中 直连路由 …

中值定理错题本

1 2 一般要构造函数 3 4 5 6 ------------------------------ 7 8 9 10

论文笔记:Retrieval-Augmented Generation forAI-Generated Content: A Survey

北大202402的RAG综述 1 intro 1.1 AICG 近年来&#xff0c;人们对人工智能生成内容&#xff08;AIGC&#xff09;的兴趣激增。各种内容生成工具已经精心设计&#xff0c;用于生产各种模态下的多样化对象 文本&代码&#xff1a;大型语言模型&#xff08;LLM&#xff09;…

【GPT5进展】GPT-5将于今年年中发布

OpenAI即将发布的GPT-5代表了人工智能技术的一个重大进步&#xff0c;这一新一代模型预计将进一步扩大OpenAI在AI应用领域的影响力。以下是关于GPT-5的几个关键点&#xff0c;旨在清晰、简洁地向读者传达这一重要更新&#xff1a; 1. 性能和功能的实质性提升 GPT-5在性能上做…

c++使用类的一些注意事项

前言&#xff1a; 本篇内容为前面的补充&#xff0c;介绍了我们使用类时需要注意些什么以及一些编译器的优化&#xff0c;可能有些理解不到位或者错误&#xff0c;请斧正。 目录 前言&#xff1a; 1.再谈构造函数 2.&#xff08;c98&#xff09;隐式类型转换中的编译器的优…

LVS几种模式介绍

备注&#xff1a;这篇真的是水文&#xff0c;不看也罢。 LVS&#xff0c;linux virtual server&#xff0c;可提供IP网络层的负载均衡。 其主要模式主要有以下几种&#xff1a; LVS-NAT 主要通过网络地址转换&#xff0c;修改目的IP实现。Network Address Translation LVS-…

rtthread

创建线程 线程优先级 当Thread1中遇到高优先级的线程时&#xff0c;Thread会先被挂起&#xff0c;rt_thread_delay()延时一定时间&#xff0c;每延时一个tick&#xff0c;执行一次判断&#xff0c;是否超时&#xff0c;如果超时&#xff0c;则调用rt_timer_init()中的rt_thread…

先进电机技术 —— 何为轮毂电机?

一、轮毂电机 轮毂电机&#xff08;Hub Motor&#xff09;是一种将电动机集成到车轮内部&#xff0c;直接驱动车轮转动的电动车驱动技术。这种设计省去了传统的传动轴、差速器、半轴等机械传动部件&#xff0c;使得动力传输更为直接、高效。 轮毂电机的优点&#xff1a; 1. 结…

源支付V7开源版2.99,修复各种提示错误

源支付V7开源版2.99&#xff0c;修复各种提示错误 加密说明&#xff1a;200拿来的&#xff0c;只有8.1这个文件加密&#xff0c;其他文件无任何加密&#xff0c;已修复各种提示错误 测试其他开源版安装提示错误&#xff0c;有几个文件是加密的 注&#xff1a;开发不易&#…

Java8之接口默认方法

Java8之接口默认方法 一、介绍二、代码1、接口2、实现类3、测试代码4、效果 一、介绍 在Java8中&#xff0c;允许为接口方法提供一个默认的实现。必须用default修饰符标记这样一个方法。默认方法也可以调用其他方法 二、代码 1、接口 public interface PersonService {void…

自定义类型(二)结构体位段,联合体,枚举

这周一时兴起&#xff0c;想写两篇文章来拿个卷吧&#xff0c;今天也是又来写一篇博客了&#xff0c;也是该结束自定义类型的学习与巩固了。 常常会回顾努力的自己&#xff0c;所以要给自己的努力留下足迹。 为今天努力的自己打个卡&#xff0c;留个痕迹吧 2024.03.30 小闭…

数据库工具——DBeaver的安装及使用

目录 一、DBeaver介绍 1.定义 2.支持的数据库 3.支持的操作系统 4.特点 二、DBeaver安装及使用 1.服务启动 2.查看连接类型 3.演示连接Mysql数据库 4.连接配置 5.成功连接 6.远程控制 6.1新建数据库 6.2新建数据表 6.3添加字段列 6.4使用SQL编辑器进行编辑 一…

mac怎么删除python

mac 默认安装了python2&#xff1b;自己后面又安装了python3&#xff1b;为了方便&#xff0c;现在想将python3换成Anaconda3。 Anaconda是一个开源的Python发行版本&#xff0c;其包含了conda、Python等180多个科学包及其依赖项。 Python3安装之后&#xff0c;在系统中不同目…