前语

  在日常作业开发中,咱们时长会遇到各种各样的需求,不部分需求是能够经过Android 原生的View来解决,而有一些是无法解决的,这时分咱们就需求自界说View,咱们先来看看本文中这个自界说View的演示作用图。

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

正文

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

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

一、什么是View?

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

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

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

<?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 的源码。

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

这儿咱们得知ConstraintLayout 承继自ViewGroup,然后咱们再检查ViewGroup的源码。

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

  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个值,那么是不是一个值一个输入框呢?那样看起来有一些繁琐,那么就定为两个值一个框。

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

  这个框咱们能看到那些款式呢?每一个框的巨细、布景色彩、边框色彩、边框巨细、文字巨细、文字色彩、分隔符,一般来说默许是英文分号( : ),不过也有运用小横杠的( – ),那么怎样去设置款式呢?在 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,我画了一个图来进行阐明(==有点笼统,能了解就能够==)。

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

④ 制作

  丈量好了之后,下面就能够开始制作了,制作就相当于在纸上画画,而画画呢,首要要有画笔,首要声明变量,代码如下:

    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);//文字居中对齐
    }

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

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

下面要进行制作了,制作分为两步,制作方框和制作文字。

1. 制作方框

  首要是制作方框,在自界说View中新增一个drawBox()办法,代码如下:

    /**
     * 制作方框
     */
    private void drawBox(Canvas canvas) {
        //每个方框的距离
        int margin = Utils.dp2px(mContext, mBoxMargin);
        for (int i = 0; i < mBoxNum; i++) {
            //制作矩形框,需求左、上、右、下四个点的方位
            float left = i * mBoxWidth + i * margin;
            float top = 0f;
            float right = (i + 1) * mBoxWidth + i * margin;
            float bottom = mBoxWidth;
            RectF rectF = new RectF(left, top, right, bottom);
            //制作圆角矩形框
            int radius = Utils.dp2px(mContext, mBoxCornerRadius);
            canvas.drawRoundRect(rectF, radius, radius, mBoxPaint);
            //制作圆角矩形边框
            float strokeWidth = mBoxStrokeWidth / 2;
            RectF strokeRectF = new RectF(left + strokeWidth, top + strokeWidth, right - strokeWidth, bottom - strokeWidth);
            float strokeRadius = radius - strokeWidth;
            canvas.drawRoundRect(strokeRectF, strokeRadius, strokeRadius, mBoxStrokePaint);
        }
    }

  这儿制作方框有必要好好阐明一下,首要是这个距离,便是方框的距离,已经说过了,然后咱们依据设置的方框数量就行遍历,需求制作6个方框,那么,int = 0,进入循环,制作第一个方框,首要咱们需求确认方框左、上、右、下4个坐标点的坐标,那么咱们将值代入到代码中看看。

float left = 0 * 48 + 0 * 4;
float top = 0f;
float right = (0 + 1) * 48 + 0 * 4;
float bottom = 48;

  得出的成果便是:left :0、top:0、right :48、bottom :48,然后经过四个点得到一个矩形,由于是圆角方框,所以在自界说View中声明变量:

    private float mBoxCornerRadius = 8f;

  然后得到px的radiu,再经过canvas.drawRoundRect()办法制作一个圆角矩形,圆角矩形制作好之后,咱们能够顺便制作圆角矩形的圆角边框,留意看下面这几行代码:

float strokeWidth = mBoxStrokeWidth / 2;
RectF strokeRectF = new RectF(left + strokeWidth, top + strokeWidth, right - strokeWidth, bottom - strokeWidth);
float strokeRadius = radius - strokeWidth;

  首要是这个mBoxStrokeWidth / 2,为什么要这么做呢?这是由于制作边框的时分实际上不是居内制作,而是居中往两侧制作,而我要做的是居内制作,为了保持制作的边框不至于太粗我就除以2,只用一半的宽度,然后便是制作边框的时分,左、上都加上了这个边框的宽,右、下都减去了这个边框的宽,这样做是为了让边框完好置于圆角矩形里边,下面的图中右侧的示例便是我想要的。

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

