2016年2月19日星期五

Android 自定义View及其在布局文件中的使用示例(ZT)

前言:
    尽管Android已经为我们提供了一套丰富的控件,如:Button,ImageView,TextView,EditText等众多控件,但是,有时候在项目开发过程中,还是需要开发者自定义一些需要重复使用的控件,使之能像Android提供的其它控件一样,使用起来方便,幸好Android为我们自定义控件过程扫除了障碍,提供了一套基础的类(如:View,Canvas等)和XML标签(如下文即将提及的resources标签,declare-styleable标签,attr标签等);
创建流程:
一,在value文件夹新建以"attrs"命名的XML文件:
看一下本例中的attrs.xml文件
attrs.xml文件:
复制代码
1 xml version="1.0" encoding="utf-8"?>
2 <resources>
3     <declare-styleable name="CustomView">
4         <attr name="textString"  format="string"></attr>
5         <attr name="colorValue" format="color"></attr>
6         <attr name="textSize" format = "dimension"></attr>
7     </declare-styleable>
8 </resources>
复制代码
attrs.xml文件中,外层引入了如下标签:
<declare-styleable name="CustomView">
这个标签就是为了让我们自定义的View,拥有自身的属性,从上面的代码中,我们可以看到,该标签内包含定义了三个属性,分别取名为:"textString","colorValue","textSize",这样我们就可以方便地使用该View的这些属性,就像我们在使用系统提供的TextView时,在布局文件中设置TextView的textSize,textColor等属性。
我们给declare-styleable的name字段取名为"CustomView",这是因为,我们将在下文给自定义的View取名为"CustomView",为什么declare-styleable的名字要跟我们自定义的这个View的名字一样呢?翻阅了google文档,找到解释:
The name of the styleable entity is, by convention, the same name as the name of the class that defines the custom view. Although it's not strictly necessary to follow this convention, many popular code editors depend on this naming convention to provide statement completion.
外层的declare-styleable标签就分析到这里,我们再来仔细看一下attr标签:
<attr name="textString"  format="string"></attr>
<attr name="colorValue" format="color"></attr><attr name="textSize" format = "dimension"></attr>
本例中,给自定义的View制定了三个属性,textString:该View显示的Text内容;colorValue:字体的颜色;textSize:字体的大小。attr标签不仅有name字段,并且给出了format字段(关于format字段都有哪些值,在附录中我们给出其具体的定义及应用示例)
二,编写布局文件,引用自定义的View
复制代码
xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/com.project.summary"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" 
    android:background="@color/BgColor">

    <com.project.summary.customview.CustomView
        android:id="@+id/customView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:colorValue="@color/textRed"
        app:textString="This the Custom View!!!"
        app:textSize="20sp"
        />

</LinearLayout>
复制代码
这里需要注意的地方有两个:
1,新增布局文件的命名空间
因为我们自定义了View,并且自定义了属性,而这些属性不再属于
http://schemas.android.com/apk/res/android
这个命名空间,而是属于
http://schemas.android.com/apk/res/[your package name].
 所以我们需要增加布局文件中的命名空间,改成 
xmlns:app="http://schemas.android.com/apk/res/com.project.summary"

2,自定义View在布局文件中的引用
<com.project.summary.customview.CustomView
我们需要把这个自定义类的包名都写完整;
另外:如果CustomView这个类是ParentCustomView类的内部类,那么在布局文件中的引用应该写成
<com.project.summary.customview.ParentCustomView$CustomView
即需要在类和之间增加字符$
3,自定义属性在布局文件中的引用
        app:colorValue="@color/textRed"
        app:textString="This the Custom View!!!"
        app:textSize="20sp"
需要在自定义的属性前面加上app字段(因为app="http://schemas.android.com/apk/res/com.project.summary",在这里app就是指代新的命名空间)。
三,编写自定义View代码
google文档要求该自定义的View中,至少要有以Context和AttributeSet为参数的构造方法,原因有两个:
1.
  To allow the Android Developer Tools to interact with your view, at a minimum you must provide a constructor that takes a Context and an AttributeSet object as parameters. This constructor allows the layout editor to create and edit an instance of your view.
