26
2017
09

Android自定义View之属性解析

在自定义View时会经常涉及到自定义属性来定制View的样式和特效,下面我们来看一些属性是怎么解析的.
注:以ImageView为例

1 属性资源定义

首先在/res/values/目录下定义attrs.xml资源文件.在文件中通过declare-styleable定义对应View的属性集.

<declare-styleable name="ImageView">
        <!-- Sets a drawable as the content of this ImageView. -->
        <attr name="src" format="reference|color" />
        <!-- Controls how the image should be resized or moved to match the size of this ImageView. See {@link android.widget.ImageView.ScaleType} -->
        <attr name="scaleType">
            <!-- Scale using the image matrix when drawing. See {@link android.widget.ImageView#setImageMatrix(Matrix)}. -->
            <enum name="matrix" value="0" />
            <!-- Scale the image using {@link android.graphics.Matrix.ScaleToFit#FILL}. -->
            <enum name="fitXY" value="1" />
            <!-- Scale the image using {@link android.graphics.Matrix.ScaleToFit#START}. -->
            <enum name="fitStart" value="2" />
            <!-- Scale the image using {@link android.graphics.Matrix.ScaleToFit#CENTER}. -->
            <enum name="fitCenter" value="3" />
            <!-- Scale the image using {@link android.graphics.Matrix.ScaleToFit#END}. -->
            <enum name="fitEnd" value="4" />
            <!-- Center the image in the view, but perform no scaling. -->
            <enum name="center" value="5" />
            <!-- Scale the image uniformly (maintain the image's aspect ratio) so both dimensions (width and height) of the image will be equal to or larger than the corresponding dimension of the view (minus padding). The image is then centered in the view. -->
            <enum name="centerCrop" value="6" />
            <!-- Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width and height) of the image will be equal to or less than the corresponding dimension of the view (minus padding). The image is then centered in the view. -->
            <enum name="centerInside" value="7" />
        </attr>
        <!-- Set this to true if you want the ImageView to adjust its bounds to preserve the aspect ratio of its drawable. -->
        <attr name="adjustViewBounds" format="boolean" />
        <!-- An optional argument to supply a maximum width for this view. See {see android.widget.ImageView#setMaxWidth} for details. -->
        <attr name="maxWidth" format="dimension" />
        <!-- An optional argument to supply a maximum height for this view. See {see android.widget.ImageView#setMaxHeight} for details. -->
        <attr name="maxHeight" format="dimension" />
        <!-- Set a tinting color for the image. By default, the tint will blend using SRC_ATOP mode. -->
        <attr name="tint" format="color" />
        <!-- If true, the image view will be baseline aligned with based on its bottom edge. -->
        <attr name="baselineAlignBottom" format="boolean" />
         <!-- If true, the image will be cropped to fit within its padding. -->
        <attr name="cropToPadding" format="boolean" />
        <!-- The offset of the baseline within this view. See {see android.view.View#getBaseline} for details -->
        <attr name="baseline" format="dimension" />
        <!-- @hide The alpha value (0-255) set on the ImageView's drawable. Equivalent to calling ImageView.setAlpha(int), not the same as View.setAlpha(float). -->
        <attr name="drawableAlpha" format="integer" />
        <!-- Blending mode used to apply the image tint. -->
        <attr name="tintMode" />
    </declare-styleable>

可以看到在这个属性集中有Android自定义View之属性中所说的全局属性tintMode和tint,也有其他只能在ImageView中使用的局部属性.并且还定义了一个枚举类型的属性scaleType来描述
ImageView中图片的填充形式.
其中tint和tintMode的定义如下:

<attr name="tint" format="color" />
<attr name="tintMode">
            <!-- The tint is drawn on top of the drawable. [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] -->
            <enum name="src_over" value="3" />
            <!-- The tint is masked by the alpha channel of the drawable. The drawable’s color channels are thrown out. [Sa * Da, Sc * Da] -->
            <enum name="src_in" value="5" />
            <!-- The tint is drawn above the drawable, but with the drawable’s alpha channel masking the result. [Da, Sc * Da + (1 - Sa) * Dc] -->
            <enum name="src_atop" value="9" />
            <!-- Multiplies the color and alpha channels of the drawable with those of the tint. [Sa * Da, Sc * Dc] -->
            <enum name="multiply" value="14" />
            <!-- [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] -->
            <enum name="screen" value="15" />
            <!-- Combines the tint and drawable color and alpha channels, clamping the result to valid color values. Saturate(S + D) -->
            <enum name="add" value="16" />
</attr>

2 属性使用

  • 布局中使用
    定义好属性资源后就可以在布局或者style中对属性赋值来定制View的效果.例如我们在使用ImageView的时候一般是这样的:
    <ImageView  android:id="@+id/iv_car" android:layout_width="match_parent" android:layout_height="50dp" android:scaleType="centerCrop" android:background="@drawable/ripple_light" />

其中scaleType就是针对ImageView自定义的属性.但是一般在Android的布局文件中我们会看到有下面代码片段:

xmlns:android="http://schemas.android.com/apk/res/android"

这是申明了一个命名空间,然后可以通过这个命名空间去访问所定义的资源,如果没有定义这个命名空间,在布局中是访问不到定义的属性资源的.
在Android开发中如果我们需要自定义View的属性时也是需要加上自己的命名空间,这样才能访问到我们自己的属性.我们的命名控件必须是下面这种形式:

xmlns:my_app="http://schemas.android.com/apk/res-auto"

比如我们自定义了一个属性集给自己的一个叫做ScaleImageView的使用:

 <declare-styleable name="ScaleImageView">
        <!-- Identifier of the bitmap file. This attribute is mandatory. -->
        <attr name="scale" format="float"/>
</declare-styleable>

然后定义一个布局文件scale_image_layout.xml

<com.alexluo.widget.ScaleImageView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:my_app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/sc_img"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    my_app:scale="0.5"
    />

通过这些工作之后就可以正常的使用自定义的属性了.

  • style中使用
    自定义的属性可以通过定义不同的style对View进行不同的效果显示.仍然以ImageView为例.我们一般是直接在布局中指定ImageView的src或者background等属性.
    也可以通过在style中去定义,然后在布局中通过style属性来引用.唯一不同的是在style中不需要申明命名空间.
    在res/values/下定义styles.xml资源文件.然后定义一个ImageView用到的style
  <style name="MyImageViewStyle">
        <item name="android:src">@drawable/ic_launcher</item>
        <item name="android:background">?android:attr/selectableItemBackgroundBorderless</item>
    </style>

然后在布局中通过style来应用这个样式:

  <ImageView  style="@style/MyImageViewStyle" android:id="@+id/iv_car" android:layout_width="wrap_content" android:layout_height="wrap_content" />
  • 主题中使用
    Android是可以通过定义Theme来改变界面主题样式的,在自定义View的时候也可以通过自定义相关的属性来在主题中应用,从而达到切换不同主题显示不同效果的功能.
    在自定义完对应View的时候需要在定义一个全局的属性,该属性主要是在Theme中使用,从而达到我们想要的效果以上面提到的ScaleImageView为例:
    在上面定义完ScaleImageView的私有属性后,我们再定义一个给主题用的属性:
<attr name="scaleImageViewStyle" format="reference" />

然后在styles.xml中定义对应主题的ScaleImageView的style

<style name="ScaleImageView.Light" parent="">
    <item name="scale">0.8<item>
</style>

最后定义一个主题样式

  <style name="AppTheme" parent="AppBaseTheme"> <item name="scaleImageViewStyle">@style/ScaleImageView.Light</item> </style>

然后再将这个主要样式应用到Activity中,或者通过切换不同的主题来实现自定义View的显示样式的不同.

3 属性解析

在定义完属性后,需要在View中进行解析才能发挥效果.属性的解析在View的构造函数中完成.以ImageView为例:
我们看ImageView的构造函数:

 public ImageView(Context context) {
        super(context);
        initImageView();
    }

    public ImageView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        initImageView();

        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);

        final Drawable d = a.getDrawable(R.styleable.ImageView_src);
        if (d != null) {
            setImageDrawable(d);
        }

        mBaselineAlignBottom = a.getBoolean(R.styleable.ImageView_baselineAlignBottom, false);
        mBaseline = a.getDimensionPixelSize(R.styleable.ImageView_baseline, -1);

        setAdjustViewBounds(a.getBoolean(R.styleable.ImageView_adjustViewBounds, false));
        setMaxWidth(a.getDimensionPixelSize(R.styleable.ImageView_maxWidth, Integer.MAX_VALUE));
        setMaxHeight(a.getDimensionPixelSize(R.styleable.ImageView_maxHeight, Integer.MAX_VALUE));

        final int index = a.getInt(R.styleable.ImageView_scaleType, -1);
        if (index >= 0) {
            setScaleType(sScaleTypeArray[index]);
        }

        if (a.hasValue(R.styleable.ImageView_tint)) {
            mDrawableTintList = a.getColorStateList(R.styleable.ImageView_tint);
            mHasDrawableTint = true;

            // Prior to L, this attribute would always set a color filter with
            // blending mode SRC_ATOP. Preserve that default behavior.
            mDrawableTintMode = PorterDuff.Mode.SRC_ATOP;
            mHasDrawableTintMode = true;
        }

        if (a.hasValue(R.styleable.ImageView_tintMode)) {
            mDrawableTintMode = Drawable.parseTintMode(a.getInt(
                    R.styleable.ImageView_tintMode, -1), mDrawableTintMode);
            mHasDrawableTintMode = true;
        }

        applyImageTint();

        final int alpha = a.getInt(R.styleable.ImageView_drawableAlpha, 255);
        if (alpha != 255) {
            setImageAlpha(alpha);
        }

        mCropToPadding = a.getBoolean(
                R.styleable.ImageView_cropToPadding, false);

        a.recycle();

        //need inflate syntax/reader for matrix
    }

在ImageView的四个参数的构造函数中对其自定义的所有属性进行解析,我们在解析自己的属性时也在这个构造函数中进行.如果你自定义了可以在
主题中使用来实现主题切换改变View样式的属性,那么可以在两个参数的构造函数中调用三个参数的构造函数时将主题中使用到的属性传递进去:

public ScaleImageView(Context context, AttributeSet attrs) { 
    this(context, attrs, com.alexluo.R.attr.scaleImageViewStyle); 
}

然后在四个参数的构造方法中解析,首先调context.obtainStyledAttributes(attrs, R.styleable.ImageView, defStyleAttr, defStyleRes)获取一个TypedArray对象,
然后可以通过这个对象的getXXX方法来获取对应的资源,其中该类型的方法第一个参数为整形index(参考Android自定义View之资源Attr)中生成R文件时局部属性的生成形式.
该类型的方法有一些有默认两个参数,其中第二个为默认值,例如在ImageView中通过getDrawable获取src图片

a.getDrawable(R.styleable.ImageView_src);

通过getInt获取图片透明度,这个有默认参数

final int alpha = a.getInt(R.styleable.ImageView_drawableAlpha, 255);

TypedArray还有一个常用的方法:

public boolean hasValue(@StyleableRes int index) 

该方法用于处理相应属性是否被赋值或者被赋值为@empty,如果被正常赋值返回为true,否则返回为false,可以参考ImageView中的相关代码:

     if (a.hasValue(R.styleable.ImageView_tintMode)) {
            mDrawableTintMode = Drawable.parseTintMode(a.getInt(
                    R.styleable.ImageView_tintMode, -1), mDrawableTintMode);
            mHasDrawableTintMode = true;
        }

最后在解析完资源之后一定要调用TypedArray的recycle()方法进行回收.

上一篇:Android Apk解析 下一篇:Android Native内存泄漏诊断