那么第一个方框制作后如下图所示。

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

  方框的布景色彩我默许设置成白色了,能够自行修正,或许在xml中进行特点设置,那么依照方才的思路,现在循环第2次,i = 1;

float left = 1 * 48 + 1 * 4;
float top = 0f;
float right = (1 + 1) * 48 + 1 * 4;
float bottom = 48;

得出的成果便是:left :52、top:0、right :100、bottom :48,那么制作出来第二个框如下图所示:

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

那么依照上述的阐明我相信你已经知道是怎样制作的了,那么下面咱们就能够制作文字了。

2. 制作文字

  现在方框有了,而文字制作咱们需求制作在方框的中间,首要咱们声明变量,代码如下:

    private final int mMacLength = 6;
    private final String[] macAddressArray = new String[mMacLength];

然后咱们在自界说View中新增一个drawMacAddress()办法。

    /**
     * 制作Mac地址
     */
    private void drawMacAddress(Canvas canvas) {
        int boxMargin = Utils.dp2px(mContext, mBoxMargin);
        for (int i = 0; i < macAddressArray.length; i++) {
            if (macAddressArray[i] != null) {
                //制作的文字
                String content = macAddressArray[i];
                //获取制作的文字鸿沟
                mTextPaint.getTextBounds(content, 0, content.length(), mTextRect);
                //制作的方位
                int offset = (mTextRect.top + mTextRect.bottom) / 2;
                //制作文字,需求确认起始点的X、Y的坐标点
                float x = (float) (getPaddingLeft() + mBoxWidth * i + boxMargin * i + mBoxWidth / 2);
                float y = (float) (getPaddingTop() + mBoxWidth / 2) - offset;
                //制作文字
                canvas.drawText(content, x, y, mTextPaint);
            }
        }
    }

假定地址数组第一个值是0A,然后经过mTextPaint.getTextBounds()得到这个文字的鸿沟,就相当于得到一个文字的鸿沟框,然后便是经过鸿沟框的上+下的坐标 / 2的鸿沟框的中间方位,由于文字的制作是从左下角到右上角进行制作的。最重要的便是去顶起始点的x、y轴坐标,

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

将 i = 0 ,offset = 12代入进去。

float x = (float) (0 + 48 * 0 + 4 * 0 + 48 / 2);
float y = (float) (0 + 48 / 2) - 12;

终究 x = 24,y = 36。

然后制作出来的成果如下图所示:

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

  后面的制作也是一样的道理,现在两个制作办法都写好了,需求在onDraw()中调用,在自界说View中新增如下代码:

    /**
     * View的制作
     *
     * @param canvas 画布
     */
    @Override
    protected void onDraw(Canvas canvas) {
        //制作方框
        drawBox(canvas);
        //制作Mac地址
        drawMacAddress(canvas);
    }

⑤ 输入

  制作的处理已经完成了,那么作为一个蓝牙Mac地址输入框,咱们需求输入的数据是什么呢?0、1、2、3、4、5、6、7、8、9、A、B、C、E、F、G,像上述的这些数据表示16进制的,那么假如运用体系的软键盘进行输入,咱们或许需求在输入的过程中挑选字符键盘,而这个字符键盘上其他的英文字母或许标点符号右不是我所需求的,那么为了方便,我打算自己做一个键盘来进行输入。

1. 键盘布局

  首要在layout下创立一个lay_hex_keyboard.xml,用于作为键盘的布局,代码如下所示:

