Android原生自定义车牌输入法 附带两种实现思路以及源码 EasyVehicleKeyBoard

1,857 阅读7分钟

前言

花费了几天的时间研究了一下Android 车牌输入法 也在网上找了各种第三方库 但是基本都很老了 或者无法满足开发的需求于是决定自己上手撸 一共写了两个版本

版本一

第一个版本是根据 github 上的 VehicleEditText 来改的 实现原理是 继承自系统的 KeyBoard 这个官方的自定义的View 通过xml描述按钮的相关参数最后在ondraw里面用自定义View的方式用Canvas绘制键盘 在这个库的基础之上我添加了些许功能 比如屏蔽了IO 这两个按钮 (PS:车牌里面是不包含这两个按钮的) 修复了这个库按键的drawable不生效的问题 效果就如同这个库所贴的图一样 这里再贴一遍我画的 img.png 但是呢 新的需求来了!老大说了 想要按钮按下有气泡 众所周知 Android 原生的弹窗里面是不能再弹窗的 否则会直接崩溃 哎 苦逼的码农只能想办法来实现这种 "合理" 的需求 !

那么只能从头开始写弹窗的部分 于是在这个库的基础上 重新用自定义的弹窗 通过xml的方式来加载这个车牌输入法View 于是气泡实现了

image.png 气泡的背景也是白的 当时没调好..不够明显

你以为到这里就完了 NO!!! 这个气泡也是用的官方的写法来实现的 官方的东西虽然稳 但是是真的难用啊 改个气泡的样式 可头疼死我了 改个宽度还要去看官方的代码海 看来看去最后还是没有达到我想要的效果

而且KeyBoard这玩意

img_5.png 官方也说了 已经在API29 弃用了 具体的替代方案居然是让我们开发者自己来复制这些代码到自己的项目里面 ! 于是乎 我就想这么费劲 最后还是要弃用 我不如自己用xml画一个 然后一段时间后... 第二版就出来了

先上第二版的效果图

img_3.png 整体效果 字母数字视图 车牌永远不会包含I 和 O 这两个字母 这里已经处理好了 I O 不可点击

img_2.png 预览小气泡 效果

第二版实现的思路

其实很简单 就是跟平时画UI一样 弹性布局 套几层线性布局就OK了 想在一个弹窗里面显示 不同的界面 其实就是通过设置View的Visibility来实现的 往往最简单的反而是最可靠的 官方的按键气泡在按下的一瞬间出来 其实也是一个道理 同样第二版气泡我也是这样实现的 详情见 The fucking source code 这里就不贴代码了

思路虽然是很简单 但是写的时候还是发现几个问题 怎么去绑定按钮的点击监听 这么多个按钮 如果一个个去写setOnClickListener那也太呆了

最后思来想去 还是用了一种奇怪的方法

            //emm 去查一遍子view找到所有的按钮...
            for (index in 1..36) {
                val id = rootLayout.resources.getIdentifier("tv_$index", "id", context.packageName)
                val button = rootLayout.findViewById<ShapeTextView>(id)
                if (button != null) {
                    //直接过滤 I O 两个键 车牌里面不会包含这两个按键
                    if (button.text != "I" && button.text != "O") {
                        mLettersButtonList.add(button)
                        bindKeyListener(button)
                    }
                } else {
                    LogUtils.e("无法找到对应的按钮Id:tv_$index 请检查xml文档id是否拼写错误")
                    throw RuntimeException("无法找到对应的按钮Id:tv_$index 请检查xml文档id是否拼写错误")
                }
            }

拼接按钮的id 然后 定死按钮的个数for循环一个个去找... 这里确实有点low 但是也没有更好的思路了

其次就是预览小气泡的问题 预览气泡要在按下的一瞬间弹出来 如果用show() / dismiss()绝对没有这么快 毕竟单身这么多年的手速了 快到系统都反应不过来 不卖官司了 其实就是上面已经讲过的 先在弹窗初始化的时候把预览的小气泡也初始化了 然后先直接就show出来 再把整个预览小气泡的根布局 visibility 设置为 INVISIBLE 这样气泡一直在那里只是看不到了 然后点击另外一个按钮的时候去刷新这个气泡的坐标 最终就完美实现 秒弹预览气泡的需求了

在实现过程中也发现了几个问题 PopupWindow的这个定位锚点貌似我怎么设置都不太起作用 所以在按下新按键 刷新预览小气泡的时候 这个坐标有点没弄明白 不过最后也是通过手动调 偏移量 成功把气泡定在了 按钮正上方

 //update之后这个锚点的定位点又是哪呢???
        //实在找不到 定位的点了 用dp去调吧... 我太菜了 哎
        mPreviewPopup.update(
            leftToRootView - ConvertUtils.dp2px(10f) + popupWindowOffsetY_px,
            topToRootView + ConvertUtils.dp2px(5f) + popupWindowOffsetX_px,
            -1,
            -1
        )

还有一个硬伤 也是关于这个预览小气泡弹窗的 当我们按下按钮A的时候显示气泡 此时不松手 移动到另外一个按钮的时候 气泡需要刷新文字 同时刷新坐标到新的按钮正上方 不过因为是采取手写xml的方式 加载一堆按钮 每个按钮之间都不存在关联 无法通过代码去监听滑动的事件

        tv.setOnTouchListener { v, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    showPreView(tv)
                }
                MotionEvent.ACTION_UP -> {
                    hidePreView()
                }
                //move无法实现... 果然还是要像官方的那样做一个整体的 自定义view才是最完美 这种写法目前无法实现滑动 气泡
//                MotionEvent.ACTION_MOVE -> {
//                    showPreView(tv)
//                }

            }
            //仅监听事件 不拦截触摸事件 让view的onTouchEvent继续执行 从而响应OnClickListener
            false
        }

正是因为这个硬伤 所以第二种写法没有官方的滑动的效果 不得不说 官方的东西还是牛的对于android 整套东西都是精通的一个层次 从一开始就规避了这样的问题 就没有我这种纯属胡闹似的写法了

还有一点 本来是想写成 静态类的 在kotlin里面 把类前面套个object关键字就行了 但是测试过 这样会导致切换Fragment> > 的时候 弹窗失效了 调试查到弹窗状态一直卡在dismissing了 不知道为什么 所以没有使用静态类 可能会导致弹窗有那么一点点慢..

项目缺陷 你需要自行规避这些问题

因为隐藏了系统的输入法 Xpopup这个弹窗库本来可以设置弹窗在输入法上面的 但是我这个弹窗就是个输入法 而且禁用了系统的输入法 对于输入法遮挡输入框的问题 暂时无从下手

第二版 预览小气泡 无法根据按钮变换丝滑的移动位置 也就是上面说的硬伤

没有写成静态类 可能会有一点点慢

如果有大佬能修复 请在文末评论区回复一下

代码使用说明

本来想把这个代码写成一个库的 因为都是原生的东西 改起来很简单 也没啥要改的 没必要写什么api了 直接下载导入到你自己的项目作为一个模块 项目一共就两个类 也方便改.. 而且我确实不怎么会写api..

用法

in Activity

fun bind(activity: AppCompatActivity, et: EditText) 

in Fragment

fun bind(fragment: Fragment, et: EditText)

如果你想设置一下弹窗的宽高 或者 偏移量 请在bind前调用

fun setPreViewOffset_px(offsetX_px: Int, offsetY_px: Int)
fun setPreViewOffset_dp(offsetX_dp: Float, offsetY_dp: Float)
fun setPreViewWidthHeight_dp(width_dp: Float, height_dp: Float)
fun setPreViewWidthHeight_px(width_px: Int, height_px: Int)

没有解绑函数 使用了Lifecycle组件自动解绑

如果需要修改布局 直接作为一个模块导入到项目里面 想怎么改就怎么改了

说到最后

不过最终而言 第二种写法的效果 和自定义的灵活度相对官方来说 自己写的东西还是更灵活一些 比如这个气泡 想怎么写就怎么写 而且代码很简单 基本就是新手看了都懂的 没有什么上手成本 也不需要写过多的xml 配置样式也是有手就行的

代码用了一些优秀的第三方库 这里就不细说第三方库的用法了 具体请参见代码下远程依赖所注释的网址

最后的最后是源码

第一个作品 就起个名字吧就叫 EasyVehicleKeyBoard

国内地址

EasyVehicleKeyBoard apk体验

github

EasyVehicleKeyBoard apk

以国内仓库为主吧 github我老push不上去呢