复制代码
2.
  When a view is created from an XML layout, all of the attributes in the XML tag are read from the resource bundle and passed into the view's constructor as an AttributeSet. Although it's possible to read values from the AttributeSet directly, doing so has some disadvantages:
A:Resource references within attribute values are not resolved;
B:Styles are not applied;
Instead, pass the AttributeSet to obtainStyledAttributes(). This method passes back a TypedArray array of values that have already been dereferenced and styled.

The Android resource compiler does a lot of work for you to make calling obtainStyledAttributes() easier. For each <declare-styleable> resource in the res directory, the generated R.java defines both an array of attribute ids and a set of constants that define the index for each attribute in the array. You use the predefined constants to read the attributes from the TypedArray.
复制代码
第一个原因:为了让我们的开发工具 layout editor创建和编辑我们自定义的View;
第二个原因:这个也是最主要的原因,当我们从布局文件中创建View的时候,布局文件中的所有标签,标签中的所有属性都被读到资源包里,并且这个资源包被包装成属性集合AttributeSet传递给自定义View的构造方法;
在构造方法中,使用 obtainStyledAttributes()方法将这些属性转化成TypedArray数组,数组里包含我们自定义的属性ID和常量集合,这样,我们就可以用我们定义的常量名称很方便地从TypedArray中读取我们定义的属性。
所以我们至少先编写包含Context和AttributeSet为参数的构造方法
复制代码
 1 public class CustomView extends View {
 2     private int color;
 3     private String mText;
 4     private int textSize;
 5 
 6     public CustomView(Context context, AttributeSet attrs) {
 7         super(context, attrs);
 8         TypedArray a = context.obtainStyledAttributes(attrs,
 9                 R.styleable.CustomView);
10         try {
11             mText = a.getString(R.styleable.CustomView_textString);
12             color = a.getColor(R.styleable.CustomView_colorValue,
13                     R.color.textRed);
14             textSize = a.getDimensionPixelOffset(
15                     R.styleable.CustomView_textSize, 20);
16         } finally {
17             a.recycle();
18         }
19     }
复制代码
从代码中可以看出,我们可以用TypedArray提供的相关方法,来取出我们在布局文件中设置的相关属性,此处还需要注意TypedArray的回收!
四,本例中,我们自定义了一个View用来实现显示文字,类似于TextView。
由于本文只是讲述如何自定义View,以及其使用,自定义View的功能部分不在本文范畴,将在下一篇中具体讲述;所以,下面只贴代码,不再具体讲述。
复制代码
public class CustomView extends View {
    private int color;
    private Paint mTextPaint;
    private String mText;
    private int textSize;
    private int mAscent;

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initLabelView();
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.CustomView);
        try {
            mText = a.getString(R.styleable.CustomView_textString);
            color = a.getColor(R.styleable.CustomView_colorValue,
                    R.color.textRed);
            if (mText != null) {
                setCustomText(mText);
            }
            setTextColor(color);
            textSize = a.getDimensionPixelOffset(
                    R.styleable.CustomView_textSize, 20);
            if (textSize > 0) {
                setTextSize(textSize);
            }
        } finally {
            a.recycle();
        }
    }

    /**
     * Sets the text to display in this label
     * 
     * @param text
     *            The text to display. This will be drawn as one line.
     */
    private void setCustomText(String text) {
        // TODO Auto-generated method stub
        mText = text;
        requestLayout();
        invalidate();
    }

    private final void initLabelView() {
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        // Must manually scale the desired text size to match screen density
        mTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density);
        mTextPaint.setColor(0xFF000000);
        setPadding(3, 3, 3, 3);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }

    /**
     * Determines the width of this view
     * 
     * @param measureSpec
     *            A measureSpec packed into an int
     * @return The width of the view, honoring constraints from measureSpec
     */
    private int measureWidth(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text
            result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
                    + getPaddingRight();
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by
                // measureSpec
                result = Math.min(result, specSize);
            }
        }

        return result;
    }

    /**
     * Determines the height of this view
     * 
     * @param measureSpec
     *            A measureSpec packed into an int
     * @return The height of the view, honoring constraints from measureSpec
     */
    private int measureHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        mAscent = (int) mTextPaint.ascent();
        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text (beware: ascent is a negative number)
            result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()
                    + getPaddingBottom();
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by
                // measureSpec
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    /**
     * Sets the text size for this label
     * 
     * @param size
     *            Font size
     */
    public void setTextSize(int size) {
        // This text size has been pre-scaled by the getDimensionPixelOffset
        // method
        mTextPaint.setTextSize(size);
        requestLayout();
        invalidate();
    }

    /**
     * Sets the text color for this label.
     * 
     * @param color
     *            ARGB value for the text
     */
    public void setTextColor(int color) {
        mTextPaint.setColor(color);
        invalidate();
    }

    /**
     * Render the text
     * 
     * @see android.view.View#onDraw(android.graphics.Canvas)
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent,
                mTextPaint);
    }
    // @Override
    // protected void onMeasure(final int widthMeasureSpec,
    // final int heightMeasureSpec) {
    // int width = MeasureSpec.getSize(widthMeasureSpec);
    // // int height = (int) (width * heightScale / widthScale);
    // int height = MeasureSpec.getSize(heightMeasureSpec);
    // if (height == 0) {
    // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // } else {
    // super.onMeasure(
    // MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
    // MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
    // }
    // }
}
复制代码
 附录:format的定义及应用示例:
1. reference:资源引用。
   属性定义:
<attr name = "background" format = "reference" />
  属性使用:  
<com.lin.gw.CustomView
   android:layout_width = "42dip"
   android:layout_height = "42dip"
   app:background = "@drawable/图片ID"
/>
2. color:颜色值。
    属性定义:       
<attr name = "textColor" format = "color" />
    属性使用:
<com.lin.gw.CustomView
   android:layout_width = "42dip"
   android:layout_height = "42dip"
   app:textColor = "#fff000"
/>
3. boolean:布尔值。
    属性定义:
<attr name = "focusable" format = "boolean" />
    属性使用:
<com.lin.gw.CustomView
   android:layout_width = "42dip"
   android:layout_height = "42dip"
   app:focusable = "true"
/>
4. dimension:尺寸值。
    属性定义:
<attr name = "customWidth" format = "dimension" />
    属性使用:
<com.lin.gw.CustomView
   app:customWidth = "42dip"
   android:layout_height = "wrap_content"
/>
5. float:浮点值。
    属性定义:
<attr name = "fromAlpha" format = "float" />
    属性使用:
<com.lin.gw.CustomView
   app:fromAlpha = "2.0"
/>
 6. integer:整型值。
    属性定义:                  
<attr name = "frameDuration" format="integer" />        
    属性使用:
<com.lin.gw.CustomView
   app:frameDuration = "20"
/>
7. string:字符串。
  属性定义:
<attr name="textString"  format="string"></attr>
    属性使用:
<com.lin.gw.CustomView
   app:textString = "hello lingling!"
/>
8. fraction:百分数。
    属性定义:          
<attr name = "pivotX" format = "fraction" />
  属性使用:
<com.lin.gw.CustomView
   app:pivotX = "30%"
/>
9. enum:枚举值。
    属性定义:
<attr name="orientation">
   <enum name="horizontal" value="0" />
   <enum name="vertical" value="1" />
 </attr>      
    属性使用:
<com.lin.gw.CustomView
   app:orientation = "vertical"
/>
10. flag:位或运算。
     属性定义:          
复制代码
<declare-styleable name="CustomView">
     <attr name="windowSoftInputMode">
           <flag name = "stateUnspecified" value = "0" />
           <flag name = "stateUnchanged" value = "1" />
           <flag name = "stateHidden" value = "2" />
           <flag name = "stateAlwaysHidden" value = "3" />
           <flag name = "stateVisible" value = "4" />
           <flag name = "stateAlwaysVisible" value = "5" />
           <flag name = "adjustUnspecified" value = "0x00" />
           <flag name = "adjustResize" value = "0x10" />
           <flag name = "adjustPan" value = "0x20" />
           <flag name = "adjustNothing" value = "0x30" />
     </attr>         
</declare-styleable>
复制代码
     属性使用:
app:windowSoftInputMode = "stateUnspecified | stateUnchanged | stateHidden">

没有评论: