Android 自定义View 之 Mac地址输入框(上)

简介: Android 自定义View 之 Mac地址输入框(上)

前言


  在日常工作开发中,我们时长会遇到各种各样的需求,不部分需求是可以通过Android 原生的View来解决,而有一些是无法解决的,这时候我们就需要自定义View,我们先来看看本文中这个自定义View的演示效果图。

86ac87104f894aa9bb254a478d806e25.gif


正文


  在了解自定义View之前,我们先了解什么是View,View就是视图,再通俗一点就是你在手机上所看到的内容,假设我们创建了一个项目,算了,我们真的去创建一个项目,创建一个名为EasyView的项目。

bc3a0e995b604efb8734af5ea0d7a702.png


一、什么是View?


  项目创建好之后,看一下activity_main.xml,我们能看到什么?白色的背景,中间有一个Hello World!的文字。

e02662b640844bc683fb7924f56bbaa3.png

这能看的出什么呢?如果从界面上你看不出什么的话,我们就从代码上来看:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


  从代码上我们看到有一个约束布局,布局里面是一个TextView,用于显示文字。这个ConstraintLayout 布局就是View,这个TextView也是View。你说是就是吗?怎么证明呢?

我们来看一下ConstraintLayout 的源码。

34687a9818374ef2b1a13b4c12018a93.png

这里我们得知ConstraintLayout 继承自ViewGroup,然后我们再查看ViewGroup的源码。

295137d6cd1f4c369853342b249fb6f6.png

  ViewGroup 继承自View,所以说ConstraintLayout是一个View并非是空穴来风,而是有真凭实据的,而TextView,你查看它的源码就会看到,它也是继承自View

  现在我们知道View是所有视图的父类,手机屏幕上看到的任何内容都是View。


二、什么是自定义View


  刚才我们所看到的ConstraintLayoutTextView都可以理解成自定义View,只不过因为这两个View都是由Google源码中提供的,所以不属于自定义View,属于系统View,也就是原生的控件,那么对于ConstraintLayoutTextView来说,它们的却别是什么?

  这里我们需要先知道ViewViewGroup的区别,View是一个视图,ViewGroup是一个容器视图,在简单一点说,View只是一个视图,而ViewGroup可以放置多个视图。ViewGroup我们通常作为布局容器来使用,例如LinearLayoutRelativeLayout等都是布局,它里面是可以放置控件的,而这个控件就是View

  通过翻来覆去的描述,可能你会更清楚两者的区别,那么系统的我们了解,所谓自定义View就是系统View之外的View,例如网上开源的图表控件、日历控件等。作为开发者我们实现自定义View有那些方式:

  1. 继承View,例如折线图等。
  2. 继承ViewGroup,例如流式布局等。
  3. 继承现有的View,例如TextView、ListView等。


  前面的两种方式我们已经知道了,那么第三种是什么意思,不知道你有没有注意到,Android 5.0时推出一个material库,这里库里面就是继承了现有的View而制作的Material UI风格的控件,下面我们将xml中的TextView改成com.google.android.material.textview.MaterialTextView,你会发现也不会报错,而我们查看MaterialTextView的源码,发现它继承自AppCompatTextView,而AppCompatTextView又继承自TextView,通过这种层层继承的方式,子类可以做很多的特性的增加,同时又具备父类的基本属性,而且相对改动较少,举一个简单的例子,你现在有一个TextView,你希望这个TextView的文字颜色可以五颜六色的,还要会发光,那么这个时候你就可以继承自View,来写你所需要的五颜六色和发光的需求,而不是继承View,所有的功能都要重新写。


三、自定义View


  首先我们创建一个自定义View,在com.llw.easyview包下新建一个MacAddressEditText类,从名字上来看这是一个Mac地址输入框。


① 构造方法

然后我们继承自View,重写里面的构造方法,代码如下:

