Android自定义数字键盘

作者: Code4Android | 来源:发表于2017-09-09 21:34 被阅读1915次

    好久没有写Android的文章了,有两三个月多了吧,刚开始搞微信小程序,后来又开搞ReactNative,现在又兴奋的开搞AI机器学习的东西,感觉挺有意思的,不过AI与其它的东西相比要难很多,需要补很多数学知识,不过我现在学的都还是皮毛,没啥深度,但是我会慢慢深入的,对AI的兴趣比较大,年轻人就要不断的折腾嘛,当然自己的老本行也要搞起啦,饭还是要吃的。

    好像说的有点多了,今天的这篇文章是介绍Android中自定义键盘的一些套路,通过定义一个数字键盘为例,本篇的文章语言是基于Kotlin实现的,如果还没有用或者不熟悉该语言的同学,可以自己补习,我之前也写过入门文章。

    效果图

    源码传送门

    加载键盘存储键属性的XML描述

    我们下面的介绍都是依靠上图的实现来展开的,首先是软键盘的布局,我们需要我们的res/xml目录下创建一个xml文件,根节点就是Keyboard,然后就是键盘的每一行Row,每一行中可以指定每一列,也就是具体的键Key,代码实现

    <?xml version="1.0" encoding="utf-8"?><!--
    isRepeatable:长按时是否重复这个操作
    -->
    <Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
        android:horizontalGap="1px"
        android:keyHeight="7%p"
        android:keyWidth="33.33%p"
        android:verticalGap="1px">
        <Row android:keyHeight="6%p">
            <Key
                android:codes="-4"
                android:keyIcon="@drawable/hidden"
                android:keyWidth="100%" />
        </Row>
        <Row>
            <Key
                android:codes="49"
                android:keyLabel="1" />
            <Key
                android:codes="50"
                android:keyLabel="2" />
            <Key
                android:codes="51"
                android:keyLabel="3" />
        </Row>
        <Row>
            <Key
                android:codes="52"
                android:keyLabel="4" />
            <Key
                android:codes="53"
                android:keyLabel="5" />
            <Key
                android:codes="54"
                android:keyLabel="6" />
        </Row>
        <Row>
            <Key
                android:codes="55"
                android:keyLabel="7" />
            <Key
                android:codes="56"
                android:keyLabel="8" />
            <Key
                android:codes="57"
                android:keyLabel="9" />
        </Row>
        <Row>
            <Key
                android:codes="46"
                android:keyLabel="." />
            <Key
                android:codes="48"
                android:keyLabel="0" />
            <Key
                android:codes="-5"
                android:isRepeatable="true"
                android:keyIcon="@drawable/delete" />
        </Row>
    </Keyboard>
    

    在Keyboard节点属性中,我们通过horizontalGap设置水平的间距,通过verticalGap设置垂直的间距,通过keyWidth设置每一个key的宽度,通过keyHeight设置。需要注意的地点是如果Keyboard ,Row和Key都可以指定宽高。通常我们可以指定在Keyboard 中设置每一个键的宽高就可以了。当然如果对特定行的宽高要有所调整,可以在Row 或者key上设置,例如我们示例图中展示的最上面的一行,它的宽度比其它行都低了一点,则我们在第一行设置了属性android:keyHeight="6%p"

    在每一个key中有下面常用属性

    • android:codes 官网介绍是说这个是该键的unicode 值或者逗号分隔值,当然我们也可以设置成我们想要的值,在源码中提供了几个特定的值
    //就不解释了,通过名字应该看得出来
        public static final int KEYCODE_SHIFT = -1;
        public static final int KEYCODE_MODE_CHANGE = -2;
        public static final int KEYCODE_CANCEL = -3;
        public static final int KEYCODE_DONE = -4;
        public static final int KEYCODE_DELETE = -5;
        public static final int KEYCODE_ALT = -6;
    
    • android:keyOutputText 设置该值后,当点击key时回调onText(text: CharSequence?)会执行,参数就是我们设置的值。
    • android:keyIcon设置key上显示的icon
    • android:keyLabel 键上显示的值
    • android:isRepeatable 当长按时是否重复该键设置的操作,例如我们删除键可以设置此属性。
    • android:keyEdgeFlags 该属性有两个值,分别是left,right,用与指定显示在最左还是最右,一般不用此属性。默认从左到右排列。
      还有其它属性,不在介绍,可以自己去查阅api

    自定义KeyboardView

    该类是用来渲染虚拟键盘的类,类中有一个接口OnKeyboardActionListener能检测按键和触摸动作,我们要自定义虚拟键盘,只需要继承该类并实现该监听接口即可,当然我这里并没有实现接口,我单独创建了一个工具类,用于将自定义键盘View和EditText关联,并设置接口监听,这些稍后介绍到再说,我们最主要关注的就是onDraw方法,它可以让我们自定义键盘的绘制,随心所欲的画我们想要的东西。当然,我们也可以不做任何实现,它默认的有一种绘制。

    class CustomKeyboardView : KeyboardView {
        private var mKeyBoard: Keyboard? = null
    
        constructor(context: Context, attrs: AttributeSet) : this(context, attrs,0) {}
    
        constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {
            //
        }
    
        override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            mKeyBoard = this.keyboard
            var keys: MutableList<Keyboard.Key>? = null
            if (mKeyBoard != null) {
                keys = mKeyBoard!!.keys
            }
            if (keys != null) {
                for (key in keys) {
                    //可以自定义自己的绘制(例如某个按钮绘制背景图片和文字,亦或者更改某个按钮颜色等)
                    if (key.codes[0] == -111) {//过滤指定某个键自定义绘制
                    }
                }
            }
        }
    }
    

    在上面的onDraw方法中,我们通过this.keyboard(即java的getKeyboard方法,是KeyboardView 中的方法)获取Keyboard对象,并通过mKeyBoard!!.keys获取键盘的Key对象,即每一个键对象,如果我们想自定义绘制,就可以自己实现绘制,当然也可以针对个人键绘制,例如键上字体颜色,背景等。例如我们针对Key的code是 -111的自定义一些绘制操作。

        if (key.codes[0] == -111) {//过滤指定某个键自定义绘制
              //绘制后,原来xml中的keyLabel以及keyIcon会被覆盖,如需显示文字
              //需要自己重新绘制,要后绘制文字,否则文字不显示
              drawBackground(R.drawable.bg_keyboardview1, canvas, key)
              drawTextOrIcon(canvas, key)
        }
    

    背景selector

    <selector
        xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:drawable="@color/btnpressed" android:state_pressed="true"/>
        <item android:drawable="@color/btnnormal"/>
    </selector>
    

    需要注意的是需要先绘制背景,再绘制文字或icon,否则文字或者icon就看不到了,相信你肯定知道为啥,真不知道的话那....

        //绘制背景
        fun drawBackground(drawableId: Int, canvas: Canvas, key: Keyboard.Key) {
            var drawable = resources.getDrawable(drawableId)
            var drawableState: IntArray = key.currentDrawableState
            if (key.codes[0] != 0) {
                drawable.state=drawableState
            }
            drawable.bounds = Rect(key.x, key.y, key.x + key.width, key.height + key.y)
            drawable.draw(canvas)
        }
    

    绘制背景前先通过key.currentDrawableState(java的getCurrentDrawableState() 方法,后面不在提了)获取当前的状态,然后设置到drawable,然后通过Rect指定绘制的区域。Rect参数分别是左上右下。key.x,key.对应的就是该key的左上角的坐标,则left=key.x, top=key.y, right=key.x+key.width, bottom=key.y+key.height然后调用 drawable.draw(canvas)开始绘制。

    绘制完成背景之后,我们开始绘制文字或者icon。

     //绘制文字或图标
        fun drawTextOrIcon(canvas: Canvas, key: Keyboard.Key) {
            var bounds = Rect()
            var paint = Paint()
            paint.color = Color.WHITE
            paint.isAntiAlias = true
            paint.textAlign = Paint.Align.CENTER
            paint.typeface = Typeface.DEFAULT
            if (key.label != null) {
                var label = key.label.toString()
                //为了将字体大小与默认绘制的Label字体大小相同,需要反射获取默认大小。然后在此处设置文字大小
                //还有一种取巧的方法在布局文件keyboardview中设置keyTextSize,labelTextSize
                var field = KeyboardView::class.java.getDeclaredField("mLabelTextSize")
                field.isAccessible = true
                var labelTextSize = field.get(this) as Int
                paint.textSize = labelTextSize.toFloat()
                paint.getTextBounds(label, 0, label.length, bounds)
                canvas.drawText(label, (key.x + key.width / 2).toFloat(), (key.y + key.height / 2 + bounds.height() / 2).toFloat(), paint)
            } else if (key.icon != null) {
                key.icon.bounds = Rect(key.x + (key.width - key.icon.intrinsicWidth) / 2, key.y + (key.height - key.icon.intrinsicHeight) / 2, key.x + (key.width - key.icon.intrinsicWidth) / 2 + key.icon.intrinsicWidth, key.y + (key.height - key.icon.intrinsicHeight) / 2 + key.icon.intrinsicHeight)
                key.icon.draw(canvas)
            }
        }
    
    

    通过上面的代码,我们做了下判断如果有label的时候就绘制文字,如果没有但是有icon就绘制icon,否则不做处理。在这里可以指定绘制文字的大小,颜色等。需要注意的一点是文字大小,为了和显示的其他默认绘制key的大小相同,需要获取KeyboardView中的mLabelTextSize或者mKeyTextSize,因为该变量没有提供暴露方法,所以需要我们反射操作。当然还有一种取巧的方法,我们可以在xml中指定字体大小,在此设置成相同大小。对于坐标区域的计算上面已经做了分析。

    布局使用

    <?xml version="1.0" encoding="utf-8"?><!--
    background:整个键盘的背景色
    keyBackground   :设置键的背景
    keyPreviewHeight:预览高度
    keyPreviewLayout   :设置预览布局
    keyPreviewOffset :设置反馈的垂直偏移量
    keyTextColor    :设置key标签文字颜色
    keyTextSize:设置key标签字体大小
    labelTextSize:设置带文本和图标的键上个的文本的小大
    
    -->
    <com.code4android.keyboard.CustomKeyboardView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/keyboard_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/keyborad_line_color"
        android:focusable="true"
        android:focusableInTouchMode="true"
        android:keyBackground="@drawable/bg_keyboardview"
        android:keyPreviewHeight="35dp"
        android:keyPreviewLayout="@layout/keyboard_key_preview"
        android:keyPreviewOffset="0dp"
        android:keyTextColor="#8a8a8a"
        android:keyTextSize="18sp"
        android:labelTextSize="18sp"
        android:paddingTop="0dp"
        android:shadowColor="#fff"
        android:shadowRadius="0.0" />
    
    

    我们创建了自定义的View之后,需要再创建上面layout供加载。keyBackground属性是设置Key的背景,一般我们可以设置一个selected选择器。keyPreviewHeight设置预览的高度,即我们点击时会有一个提示效果。keyPreviewLayout是我们预览的布局,它需要是一个TextView 。keyPreviewOffset是预览的偏移量,keyTextColor设置key字体颜色,shadowRadius我们一般设置为0,它表示字体的阴影,如果不设置0.看起来回模糊。

    创建工具类

    在工具类中创建了两个构造方法

    
        constructor(activity: Activity) : this(activity, true, false)
        /**
         * @param activity
         * @param isRandom  是否时随机键盘
         * @param mIsDecimal  是否支持小数输入
         */
        constructor(activity: Activity, isRandom: Boolean, isDecimal: Boolean) {
            mActivity = activity
            mIsRandom = isRandom
            mIsDecimal = isDecimal
            mKeyboard = Keyboard(mActivity, R.xml.keyboard)
            addViewToRoot()
        }
    
    //加载自定义的键盘layout
        private fun addViewToRoot() {
            mKeyBoardViewContainer = mActivity.layoutInflater.inflate(R.layout.keyboardview, null)
            //var frameLayout: FrameLayout = mActivity.window.decorView as FrameLayout//不要直接往DecorView(状态栏,内容,导航栏)中addView,如使用这个则最后显示布局不全(一部分内容在导航栏区域)
            var frameLayout: FrameLayout = mActivity.window.decorView.find(android.R.id.content)
            var lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT)
            lp.gravity = Gravity.BOTTOM
            frameLayout.addView(mKeyBoardViewContainer, lp)
            mKeyBoardView = mKeyBoardViewContainer.find(R.id.keyboard_view)
        }
    

    在构造方法中初始化Keyboard,以及布局文件,在代码中我们看到我们获取到DecorView中id为android.R.id.content的布局,该布局是FrameLayout 布局,我们创建的布局都是放在这个布局中了,对这方面不理解的可以看看我之前写的文章深入分析setContentView。为了让我们自定义的键盘显示在最下面,设置Gravity为BOTTOM,然后通过frameLayout.addView(mKeyBoardViewContainer, lp)添加到FrameLayout 中。

    除此之外,我们创建一个函数attachTo(EditText)将EditText与我们自定义的键盘绑定

    fun attachTo(editText: EditText) {
            //如果editText与上次设置的是同一个对象,并且键盘已经正在在显示,不再执行后续操作
            if (mEditText != null && mEditText == editText && mKeyBoardView.visibility == View.VISIBLE) return
            mEditText = editText
            Log.e(TAG, "attachTo")
            //根据焦点及点击监听,来显示或者隐藏键盘
            onFoucsChange()
            //隐藏系统键盘
            hideSystemSoftKeyboard()
            //显示自定义键盘
            showSoftKeyboard()
        }
    
        private fun onFoucsChange() {
            mEditText!!.setOnFocusChangeListener { v, hasFocus ->
                Log.e(TAG, "onFoucsChange:$hasFocus" + v)
                //如果获取焦点,并且当前键盘没有显示,则显示,并执行动画
                if (hasFocus && mKeyBoardView.visibility != View.VISIBLE) {
                    mKeyBoardView.visibility = View.VISIBLE
                    startAnimation(true)
                } else if (!hasFocus && mKeyBoardView.visibility == View.VISIBLE) {
                    //如果当前时失去较大,并且当前在键盘正在显示,则隐藏
                    mKeyBoardView.visibility = View.GONE
                    startAnimation(false)
                }
            }
    
            mEditText!!.setOnClickListener {
                Log.e(TAG, "setOnClickListener")
                //根据上面焦点的判断,如果已经获取到焦点,并且键盘隐藏。再次点击时,
                // 焦点改变函数不会回调,所以在此判断如果隐藏就显示
                if (mKeyBoardView.visibility == View.GONE) {
                    mKeyBoardView.visibility = View.VISIBLE
                    startAnimation(true)
                }
            }
        }
    
        private fun hideSystemSoftKeyboard() {
            //11版本开始需要反射setShowSoftInputOnFocus方法设置false,来隐藏系统软键盘
            if (Build.VERSION.SDK_INT > 10) {
                var clazz = EditText::class.java
                var setShowSoftInputOnFocus: Method? = null
                setShowSoftInputOnFocus = clazz.getMethod("setShowSoftInputOnFocus", Boolean::class.java)
                setShowSoftInputOnFocus.isAccessible = true
                setShowSoftInputOnFocus.invoke(mEditText, false)
            } else {
                mEditText!!.inputType = InputType.TYPE_NULL
            }
            var inputMethodManager = mActivity.applicationContext.inputMethodManager
            inputMethodManager.hideSoftInputFromWindow(mEditText!!.windowToken, 0)
        }
    
    private fun showSoftKeyboard() {
            if (mIsRandom) {
                //生成随机键盘
                generateRandomKey()
            } else {
                //有序键盘
                mKeyBoardView.keyboard = mKeyboard
            }
            mKeyBoardView.isEnabled = true
            //设置预览,如果设置false,则就不现实预览效果
            mKeyBoardView.isPreviewEnabled = true
            //设置可见
            mKeyBoardView.visibility = View.VISIBLE
            //指定键盘弹出动画
            startAnimation(true)
            //设置监听
            mKeyBoardView.setOnKeyboardActionListener(mOnKeyboardActionListener())
        }
    
        private fun generateRandomKey() {
            var keys = mKeyboard.keys
            var numberKeys = mutableListOf<Keyboard.Key>()
            //保存数字
            var nums = mutableListOf<Int>()
            //0的ASCII码是48,之后顺序加1
            for (key in keys) {
                //过滤数字键盘
                if (key.label != null && "0123456789".contains(key.label)) {
                    nums.add(Integer.parseInt(key.label.toString()))
                    numberKeys.add(key)
                }
            }
            var random = Random()
            var changeKey = 0//更改numberKeys对应的数值
            while (nums.size > 0) {
                var size = nums.size
                var randomNum = nums[random.nextInt(size)]
                var key = numberKeys[changeKey++]
                key.codes[0] = 48 + randomNum
                key.label = randomNum.toString()
                nums.remove(randomNum)
            }
            mKeyBoardView.keyboard = mKeyboard
        }
    

    具体的解释已在代码中体现。

    设置键盘监听

    在上面代码中我们看一句mKeyBoardView.setOnKeyboardActionListener(mOnKeyboardActionListener()),它就是设置键盘的监听。OnKeyboardActionListener接口是KeyboardView的内部类,我们在此设置监听可以指定在对应的回调种操作EditText。该接口回调方法如下

    • swipeUp()
      当用户快速将手指从下向上移动时调用
    • swipeDown 方法
      当用户快速将手指从上向下移动时调用
    • swipeLeft
      当用户快速将手指从右向左移动时调用
    • swipeRight()
      当用户快速将手指从左向右移动时调用
    • onPress(primaryCode: Int)
      点击key时调用primaryCode时对应key的codes值
    • onRelease(primaryCode: Int)
      释放key时调用
    • onKey(primaryCode: Int, keyCodes: IntArray?)
      我选择在此对EditText的编辑,onPress之后调用的。
    • onText(text: CharSequence?)
      设置keyOutputText时会会回调

    具体实现

     inner class mOnKeyboardActionListener : KeyboardView.OnKeyboardActionListener {
            override fun swipeRight() {
                Log.e(TAG, "swipeRight")
            }
    
            override fun onPress(primaryCode: Int) {
                Log.e(TAG, "onPress")
                //添加震动效果
                mActivity.applicationContext.vibrator.vibrate(50)
                ////指定隐藏(确定)删除不显示预览
                mKeyBoardView.isPreviewEnabled = !(primaryCode == Keyboard.KEYCODE_DONE || primaryCode == Keyboard.KEYCODE_DELETE)
            }
    
            override fun onRelease(primaryCode: Int) {
                Log.e(TAG, "onRelease")
            }
    
            override fun swipeLeft() {
                Log.e(TAG, "swipeLeft")
            }
    
            override fun swipeUp() {
                Log.e(TAG, "swipeUp")
            }
    
            override fun swipeDown() {
                Log.e(TAG, "swipeDown")
            }
    
            override fun onKey(primaryCode: Int, keyCodes: IntArray?) {
                Log.e(TAG, "onKey primaryCode:$primaryCode keyCodes:$keyCodes")
                if (mEditText == null) throw RuntimeException("The mEditText is null,Please call attachTo method")
    
                mEditText?.let {
                    var editable: Editable = it.text
                    var textString = editable.toString()
                    //获取光标位置
                    var start = it.selectionStart
                    when (primaryCode) {
                        //如果是删除键,editable有值并且光标大于0(即光标之前有内容),则删除
                        Keyboard.KEYCODE_DELETE -> {
                            if (!editable.isNullOrEmpty()) {
                                if (start > 0) {
                                    editable.delete(start - 1, start)
                                } else {
                                }
                            } else {
                            }
                        }
                        Keyboard.KEYCODE_DONE -> {
                            hideSoftKeyboard()
                            mOnOkClick?.let {
                                //点击确定时,写一个回调,如果你对有确定的需求
                                it.onOkClick()
                            }
                        }
                        else -> {
                            //   由于promaryCode是用的ASCII码,则直接转换字符即可,46是小数点
                            if (primaryCode != 46 ) {
                                //如果点击的是数字,不是小数点,则直接写入EditText,由于我codes使用的是ASCII码,
                                // 则可以直接转换为数字。当然可以你也可以获取label,或者根据你自己随便约定。
                                editable.insert(start, Character.toString(primaryCode.toChar()))
                            } else {
                                //如果点击的是逗号
                                if (mIsDecimal && primaryCode == 46) {
                                    if ("" == textString) {
                                        //如果点的是小数点,并且当前无内容,自动加0
                                        editable.insert(start, "0.")
                                    } else if (!textString.contains(".")) {
                                        //当前内容不含有小数点,并且光标在第一个位置,依然加0操作
                                        if (start == 0) {
                                            editable.insert(start, "0.")
                                        } else {
                                            editable.insert(start, ".")
                                        }
                                    } else {
                                        //如果是不允许小数输入,或者允许小数,但是已经有小数点,则不操作
                                    }
                                } else {
                                }
                            }
                        }
                    }
                }
            }
    
            override fun onText(text: CharSequence?) {
                Log.e(TAG, "onText:" + text.toString())
            }
    
        }
     fun hideSoftKeyboard(): Boolean {
            if (mEditText == null) return false
            var visibility = mKeyBoardView.visibility
            if (visibility == View.VISIBLE) {
                startAnimation(false)
                mKeyBoardView.visibility = View.GONE
                return true
            }
            return false
        }
    
        fun startAnimation(isIn: Boolean) {
            Log.e(TAG, "startAnimation")
            var anim: Animation
            if (isIn) {
                anim = AnimationUtils.loadAnimation(mActivity, R.anim.anim_bottom_in)
            } else {
                anim = AnimationUtils.loadAnimation(mActivity, R.anim.anim_bottom_out)
            }
            mKeyBoardViewContainer.startAnimation(anim)
        }
    

    当点击的是KEYCODE_DONE 时,调用hideSoftKeyboard函数隐藏键盘,并执行隐藏动画,动画的xml文件就不在贴出了。

    具体使用方式如下

            keyboardUtli = KeyBoardUtil(this@KeyBoardDemoActivity)
            et_keyboard.setOnTouchListener { v, event ->
                keyboardUtli?.attachTo(et_keyboard)
               //设置是否可以输入小数
                keyboardUtli?.mIsDecimal = true
                false
            }
            et_keyboard2.setOnTouchListener { v, event ->
                keyboardUtli?.attachTo(et_keyboard2)
                keyboardUtli?.mIsDecimal = false
                false
            }
    

    好了,到此,这篇文章也就结束了,如果有错误之处多多指正,毕竟我还不是一个大牛。哈哈哈,Have a wonderful day。

    相关文章

      网友评论

      本文标题:Android自定义数字键盘

      本文链接:https://www.haomeiwen.com/subject/mmmojxtx.html