Android自定义View实现随机数验证码

2022-06-30 14:46:36
目录
前言效果自定义 View 分类步骤1.自定义属性2.添加构造方法3.在构造里获取自定义样式4.重写 onDraw 计算坐标绘制5.重写 onMeasure 测量宽高6.设置点击事件总结

前言

本文面向自定义 view 新手,但是希望你最好有一定的理论知识,或基础概念,有的地方可能会一笔带过并不会细讲,细讲篇幅就太长了。

本文仿写自鸿洋的自定义View>

效果

自定义>

简单介绍一下自定义 View 分类:

    组合控件,继承自已有的 layout ,比如 LinearLayout,然后通过 LayoutInflater 引入布局,然后处理相关事件,这种方式的好处在于,不需要过度关注 view 内部的绘制机制,而且扩展性也很强。继承自现有的系统控件,修改或扩展某些属性或方法即可,比如 AppCompatTextView继承自 view 或 viewgroup ,这种相对要复杂一些,因为我们要自己控制绘制流程,但是相对的,也有更大的想象空间。

    步骤

    先分析一下上图中的效果:

      带颜色的矩形背景居中的文本

      比较简单,老手稍微想一下就已经有思路了:

        1.自定义属性2.添加构造方法3.在构造里获取自定义样式4.重写 onDraw 计算坐标绘制5.重写 onMeasure 测量宽高6.设置点击事件

        先分析效果图,然后构思,随后不断的调整优化。

        1.自定义属性

        这一步也不一定非要写在前面,可能有些人觉得不一定就能事先知道会用到哪些属性,由于例子比较简单,暂且放在前面吧,看个人习惯。

        在 res/values/ 下建立一个 attrs.xml 文件>

        <?xml version="1.0" encoding="utf-8"?>
        <resources>
        
            <attr name="randomText" format="string"/>
            <attr name="randomTextColor" format="color"/>
            <attr name="randomTextSize" format="dimension"/>
        
            <declare-styleable name="RandomTextView" >
                <attr name="randomText"/>
                <attr name="randomTextColor"/>
                <attr name="randomTextSize"/>
            </declare-styleable>
        
        </resources>

        format 是值该属性的取值类型:一共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag

        在 xml 布局中的引用:

         <com.yechaoa.customviews.randomtext.RandomTextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:padding="20dp"
                app:randomText="1234"
                app:randomTextColor="@color/colorAccent"
                app:randomTextSize="50sp" />

        注意引入 命名空间 :

        xmlns:app="http://schemas.android.com/apk/res-auto"

        2.添加构造方法

        新建一个 RandomTextView 类,继承 View ,并添加3个构造方法

        class RandomTextView : View {
        
            //文本
            private var mRandomText: String
        
            //文本颜色
            private var mRandomTextColor: Int = 0
        
            //文本字体大小
            private var mRandomTextSize: Int = 0
        
            private var paint = Paint()
            private var bounds = Rect()
        
            //调用两个参数的构造
            constructor(context: Context) : this(context, null)
        
            //xml默认调用两个参数的构造,再调用三个参数的构造,在三个参数构造里获取自定义属性
            constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)
        
            constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super(context, attributeSet, defStyle) {
                ...
            }
            ...
        }
        

        这里要注意的是,所有的构造方法,都指向的是第三个构造方法,前两个构造的继承是 this ,而不是 super 。

        第一个构造比如我们可以是 new 创建的,第二个是 xml 中默认调用的,我们在第三个构造中去获取自定义属性。

        3.在构造里获取自定义样式

          constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super(context, attributeSet, defStyle) {
                //获取自定义属性
                val typedArray = context.theme.obtainStyledAttributes(
                    attributeSet,
                    R.styleable.RandomTextView,
                    defStyle,
                    0
                )
        
                mRandomText = typedArray.getString(R.styleable.RandomTextView_randomText).toString()
                mRandomTextColor = typedArray.getColor(R.styleable.RandomTextView_randomTextColor, Color.BLACK)//默认黑色
                mRandomTextSize = typedArray.getDimensionPixelSize(
                    R.styleable.RandomTextView_randomTextSize,
                    TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, 16F, resources.displayMetrics ).toInt()
                )
        
                //获取完回收
                typedArray.recycle()
        
                paint.textSize = mRandomTextSize.toFloat()
        
                //返回文本边界,即包含文本的最小矩形,没有所谓“留白”,返回比measureText()更精确的text宽高,数据保存在bounds里
                paint.getTextBounds(mRandomText, 0, mRandomText.length, bounds)
            }
        

        通过 obtainStyledAttributes 获取自定义属性,返回一个 TypedArray ,这里用到了我们在 attrs.xml 文件中声明的样式(R.styleable.RandomTextView),返回的 TypedArray 即包含了这里面的属性。

        拿到自定义 view 属性集合,然后赋值,赋值之后就可以用 paint 去画了。

        然后用到了 paint 的 getTextBounds 方法:

        paint.getTextBounds(mRandomText, 0, mRandomText.length, bounds)
        

        简单理解就是,把文字放在一个矩形里,通过矩形的宽高即可知道文字的宽高,所以宽高会保存在 bounds 里,bounds 是一个矩形 Rect ,为什么要这么做呢,因为后面我们要计算文字居中的时候会用到。

        ok,接下来开始画布局。

        4.重写>
         @SuppressLint("DrawAllocation")
            override fun onDraw(canvas: Canvas?) {
                super.onDraw(canvas)
        
                /**
                 * 自定义View时,需要我们自己在onDraw中处理padding,否则是不生效的
                 * 自定义ViewGroup时,子view的padding放在onMeasure中处理
                 */
        
                /**
                 * 矩形背景
                 */
                paint.color = Color.YELLOW
                //计算坐标,因为原点是在文字的左下角,左边要是延伸出去就还要往左边去,所以是减,右边和下边是正,所以是加
                canvas?.drawRect(
                    (0 - paddingLeft).toFloat(),
                    (0 - paddingTop).toFloat(),
                    (measuredWidth + paddingRight).toFloat(),
                    (measuredHeight + paddingBottom).toFloat(),
                    paint
                )
        
                /**
                 * 文本
                 */
                paint.color = mRandomTextColor
                //注意这里的坐标xy不是左上角,而是左下角,所以高度是相加的,在自定义view中,坐标轴右下为正
                //getWidth 等于 measuredWidth
                canvas?.drawText(
                    mRandomText,
                    (width / 2 - bounds.width() / 2).toFloat(),
                    (height / 2 + bounds.height() / 2).toFloat(),
                    paint
                )
            }

        上面的代码就是在 onDraw 里面显示绘制了一个 YELLOW 颜色的矩形背景,然后绘制了一个自定义属性颜色的居中的文本。

        这里要注意我们计算位置时的 坐标 ,在自定义 view 中,原点是 view 的 左上角 ,而在数学坐标系中,原点(0,0)是在 中间 的,二者是有区别的。

        其次,假如 xml 布局中有 padding ,或者预判会使用到 padding,在重写 onDraw 的时候也要把 padding 的数据加上,否则 padding 是不生效的。如果是继承 ViewGroup 时, 子view 的 padding 放在 onMeasure 中处理。

        来看此时的效果:

        此时是不是有疑惑,xml 里面的宽高明明是 wrap_content ,为什么会充满父布局呢?

        这就涉及到 onMeasure 的知识点了,往下看。

        5.重写>

        我们在 xml 设置 view 宽高有 3 种方式:

          match_parentwrap_content具体数据,比如 100dp

          onMeasure 中 MeasureSpec 的 mode 也有 3 种模式:

            EXACTLY:一般是设置了明确的值或者是 MATCH_PARENTAT_MOST:表示子布局限制在一个最大值内,一般为 WARP_CONTENTUNSPECIFIED:表示子布局想要多大就多大,很少使用

            由于我们 xml 用的是 wrap_content ,也就是对应 AT_MOST ,所以效果就是会占满父布局中的 可用空间 ,而父布局是填充屏幕,所以我们自定义的 view 也会占满全屏。

            而我们实际想要的效果是 view 包裹自己,而不是铺满全屏,所以我们需要在 onMeasure 中进行处理

              override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
                    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
            
                    /**
                     * EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
                     * AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
                     * UNSPECIFIED:表示子布局想要多大就多大,很少使用
                     */
                    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
                    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
            
                    val heightMode = MeasureSpec.getMode(heightMeasureSpec)
                    val heightSize = MeasureSpec.getSize(heightMeasureSpec)
            
                    var width = 0
                    var height = 0
            
                    //如果指定了宽度,或不限制宽度,用可用宽度即可,如果是WARP_CONTENT,则用文本宽度,再加上左右padding
                    when (widthMode) {
                        MeasureSpec.UNSPECIFIED,
                        MeasureSpec.EXACTLY -> {
                            width = widthSize + paddingLeft + paddingRight
                        }
                        MeasureSpec.AT_MOST -> {
                            width = bounds.width() + paddingLeft + paddingRight
                        }
                    }
            
                    //如果指定了高度,或不限制高度,用可用高度即可,如果是WARP_CONTENT,则用文本高度,再加上上下padding
                    when (heightMode) {
                        MeasureSpec.UNSPECIFIED,
                        MeasureSpec.EXACTLY -> {
                            height = heightSize + paddingTop + paddingBottom
                        }
                        MeasureSpec.AT_MOST -> {
                            height = bounds.height() + paddingTop + paddingBottom
                        }
                    }
            
                    //保存测量的宽高
                    setMeasuredDimension(width, height)
                }

            上面的代码呢,主要做了两件事:

              获取 view 宽高的模式针对不同的模式,对宽高进行重新测量

              最后别忘记调用 setMeasuredDimension 保存新测量的宽高,否则没用哦。

              此时再看效果就是效果图中的样子了。

              6.设置点击事件

              ok,到这,view>

              constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super(context, attributeSet, defStyle) {
                      ...
                      /**
                       * 添加点击事件
                       */
                      this.setOnClickListener {
                          mRandomText = randomText()
                          //更新
                          postInvalidate()
                      }
                  }

              randomText 方法:

               /**
                   * 根据文本长度 随意数字
                   */
                  private fun randomText(): String {
                      val list = mutableListOf<Int>()
                      for (index in mRandomText.indices) {
                          list.add(Random.nextInt(10))
                      }
                      val stringBuffer = StringBuffer()
                      for (i in list) {
                          stringBuffer.append("" + i)
                      }
                      return stringBuffer.toString()
                  }

              触发事件之后,文字更新,然后 view 重绘 更新 页面即可。

              关于数据获取,也就是变化后的数字,可以写个 onTextChanged 接口,也可以写个开放 方法 获取。

              总结

              其实看效果的话,还不如 TextView 来的简单,而且>

              所以本文的重点并不是实现效果,而是 学习理解 自定义 View 以及其 绘制流程 。

              理论看的再多也需要实践才行,不如跟着敲两遍,理解消化一下。

              注释 还是非常详细的,甚至有点啰嗦。。

              以上就是Android自定义View实现随机数验证码的详细内容,更多关于Android随机数验证码的资料请关注易采站长站其它相关文章!