Android自定義數字鍵盤

Code4Android發表於2019-03-04

好久沒有寫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。

相關文章