<?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="wrap_content"
    android:background="#eff4f9">
    <Button
        android:id="@+id/btn_a"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="A"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_9"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/btn_9"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="9"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_8"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btn_a"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/btn_8"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="8"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_7"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btn_9"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/btn_7"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="7"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_del"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btn_8"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/btn_del"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:layout_marginEnd="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="删去"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btn_7"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/btn_b"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="B"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_6"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="@+id/btn_a"
        app:layout_constraintTop_toBottomOf="@+id/btn_a" />
    <Button
        android:id="@+id/btn_6"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="6"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_5"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btn_b"
        app:layout_constraintTop_toBottomOf="@+id/btn_a" />
    <Button
        android:id="@+id/btn_5"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="5"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_4"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btn_6"
        app:layout_constraintTop_toBottomOf="@+id/btn_a" />
    <Button
        android:id="@+id/btn_4"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="4"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_delete_all"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btn_5"
        app:layout_constraintTop_toBottomOf="@+id/btn_a" />
    <Button
        android:id="@+id/btn_delete_all"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="全删"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="@+id/btn_del"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btn_4"
        app:layout_constraintTop_toBottomOf="@+id/btn_a" />
    <Button
        android:id="@+id/btn_c"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="C"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_3"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="@+id/btn_b"
        app:layout_constraintTop_toBottomOf="@+id/btn_b" />
    <Button
        android:id="@+id/btn_3"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="3"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_2"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btn_c"
        app:layout_constraintTop_toBottomOf="@+id/btn_b" />
    <Button
        android:id="@+id/btn_2"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="2"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_1"
        app:layout_constraintStart_toEndOf="@+id/btn_3"
        app:layout_constraintTop_toBottomOf="@+id/btn_b" />
    <Button
        android:id="@+id/btn_1"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="1"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="@+id/btn_4"
        app:layout_constraintStart_toEndOf="@+id/btn_2"
        app:layout_constraintTop_toBottomOf="@+id/btn_b" />
    <Button
        android:id="@+id/btn_d"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginTop="4dp"
        android:layout_marginBottom="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="D"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/btn_e"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="@+id/btn_c"
        app:layout_constraintTop_toBottomOf="@+id/btn_c" />
    <Button
        android:id="@+id/btn_e"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="E"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_f"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btn_d"
        app:layout_constraintTop_toBottomOf="@+id/btn_c" />
    <Button
        android:id="@+id/btn_f"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="F"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_0"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btn_e"
        app:layout_constraintTop_toBottomOf="@+id/btn_c" />
    <Button
        android:id="@+id/btn_0"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="0"
        android:textColor="@color/key_tx_color"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@+id/btn_complete"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btn_f"
        app:layout_constraintTop_toBottomOf="@+id/btn_c" />
    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_complete"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="4dp"
        android:backgroundTint="@color/key_complete_bg_color"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="完成"
        android:textColor="@color/white"
        android:textSize="16sp"
        app:iconGravity="start|end"
        app:layout_constraintBottom_toBottomOf="@+id/btn_0"
        app:layout_constraintEnd_toEndOf="@+id/btn_delete_all"
        app:layout_constraintStart_toEndOf="@+id/btn_0"
        app:layout_constraintTop_toBottomOf="@+id/btn_delete_all" />
</androidx.constraintlayout.widget.ConstraintLayout>

布局的预览作用如下图所示:

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

  这个布局从运用上来说就很简略了,基本上一目了然,这儿咱们能够写一个接口用来处理键盘上按钮点击的事情。

2. 键盘接口

  在com.llw.easyview下新建一个HexKeyboardListener接口,代码如下所示:

public interface HexKeyboardListener {
    /**
     * Hex字符
     * @param hex 0~9,A~F
     */
    void onHex(String hex);
    /**
     * 删去
     */
    void onDelete();
    /**
     * 全删
     */
    void onDeleteAll();
    /**
     * 完成
     */
    void onComplete();
}

  现在接口有了,接口中的办法基本上覆盖了键盘上一切按钮点击时触发的事情处理,下面咱们来写一个弹窗,用来点击Mac地址输入框时弹出这个键盘。

