移动端自定义输入框和键盘的实现。(借鉴antd mobile源码)

3,360 阅读3分钟

近期,大把时间花费在浪费生命的手机键盘中,最后无奈被各大机型的各种兼容性打败~

你以为我这样就输了吗? No No No !

来自产品方的需求:(验证码页面)

1. 拉起数字键盘

2. 只允许用户输入6位数字

哎吆喂,就2个需求嘛,so easy!开始我们的input的type为number,给它来个最大的可输入长度。盘它!你会发现不管是在安卓手机还是苹果手机,系统自带的输入法,用户装的输入法,各式各样的奇葩都在里面。再加上在各种浏览器里面也会有所不同,再加上样式展现的不同,与其折磨自己,不如去参考别人的实现。巧了,思路由此展开。

根据上面异样,给出了实现的答案。

1. 自己用div去模仿假的input,用li循环键盘。

2. 在安卓端用自己写的高仿的,在苹果端还是用自带的键盘。

以上解析你可能会有疑问?

  1. 自己模拟的input,可以在数字的中间删除数字,光标的位置可以改变吗?

  2. 为什么要在安卓端和苹果端写入的键盘要不一致?

第一个问题,我们针对这个没有做处理,如有需要,请查看笔者的文章,用div模仿的input,里面有思路如何改变光标。

第二个问题是因为苹果端有提供我们更加便捷的方式,利大于弊。点击验证码直接进入输入框,减少用户的操作。

开始步入正题,参考antd mobile的自定义键盘,开发我们自己的键盘。

自己高仿键盘的组件

import React from 'react';
import TouchFeedback from 'rmc-feedback'; // 点击数字高亮的插件
import IconSvg from './iconSvg' // 这个是x号的svg,大家可以替换自己的
import './style.scss'
const numArray = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '', '0']
interface KeyboardItemProps {
    children?: any,
    className?: string,
    onClick?: () => void
}
class KeyboardItem extends React.Component<KeyboardItemProps, any> {
    render() {
        const { children, onClick, className = '' } = this.props
        let value = (className == 'delete' ? className : children)
        return (
            <TouchFeedback activeClassName={`active`}>
                <li
                    onClick={e => onClick(e, value as string)}
                    className={`${children == '' ? 'hidden' : className}`}
                >
                    {children}
                </li>
            </TouchFeedback>
        )
    }
}
class CustomKeyboard extends React.Component<any, any> {
    // 每一个数字的点击事件
    onKeyboardClick = (e?, value?: string) => {
        e.nativeEvent.stopImmediatePropagation();
        this.props.onKeyboardClick(value)
    }
    // 每一个键盘的render
    renderKeyboardItem = (item: string, index: number) => {
        return (
            <KeyboardItem key={`item-${item}-${index}`} onClick={this.onKeyboardClick}>{item}</KeyboardItem>
        )
    }
    render() {
        const { className } = this.props
        return (
            <div className={`keyboard-box ${className}`}>
                <div className="custom-header">
                    <div className="complete" onClick={(e) => this.onKeyboardClick(e, 'complete')}>完成</div>
                </div>
                <ul className={`number-keyboard`}>
                    {
                        numArray.map((item, index) => this.renderKeyboardItem(item, index))
                    }
                    <KeyboardItem className="delete" key={`item-${10}`} onClick={this.onKeyboardClick}>
                        <i><IconSvg color="#3F434A;"/></i>
                    </KeyboardItem>
                </ul>
            </div>
        )
    }
}
export default CustomKeyboard

自己高仿的input,用伪类做光标。

import React from 'react'
import CustomKeyboard from '../CustomKeyboard'
import './style.scss'

interface NumberInputProps {
    placeholder?: string,
    editInput?: boolean,
    disabled?: boolean,
    value?: any,
    className?: string,
    maxLength?: number,
    fakeInputClassName?: string,
    children?: any,
    labelNumber?: number,
    extra?: any,
    onChange?: (value: string) => void,
    onBlur?: (value: string) => void,
    onExtraClick?: () => void,
}

