Android自定义控件学习笔记一

概述

本篇文章主要是介绍用系统控件重新组合并添加上自定义属性,来实现特定效果,重点讲述自定义属性的使用。在Android系统中,自定义控件就是自定义一个类来继承View或这继承ViewGroup或者用系统控件重新组合来实现我们想要的效果。

既然说到自定义控件,那么我们为什么要使用自定义控件呢,大概原因有以下两个方面吧:

  1. 同样的控件在不同的版本上差异太大,最明显的dialog弹出框在2.x与4.x上的差异太大;
  2. 系统提供的控件功能有限或者视觉交互效果达不到UI要求,如密码输入框、刷新加载等。

View三个构造函数

我们要实现一个自定义控件,实际上都直接或间接的实现了View类,我们发现系统会让我们来实现View的构造方法,一般而言都会实现含两个参数的构造方法View(Context context, AttributeSet attrs),因为当我们在布局文件中设置控件布局属性时系统就会调用该构造方法来生成一个View实例。

  • View(Context context) 当我们在代码中创建一个View对象是,会默认调用该构造方法;
  • View(Context context, AttributeSet attrs) 当从布局文件中获取一个View时,会调用的方法;
  • View(Context context, AttributeSet attrs, int defStyleAttr) 当从布局文件中获取一个View并且这个View拥有自定义style属性是会调用该方法。

示例分析一:仿设置页面

就是这样一个简单的设置页面,当我们点击一下条目,开启则变为关闭状态,关闭则变为开启。

xml布局文件代码不在这里就贴出来了,直接贴出类源码:

public class SettingItemView extends RelativeLayout implements View.OnClickListener {
	private static final String NAMESPACE = "http://schemas.android.com/apk/res/com.sunny.customview";
	private CheckBox cb_status;
	private TextView tv_desc;
	private TextView tv_title;

	private String desc_on;
	private String desc_off;
	private boolean checked;

	// http://blog.csdn.net/altair86/article/details/34438989
	public SettingItemView(Context context) {
		this(context, null);
	}

	public SettingItemView(Context context, AttributeSet attrs) {
		// 第三个参数为0
		this(context, attrs, 0);
	}

	public SettingItemView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		iniView(context);
		String title = attrs.getAttributeValue(NAMESPACE, "title");
		desc_on = attrs.getAttributeValue(NAMESPACE, "desc_on");
		desc_off = attrs.getAttributeValue(NAMESPACE, "desc_off");
		checked = attrs.getAttributeBooleanValue(NAMESPACE, "checked", false);
		tv_title.setText(title);
		setChecked(checked);
	}

	/**
	 * 初始化布局文件
	 * 
	 * @param context
	 */
	private void iniView(Context context) {
		View.inflate(context, R.layout.setting_item_view, this);
		cb_status = (CheckBox) this.findViewById(R.id.cb_status);
		tv_desc = (TextView) this.findViewById(R.id.tv_desc);
		tv_title = (TextView) this.findViewById(R.id.tv_title);
		this.setOnClickListener(this);

	}

	/**
	 * 校验组合控件是否选中
	 */

	public boolean isChecked() {
		return cb_status.isChecked();
	}

	/**
	 * 设置组合控件的状态
	 */

	public void setChecked(boolean checked) {
		if (checked) {
			setDesc(desc_on);
		} else {
			setDesc(desc_off);
		}
		cb_status.setChecked(checked);
	}

	/**
	 * 设置 组合控件的描述信息
	 */

	public void setDesc(String text) {
		tv_desc.setText(text);
	}

	@Override
	public void onClick(View v) {
		checked = !checked;
		setChecked(checked);
	}
}

一般情况下用原有控件组合自定义一个控件需要这样四个步骤:

  1. 重写构造方法
  2. 初始化自定义控件中各个子控件;
  3. 获取自定义属性值并初始化各子控件值;
  4. 暴露或这自定义回调接口方面调用。

重写构造方法

在这里我们是两个参数的构造方法调用三个参数的构造方法,同理一个参数的构造方法调用两个参数的构造方法,这里我们简单看一下两个参数构造方法:

public SettingItemView(Context context, AttributeSet attrs) {
	this(context, attrs, 0);
}

this(context, attrs, 0),这里第三个参数0,为什么要传0,我么看一下api中的解释

If 0, no default style will be applied.

在查看一下View的源码:

public View(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

View本身也是这么使用的。

初始化自定义控件中各个子控件;

重点在这一行代码:

private void iniView(Context context) {
	View.inflate(context, R.layout.setting_item_view, this);
	cb_status = (CheckBox) this.findViewById(R.id.cb_status);
	tv_desc = (TextView) this.findViewById(R.id.tv_desc);
	tv_title = (TextView) this.findViewById(R.id.tv_title);
	this.setOnClickListener(this);
}

在这里重点看这一行代码View.inflate(context, R.layout.setting_item_view, this),既然自定义控件,我们当然是返回当前控件了,所以最后一个参数是this,这样当前控件就会作为root返回了,更详细的可以参看LayoutInflater学习笔记这篇文章。

获取属性值

在这个示例中都是依据命名控件来获取的自定义属性值,首先声明一个命名空间: http://schemas.android.com/apk/res/com.sunny.customview,重点看后面的com.sunny.customview,这是应用的包名。在布局文件中:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:trr="http://schemas.android.com/apk/res/com.sunny.customview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <com.sunny.customview.view.SettingItemView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        trr:desc_off="已关闭"
        trr:desc_on="已开启"
        trr:checked="true"
        trr:title="启用数据流量" />
</LinearLayout>

在代码中获取属性值:

String title = attrs.getAttributeValue(NAMESPACE, "title");
desc_on = attrs.getAttributeValue(NAMESPACE, "desc_on");
desc_off = attrs.getAttributeValue(NAMESPACE, "desc_off");
checked = attrs.getAttributeBooleanValue(NAMESPACE, "checked", false);

事实上这种获取属性值在最新的Android版本中已经不在推荐使用了,从Android4.2.2版本开始Android官方就建议使用res-auto方式来声明属性命名空间。

暴露或这自定义回调接口方面调用

在本例中只是定义了一部分方法供外面调用,当然了,也可以定义几个回调接口。

Android自定义属性

现在从github上下载的所有Android组件几乎都设置了自定义属性,为了规范化自定义属性,所以Android中使用的命名空间,如果我们不用命名空间,也可以获取到属性值:

<com.sunny.customview.view.CustomView
        android:layout_width="200dp"
        android:layout_height="100dp"
        android:layout_gravity="center"
        desc="this is first view"
        android:background="#f00"
        trr:flag="true" />
int count = attrs.getAttributeCount();
for (int i = 0; i < count; i++) {
	System.out.println("aaaaaaaa:" + attrs.getAttributeName(i) + " : " + attrs.getAttributeValue(i));
}

没有命名空间,我们仍然获取到了desc的属性值,所以当我们获取属性的时候命名空间并不是必须的,但是为了使我们的自定义控件看上去是规范专业人写的,也为了使用者更好的使用,Android4.2.2开始,自定义控件命名空间采用xmlns:app="http://schemas.android.com/apk/res-auto"。目的就是为了解决当我们的控件是一个lib库的时候,这时候如果仍然使用xmlns:app="http://schemas.android.com/apk/res/包路径",就会出现找不到自定义属性的错误 。

attr属性

在Android系统中attr属性有10中,属性名全部小写:

  • reference:参考某一资源ID <attr name = "background" format = "reference" />
  • color:颜色值 <attr name = "textColor" format = "color" />
  • boolean:布尔值 <attr name = "focusable" format = "boolean" />
  • dimension:尺寸值 <attr name = "layout_width" format = "dimension" />
  • float:浮点值<attr name = "fromAlpha" format = "float" />
  • integer:整型值<attr name = "frameDuration" format="integer" />
  • string:字符串<attr name = "apiKey" format = "string" />
  • fraction:百分数<attr name = "pivotX" format = "fraction" />
  • enum:枚举值 <attr name="orientation"><enum name="horizontal" value="0"/><enum name="vertical" value="1" /></attr>
  • flag:位或运算<attr name="windowSoftInputMode"><flag name = "adjustResize" value = "0x10" /></attr>

自定义属性示例解析

在矩形中间区域画一个图形,如果flag为true,则画一个圆,否则画一个正方形,所化图形的色彩值可以自定义,也可以不定义直接继承theme中默认样式。

<com.sunny.customview.view.CustomView
	android:layout_width="200dp"
	android:layout_height="100dp"
	android:layout_gravity="center"
	android:background="#f00"
	trr:bg_color="#ff0"
	trr:flag="true" />

<com.sunny.customview.view.CustomView
	android:layout_width="200dp"
	android:layout_height="100dp"
	android:layout_gravity="center"
	android:layout_marginTop="15dp"
	android:background="#f00" />

可以看出第二个自定义CustomView没有添加任何自定义属性样式,仍然画了一个圆,而且色彩是绿色的。

CustomView(Context context, AttributeSet attrs, int defStyle)

public CustomView(Context context, AttributeSet attrs, int defStyle) {
	super(context, attrs, defStyle);
	TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
	width = a.getLayoutDimension(0, width);
	height = a.getLayoutDimension(1, height);
	a.recycle();

	a = context.obtainStyledAttributes(attrs, R.styleable.CustomView, R.attr.customViewStyle,
			R.style.customViewWidget2);

	flag = a.getBoolean(R.styleable.CustomView_flag, flag);
	bgColor = a.getColor(R.styleable.CustomView_bg_color, bgColor);
	a.recycle();

	paint = new Paint();
	r = height / 2;
}

将三个参数的构造方法重点就在obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)这个方法的使用上

  • AttributeSet set这个参数基本不用管直接将构造方法中的参数传入就可以了。
  • Int[] attrs这个参数实际上是获取的我们在attr文件中声明的declare-styleablename属性
  • defStyleAttr,这个是我们在attr中定义的一个属性<attr name="customViewStyle" format="reference"></attr>
  • defStyleRes,这个参数是我们样式文件中定义的一个样式引用。

defStyleAttrdefStyleRes有什么区别呢,如果有人写过css样式的应该都清楚:

hello world

defStyleRes是一个样式,包含了属性名称和值,相当于width:200px,是一个键值对,而defStyleAttr仅仅是样式中属性名称,是一个width名称的声明,只有已经声明过的attr属性在style样式中才有意义,所以在自定义属性是我们经常会用到两个资源文件attr和values。

recycle

在代码中我们每次使用过TypedArray之后都会使用一次recycle方法,如果不用编译器会警告” This TypedArray should be recycled after use with #recycle()”,官方解释是:回收TypedArray,以便后面重用。在调用这个函数后,你就不能再使用这个TypedArray。在TypedArray后调用recycle主要是为了缓存。当recycle被调用后,这就说明这个对象从现在可以被重用了。TypedArray 内部持有部分数组,它们缓存在Resources类中的静态字段中,这样就不用每次使用前都需要分配内存。你可以看看TypedArray.recycle()中的代码:

public void recycle() {
        synchronized (mResources.mTmpValue) {
            TypedArray cached = mResources.mCachedStyledAttributes;
            if (cached == null || cached.mData.length < mData.length) {
                mXml = null;
                mResources.mCachedStyledAttributes = this;
            }
        }
}

如果我们想在theme中直接使用默认样式,一般要有以下步骤:

声明attr属性名称:

<resources>
    <declare-styleable name="CustomView">
        <attr name="flag" />
        <attr name="bg_color"/>
    </declare-styleable/>

    <attr name="customViewStyle" format="reference"/>
    <attr name="flag" format="boolean" />
    <attr name="bg_color" format="color" />
</resources>

将声明的一个样式引用作为values中theme中一个item,并且设置默认样式属性:

<style name="AppBaseTheme" parent="android:Theme.Light">
	<!-- 设置defStyleAttr,并且与attr中声明名称一致 -->
	<item name="customViewStyle">@style/customViewWidget</item>
</style> 
<style name="customViewWidget">
	<item name="bg_color">#00ff00</item>
	<item name="flag">true</item>
</style>

<!-- 设置defStyleRes,当在defStyleAtrr中找不到时使用-->
<style name="customViewWidget2">
	<item name="bg_color">#0000ff</item>
	<item name="flag">true</item>
</style>

自定义属性的优先级

优先级自上而下依次递减

  1. 直接在xml布局文件设置属性值
  2. declare-styleable中声明的CustomView
  3. defStyleAttr对应theme中样式
  4. defStyleRes对应style中样式
  5. TypedArray.getXXX(index, defValue)中defvalue属性值

获取Android系统自带样式属性

int[] ATTRS = new int[] { android.R.attr.layout_width, android.R.attr.layout_height } 其它不多讲了,使用过ArrayAdapter的都应该很熟悉android.R.layout.simple_list_item_1这各个属性,跟Adapter方式一样,我们也通过android.R.attr来获取各种内置attr属性资源。

最后附上CustomView的源代码:

public class CustomView extends View {
	private Paint paint;
	private int height = 0;
	private int width = 0;
	private int r;
	private int bgColor = 0xffffffff;
	private boolean flag = false;
	private static final int[] ATTRS = new int[] { android.R.attr.layout_width, android.R.attr.layout_height };

	public CustomView(Context context) {
		this(context, null);
	}

	public CustomView(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public CustomView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
		width = a.getLayoutDimension(0, width);
		height = a.getLayoutDimension(1, height);
		a.recycle();

		// a = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
		a = context.obtainStyledAttributes(attrs, R.styleable.CustomView, R.attr.customViewStyle,
				R.style.customViewWidget2);

		flag = a.getBoolean(R.styleable.CustomView_flag, flag);
		bgColor = a.getColor(R.styleable.CustomView_bg_color, bgColor);
		a.recycle();
		paint = new Paint();
		r = height / 2;
	}

	@Override
	protected void onDraw(Canvas canvas) {
		paint.setAntiAlias(true);
		paint.setColor(bgColor);
		if (flag) {
			canvas.drawCircle(width / 2, height / 2, r, paint);
		} else {
			canvas.drawRect(width / 2 - r, 0, width / 2 + r, height, paint);
		}
	}

}

评论

您确定要删除吗?删除之后不可恢复