3. 键盘弹窗

  这个弹窗,我就写在Utils类中了,在里边新增如下办法代码:

    /**
     * 显现Hex键盘弹窗
     *
     * @param context  上下文
     * @param listener Hex键盘按键监听
     */
    public static void showHexKeyboardDialog(@NonNull Context context, @NonNull HexKeyboardListener listener) {
        BottomSheetDialog dialog = new BottomSheetDialog(context);
        //依据xml获取布局视图
        View view = LayoutInflater.from(context).inflate(R.layout.lay_hex_keyboard, null, false);
        //点击按键触发接口回调
        view.findViewById(R.id.btn_a).setOnClickListener(v -> listener.onHex("A"));
        view.findViewById(R.id.btn_b).setOnClickListener(v -> listener.onHex("B"));
        view.findViewById(R.id.btn_c).setOnClickListener(v -> listener.onHex("C"));
        view.findViewById(R.id.btn_d).setOnClickListener(v -> listener.onHex("D"));
        view.findViewById(R.id.btn_e).setOnClickListener(v -> listener.onHex("E"));
        view.findViewById(R.id.btn_f).setOnClickListener(v -> listener.onHex("F"));
        view.findViewById(R.id.btn_0).setOnClickListener(v -> listener.onHex("0"));
        view.findViewById(R.id.btn_1).setOnClickListener(v -> listener.onHex("1"));
        view.findViewById(R.id.btn_2).setOnClickListener(v -> listener.onHex("2"));
        view.findViewById(R.id.btn_3).setOnClickListener(v -> listener.onHex("3"));
        view.findViewById(R.id.btn_4).setOnClickListener(v -> listener.onHex("4"));
        view.findViewById(R.id.btn_5).setOnClickListener(v -> listener.onHex("5"));
        view.findViewById(R.id.btn_6).setOnClickListener(v -> listener.onHex("6"));
        view.findViewById(R.id.btn_7).setOnClickListener(v -> listener.onHex("7"));
        view.findViewById(R.id.btn_8).setOnClickListener(v -> listener.onHex("8"));
        view.findViewById(R.id.btn_9).setOnClickListener(v -> listener.onHex("9"));
        view.findViewById(R.id.btn_del).setOnClickListener(v -> listener.onDelete());
        view.findViewById(R.id.btn_delete_all).setOnClickListener(v -> listener.onDeleteAll());
        view.findViewById(R.id.btn_complete).setOnClickListener(v -> {
            listener.onComplete();
            dialog.dismiss();
        });
        //点击外部不消失
        dialog.setCancelable(false);
        //设置内容视图
        dialog.setContentView(view);
        if (dialog.getWindow() != null) {
            //去掉弹窗布景通明
            WindowManager.LayoutParams params = dialog.getWindow().getAttributes();
            params.dimAmount = 0.0f;
            dialog.getWindow().setAttributes(params);
        }
        //显现弹窗
        dialog.show();
    }

  这儿便是一个底部弹窗,然后设置布局视图,设置接口回调,设置布景通明,终究显现出来。那么下一步要做的便是点击输入框调用这个弹窗显现键盘。

4. 显现键盘

  在View中是能够获取到点击接触事情的,那么咱们能够在自界说View中新增如下代码:

    /**
     * 接触事情
     */
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event != null) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                //显现Hex键盘弹窗
                Utils.showHexKeyboardDialog(mContext, this);
                return true;
            }
        }
        return super.onTouchEvent(event);
    }

  这儿的代码便是当咱们的手机点击这个Mac地址输入框的时分,会先触发接触事情,然后才是点击事情,而在这儿咱们便是在接触到的时分显现键盘弹窗,然后返回 true,这儿就会进行事情的拦截,这儿的这个this,便是咱们当时的自界说View需求完成的回调接口,将鼠标放在这个this后面,然后Alt + Enter的组合键,会呈现弹窗,如下图所示:

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

这儿点击第四项,会呈现一个弹窗,如图所示:

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

  点击OK就能够快速完成这个接口的回调,重写接口的办法,你会看到自界说View新增了四个办法,代码如下:

    @Override
    public void onHex(String hex) {
    }
    @Override
    public void onDelete() {
    }
    @Override
    public void onDeleteAll() {
    }
    @Override
    public void onComplete() {
    }

