【入门到精通】鸿蒙next开发:基于TextInput的常见自定义效果解决方案

往期鸿蒙5.0全套实战文章必看:(文中附带全栈鸿蒙5.0学习资料)


基于TextInput的常见自定义效果解决方案

场景描述

场景一:英文、数字及特殊字符和中文字符自动间隔一个空格的距离场景二:当输入的内容(纯数字可能为小数)大于999时,禁止输入,注意最后一位光标不要闪烁场景三:限制两位小数,不能以小数点开头的场景场景四:TextInput如何在输入、粘贴、剪切动作之后,内容显示之前控制显示内容场景五:电话号码格式化时,删除号码中间的空格为删除前一位数字场景六:textInput防抖与节流

场景一:TextInput实现输入框热搜词自动滚动及右侧文字内容颜色渐变

效果图:

4.gif

方案:

1、封装一个正则函数,英文数字及特殊字符用[a-zA-Z0-9~!@#$%^&*()\-_=+\[\]{}\\|;:'",<.>/?],中文用[\u4e00-\u9fa5], 在正则中$1 和 $2 是反向引用,它们用于引用正则表达式中捕获组匹配到的文本,捕获组是用圆括号 () , 每个捕获组匹配到的文本可以在替换字符串中通过 $1 和 $2 等来引用。

// 场景1 英文、数字及特殊字符和中文字符自动间隔空格的距离 
formatStringOne(value: string) { 
  // 中文与英文或者特殊字符空格 /g全局 
  // 每个捕获组匹配到的文本可以在替换字符串中通过 $1、$2 等来引用。 
  let pattern = /([\u4e00-\u9fa5])([a-zA-Z0-9`~!@#$%^&*()\-_=+\[\]{}\\|;:'",<.>/?])/g; 
  let result = value.replace(pattern, '$1 $2'); 
  let pattern1 = /([a-zA-Z0-9`~!@#$%^&*()\-_=+\[\]{}\\|;:'",<.>/?])([\u4e00-\u9fa5])/g; 
  let result1 = result.replace(pattern1, '$1 $2'); 
  return result1; 
}

2、在onChange回调中调用封装的正则匹配函数formatStringOne。

TextInput({ text: this.textOne, placeholder: 'input your word ...', controller: this.controllerOne }) 
  .onChange((value:string)=>{ 
    this.textOne = this.formatStringOne(value) 
  })

场景二:当输入的内容(纯数字可能为小数)大于999时,禁止输入,注意最后一位光标不要闪烁

效果图:

5.gif

方案:

1、onWillInsert是在将要输入时调用的回调。在返回true时,表示正常插入,返回false时,表示不插入。在预上屏操作时,该回调不触发。注意:仅支持系统输入法输入的场景。如果在onChange中判断会先显示最后一位输入的字符,然后判断不合法后删除,时机有些滞后,这里需要在字符显示之前拦截掉,所以考虑用onWillInsert。

2、在onWillInsert中对输入之前对内容进行判断,当输入框内容大于等于999且将要插入的索引大于等于3,或者输入框内容长度等于3且内容不包含'.'将要输入的内容也非'.',则返回false,不让输入。

TextInput({ text: this.textTwo, placeholder: 'input your word...', controller: this.controllerTwo }) 
  .type(InputType.NUMBER_DECIMAL)// 带小数点的数字输入模式。 
  .onChange((value: string) => { 
    if (value === '.') { 
      this.textTwo = '0.'; 
    } else { 
      this.textTwo = value; 
    } 
  }) 
  .onWillInsert((info: InsertValue) => { 
    if ((Number(this.textTwo) >= 999 && info.insertOffset >= 3) || 
      this.textTwo.length === 3 && info.insertValue !== '.' && this.textTwo.includes('.')===false) { 
      return false 
    } 
    return true 
  })

场景三:限制两位小数,不能以小数点开头的场景

效果图:

6.gif

方案:

1、限制输入内容只能是两位小数,如在onChange中用正则判断输入内容是否符合要求,会导致光标有闪烁现象,根因同场景2因为onChange中判断时机滞后,这种场景需要在输入之前进行拦截

2、在onChange中补齐0,如果直接输入.会在.之前补0.

3、在onWillInsert中判断如果已输入的字符包含.且将要输入的字符是. 或如已输入的内容满足两位小数条件,返回false不让输入,否则返回true

TextInput({ text: this.textThree, placeholder: 'input your word...', controller: this.controllerThree }) 
  .type(InputType.NUMBER_DECIMAL) 
  .onChange((value: string) => { 
    // 当直接输入‘.’的时候自动补齐‘0.’,否则正常显示输入的字符 
    if (value === '.') { 
      this.textThree = '0.'; 
    } else { 
      this.textThree = value; 
    } 
  }) 
    // 在返回true时,表示正常插入,返回false时,表示不插入 
  .onWillInsert((info: InsertValue) => { 
    // ^ 和 $ 分别指字符串的开始与结束,以数字开始,以小数点+两位数字结束 
    let reg = /^\d+\.\d{2}$/ 
    // 如果输入的文本包含'.',且将要输入的字符是'.' 或者输入框的内容已经满足两位小数,返回false不让输入 
    if (this.textThree.includes('.') && info.insertValue === '.' || reg.test(this.textThree)) { 
      return false 
    } 
    return true 
  })

场景四:TextInput如何在输入、粘贴、剪切动作之后,内容显示之前控制显示内容

效果图:

7.gif

方案:

1、封装一个格式化字符串的函数,限制输入字符串数量,且每隔固定字符空一格。

formatStringFour(value: string) { 
  let result = ''; 
  let num = 0; // 计算每隔固定字符空一格 
  let message = ''; // 每固定字符为一段 
  for(let i=0;i<value.length;i++){ 
    if(value[i] != ' '){ 
      message += value[i] 
      num += 1 
    } 
    if(num == CHUNKSIZE){ 
      result += message + ' ' 
      num = 0 
      message = '' 
    } 
  } 
  result += message 
  result = result.slice(0, LIMIT); 
  return result; 
}

2、通过记录下次光标设置的位置,设置光标的位置。

setCaret(controller:TextInputController) { 
  if (this.nextCaret != -1) { 
    console.log("to keep caret position right, change caret to", this.nextCaret) 
    controller.caretPosition(this.nextCaret) 
    this.nextCaret = -1 
  } 
}

3、在onWillInsert中对输入框字符数量做限制,当大于输入限制的字符数或者输入内容为空时不让输入,通过onTextSelectionChange中记录光标的位置,然后在onPaste回调处理光标前后以及粘贴的文字,并通过格式化字符串函数过滤输入的内容。

4、封装一个记录光标位置的函数,传入onChange改变前去空格的字符以及改变后去空格的字符,通过判断改变前长度小于改变后长度为插入场景,反之为删除的场景。在onChange中判断如果数字已经格式化完成了,在这个时候通过setCaret改变光标位置不会被重置掉,否则用calcCaretPosition记录下次光标位置,等格式化完成后设置。

calcCaretPosition(text: string, nextText: string) { 
  let befNumberNoSpace: string = this.removeSpace(text) 
  this.actualCh = 0 
  if (befNumberNoSpace.length < this.numberNoSpace.length) { // 插入场景 
    for (let i = 0; i < this.selectionStart; i++) { 
      if (text[i] != ' ') { 
        this.actualCh += 1 
      } 
    } 
    this.actualCh += this.numberNoSpace.length - befNumberNoSpace.length 
    for (let i = 0; i < nextText.length; i++) { 
      if (nextText[i] != ' ') { 
        this.actualCh -= 1 
        if (this.actualCh <= 0) { 
          this.nextCaret = i + 1 
          break; 
        } 
      } 
    } 
  } else if (befNumberNoSpace.length > this.numberNoSpace.length) { // 删除场景 
    if (this.selectionStart === text.length) { 
      console.log("Caret at last, no need to change") 
    } else if (this.selectionStart === this.selectionEnd) { 
      // 按键盘上回退键一个一个删的情况 
      for (let i = this.selectionStart; i < text.length; i++) { 
        if (text[i] != ' ') { 
          this.actualCh += 1 
        } 
      } 
      for (let i = nextText.length - 1; i >= 0; i--) { 
        if (nextText[i] != ' ') { 
          this.actualCh -= 1 
          if (this.actualCh <= 0) { 
            this.nextCaret = i 
            break; 
          } 
        } 
      } 
    } else { 
      // 剪切/手柄选择 一次删多个字符 
      this.nextCaret = this.selectionStart // 保持光标位置 
      console.info(`chenbilian nextCaret:${this.nextCaret}`) 
    } 
  } 
}
TextInput({ text: this.textFour, placeholder: 'input your word...', controller: this.controllerFour }) 
  .onWillInsert((info: InsertValue) => { 
    console.log(`onWillInsert:${info.insertValue} ${info.insertOffset}`) 
    // 当输入的字符串数量大于限制或者输入空格,返回false 
    if (info.insertOffset >= LIMIT || info.insertValue === ' ' || this.textFour.length >= LIMIT) { 
      return false 
    } 
    return true; 
  }) 
  .onChange((value: string) => { 
    this.numberNoSpace = this.removeSpace(value); 
    let nextText: string = "" 
    nextText = this.formatStringFour(value) 
    if (this.textFour === nextText && nextText === value) { 
      this.setCaret(this.controllerFour) 
    } else { 
      this.calcCaretPosition(this.textFour, nextText) 
    } 
    this.textFour = nextText 
  }) 
  .onPaste((value: string, event: PasteEvent) => { 
    // 获取粘贴板内容 
    let clipboardText = value; 
    // 记录当前光标前后的内容 
    let textBeforeCursor = this.textFour.slice(0, this.selectionStart); 
    let textAfterCursor = this.textFour.slice(this.selectionEnd); 
    // 将粘贴的内容插入光标中间 
    this.textFour = textBeforeCursor + clipboardText + textAfterCursor; 
    // 通过条件过滤文本 
    this.textFour = this.formatStringFour(this.textFour) 
  }) 
  .onTextSelectionChange((selectionStart, selectionEnd) => { 
    // 记录光标位置 
    this.selectionStart = selectionStart 
    this.selectionEnd = selectionEnd 
  })

场景五:电话号码格式化时,删除号码中间的空格为删除前一位数字

方案:

1、相对于官方文档示例6,主要是在onChange中对删除的场景做判断,如果上次输入框字符长度大于当前的字符长度判断为删除,反之为插入。

2、通过判断删除的字符串索引8-9或者3-4的地方是否为空字符,如果不是空字符可以判断是删除了空格,再通过removeCharAt函数对输入框字符串做截取,将空格前一位数字删掉。

3、在onChange中判断如果数字已经格式化完成了,在这个时候通过setCaret改变光标位置不会被重置掉,否则用calcCaretPosition记录下次光标位置,等格式化完成后设置。

//删除指定位置的字符 
removeCharAt(str:string, index:number) { 
  if (index >= 0 && index < str.length) { 
    return str.slice(0, index) + str.slice(index + 1); 
  } 
  return str; // 如果索引超出范围,返回原字符串 
}
TextInput({...}) 
  .onChange((value: 
this.teleNumberNoSpace = this.removeSpace(value); 
console.log(':::number',value.substring(8, 9)) 
//判断 删除场景 去空字符串小于等于11 变动的字符串小于13 判断是否有特殊字符 
if (this.textFive.length > value.length && this.teleNumberNoSpace.length <= 11 
  && value.length<NUM_TEXT_MAXSIZE_LENGTH && this.checkNeedNumberSpace(value)) { 
  //判断是否删除了空白字符,用空格分隔字符串 
  let parts = value.split(' ') 
  console.info(`parts:${parts.length}`) 
  if (parts.length - 1 != 2) { 
    //删除第二个空白字符场景 
    if (value.substring(8, 9) != ' ') { 
      //012 4567 9012 
      value = this.removeCharAt(value,7) 
    } 
    //删除第一个空白字符场景 
    if (value.substring(3, 4) != ' ') { 
      //012 4567 9012 
      value = this.removeCharAt(value,2) 
    } 
  } 
} 
//更新去空后的电话号码 
this.teleNumberNoSpace = this.removeSpace(value); 
let nextText: string = "" 
if (this.teleNumberNoSpace.length > NUM_TEXT_MAXSIZE_LENGTH - 2) { 
  nextText = this.teleNumberNoSpace 
} else if (this.checkNeedNumberSpace(value)) { 
  if (this.teleNumberNoSpace.length <= 3) { 
    nextText = this.teleNumberNoSpace 
  } else { 
    let split1: string = this.teleNumberNoSpace.substring(0, 3) 
    let split2: string = this.teleNumberNoSpace.substring(3) 
    nextText = split1 + ' ' + split2 
    if (this.teleNumberNoSpace.length > 7) { 
      split2 = this.teleNumberNoSpace.substring(3, 7) 
      let split3: string = this.teleNumberNoSpace.substring(7) 
      nextText = split1 + ' ' + split2 + ' ' + split3 
    } 
  } 
} else { 
  nextText = value 
} 
console.log("onChange Triggered:" + this.textFive + "|" + nextText + "|" + value) 
if (this.textFive === nextText && nextText === value) { 
  // 此时说明数字已经格式化完成了 在这个时候改变光标位置不会被重置掉 
  console.info(`chenbilian nextCaret1:${this.nextCaret}`) 
  this.setCaret(this.controllerFive) 
} else { 
  this.calcCaretPosition(this.textFive,nextText) 
} 
this.textFive = nextText 
})

场景六:textInput防抖与节流

8.png

方案:

1、防抖:规定事件内只触发一次,在事件触发后等待一定的延迟时间,如果在这段延迟时间内事件再次被触发,则重新开始计时封装一个输入框的防抖函数,当频繁触发某个事件时会清空计时器重新计时,这样可以达到防抖的效果,我们可以将需要请求的数据放在debouncedChangeValue 回调中处理。

debouncedChangeValue = this.debounce(() => { 
  // 处理文本变化的逻辑,如请求搜索数据   300ms 的防抖时间 
  console.log('debouncedChangeValue:', this.textSix); 
}, 300); 
// 防抖函数 
private debounce(func: () => void, delay: number) { 
  let timer: number | null = null; // 定时器 
  return () => { 
    if (timer !== null) { 
      clearTimeout(timer); 
    } 
    timer = setTimeout(() => { 
      func(); 
    }, delay); 
  }; 
}
TextInput({ text: this.textSix, placeholder: 'input your word...', controller: this.controllerSix }) 
  .onChange((value: string)=>{ 
    this.textSix = value 
    // 在debouncedOnChange中处理发送请求等逻辑 
    this.debouncedChangeValue() 
  })

2、节流:当持续触发事件时,保证一定时间段内只调用一次事件处理函数,防抖节流函数可以根据具体业务需求使用。

throttleChangeValue = this.throttle(() => { 
  // 处理文本变化的逻辑,如请求搜索数据   1000ms 的节流时间 
  console.info(`ChangeValue:${this.changeValue}`) 
}, 1000); 
// 节流函数 
private throttle(fn: () => void, delay:number) { 
  let timer : number | null = null; // 定时器 
  let lastTime = 0 // 上一次执行时间 
  return () => { 
    const nowTime =Number(new Date()) // 获取当前时间 
    const remainingTime = delay-(nowTime-lastTime) // 剩余时间 
    console.info(`chen---nowTime:${nowTime}---remainingTime:${remainingTime}`) 
    if(remainingTime <= 0){ // 如果剩余时间小于等于0 
      clearTimeout(timer) // 清除定时器 
      lastTime = nowTime // 更新上次执行时间 
      fn() 
    }else if(!timer){ // 如果定时器为null 
      timer = setTimeout(()=>{ // 创建新的定时器 
        lastTime = Number(new Date()) // 更新上一次执行时间 
        timer = null // 清空定时器变量 
        fn() 
      },delay) 
    } 
  } 
}

FAQ

1、关于textInput禁用拍照输入功能

textinput无法禁止拍摄输入,这是键盘自带功能,但是可以自定义键盘取消拍摄输入:支持开发者定制系统菜单选项,通过editmenuoptions接口,可以把菜单中的拍摄输入屏蔽掉editMenuOptions。

2、关于textInput禁止粘贴。

修改onPaste中的相关实现逻辑,调用event.preventDefault!()可实现禁止粘贴。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值