在 Android 中,一个设计精良的自定义 View 就像其他设计精良的类一样,它封装了一些特殊的功能并且有一个方便使用的界面。真正设计好的自定义 View ,可以更有效地利用 CPU 和内存的资源。一个自定义 View 需要符合以下几点要求:

  • 符合 Android 设计标准

  • 为 Android XML layouts 界面提供自定义的 styleable 属性。

  • 能发送可访问的事件

  • 兼容更多的 Android 平台版本

一、创建自定义 view 类

Android 平台所有的 view classes都是继承于 View,我们的自定义 view 可以直接继承于 View ,也可以直接继承有更多功能的 View 的子类。比如 Button 、 TextView。

为了 Android Studio 的布局编辑器能创建并编辑你的自定义 View,你至少要提供一个参数为 Context 和 AttrubuteSet 的构造器

class CoolView extends View {    public CoolView(Context context, AttributeSet attrs) {        super(context, attrs);    }}

二、自定义属性


自定义 view 的第一个步骤,为其定义自定义属性,并且能够使用元素属性来控制它的外观和行为。一般来说有以下 4 步:

  1. 通过 资源标签来定义你的自定义的属性

  2. 在 XMl 布局中为你的自定义属性指定一个值

  3. 运行时能获取到你指定的属性值

  4. 根据获取到的属性值对 view 做出相应的修改

1. 定义自定义属性

为了在使用自定义 view 的时候能够使用我们自己定义的属性标签,就像 Android 内置的属性一样。你要做的,是为你的自定义 view 设置自定义属性,使得在使用的时候看起来像内置的一样。

在你的项目中添加包含 的资源文件,一般放在 res/values/attrs.xml 文件中,以下是官方的例子:

<resources>   <declare-styleable name="CoolView">       <attr name="showText" format="boolean" />       <attr name="labelPosition" format="enum">           <enum name="left" value="0"/>           <enum name="right" value="1"/>       attr>   declare-styleable>resources>

上面这段代码定义了一个名为 CoolView 自定义样式,showTextlabelPosition 是其中的两个属性值。对于自定义属性样式的命名,官方建议保持与类名相同

自定义属性的format,可以有以下多种:

  • reference
  • string
  • color
  • dimension
  • boolean
  • integer
  • float
  • fraction
  • enum
  • flag

2. 在布局文件中使用自定义属性

现在你可以在布局文件中使用像是内置属性一样的自定义的属性,唯一有一点不同的地方,就是 XML 的命名空间。官方建议使用 xmlns:custom="http://schemas.android.com/apk/res/[your package name]" 来定义你自定义的命名空间。
看下 XML 文件

<?xml version="1.0" encoding="utf-8"?><android.support.design.widget.CoordinatorLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"/>    <com.smartni.userinterface.sample.main.CoolView        android:layout_width="wrap_content"        android:layout_height="wrap_content"        nj:labelPosition="left"        nj:showText="true"/>android.support.design.widget.CoordinatorLayout>

但是在实践的时候,发现提示 In Gradle Projects,always use xmlns:app="http://schemas.android.com/apk/res-auto" 为自定义属性的命名空间 。也就是说在 Gradle 文件中使用 res-auto 代表统一的自定义的命名空间,这样也比较方便,不管自定义 view 内部是否有内部类,都可以统一使用这个命名空间。

3. 在运行时获取指定的属性值

我们定义在自定义属性,然后在布局文件中使用并写上了指定的值,下一步是如何获取到这些值。

为了解决这个问题,先直接推荐使用:

 Resources.Theme.obtainStyledAttributes() 

4. 做你想要做的

它能自动把引用资源解析出来,并且能应用主题样式。看一下代码:

public class CoolView extends View {    boolean mShowText;    Integer mLabelPosition;    public CoolView(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        TypedArray a = context.getTheme().obtainStyledAttributes(                attrs,                R.styleable.CoolView,                0,0);        try{             mShowText = a.getBoolean(R.styleable.CoolView_showText,false);             mLabelPosition = a.getInteger(R.styleable.CoolView_labelPosition,0);        }finally {            a.recycle();        }    }}

注意 R.styleable.CoolView_showText 的形式。还有 TypedArrayy 使用完要回收。

AttributeSet 和 TypedArray

  • AttributeSet

记得在开头定义的那个 CoolView 中的构造函数中有个 AttributeSet 参数,它就是 XML 布局文件里这个 view 的一系列属性的集合。它的确可以获取到自定义属性的值,来看看代码:

简单修改后的XML

<com.smartni.userinterface.sample.main.CoolView    android:id="@+id/cv"    android:layout_width="100dp"    android:layout_height="100dp"    app:labelPosition="right"    app:text="@string/hello"    app:showText="true"/>