5. 处理输入

  现在自界说View已经完成了键盘的点击事情回调,那么下面便是怎样处理这些事情,首要咱们需求声明两个变量

    private final int mInputLength = 12;
    private final String[] inputArray = new String[mInputLength];
    private int currentInputPosition = 0;
    /**
     * 操作标识
     * -1:增加,
     * 0:删去,
     * 1:全删
     */
    private int flag = -1;

  这个地方便是输入的长度、保存输入的数组、当时输入的方位,这儿的12,便是咱们实际上输入一个完好的Mac地址,去掉分隔符实际长度是12,而分隔符咱们能够自己去设置要用什么分隔符。首要是修正制作文字的处理,什么时分会触发制作文字呢?当咱们修正inputArray的内容时,增加、删去之类的操作,这儿还有一个标识位用来记录当时的制作文字办法,在自界说View中增加一个处理Mac文字制作的办法,代码如下:

    /**
     * 处理Mac地址制作
     */
    private void processMacDraw() {
        if (flag == 1) {    //全删
            currentInputPosition = 0;
            Arrays.fill(inputArray,null);
            Arrays.fill(macAddressArray,"");
        } else {    //增加或删去
            String hex = "";
            int hexPos = 0;
            for (String input : inputArray) {
                if (input == null) {
                    input = "";
                }
                hex = hex + input;
                macAddressArray[hexPos] = hex;
                if (hex.length() == 2) {
                    hexPos++;
                    hex = "";
                }
            }
        }
        //改写View
        postInvalidate();
    }

  这个办法便是当inputArray发生变化时,一起改变macAddressArray,而咱们的文字制作是依据macAddressArray来的。当点击全删的时分就两个数组置为null和空字符串。然后便是增加或删去的时分遍历inputArray,满足两个字符长度就给macAddressArray进行一次赋值,终究调用postInvalidate()改写View,会从头调用onDraw进行制作。下面咱们再修正一下onHex()办法,代码如下:

    @Override
    public void onHex(String hex) {
        //输入长度满足12
        if (currentInputPosition == mInputLength) return;
        //不满足12
        inputArray[currentInputPosition] = hex;
        currentInputPosition++;
        flag = -1;
        processMacDraw();   //增加时制作
    }

  这儿的代码便是在inputArray中增加数据,然后调用制作文字办法,下面再修正一下onDelete()办法,代码如下:

    @Override
    public void onDelete() {
        if (currentInputPosition == 0) return;
        currentInputPosition--;
        inputArray[currentInputPosition] = null;
        flag = 0;
        processMacDraw();   //删去时制作
    }

  删去后制作,终究咱们修正一下onDeleteAll()办法,代码如下:

    @Override
    public void onDeleteAll() {
        flag = 1;
        processMacDraw();   //全删时制作
    }

  终究便是在输入完成的时分获取当时输入的Mac地址数据,在自界说View中新增getMacAddress()办法。

    /**
     * 获取Mac地址
     * @return 完好的Mac地址
     */
    public String getMacAddress() {
        StringBuilder builder = new StringBuilder();
        for (String macAddress : macAddressArray) {
            if (macAddress == null) continue;
            if (macAddress.isEmpty()) continue;
            if (builder.toString().isEmpty()) {
                builder.append(macAddress);
            } else {
                builder.append(mSeparator == null ? ":" : mSeparator).append(macAddress);
            }
        }
        return builder.toString();
    }

  终究咱们修正onComplete()办法,在里边进行打印,代码如下所示:

    @Override
    public void onComplete() {
        Log.d("TAG", "onComplete: " + getMacAddress());
    }

四、运用自界说View

  现在自界说View写好了,能够运用了,修正activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".MainActivity">
    <com.llw.easyview.MacAddressEditText
        android:id="@+id/mac_et"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <Button
        android:id="@+id/btn_mac"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:text="获取地址" />
</LinearLayout>

  假如你发现XML预览不了,看不到这个自界说View,就Rebuild Project一下,就能看到了,预览作用如下图所示:

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

  下面进入到MainActivity中去运用,修正代码如下所示:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MacAddressEditText macEt = findViewById(R.id.mac_et);
        Button btnMac = findViewById(R.id.btn_mac);
        btnMac.setOnClickListener(v -> {
            String macAddress = macEt.getMacAddress();
            if (macAddress.isEmpty()){
                Toast.makeText(this, "请输入Mac地址", Toast.LENGTH_SHORT).show();
                return;
            }
            btnMac.setText(macAddress);
        });
    }
}

  这儿的代码就很简略,获取View,然后点击按钮时获取输入框的值,获取到值显现在按钮上,下面运行测试一下。

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

五、源码

假如对你有所帮助的话,无妨 Star 或 Fork,山高水长,后会有期~

源码地址:EasyView