欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android开发之如何自定义数字键盘详解

程序员文章站 2023-12-19 20:10:34
前言 这篇文章是介绍android中自定义键盘的一些套路,通过定义一个数字键盘为例,本篇的文章语言是基于kotlin实现的,如果还没有用或者不熟悉该语言的同学,可以自己补...

前言

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

Android开发之如何自定义数字键盘详解
效果图

github:源码传送门

本地下载:源码传送门

加载键盘存储键属性的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中有下面常用属性

1、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;

2、android:keyoutputtext 设置该值后,当点击key时回调ontext(text: charsequence?)会执行,参数就是我们设置的值。

3、android:keyicon设置key上显示的icon

4、android:keylabel 键上显示的值

5、android:isrepeatable 当长按时是否重复该键设置的操作,例如我们删除键可以设置此属性。

6、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.xtop=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。该接口回调方法如下

1、swipeup()

当用户快速将手指从下向上移动时调用

2、swipedown 方法

当用户快速将手指从上向下移动时调用

3、swipeleft

当用户快速将手指从右向左移动时调用

4、swiperight()

当用户快速将手指从左向右移动时调用

5、onpress(primarycode: int)

点击key时调用primarycode时对应key的codes值

6、onrelease(primarycode: int)

释放key时调用

7、onkey(primarycode: int, keycodes: intarray?)

我选择在此对edittext的编辑,onpress之后调用的。

8、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
 }

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。

上一篇:

下一篇: