<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"><span style="white-space:pre"> </span>想进阶成为android高手,自定义控件是必须要玩转的,那么就从入门开始,记录自己的学习过程。为啥要使用自定义控件呢?我的理解是可以类比一下style的抽取。在应用中,比如字体,可能有多处字体的大小,样式,颜色等等属性都是一样的,这样就可以进行抽取公共的样式,在xml中使用@style可以大大的简化这些繁琐的属性。应用中也有可能有这样的组件,在多处出现,但又不是系统提供的,那么就需要用到自定义控件了,将这个控件功能进行抽取,自定义出来,就能大大提高开发效率。</span>
一、自定义属性
自定义属性,比如TextView的text属性,在使用的时候,只需要在TextView中直接使用 Android:text="xxx"就可以了。简单来说,就是要能直接在xml文件中声明出来的属性。那么如何自定义属性呢?在values文件夹下面建立attrs.xml文件,根节点是<Resource>。然后我们参考系统的属性文件是怎么写的。打开SDK->platforms->android.xx->data->res->values->attr.xml。发现有这样一个节点 <declare-styleable name="Theme">,下面是系统自己的一些属性。那么我们就可以仿造来定义自己的控件属性。
<resources>
<declare-styleable name="custom">
<attr name="title" format="string"/>
<attr name="content" format="integer"/>
<attr name="key" format="boolean"/>
</declare-styleable>
</resources>
attrs属性的format格式有这么几种:
"reference" //引用
"color" //颜色
"boolean" //布尔值
"dimension" //尺寸值
"float" //浮点值
"integer" //整型值
"string" //字符串
"fraction" //百分数,比如200%
< attr name="orientation">
< enum name="horizontal" value="0" />
< enum name="vertical" value="1" />
< /attr>
name就是你自定义的属性名,format用来指定该属性的格式。
现在我们来自定义一个这样的设置控件,两个textview+一个checkbox组合而成,给它取名为SettingView。既然它包含了这样几个子控件,我们在构造这个SettingView的时候,必须继承ViewGroup。我这里继承的是RelativeGroup。
public class SettingView extends RelativeLayout implements OnCheckedChangeListener {
public SettingView(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
public SettingView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
public SettingView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
这三个构造方法有何区别?
构造一是从java代码中构造视图而不是xml布局中填充视图时使用的,简单来说,就是new出来的时候要用的
构造二是在xml创建但没有指定style时调用的
构造三是在xml创建,又指定style调用
这里只需要调用2个参数的构造就行了。当然也可以全部实现。那有何区别,下文会说到。
那么如何如何将xml布局和SettingView关联起来?当然要是用布局填充。在构造中将xml布局填充进来
View.inflate(context, R.layout.view_setting, null);
可能下意识就这样写了,可是这时xml和settingview有关联吗?肯定是没有的。那么这里第三个参数就不能为null,而是this。
二者有何区别?为null时,是将参数二指定的布局文件填充成视图返回,而不为空时,是将布局文件填充成视图,然后添加到参数三当中,参数三此时是一个跟布局root,然后将参数三返回。可以点进去阅读源码。也就是说,这里将view_setting填充成视图,添加到了SettingView当中,所以就不需要返回值了,添加过程可阅读源码。
然后自定义属性,这里定义了三个属性 title 、content和key。它们的format都是string类型。
接着就要在activity_main当中引用我们自定义的属性了:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:vladivostok="http://schemas.android.com/apk/res/com.vladivostok.customview"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical"
>
<com.vladivostok.customview.SettingView
android:id="@+id/sv"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
vladivostok:title="我的自动更新设置"
vladivostok:content="自动更新已经开启"
vladivostok:key="toggle"
/>
这里注意,要为自定义的控件声明命名控件:
xmlns:vladivostok="http://schemas.android.com/apk/res/com.vladivostok.customview"
很好理解,因为可能你定义的属性名和系统的属性名重复,声明命名空间就是为了区分自定义的属性和系统属性.命名空间最后是你工程的包名。
OK,属性声明好了,那如何在代码中去获取?也就是说使用者在xml中为你定义的属性符的值怎么获取?
有两种方式:
//方式一、获取命名空间,根据自定义的属性名获取
String namespace="http://schemas.android.com/apk/res/com.vladivostok.customview";
String title = attrs.getAttributeValue(namespace, "title");
content = attrs.getAttributeValue(namespace, "content");
key = attrs.getAttributeValue(namespace, "key");
//方式二、根据自定义的styleable的名字获取属性集,再根据对应的索引获取自定义属性的值
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.custom);
String title = array.getString(0);
content=array.getString(1);
key=array.getString(2);
//array要记得recycle()掉
array.recycle();
package com.vladivostok.customview;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.RelativeLayout;
import android.widget.TextView;
public class SettingView extends RelativeLayout implements
OnCheckedChangeListener {
private CheckBox cb;
private String key;
private TextView tv_content;
private String content;
// xml中使用的
public SettingView(Context context, AttributeSet attrs) {
super(context, attrs);
View.inflate(context, R.layout.view_setting, this);
TextView tv_title = (TextView) findViewById(R.id.tv_title);
tv_content = (TextView) findViewById(R.id.tv_content);
cb = (CheckBox) findViewById(R.id.cb);
// 获取自定义属性的2种方式
// 方式一、获取命名空间,根据自定义的属性名获取
String namespace = "http://schemas.android.com/apk/res/com.vladivostok.customview";
String title = attrs.getAttributeValue(namespace, "title");
content = attrs.getAttributeValue(namespace, "content");
key = attrs.getAttributeValue(namespace, "key");
// 方式二、根据自定义的styleable的名字获取属性集,再根据对应的索引获取自定义属性的值
TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.custom, 0, R.style.style_CustomView);
String title = array.getString(0);
content = array.getString(1);
key = array.getString(2);
// array要记得recycle()掉
array.recycle();
tv_title.setText(title);
tv_content.setText(content);
//将content切割使用,避免硬编码
content = content.split("已经")[0];
cb.setOnCheckedChangeListener(this);
}
public void setChecked() {
cb.setChecked(SPutils.getBoolean(getContext(), key));
}
@Override
public void onCheckedChanged(CompoundButton arg0, boolean isChecked) {
SPutils.putValue(getContext(), key, isChecked);
tv_content.setText(content + (isChecked ? "开启" : "关闭"));
}
}
MainActivity中的代码:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SettingView sv= (SettingView) findViewById(R.id.sv);
sv.setChecked();
}
}
控件的使用就很简单了。实现的功能就是点击checkbox实现开启/关闭状态的切换,并将状态记录。下次重新进入可将checkbox状态和文字还原。
其实脑中有了思路,按照逻辑一步步来,也不会很难,但是每个细节有力求弄透彻。
上文中提到获取属性集的两种方式,其中方式二:
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.custom);
其实最终调用的是:
public final TypedArray obtainStyledAttributes( AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
return getTheme().obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);
}
那后两个参数defStyleAttr和defStyleRes分别代表什么??先看参数4。
在style.xml中定义一个
<span style="white-space:pre"> </span></style>
<style name="style_CustomView">
<item name="title">i am title </item>
<item name="content">content is here</item>
<item name="key">it is key</item>
</style>
引用到方法中,TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.custom,0,R.style.
style_CustomView);
在使用SettingView时没有定义任何属性,
<com.vladivostok.customview.SettingView
android:id="@+id/sv_1"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
/>
很显然,在不设置任何属性的情况下,会从这个style中读取相关属性
参数3:defStyleAttr 它是一个引用类型的属性,上文提到过。并且指向一个style,并且要在当前Theme中设置。于是在attr中添加一条属性
<attr name="settingViewStyleRef" format="reference"/>
并在style.xml的Theme添加这样一条item:
<!-- Application theme. -->
<style name="AppTheme" parent="AppBaseTheme">
<!-- All customizations that are NOT specific to a particular API-level can go here. -->
<item name="settingViewStyleRef">@style/style_CustomView</item>
</style>
修改一句代码:
TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.custom,R.attr.settingViewStyleRef,0);
再次运行,效果同上。
这就是为什么切换不同的样式时,控件的样式也会发生变化,就是应为不同的主题设置了不同的style,系统的很多空间都使用了第三个参数。第三个参数的优先级更高。