这里把新增了一个 text 属性(format = “reference” ),值写成了资源Id。
然后在 CoolView.java 中

    private void initAttrs(Context context, AttributeSet attrs) {        int count = attrs.getAttributeCount();        for (int i = 0; i < count; i++) {            String attrName = attrs.getAttributeName(i);            String attrVal = attrs.getAttributeValue(i);            Log.e(TAG, "attrName = " + attrName + " , attrVal = " + attrVal);        }    }

得到结果

CoolView: attrName = id , attrVal = @2131558529CoolView: attrName = layout_width , attrVal = 100.0dipCoolView: attrName = layout_height , attrVal = 100.0dipCoolView: attrName = labelPosition , attrVal = 1CoolView: attrName = text , attrVal = @2131361809CoolView: attrName = showText , attrVal = true

可以看到 text 的值是 @xxxx ,这明显不符合要求。说明 AttributeSet 不会去检索引用资源。其实还有主题也不会解析。

那么如何使用 AttributeSet 得到引用类型的属性呢?。

int resId = attrs.getAttributeResourceValue(1, -1);String text = getResources().getDimension(widthDimensionId));

一句话,不推荐直接使用它。

  • TypedArray

保持 XML 保持不变,我们使用 TypedArray ,来看看代码:

private void initAttrs(Context context, AttributeSet attrs) {    TypedArray a = context.getTheme().obtainStyledAttributes(            attrs,            R.styleable.CoolView,            0, 0);    try {        mShowText = a.getBoolean(R.styleable.CoolView_showText, false);        mLabelPosition = a.getInteger(R.styleable.CoolView_labelPosition, 0);        mText = a.getString(R.styleable.CoolView_text);    } finally {        a.recycle();    }    Log.d(TAG, "mShowText:" + mShowText);    Log.d(TAG, "mLabelPosition:" + mLabelPosition);    Log.d(TAG, "mText:" + mText);}

结果是:

CoolView: mShowText:trueCoolView: mLabelPosition:1CoolView: mText:你好

TypedArray 的意义就是帮助我们自动处理引用类型的资源和主题(style)资源,来简化我们的工作。

declare-styleable

众所周知,系统提供的一个默认的属性: android:text ,鸿洋大神提出的一个问题

如果系统中已经有了语义比较明确的属性,我可以直接使用嘛?

答案是可以的,只要修改 attrs 文件

   <declare-styleable name="test">        <attr name="android:text" />        <attr name="testAttr" format="integer" />    declare-styleable>

唯一差别就是,它没有 format 属性!!!

然后在类中这么获取:a.getString(R.styleable.test_android_text);布局文件中直接 android:text="@string/hello_world" 即可。

还有一个问题

styleable 的含义是什么?可以不写嘛?我自定义属性,我声明属性就好了,为什么一定要写个styleable呢?

确实是可以不用写的,先看下现在 attrs.xml 文件里的内容

<resources>    <declare-styleable name="CoolView">        <attr name="showText" format="boolean"/>        <attr name="labelPosition" format="enum">            <enum name="left" value="0"/>            <enum name="right" value="1"/>        attr>    declare-styleable>resources>

其实,在 R.java 文件中 android 已经帮我生成了相应的代码

    public static final int[] CoolView = {        0x7f0100e6, 0x7f0100e7    };    public static final int CoolView_showText = 0;    public static final int CoolView_labelPosition = 1;

现在修改 attrs.xml

<resources>        <attr name="showText" format="boolean"/>        <attr name="labelPosition" format="enum">            <enum name="left" value="0"/>            <enum name="right" value="1"/>        attr>        <attr name="onItemClick" format="string"/>resources>

编译一下,发现 R.java 中少了 CoolView 那个 int 数组常量。我想,原来在 obtainStyledAttributes() 方法中第二个参数传递的是 R.styleable.CoolView ,其实这个参数在 R 文件中就是这个 int 常量数组。

那我是不是可以模拟一下这个数组呢?

public class CoolView extends android.support.v7.widget.AppCompatTextView {    private static  final int[] mAttr= {android.R.attr.showText,R.attr.labelPosition};    private static  final int mShowText = 0;    private static final int mLabelPosition = 1; private void initAttrs(Context context, AttributeSet attrs) {        TypedArray a = context.getTheme().obtainStyledAttributes(                attrs,                mAttr,                0, 0);        try {            boolean showText = a.getBoolean(mShowText, false);            int labelPosition = a.getInteger(mLabelPosition, -1);            Log.d(TAG, "showText:" + showText);            Log.d(TAG, "labelPosition:" + labelPosition);        } finally {            a.recycle();        }    }}

结果一样:

CoolView: showText:falseCoolView: labelPosition:1

开始,不用解释了吧。这个数组中的元素就像上面 R 文件中的一样,定义的是我们要获取的 attr 的 id 。然后根据元素在数组中的位置,定义一些整形的常量代表下表。

这样,就和没有写 declare-styleable 一样了。

