自定义 view
自定义 view 的主要步骤如下:
- 自定义 view 属性
- 在 view 的构造方法中,加载自定义的属性
- 重写 onLayout
- 重写 onMeasure
- 重写 onDraw
自定义 view 属性
自定义View的属性,首先在res/values/ 下建立一个attrs.xml
stye.xml
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="customViewStyle">@style/custom_view_style</item>
</style>
<style name="text_style">
<item name="android:text">text from style</item>
</style>
<style name="default_view_style">
<item name="attr4">attr4 from default_view_style</item>
</style>
<style name="custom_view_style">
<item name="attr3">attr3 from custom_view_style</item>
<item name="attr4">attr4 from custom_view_style</item>
</style>
<style name="xml_style">
<item name="attr2">attr2 from xml_style</item>
<item name="attr3">attr3 from xml_style</item>
</style>
<attr name="myViewText" format="string" />
<attr name="myViewTextColor" format="color" />
<attr name="myViewTextSize" format="dimension" />
<declare-styleable name="myView">
<attr name="myViewText" />
<attr name="myViewTextColor" />
<attr name="myViewTextSize" />
<attr name="attr1" format="string" />
<attr name="attr2" format="string" />
<attr name="attr3" format="string" />
<attr name="attr4" format="string" />
<attr name="attr5" format="string" />
<attr name="attr6" format="string" />
</declare-styleable>
<attr name="customViewStyle" format="reference" />
</resources>
在主布局中加载自定义的 view
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:custum="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<!--注意xmlns导入的是整个工程的包名,不是 view 的包名-->
<!-- 在 gradle工程中只需要 xmlns:custum="http://schemas.android.com/apk/res-auto"
-->
<io.github.xuyushi.viewtest.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
custum:myViewText="test"
custum:myViewTextColor="#ffff00"
custum:myViewTextSize="30sp"
/>
</RelativeLayout>
注意xmlns导入的是整个工程的包名,不是 view 的包名.在 gradle工程中只需要 xmlns:custum="http://schemas.android.com/apk/res-auto"
2.自定义 view
自定义 view 继承 view
package io.github.xuyushi.viewtest;
import android.content.Context;
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.util.Log;
import android.view.View;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
/**
* Created by xuyushi on 15/10/23.
*/
public class MyView extends View {
static final String LOG_TAG = "MyView";
private String mText;
private int mextColor;
private int mextSize;
private Rect mBound;
private Paint mPaint;
// TODO: 15/10/23 三个构造函数分别什么时候调用
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.customViewStyle);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.getTheme()
.obtainStyledAttributes(attrs, R.styleable.myView, defStyleAttr, R.style.default_view_style);
int n = array.getIndexCount();
Log.d(LOG_TAG, "attr1 => " + array.getString(R.styleable.myView_attr1));
Log.d(LOG_TAG, "attr2 => " + array.getString(R.styleable.myView_attr2));
Log.d(LOG_TAG, "attr3 => " + array.getString(R.styleable.myView_attr3));
Log.d(LOG_TAG, "attr4 => " + array.getString(R.styleable.myView_attr4));
Log.d(LOG_TAG, "attr5 => " + array.getString(R.styleable.myView_attr5));
Log.d(LOG_TAG, "attr6 => " + array.getString(R.styleable.myView_attr6));
for (int i = 0; i < n; i++) {
int attr = array.getIndex(i);
switch (attr) {
case R.styleable.myView_myViewText:
mText = array.getString(attr);
break;
case R.styleable.myView_myViewTextColor:
mextColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.myView_myViewTextSize:
mextSize = array.getDimensionPixelSize(attr,
getResources().getDimensionPixelSize(R.dimen.textsize));
break;
}
}
array.recycle(); //注意释放
mPaint = new Paint();
mPaint.setTextSize(mextSize);
mBound = new Rect();
Log.d("MyView", "height" + mBound.height());
mPaint.getTextBounds(mText, 0, mText.length(), mBound);
//调用getTextBounds 方法之后,会根据mText计算mbound 的大小
Log.d("MyView", "height" + mBound.height());
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mText = randText();
//长度改变,需要重新 layout
requestLayout();
invalidate();
}
});
}
private String randText() {
Random random = new Random();
Set<Integer> set = new HashSet<Integer>();
while (set.size() < 4) {
int randomInt = random.nextInt(10);
set.add(randomInt);
}
StringBuffer sb = new StringBuffer();
for (Integer i : set) {
sb.append("" + i);
}
// return sb.toString();
return "1234567";
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
//match parent 或者直接写定大小
width = widthSize;
} else {
//wrap content
mPaint.setTextSize(mextSize);
mPaint.getTextBounds(mText, 0, mText.length(), mBound);
float textWidth = mBound.width();
int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
width = desired;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
mPaint.setTextSize(mextSize);
mPaint.getTextBounds(mText, 0, mText.length(), mBound);
float textHeight = mBound.height();
int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
height = desired;
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.RED);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
mPaint.setColor(mextColor);
canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2,
getHeight() / 2 + mBound.height() / 2
, mPaint);
Log.d("onDraw", "height" + mBound.height());
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
}
函数词典
/*
public void getTextBounds (char[] text, int index, int count, Rect bounds)
Added in API level 1
Return in bounds (allocated by the caller) the smallest rectangle that encloses all of the characters, with an implied origin at (0,0).
Parameters
text Array of chars to measure and return their unioned bounds
index Index of the first char in the array to measure
count The number of chars, beginning at index, to measure
bounds Returns the unioned bounds of all the text. Must be allocated by the caller.
*/
/*
Added in API level 1
Draw the text, with origin at (x,y), using the specified paint. The origin is interpreted based on the Align setting in the paint.
Parameters
text The text to be drawn
x The x-coordinate of the origin of the text being drawn
y The y-coordinate of the baseline of the text being drawn
paint The paint used for the text (e.g. color, size, style)
*/
view的构造函数
可以看到自定义的 view 有三个构造方法
- public void MyView(Context context) {}
- public void MyView(Context context, AttributeSet attrs) {}
- public void MyView(Context context, AttributeSet attrs, int defStyle) {}
三个构造方法调用的情况
- code动态创建一个view而不使用布局文件xml inflate
- 多了一个AttributeSet类型的参数,在通过布局文件xml创建一个view时,这个参数会将xml里设定的属性传递给构造函数。如果你采用xml inflate的方法却没有在code里实现C2,那么运行时就会报错。但是由于编译能顺利通过
- 在源码中通常是自己的显示调用
官方文档如下
Perform inflation from XML and apply a class-specific base style. This constructor of View allows subclasses to use their own base style when they are inflating. For example, a Button class's constructor would call this version of the super class constructor and supply R.attr.buttonStyle for defStyle; this allows the theme's button style to modify all of the base view attributes (in particular its background) as well as the Button class's attributes.
对第三个参数的解释是:
An attribute in the current theme that contains a reference to a style resource to apply to this view. If 0, no default style will be applied.
代码第40
行, **注意传入的参数是0 **
TypedArray array = context.getTheme()
.obtainStyledAttributes(attrs, R.styleable.myView, defStyleAttr, 0);
接着分析obtainStyledAttributes
官方文档
public TypedArray obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
- set:在XML中明确写出了的属性集合,如
android:layout_width
, 在本例子中即myView_myViewTextColor
- defStyleAttr: 代表需要查询自定义的属性值的 view,在本程序中指
myView
- defStyleAttr:这是一个定义在attrs.xml文件中的attribute。这个值起作用需要两个条件:1. 值不为0;2. 在Theme中使用了
- defStyleRes:这是在styles.xml文件中定义的一个style。只有当defStyleAttr没有起作用,才会使用到这个值
如果在Code中实例化一个View会调用第一个构造函数,如果在xml中定义会调用第二个构造函数,而第三个函数系统是不调用的,要由View(我们自定义的或系统预定义的View,如此处的CustomTextView和Button)显式调用,比如在这里我们在第二个构造函数中调用了第三个构造函数,并将R.attr.CustomizeStyle传给了第三个参数。
第三个参数的意义就如同它的名字所说的,是默认的Style,只是这里没有说清楚,这里的默认的Style是指它在当前Application或Activity所用的Theme中的默认Style
显然,一个属性最终的取值,有一个顺序问题,这个顺序优先级从高到低依次是:
结论
- 直接在XML文件中定义的。
- 在XML文件中通过style这个属性定义的。
- 通过defStyleAttr定义的。
- 通过defStyleRes定义的。
- 直接在当然工程的theme主题下定义的。
实现过程
1. 在 view 的 attr 中增加属性
<declare-styleable name="myView">
<attr name="myViewText" />
<attr name="myViewTextColor" />
<attr name="myViewTextSize" />
<attr name="attr1" format="string" />
<attr name="attr2" format="string" />
<attr name="attr3" format="string" />
<attr name="attr4" format="string" />
<attr name="attr5" format="string" />
<attr name="attr6" format="string" />
</declare-styleable>
<attr name="customViewStyle" format="reference" />
注意要将customViewStyle
的格式为reference
2. 在 style 中定义custom_view_style
<style name="custom_view_style">
<item name="attr3">attr3 from custom_view_style</item>
<item name="attr4">attr4 from custom_view_style</item>
</style>
3.在xml 中定义 xml_style
<style name="xml_style">
<item name="attr2">attr2 from xml_style</item>
<item name="attr3">attr3 from xml_style</item>
</style>
4. 为了使 theme 生效
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!— Customize your theme here. —>
<item name="customViewStyle">@style/custom_view_style</item>
<item name="attr5">attr5 from AppTheme</item>
<item name="attr6">attr6 from AppTheme</item>
</style>
5. 布局文件中定义
<io.github.xuyushi.viewtest.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
custum:myViewText="test"
custum:myViewTextColor="#ffff00"
custum:myViewTextSize="30sp"
custum:attr1="attr1 from xml"
custum:attr2="attr2 from xml"
style="@style/xml_style" />
6. 将最后一个参数由0变为
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomView, defStyle, R.style.default_view_style);
7. 在 view 的构造函数中获取这些 attr1-6的属性
log
10-24 02:05:37.136 4769-4769/? D/MyView﹕ attr1 => attr1 from xml
10-24 02:05:37.136 4769-4769/? D/MyView﹕ attr2 => attr2 from xml
10-24 02:05:37.136 4769-4769/? D/MyView﹕ attr3 => attr3 from xml_style
10-24 02:05:37.136 4769-4769/? D/MyView﹕ attr4 => attr4 from default_view_style
10-24 02:05:37.136 4769-4769/? D/MyView﹕ attr5 => attr5 from default_view_style
10-24 02:05:37.136 4769-4769/? D/MyView﹕ attr6 => null
分析结果
- attr1只在xml布局文件中设置,所以值为attr1 from xml。
- attr2在xml布局文件和xml style中都设置了,取值为布局文件中设置的值,所以为attr2 from xml。
- attr3没有在xml布局文件中设置,但是在xml style和defStyleAttr定义的style中设置了,取xml style中的值,所以值为attr3 from xml_style。
- attr4只在defStyleAttr定义的style中设置了,所以值为attr4 from custom_view_style。
- attr5和attr6没有在任何地方设置值,所以为null。
总结
- 直接在XML布局文件中设置的值优先级最高,如果这里设置了值,就不会去取其他地方的值了。
- XML布局文件中有一个叫“style”的属性,它指向一个style,在这个style中设置的属性值优先级次之。
- 如果上面两个地方都没有设置值,那么就会根据View带三个参数的构造方法中的第三个参数attribute指向的style设置值,前提是这个attribute的值不为0。
- 如果上面的attribute设置为0了,我们就根据obtainStyledAttributes()方法中的最后一个参数指向的style来设置值。
- 如果仍然没有设置到值,就会用theme中直接设置的属性值,而不会去管第3步和第4步中是否设置了值。
** 参考 http://www.cnblogs.com/angeldevil/p/3479431.html 详细解释**
onMeasure
onMeasure 是测量视图的大小的
MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型
- EXACTLY:表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
- AT_MOST:最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。通常是 wrapcontent
- UNSPECIFIED:表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制
当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure
计算长宽 最后调用 setMeasuredDimension(width, height);
onLayout
确定视图的位置,主要是在 viewgroup 中使用
ViewGroup中的onLayout()方法是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法。这个例子 layout 只有一个子 view
public class SimpleLayout extends ViewGroup {
public SimpleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 0) {
View childView = getChildAt(0);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
}
代码非常的简单,我们来看下具体的逻辑吧。你已经知道,onMeasure()方法会在onLayout()方法之前调用,因此这里在onMeasure()方法中判断SimpleLayout中是否有包含一个子视图,如果有的话就调用measureChild()方法来测量出子视图的大小。
接着在onLayout()方法中同样判断SimpleLayout是否有包含一个子视图,然后调用这个子视图的layout()方法来确定它在SimpleLayout布局中的位置,这里传入的四个参数依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分别代表着子视图在SimpleLayout中左上右下四个点的坐标。其中,调用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中测量出的宽和高。
在onLayout()过程结束后,我们就可以调用getWidth()方法和getHeight()方法来获取视图的宽高了,否则得到是 0
onDraw
ViewGroup
参考
http://blog.csdn.net/lmj623565791/article/details/24252901
http://blog.csdn.net/lmj623565791/article/details/38339817