虽然Android API给我们提供了众多控件View来使用,但是鉴于Android的开发性,自然少不了根据需求自定义控件View了。比如说QQ头像是圆形的,但是纵观整个Android控件也找不到一个加载圆形图片的Button或者ImageView,那么咋办?废话,肯定是自定义一个圆形RoundImageView控件啦!这里我们可以继承ImageView重写里面的方法来实现这一效果。还有一种自定义控件是继承View重写里面的onDraw()方法,这类自定义View需要定义自己的属性以备在xml布局文件中使用。
自定义View的步骤
- 自定义View的属性
- 在自定义View的构造方法中获得View属性值
- 重写onMeasure(int,int)方法。(该方法可重写可不重写,具体看需求)
- 重写onDraw(Canvas canvas)方法。
- 在xml布局文件中如何使用自定义view的属性?
自定义View的属性
在res/values下面新建attrs.xml属性文件。我们看看atrrs.xml文件怎么写?
<?xml version="1.0" encoding="utf-8"?> <resources> <!--name 是自定义属性名,一般采用驼峰命名,可以随意。 format 是属性的单位--> <attr name="titleSize" format="dimension"></attr> <attr name="titleText" format="string"></attr> <attr name="titleColor" format="color"></attr> <attr name="titleBackgroundColor" format="color"></attr> <!--name 是自定义控件的类名--> <declare-styleable name="MyCustomView"> <attr name="titleSize"></attr> <attr name="titleText"></attr> <attr name="titleColor"></attr> <attr name="titleBackgroundColor"></attr> </declare-styleable> </resources>
自定义属性分两步:
- 定义公共属性
- 定义控件的主题样式
如上面的xml文件第一部分是公共的属性,第二部分是自定义控件MyCustomView的主题样式,该主题样式里的属性必须包含在公共属性里面。言外之意就是公共属性可以被多个自定义控件主题样式使用。有些人可能会纠结format字段后面都有哪些属性单位?如果你是使用AS开发的话IDE会自动有提示,基本包括如下:
dimension(字体大小)string(字符串)color(颜色)boolean(布尔类型)float(浮点型)integer(整型)enmu(枚举)fraction(百分比)等。不了解的可以百度一把。
获得View属性值
自定义View一般需要实现一下三个构造方法
public MyCustomView(Context context) { this(context, null); } public MyCustomView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
从代码中不难看出,这三个构造方法是层调用一层的,是个递进关系,因此,我们只需要在最后一个构造方法中来获得View的属性了。看代码:
public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); final Resources.Theme theme = context.getTheme(); TypedArray a = theme.obtainStyledAttributes(attrs, R.styleable.MyCustomView, defStyleAttr, 0); if (null != a) { int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.MyCustomView_titleColor: titleColor = a.getColor(attr, Color.BLACK); break; case R.styleable.MyCustomView_titleSize: titleSize = a.getDimensionPixelSize(attr, titleSize); break; case R.styleable.MyCustomView_titleText: titleText = a.getString(attr); break; case R.styleable.MyCustomView_titleBackgroundColor: titleBackgroundColor = a.getColor(attr, Color.WHITE); break; } } a.recycle(); init(); } }
第一步通过theme.obtainStyledAttributes()方法获得自定义控件的主题样式数组。第二步就是遍历每个属性来获得对应属性的值,也就是我们在xml布局文件中写的属性值。注意:在分支case里R.styleable.后面的属性名称有一个规则:控件的样式主题名 +“_”+ 属性名,循环结束之后记得调用a.recycle()回收资源。至此就获得了自定义控件的属性值了。至于为什么这样来获得属性值?具体可以参考Android 系统的TextView源码里的构造方法。
重写onDraw()方法来绘制View控件
这一步进行的操作是将你需要显示的控件View的内容绘制到画布Canvas上面。例如我们在一个圆里面写字,先来效果图
onDraw方法实现如下:
............. /** * 初始化 */ private void init() { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setTextSize(titleSize); /** * 得到自定义View的titleText内容的宽和高 */ mBound = new Rect(); mPaint.getTextBounds(titleText, 0, titleText.length(), mBound); } ................ @Override protected void onDraw(Canvas canvas) { mPaint.setColor(titleBackgroundColor); canvas.drawCircle(getWidth() / 2f, getWidth() / 2f, getWidth() / 2f, mPaint); mPaint.setColor(titleColor); canvas.drawText(titleText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint); }
先new一个Paint实例初始化画笔,给画笔设置文字大小,然后先给画笔设置一个背景颜色,在画一个圆,再次设置画笔的文字颜色,在绘制字符串到画布,最后就得到如上图片的效果了。
布局中使用自定义View
使用自定义View控件需要在根布局中添加xmlns:custom=”http://schemas.android.com/apk/res-auto”命名空间。其中前缀名:”custom” 也是自定义的,可以是除了被Android系统使用过的字眼以外的任何字符串,自然你这里了也可以写成“myCustom”。不知道在Android哪个版本之前命名控件是这样应用的xmlns:custom=”http://schemas.android.com/apk/res/com.xjp.customview。res/后面的是自定义控件所在的包名。当然只要你代码不报错两种命名空间都是可以的。只是我用的AS开发,然后targetSdkVersion是21,因此我用的是第一种命名空间。
代码如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="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"> <com.xjp.customview.MyCustomView android:layout_width="wrap_content" android:layout_height="match_parent" custom:titleColor="@android:color/black" custom:titleSize="25sp" custom:titleBackgroundColor="#ff0000" custom:titleText="自定义的View" /> </RelativeLayout>
从上面的代码你会发现,凡是自定义的属性使用时候的前缀是命名空间名称 custom。
至此,整个自定义View的流程就跑通了。贴出整个代码部分如下:
package com.xjp.customview; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; /** * Description:自定义控件View * User: xjp * Date: 2015/5/27 * Time: 14:50 */ public class MyCustomView extends View { private static final String TAG = "MyCustomView"; private static final boolean DEBUG = false; private String titleText = "Hello world"; private int titleColor = Color.BLACK; private int titleBackgroundColor = Color.WHITE; private int titleSize = 16; private Paint mPaint; private Rect mBound; public MyCustomView(Context context) { this(context, null); } public MyCustomView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); final Resources.Theme theme = context.getTheme(); TypedArray a = theme.obtainStyledAttributes(attrs, R.styleable.MyCustomView, defStyleAttr, 0); if (null != a) { int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.MyCustomView_titleColor: titleColor = a.getColor(attr, Color.BLACK); break; case R.styleable.MyCustomView_titleSize: titleSize = a.getDimensionPixelSize(attr, titleSize); break; case R.styleable.MyCustomView_titleText: titleText = a.getString(attr); break; case R.styleable.MyCustomView_titleBackgroundColor: titleBackgroundColor = a.getColor(attr, Color.WHITE); break; } } a.recycle(); init(); } } /** * 初始化 */ private void init() { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setTextSize(titleSize); /** * 得到自定义View的titleText内容的宽和高 */ mBound = new Rect(); mPaint.getTextBounds(titleText, 0, titleText.length(), mBound); } @Override protected void onDraw(Canvas canvas) { mPaint.setColor(titleBackgroundColor); canvas.drawCircle(getWidth() / 2f, getWidth() / 2f, getWidth() / 2f, mPaint); mPaint.setColor(titleColor); canvas.drawText(titleText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint); } }
运行结果图:
细心的你会发现,跟我们上面预期的效果图有不一样啊?怎么回事?布局大小的问题?
android:layout_width="wrap_content" android:layout_height="match_parent"
从布局大小来看宽度应该包裹内容,但是却充满了整个屏幕。接下来我们就要想到其实我们在自定义View的流程中还有一个onMeasure方法没有重写。
重写onMeasure控制View大小
当你没有重写onMeasure方法时候,系统调用默认的onMeasure方法。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); }
这个方法的作用是:测量控件的大小。其实Android系统在加载布局的时候是由系统测量各子View的大小来告诉父View我需要占多大空间,然后父View会根据自己的大小来决定分配多大空间给子View。那从上面的效果来看:当你在布局中设置View的大小为”wrap_content”时,其实系统测量出来的大小是“match_parent”。为什么会是这样子呢?那得从MeasureSpec的specMode模式说起了。一共有三种模式:
- MeasureSpec.EXACTLY:父视图希望子视图的大小是specSize中指定的大小;一般是设置了明确的值或者是MATCH_PARENT
- MeasureSpec.AT_MOST:子视图的大小最多是specSize中的大小;表示子布局限制在一个最大值内,一般为WARP_CONTENT
- MeasureSpec.UNSPECIFIED:父视图不对子视图施加任何限制,子视图可以得到任意想要的大小;表示子布局想要多大就多大,很少使用。
我们跳进源码看看系统默认的 super.onMeasure(widthMeasureSpec, heightMeasureSpec);是怎么实现的
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } .................. public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }
从上面的代码getDefaultSize()方法中看出,原来MeasureSpec.AT_MOST和MeasureSpec.EXACTLY走的是同一个分支,也就是父视图希望子视图的大小是specSize中指定的大小。
int specSize = MeasureSpec.getSize(measureSpec);
得出来的默认值就是填充整个父布局。因此,不管你布局大小是”wrap_content”还是“match_parent”效果都是充满整个父布局。那我想要”wrap_content”的效果怎么办?不着急,只有重写onMeasure方法了。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { /** * 测量模式 */ int widthMode = MeasureSpec.getMode(widthMeasureSpec); /** * 父布局希望子布局的大小,如果布局里面设置的是固定值,这里取布局里面的固定值和父布局大小值中的最小值. * 如果设置的是match_parent,则取父布局的大小 */ int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (DEBUG) Log.e(TAG, "the widthSize:" + widthSize + " the heightSize" + heightSize); int width; int height; Rect mBounds = new Rect(); if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else { mPaint.setTextSize(titleSize); mPaint.getTextBounds(titleText, 0, titleText.length(), mBounds); float textWidth = mBounds.width(); int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight()); width = desired; } if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { height = width; } /** * 最后调用父类方法,把View的大小告诉父布局。 */ setMeasuredDimension(width, height); }
这样就可以实现第一张图片的效果了。解释都在代码里了。
来源:https://www.cnblogs.com/Free-Thinker/p/6113461.html