前言
在日常工作开发中,我们时长会遇到各种各样的需求,不部分需求是可以通过Android 原生的View来解决,而有一些是无法解决的,这时候我们就需要自定义View,我们先来看看本文中这个自定义View的演示效果图。
正文
在了解自定义View之前,我们先了解什么是View,View就是视图,再通俗一点就是你在手机上所看到的内容,假设我们创建了一个项目,算了,我们真的去创建一个项目,创建一个名为EasyView的项目。
一、什么是View?
项目创建好之后,看一下activity_main.xml,我们能看到什么?白色的背景,中间有一个Hello World!的文字。
这能看的出什么呢?如果从界面上你看不出什么的话,我们就从代码上来看:
<?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
的源码。
这里我们得知ConstraintLayout
继承自ViewGroup
,然后我们再查看ViewGroup
的源码。
ViewGroup
继承自View
,所以说ConstraintLayout
是一个View并非是空穴来风,而是有真凭实据的,而TextView
,你查看它的源码就会看到,它也是继承自View
。
现在我们知道View是所有视图的父类,手机屏幕上看到的任何内容都是View。
二、什么是自定义View
刚才我们所看到的ConstraintLayout
和TextView
都可以理解成自定义View,只不过因为这两个View都是由Google源码中提供的,所以不属于自定义View,属于系统View,也就是原生的控件,那么对于ConstraintLayout
和TextView
来说,它们的却别是什么?
这里我们需要先知道View
和ViewGroup
的区别,View
是一个视图,ViewGroup
是一个容器视图,在简单一点说,View
只是一个视图,而ViewGroup
可以放置多个视图。ViewGroup
我们通常作为布局容器来使用,例如LinearLayout
、RelativeLayout
等都是布局,它里面是可以放置控件的,而这个控件就是View
。
通过翻来覆去的描述,可能你会更清楚两者的区别,那么系统的我们了解,所谓自定义View就是系统View之外的View,例如网上开源的图表控件、日历控件等。作为开发者我们实现自定义View有那些方式:
- 继承View,例如折线图等。
- 继承ViewGroup,例如流式布局等。
- 继承现有的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个值,那么是不是一个值一个输入框呢?那样看起来有一些繁琐,那么就定为两个值一个框。
这个框我们能看到那些样式呢?每一个框的大小、背景颜色、边框颜色、边框大小、文字大小、文字颜色、分隔符,一般来说默认是英文分号( : ),不过也有使用小横杠的( - ),那么怎么去设置样式呢?在 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_width
、layout_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_height
为match_parent
,实际上也是wrap_content
。那么根据测量的结果最后就是一个局限性,如果我们没有设置方框的大小的话,那么默认是48,间距为4,那么最终结果就是宽:308,高:48,我画了一个图来进行说明(有点抽象,能理解就可以)。
④ 绘制
测量好了之后,下面就可以开始绘制了,绘制就相当于在纸上画画,而画画呢,首先要有画笔,首先声明变量,代码如下:
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地址输入框(下)https://developer.aliyun.com/article/1407655