class YzmInput extends React.Component<NumberInputProps, any>{
    constructor(props) {
        super(props)
        this.state = {
            focus: false, // 默认不聚焦
            value: props.value || '',
        }
    }
    fakeInput = null
    // 伪造的input的点击事件
    onFakeInputClick = () => {
        this.focus()
    }
    focus = () => {
        this.removeBlurListener()
        const { focus } = this.state
        if (!focus) {
            this.onInputFocus()
        }
        setTimeout(() => {
            this.addBlurListener()
        }, 50)
    }
    onChange = (value: any) => {
        this.setState({ value })
        this.props.onChange(value)
    }
    onInputFocus = () => {
        this.setState({
            focus: true
        })
    }
    onInputBlur = (value: string) => {
        const { focus } = this.state
        if (focus) {
            this.setState({
                focus: false
            })
            this.props.onBlur && this.props.onBlur(value)
        }
    }
    doBlur = (ev: MouseEvent) => {
        const { value } = this.state;
        if (ev.target !== this.fakeInput) {
            this.onInputBlur(value);
        }
    }
    // addBlurListener,removeBlurListener就是为了处理blur&focus事件
    addBlurListener = () => {
        document.addEventListener('click', this.doBlur, false);
      }
    removeBlurListener = () => {
        document.removeEventListener('click', this.doBlur, false);
    }
    // 是点击键盘的每一个dom的处理方式
    handleKeyboard = (keyboardVal: string) => {
        const { maxLength } = this.props
        const { value } = this.state
        const { onChange } = this
        let valueAfterChange
        if (keyboardVal === 'delete') {
            valueAfterChange = value.substring(0, value.length - 1)
            onChange(valueAfterChange)
        } else if (keyboardVal === 'complete') {
            valueAfterChange = value
            onChange(valueAfterChange)
            this.setState({
                focus: false
            })
        } else {
            if (maxLength !== undefined && maxLength >= 0 && (value + keyboardVal).length > maxLength) {
                valueAfterChange = (value + keyboardVal).substr(0, maxLength)
                onChange(valueAfterChange)
            } else {
                valueAfterChange = value + keyboardVal
                onChange(valueAfterChange)
            }
        }
    }
    render() {
        let { value, focus } = this.state
        const {
            placeholder = '输入验证码',
            editInput = true,
            disabled = false,
            className = '',
            fakeInputClassName = '',
            labelNumber = 1,
            children,
            extra,
            onExtraClick
        } = this.props
        const preventKeyboard = disabled || !editInput
        return (
            <>
                <div className={`${className} fake-input-box`}>
                    {children ? (<div style={{ width: 16 * labelNumber + 'px' }} className="label">{children}</div>) : ''}
                    <div className='fake-input-content' onClick={preventKeyboard ? () => { } : this.onFakeInputClick}>
                        {value == '' && (<div className="fake-input-placeholder">{placeholder}</div>)}
                        <div
                            className={`${fakeInputClassName} fake-input ${focus ? 'focus' : ''}`}
                            role="textbox"
                            aria-label={value || placeholder}
                            ref={el => this.fakeInput = el}
                        >
                            {value}
                        </div>
                    </div>
                    {extra && <div className="extra" dangerouslySetInnerHTML={{ __html: extra }} onClick={onExtraClick}></div>}
                </div>
                <CustomKeyboard className={focus ? '' : 'none'} onKeyboardClick={this.handleKeyboard} />
            </>
        )
    }
}
export default YzmInput

实际在页面中的使用。

    <div className="yzm-box">
        <BcNumberInput
            className="yzm-input"
            labelNumber={4}
            maxLength={6}
            onChange={(value) => change(value, flagTime)}
            placeholder="输入验证码"
            extra={`|<div id=${flagTime ? 'timer' : 'retrieve'}>${flagTime ? time + '秒后重发' : '重新获取'}</div>`}
            onExtraClick={flagTime ? () => {} : () => click(this.actionCountDown.bind(this))}
        >验证码</BcNumberInput>
    </div>

以上代码不足的地方还有很多。基于现在的实现已经满足我方这边的需求,后续还会在优化~

问题。(antd mobile的自定义键盘已经实现的很完美,可以参考下)

假设我的input在底部的位置,那么我键盘抬起的时候,是不是需要将滚动条往上移,让用户输入的同时可看见输入里面的内容。

还有我们的键盘是不是可以做成用户可配置,可配置键盘的内容,可配置键盘的头部。

等等一系列,还需要我们学习的地方,小伙伴们,Come on ! 有不足的地方,欢迎指正。