Android学习总结 :自定义 View(一)

在 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 文件


<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 = @2131558529
CoolView: attrName = layout_width , attrVal = 100.0dip
CoolView: attrName = layout_height , attrVal = 100.0dip
CoolView: attrName = labelPosition , attrVal = 1
CoolView: attrName = text , attrVal = @2131361809
CoolView: 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:true
CoolView: mLabelPosition:1
CoolView: 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:false
CoolView: 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中的自定义属性

你可能感兴趣的:(android)