但是呢,明明有更方便的 styleable ,我们为什么不用呢?Google 推荐 declare-styleable 的 name 属性一般与自定义 view 的名字一样。

添加属性和事件


属性

之前的代码只能在 XML 中提前定义,在实际中我们肯定需要在运行时动态改变它的属性值。看下代码:

public boolean isShowText() {   return mShowText;}public void setShowText(boolean showText) {   mShowText = showText;   invalidate();   requestLayout();}

invalidate():在任何可能改变 view 内容的操作后都要调用。
requestLayout(): 在任何可能改变 view 大小或者形状的操作后调用。

事件

当然也可以添加事件,先在 attrs 中定义属性。

"onItemClick" format="string"/>

然后在布局文件中

 <com.smartni.userinterface.sample.main.CoolView        android:id="@+id/cv"        android:layout_width="100dp"        android:layout_height="100dp"        android:gravity="center"        android:textSize="20sp"        android:background="@android:color/holo_purple"        android:text="你好"        app:labelPosition="right"        app:layout_anchor="@id/rv"        app:layout_anchorGravity="bottom|center"        android:onClick="onDo"        app:onItemClick="onItemClick"        app:showText="true"/>

这里我定义了默认的点击事件和自定义点击事件的属性。

参照 Android 官方那样,在布局文件中定义好 onClick 的名字,然后在 Activity 中定义带 View 参数的同名函数。

我也模仿着实现了一下:

public class CoolView extends android.support.v7.widget.AppCompatTextView {    ...    public void onItemClick() throws NoSuchMethodException {        Class clz = mContext.getClass();        try {            Method method = clz.getMethod(mOnItemClick, View.class);            if(method != null){                method.invoke(mContext,this);            }        } catch (Exception e) {            throw new NoSuchMethodException("make sure  define your onItemClick :" +                    mOnItemClick +" method in your Activity.");        }    }    ...}

通过反射调用 Activity 中,同名的方法。然后:

    ...    private void bindListener()  {        this.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                try {                    onItemClick();                } catch (NoSuchMethodException e) {                    e.printStackTrace();                }            }        });    }    ...

然后通过调用系统的 onClickListener 调用我们自己的事件。bindListener 可以在构造函数中调用。

在 Activity 中就可以定义 XML 中设置的名字的方法,要有一个 View 参数。

    ...    public void onDo(View view){        Log.d("CoolView", "hello");        Snackbar.make(mCoordinatorLayout,"我是默认的点击事件",Snackbar.LENGTH_SHORT).show();    }    //这个方法被调用    public void onItemClick(View view){        Log.d(TAG, "hello , my custom View!");        Snackbar.make(mCoordinatorLayout,"我是自定义的点击事件",Snackbar.LENGTH_SHORT).show();    }    ...

当我点击这个 view 的时候结果是
输出了 hello , my custom View! 并没有输出 hello
这里默认的点击事件被我们自定义的 View 覆盖了。至于为什么,就必须要谈到 Android 的事件分发机制了。这两天我就去好好研究一下,到时候写一个总结。

总结一下:

  1. 我们编写自定义 view 的 4 个一般步骤。

  2. attrs.xml里面的 declare-styleable 以及 item,android 会根据其在 R.java 中生成一些常量方便我们使用(aapt干的),本质上,我们可以不声明declare-styleable仅仅声明所需的属性即可。

  3. 我们在 View 的构造方法中,可以通过AttributeSet去获得自定义属性的值,但是比较麻烦,而TypedArray可以很方便的便于我们去获取。

  4. 我们在自定义View的时候,可以使用系统已经定义的属性。

  5. TypedArray 的意义就是帮助我们自动处理引用类型的资源和主题(style)资源,来简化我们的工作。

参考自:

  • 官方 training

  • 鸿洋大神的 深入理解Android中的自定义属性

更多相关文章

  1. Android中的一个简单的List应用
  2. android 滚动条颜色设置(android Progressbar color)
  3. Android(安卓)总结:ContentProvider 的使用
  4. android framework层 学习笔记(一)
  5. Android修改主题,去掉ActionBar、TitleBar
  6. Hack4-自定义PreferenceActivity界面
  7. Android(安卓)Studio编译jar架包必看
  8. Android笔记 - Android启动之Android(安卓)Framework启动
  9. android定制化软件修改或添加按键驱动的核心操作步骤讲解

随机推荐

  1. android图表ichartjs
  2. Android(安卓)Activity的启动
  3. Android调用天气预报的WebService简单例
  4. haproxy根据客户端浏览器进行跳转
  5. Android--SoLoader,android动态加载so库
  6. 第三章 Android程序设计基础
  7. Android(安卓)报错:Caused by: android.os
  8. Android使用Retrofit进行网络请求
  9. Android(安卓)给 app默认权限(不弹窗申请
  10. Android(安卓)触摸提示音