一、前言
对于自定义View相信这是每一个初学者心里的痛,但开发久了,慢慢的你就会发现,其实自定义View并不难,看几篇基础文章,懂得了原理和流程套路,用的多了,实战多了,自然而然就掌握了。近期在时间上还算空余,所以打算在学习新的东西的同时,把旧的基础的东西也好好梳理一下,总不能捡一路丢一路吧。
二、自定义View
首先先总结一下一般自定义View的步骤,让我们在写的时候能有一个大致的方向:
1.继承一个View或者一个我们需要的View的子类,并添加构造方法
2.重写onMeasure方法
3.重写onDraw方法
4.在xml中自定义View属性
看了很多文章都是先设置View属性,这主要是看个人习惯吧,因为我在写自定义View的时候,有时候属性想的不够全面或者写的多余最后用不到,修改起来比较频繁,所以我就习惯先将需要的属性通过成员变量赋予固定的测试值,当自定义的view成型了,再把测试值替换成xml中设置的View属性值。
闲话不多说了,我们直接来拿一个小例子来从头到尾的实现一下吧,虽然没有实际用处吧,这种效果完全可以通过xml的shape实现,在这里就当是为了熟悉一下自定义View的流程吧,效果图如下:
这是我们自定义的View类,我们添加了不同参数的构造方法,并进行了相关初始化
public class MyCustomSimpleView extends View{
//显示的文本
String title;
//字体的大小
int wordSize;
//字体的颜色
int wordCorlor;
//背景颜色
int bgCorlor;
//搜索栏出去左右两个半圆的长度
int searchLength;
//搜索栏的高度
int searchHight;
//搜索栏文本所占的区域
Rect rect;
//写字的笔
Paint worldPaint;
//画背景的笔
Paint bgPaint;
//在代码中直接new的时候,会调用一个参数构造方法
public MyCustomSimpleView(Context context) {
this(context, null);
}
//在xml中使用自定义控件的时候,不管有没有使用我们自己的自定义属性都会调用两个参数构造方法
public MyCustomSimpleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
//这个构造方法系统不会默认调用,需要我们自己主动调用
public MyCustomSimpleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
wordSize = 16;
title = "搜索更多内容";
wordCorlor = 0xFF6a6aff;//浅蓝
bgCorlor = 0xFFbebebe;//浅灰
worldPaint = new Paint();
worldPaint.setTextSize(CommentUtils.sp2px(context, wordSize));
worldPaint.setColor(wordCorlor);
bgPaint = new Paint();
bgPaint.setColor(bgCorlor);
rect = new Rect();//通过rect我们要获取title内容的宽度和高度
worldPaint.getTextBounds(title, 0, title.length(), rect);
}
}
通常我们习惯写上三个构造方法,这三个构造方法的使用时机请参考代码的注释
我们要重写onMeasure方法,我们知道,在xml中我们的layout_width和layout_height可以是wrap_content、match_parent或者固定大小,可其中wrap_content并没有指定真正大小,可是我们要绘制的view是要有具体的宽度的,所以这就要求我们自己去测量尺寸提供给父View让其给我们提供期望大小的区域,为此系统给我们提供了MeasureSpec类,通过MeasureSpec类我们可以获取父view提供给我们的宽高尺寸和其所用的测量模式。
测量模式分为3种:
1.EXACTLY:提供的尺寸就是当前view所应该取的尺寸,一般设置了明确的固定大小或者match_parent
2.AT_MOST:提供的尺寸就是当前view所能取的最大尺寸,一般设置了wrap_content
3.UNSPECIFIED:父view对当前view的尺寸没有限制,当前view想要多大都可以,一般很少用到
这是我们的onMeasure方法,对于layout_width和layout_height的不同情况做了相应的处理
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthModel = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightModel = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
switch (heightModel) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
//文字高度+上下边距
heightSize = rect.height() + getPaddingTop() + getPaddingBottom();
//当高度为wrap_content时
searchHight = rect.height();
break;
case MeasureSpec.EXACTLY:
//当高度全屏或者固定尺寸时候
searchHight = heightSize - getPaddingTop() - getPaddingBottom();
break;;
}
switch (widthModel) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
//文字宽度+两侧半圆半径
widthSize = rect.width() + getPaddingLeft() + getPaddingRight()
+ searchHight/2 + getPaddingRight() + searchHight/2 + getPaddingLeft();
//当宽度为wrap_content时,搜索栏宽度(不包括两边半圆)为文字的宽度
searchLength = rect.width();
break;
case MeasureSpec.EXACTLY:
//当宽度全屏或者固定尺寸时候,宽度-左右半圆宽度-左右边距
searchLength = widthSize - (searchHight/2 + getPaddingRight() + searchHight/2 + getPaddingLeft()) - getPaddingLeft() - getPaddingRight();
break;
}
setMeasuredDimension(widthSize, heightSize);
}
重写onDraw方法,通过我们上面定义的画笔在canvas上绘制就行啦,其实不管多么复杂的画面,都是由一个个最基础的元素构成的,Android为我们提供了画线、画圆、画弧形、画矩形等等的方法,我们用到的时候直接去查看api开发文档就行了,不需要死记硬背那么多,同样本例中的绘制方法也不需要完全缕清,无非是找到需要的点的坐标罢了,跟对于自定义view的理解没有关系
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(worldPaint != null){
//左侧加个半圆
RectF rectF_left = new RectF(0, 0
, 2 * getPaddingLeft() + searchHight
, searchHight + getPaddingTop() + getPaddingBottom());
canvas.drawArc(rectF_left, 90, 180, false, bgPaint);
//画一个矩形
canvas.drawRect(searchHight/2 + getPaddingLeft(), 0
, searchLength + 2*getPaddingLeft() + getPaddingRight() + searchHight/2
, searchHight + getPaddingTop() +getPaddingBottom(), bgPaint);
//在画的矩形区域上写内容,在这里默认都是padding 上下内边距一致 左右内边距一致
canvas.drawText(title
, getPaddingLeft() + getPaddingLeft() + searchHight/2
, (searchHight + getPaddingTop() + getPaddingBottom())/2 + rect.height()/2, worldPaint);
//右侧加个半圆
RectF rectF_right = new RectF(searchLength + 2 * getPaddingLeft()
, 0, searchLength + 2 * getPaddingLeft() + 2 * getPaddingRight() + searchHight
, searchHight + getPaddingTop() + getPaddingBottom());
canvas.drawArc(rectF_right, -90, 180, false, bgPaint);
}
}
好了,我们的自定义View基本已经成型了,但是你会发现我们的属性都是写死的测试值,不用担心,用我们自定义的属性替换下就好啦。我们先在res/values目录下新建一个attrs.xml:
<resources>
<!--最好是自定义View的类名,查看起来一目了然-->
<declare-styleable name="MyCustomSimpleView">
<attr name="wordSize" format="integer"/>
<attr name="test" format="string"/>
<attr name="wordCorlor" format="integer"/>
<attr name="bgCorlor" format="integer"/>
</declare-styleable>
</resources>
我们的xml布局文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:custom="http://schemas.android.com/apk/res-auto">
<custom.com.mycustomviewdemo.widget.MyCustomSimpleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_margin="20dp"
custom:test="搜索更多内容"
custom:wordSize="16"
custom:wordCorlor="0xFF6a6aff"
custom:bgCorlor="0xFFbebebe"
/>
</RelativeLayout>
注意需要在根目录引入我们的命名空间xmlns:custom=”http://schemas.android.com/apk/res-auto”,其中custom我们可以更改,没有具体名字要求
将我们自定义View里的测试值改成从xml属性中获取
private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
TypedArray array = context.getTheme().obtainStyledAttributes(attrs
, R.styleable.MyCustomSimpleView
, defStyleAttr, 0);
title = array.getString(R.styleable.MyCustomSimpleView_test);
wordSize = array.getInt(R.styleable.MyCustomSimpleView_wordSize, 16);
wordCorlor = array.getInt(R.styleable.MyCustomSimpleView_wordCorlor, 0xFF6a6aff);//默认浅蓝
bgCorlor = array.getInt(R.styleable.MyCustomSimpleView_bgCorlor, 0xFFbebebe);//默认浅灰
//记得使用完销毁
array.recycle();
worldPaint = new Paint();
worldPaint.setTextSize(CommentUtils.sp2px(context, wordSize));
worldPaint.setColor(wordCorlor);
bgPaint = new Paint();
bgPaint.setColor(bgCorlor);
rect = new Rect();
worldPaint.getTextBounds(title, 0, title.length(), rect);
}
三、总结
到此我已经基本介绍完自定义简单view的流程了,但其中涉及到的很多知识点,都没有具体的去深入介绍,充其量只是有个使用方法罢了,如果你感兴趣可以通过切入一个知识点,单独的再去查资料深入剖析一下。