public class MacAddressEditText extends View {
    /**
     * 构造方法 1
     * 在代码中使用,例如Java 的new MacEditText(),Kotlin 的MacEditText()
     *
     * @param context 上下文
     */
    public MacAddressEditText(Context context) {
        super(context);
    }
    /**
     * 构造方法 2
     * 在xml布局文件中使用时自动调用
     *
     * @param context 上下文
     * @param attrs   属性设置
     */
    public MacAddressEditText(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    /**
     * 构造方法 3
     * 不会自动调用,如果有默认style时,在第二个构造函数中调用
     *
     * @param context      上下文
     * @param attrs        属性设置
     * @param defStyleAttr 默认样式
     */
    public MacAddressEditText(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}


这里重写了3个构造方法,通过方法上的注释你应该就可能够明白分别是怎么使用的,因为我们会涉及到样式,那么最终是使用构造方法 3, 所以对上面的方法我们再改动一下,修改后代码如下:

public class MacAddressEditText extends View {
    private Context mContext;
    public MacAddressEditText(Context context) {
        this(context,null);
    }
    public MacAddressEditText(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }
    public MacAddressEditText(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
    }
}


  这里增加一个上下文变量,然后就是构造方法1 调用2,2调用3。现在你在java代码和xml中就都可以正常使用了。我们在使用系统的View的时候通常会在xml中设置一些参数样式,那么自定义里面怎么设置样式呢?


② XML样式

  在设置样式之前需要先知道我们的自定义View要做什么,Mac地址输入框,主要就是蓝牙的Mac地址输入,一个完整的Mac地址格式是12:34:56:78:90:21,我们去掉分号,就是12个值,那么是不是一个值一个输入框呢?那样看起来有一些繁琐,那么就定为两个值一个框。

ebbb73b115644a7bbcc8ff3819587585.png

  这个框我们能看到那些样式呢?每一个框的大小、背景颜色、边框颜色、边框大小、文字大小、文字颜色、分隔符,一般来说默认是英文分号( : ),不过也有使用小横杠的( - ),那么怎么去设置样式呢?在 res →values 下新建一个attrs.xml文件,里面我们可以写自定义的样式,代码如下所示:

    <declare-styleable name="MacAddressEditText">
        <!-- 方框大小,宽高一致 -->
        <attr name="boxWidth" format="dimension" />
        <!-- 方框背景颜色 -->
        <attr name="boxBackgroundColor" format="color|reference" />
        <!-- 方框描边颜色 -->
        <attr name="boxStrokeColor" format="color|reference" />
        <!-- 方框描边宽度 -->
        <attr name="boxStrokeWidth" format="dimension" />
        <!--文字颜色-->
        <attr name="textColor" format="color|reference" />
        <!--文字大小-->
        <attr name="textSize" format="dimension" />
        <!--分隔符,: 、- -->
        <attr name="separator" format="string|reference" />
    </declare-styleable>


  这里我们声明View的样式,里面是样式的一些设置属性,重点看属性值,dimension表示dp、sp之类,reference表示可以引用资源,你可以理解为间接引用,那么其他的属性值格式就顾名思义了,很简单。

  属性样式定义好了,还有一些颜色值需要定义,在colors.xml中增加如下代码:

    <color name="key_bg_color">#fcfcfc</color>
    <color name="key_tx_color">#1b1b1b</color>
    <color name="key_complete_bg_color">#009C3A</color>
    <color name="box_default_stroke_color">#009C3A</color>
    <color name="box_default_bg_color">#f8f8f8</color>
    <color name="tx_default_color">#0C973F</color>


  xml中的dp、sp之类的在绘制的时候需要转换,转成px,我们可以写一个自定义View,在com.llw.easyview下新建一个Utils类,代码如下所示:

public class Utils {
    /**
     * dp转px
     *
     * @param dpValue dp值
     * @return px值
     */
    public static int dp2px(Context context, final float dpValue) {
        final float scale = context.getApplicationContext().getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
    /**
     * sp 转 px
     *
     * @param spValue sp值
     * @return px值
     */
    public static int sp2px(Context context, final float spValue) {
        final float fontScale = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
}


下面我们回到View中去使用,先声明变量,代码如下:

    private int mBoxWidth;
    private final int mBoxBackgroundColor;
    private final int mBoxStrokeColor;
    private final int mBoxStrokeWidth;
    private final int mTextColor;
    private final float mTextSize;
    private final String mSeparator;


然后修改第三个构造函数,代码如下所示:

    public MacAddressEditText(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        //根据设置的样式进行View的绘制参数设置
        @SuppressLint("CustomViewStyleable")
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MacAddressEditText);
        mBoxWidth = (int) typedArray.getDimensionPixelSize(R.styleable.MacAddressEditText_boxWidth, 48);
        mBoxBackgroundColor = typedArray.getColor(R.styleable.MacAddressEditText_boxBackgroundColor, ContextCompat.getColor(context, R.color.white));
        mBoxStrokeColor = typedArray.getColor(R.styleable.MacAddressEditText_boxStrokeColor, ContextCompat.getColor(context, R.color.box_default_stroke_color));
        mBoxStrokeWidth = (int) typedArray.getDimensionPixelSize(R.styleable.MacAddressEditText_boxStrokeWidth, 2);
        mTextColor = typedArray.getColor(R.styleable.MacAddressEditText_textColor, ContextCompat.getColor(context, R.color.tx_default_color));
        mTextSize = typedArray.getDimensionPixelSize(R.styleable.MacAddressEditText_textSize, (int) TypedValue
                        .applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics()));
        mSeparator = typedArray.getString(R.styleable.MacAddressEditText_separator);
        typedArray.recycle();
    }


  这里通过MacAddressEditText得到TypedArray,通过TypedArray获取MacAddressEditText中的属性,然后进行赋值,注意一点就是数值类型的需要默认值,有一些默认颜色值,就是我刚才写到colors.xml中的String类型不需要。数值类型就涉及到dp/sp转px的,此时我们调用了刚才工具类中的方法。


③ 测量

  测量只是的了解View的宽和高,得出绘制这个View需要的大小范围。这里我们就不考虑padding了,只计算每一个方框的大小和方框之间的间距,首先我们在自定义View中定义两个变量,代码如下:

  private final int mBoxNum = 6;
  private int mBoxMargin = 4;


这里表示方框个数,和方框间的间距,然后我们重写onMeasure()方法,代码如下:

    /**
     * View的测量
     *
     * @param widthMeasureSpec  宽度测量
     * @param heightMeasureSpec 高度测量
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = 0;
        int margin = dp2px(mBoxMargin);
        switch (MeasureSpec.getMode(widthMeasureSpec)) {
            case MeasureSpec.UNSPECIFIED:
            case MeasureSpec.AT_MOST:   //wrap_content
                width = mBoxWidth * mBoxNum + margin * (mBoxNum - 1);
                break;
            case MeasureSpec.EXACTLY:   //match_parent
                width = MeasureSpec.getSize(widthMeasureSpec);
                break;
        }
        //设置测量的宽高
        setMeasuredDimension(width, mBoxWidth);
    }


  这里的代码说明一下,首先是获取px的margin值,这里因为有6个方框,所以就有5个间距,然后来看测量模式,这里的模式和XML中设置layout_widthlayout_height的值有关,无非就是三种值,具体是大小,比如100dp,然后就是wrap_content,最后是match_parent,MeasureSpec.EXACTLY表示match_parent / 具体的值MeasureSpec.AT_MOST表示wrap_content

  width = mBoxWidth * mBoxNum + margin * (mBoxNum - 1)


  这里的 宽 = 方框的宽 * 6 + 方框间距 * 5,这很好理解,然后就是高,高就是宽,这里就算你在xml设置layout_heightmatch_parent,实际上也是wrap_content。那么根据测量的结果最后就是一个局限性,如果我们没有设置方框的大小的话,那么默认是48,间距为4,那么最终结果就是宽:308,高:48,我画了一个图来进行说明(有点抽象,能理解就可以)。

f119a31dbd644f69bb01d90531142640.png

④ 绘制

  测量好了之后,下面就可以开始绘制了,绘制就相当于在纸上画画,而画画呢,首先要有画笔,首先声明变量,代码如下:

    private Paint mBoxPaint;
    private Paint mBoxStrokePaint;
    private Paint mTextPaint;
    private final Rect mTextRect = new Rect();


然后我们需要对3个画笔(方框、方框边框、文字)进行设置,因为绘制文字稍微有一些不同,所以加了一个Rect,下面我们在View中新增一个初始化画笔的方法,代码如下所示:

    /**
     * 初始化画笔
     */
    private void initPaint() {
        //设置方框画笔
        mBoxPaint = new Paint();
        mBoxPaint.setAntiAlias(true);// 抗锯齿
        mBoxPaint.setColor(mBoxBackgroundColor);//设置颜色
        mBoxPaint.setStyle(Paint.Style.FILL);//风格填满
        //设置方框描边画笔
        mBoxStrokePaint = new Paint();
        mBoxStrokePaint.setAntiAlias(true);
        mBoxStrokePaint.setColor(mBoxStrokeColor);
        mBoxStrokePaint.setStyle(Paint.Style.STROKE);//风格描边
        mBoxStrokePaint.setStrokeWidth(mBoxStrokeWidth);//描边宽度
        //设置文字画笔
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(mTextSize);//文字大小
        mTextPaint.setTextAlign(Paint.Align.CENTER);//文字居中对齐
    }


然后在第三个构造方法中去调用,如下图所示:

2656c71fc8a2497983df67d4807fb4da.png


Android 自定义View 之 Mac地址输入框(下)https://developer.aliyun.com/article/1407655

相关文章
|
26天前
|
XML Java Android开发
Android实现自定义进度条(源码+解析)
Android实现自定义进度条(源码+解析)
53 1
|
3月前
|
Android开发 容器
Android UI设计: 什么是View和ViewGroup?
Android UI设计: 什么是View和ViewGroup?
36 0
|
5天前
|
移动开发 Java Unix
Android系统 自动加载自定义JAR文件
Android系统 自动加载自定义JAR文件
23 1
|
5天前
|
Shell Android开发 开发者
Android系统 自定义动态修改init.custom.rc
Android系统 自定义动态修改init.custom.rc
23 0
|
5天前
|
存储 安全 Android开发
Android系统 自定义系统和应用权限
Android系统 自定义系统和应用权限
19 0
|
6天前
|
Android开发
Android Mediatek NVRAM 加载 MAC 地址并禁用 MAC 地址更新
Android Mediatek NVRAM 加载 MAC 地址并禁用 MAC 地址更新
6 0
|
30天前
|
Android开发
Android 开发 pickerview 自定义选择器
Android 开发 pickerview 自定义选择器
12 0
|
4月前
|
XML API Android开发
Android 自定义View 之 Dialog弹窗
Android 自定义View 之 Dialog弹窗
|
开发工具 Android开发 iOS开发
|
1月前
|
开发工具 git iOS开发
Mac 安装软件包管理工具Homebrew
Mac 安装软件包管理工具Homebrew