虽然Android API给我们提供了众多控件View来使用,但是鉴于Android的开发性,自然少不了根据需求自定义控件View了。比如说QQ头像是圆形的,但是纵观整个Android控件也找不到一个加载圆形图片的Button或者ImageView,那么咋办?废话,肯定是自定义一个圆形RoundImageView控件啦!这里我们可以继承ImageView重写里面的方法来实现这一效果。还有一种自定义控件是继承View重写里面的onDraw()方法,这类自定义View需要定义自己的属性以备在xml布局文件中使用。

自定义View的步骤

  1. 自定义View的属性
  2. 在自定义View的构造方法中获得View属性值
  3. 重写onMeasure(int,int)方法。(该方法可重写可不重写,具体看需求)
  4. 重写onDraw(Canvas canvas)方法。
  5. 在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>

自定义属性分两步:

  1. 定义公共属性
  2. 定义控件的主题样式

如上面的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模式说起了。一共有三种模式:

  1. MeasureSpec.EXACTLY:父视图希望子视图的大小是specSize中指定的大小;一般是设置了明确的值或者是MATCH_PARENT
  2. MeasureSpec.AT_MOST:子视图的大小最多是specSize中的大小;表示子布局限制在一个最大值内,一般为WARP_CONTENT
  3. 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);
}

这样就可以实现第一张图片的效果了。解释都在代码里了。

源码下载

Android自定义控件View(一)的更多相关文章

  1. Android自定义控件View(三)组合控件

    不少人应该见过小米手机系统音量控制UI,一个圆形带动画效果的音量加减UI,效果很好看.它是怎么实现的呢?这篇博客来揭开它的神秘面纱.先上效果图 相信很多人都知道Android自定义控件的三种方式,An ...

  2. Android自定义控件View(二)继承控件

    在前一篇博客中学习了Android自定义控件View的流程步骤和注意点,不了解的童鞋可以参考Android自定义控件View(一).这一节开始学习自定义控件View(二)之继承系统已有的控件.我们来自 ...

  3. android 自定义控件View在Activity中使用findByViewId得到结果为null

    转载:http://blog.csdn.net/xiabing082/article/details/48781489 1.  大家常常自定义view,,然后在xml 中添加该view 组件..如果在 ...

  4. android自定义控件一站式入门

    自定义控件 Android系统提供了一系列UI相关的类来帮助我们构造app的界面,以及完成交互的处理. 一般的,所有可以在窗口中被展示的UI对象类型,最终都是继承自View的类,这包括展示最终内容的非 ...

  5. Android自定义控件之自定义ViewGroup实现标签云

    前言: 前面几篇讲了自定义控件绘制原理Android自定义控件之基本原理(一),自定义属性Android自定义控件之自定义属性(二),自定义组合控件Android自定义控件之自定义组合控件(三),常言 ...

  6. Android自定义控件之自定义组合控件

    前言: 前两篇介绍了自定义控件的基础原理Android自定义控件之基本原理(一).自定义属性Android自定义控件之自定义属性(二).今天重点介绍一下如何通过自定义组合控件来提高布局的复用,降低开发 ...

  7. Android自定义控件之自定义属性

    前言: 上篇介绍了自定义控件的基本要求以及绘制的基本原理,本篇文章主要介绍如何给自定义控件自定义一些属性.本篇文章将继续以上篇文章自定义圆形百分比为例进行讲解.有关原理知识请参考Android自定义控 ...

  8. Android自定义控件之基本原理

    前言: 在日常的Android开发中会经常和控件打交道,有时Android提供的控件未必能满足业务的需求,这个时候就需要我们实现自定义一些控件,今天先大致了解一下自定义控件的要求和实现的基本原理. 自 ...

  9. Android 自定义View合集

    自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...

随机推荐

  1. 【ASP.NET开发】.NET三层架构简单解析

    对于三层架构来说,主要是使用设计模式的思想,对于项目的各个模块实现"高内聚,低耦合"的思想.这里就不做详细的介绍了,如果大家有兴趣,可以阅读软件工程和设计模式相关文章. 对于三层架 ...

  2. IDEA配置svn地址方法及出现的问题的解决办法

    1.在IDEA中点击File-Settings里面,如图所示,选择你本地装的svn的exe路径: 2.在如图所示菜单中配置svn地址: 问题1:如果svn路径下没有exe文件,则是装svn的时候没有安 ...

  3. 【博客之星】CSDN2013博客之星--分析和预测

    本文纯属个人见解,多有得罪啊! 具体结果,还是看最后CSDN给的结果吧! 昵称 名字 题材 质量 数量 知名度 预测 阳光岛主 杨刚 Python,Clojure,SAE 很高 346+ 很大 一定( ...

  4. 【例题 7-13 UVA-1374】Power Calculus

    [链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] 结论:每次只用新生成的数字就好了. 然后就是IDA*了. 迭代深搜+剪枝. [代码] /* 1.Shoud it use long ...

  5. mycat 之datanode datahost writehost readhost 区别(转)

    <?xml version="1.0"?> <!DOCTYPE mycat:schema SYSTEM "schema.dtd"> &l ...

  6. spring扫描自定义注解并进行操作

    转载:http://blog.csdn.net/cuixuefeng1112/article/details/45331233 /**  * 扫描注解添加服务到缓存以供判断时候为对外开放service ...

  7. 关于jsonp跨域的问题以及解决方法(跨域、同源与非同源)

    什么是跨域? 想要了解跨域,首先需要了解下浏览器的同源机制: JSONP和AJAX相同,都是客户端向服务器端发送请求:给服务器端传递数据 或者 从服务器端获取数据 的方式 JSONP属于非同源策略(跨 ...

  8. 【Codeforces Round #445 (Div. 2) A】ACM ICPC

    [链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] 三重循环 [代码] #include <bits/stdc++.h> using namespace std; int ...

  9. 百度糯米iOSclient登录BUG

    环境 设备:iphone5s 网络:WIFI App版本号: 操作步骤 1.进入登录界面 2.输入手机号 3.点击[获取验证码],等待接收验证码后 4.点击[X]退出登录界面 5.反复1-2-3,提示 ...

  10. VMware linux虚拟机在线识别新添加磁盘

    登录进虚拟机linux系统中执行以下命令,识别新增加的硬盘 echo "- - -" > /sys/class/scsi_host/host0/scan # ls /sys/ ...