<header>
	<p>Jan 6, 2015 • CoderSimple <a href="http://codersimple.github.io/android/2015/01/06/custom-view.html">原文传送阵</a></p>
</header>

<article>
	<p>虽然官方提供了不少的控件给我们使用,但是也无法满足我们在各种项目的某些需要,在很多情况下需要我们自定义我们想要的控件,因此学会自定义控件显得非常重要,为了我们自定义的控件能够良好的运行,我们需要遵循以下要求:</p>
  1. Conform to Android standards
  2. Provide custom styleable attributes that work with Android XML layouts
  3. Send accessibility events
  4. Be compatible with multiple Android platforms

如何自定义一个 View?

创建一个类并继承与 View,并实现构造方法,这样就成功的自定义我们的 View 了,如下:

package com.example.coder.mycustomview;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;

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

如何使用我们自定义的 View 呢?

在 activity_main.xml 使用如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.coder.mycustomview.MyCustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

至此,我们已经成功的完成了 View 的自定义及其使用,但是我们运行的时候看不到什么东西,总有些不放心,把 activity_main.xml 改成:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.coder.mycustomview.MyCustomView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#FF0000"/>

</RelativeLayout>

终于看到左上角有个 100 x 100 的方块,说明我们自定义 View 可以运行了。

如何自定义属性?

下面我们自定义一个形状(shape)的属性,在 values/ 下创建 attrs.xml 文件,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyCustomView">
        <attr name="shape" format="string"/>
    </declare-styleable>
</resources>

再在 strings.xml 中添加两个形状值:

<string name="shape_rectangle">Rectangle</string>
<string name="shape_circle">circle</string>

如何使用我们自定义的属性?

  1. 首先我们要知道什么是命名空间?,xmlns:android="http://schemas.android.com/apk/res/android",这是 andriod 默认的命名 空间,其中等号左边的 android 是命名空间的简写,等号后边是完整的命名空间,我们在使用属性时是 命名空间:属性=属性值,如果使 用完整的命名空间,那语句就会变得很长,因此我们多使用简写命名空间。
  2. 自定义我们的命名空间 xmlns:custom="http://schemas.android.com/apk/res-auto"(android studio 推荐使用)并使用:
<RelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:custom="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     tools:context=".MainActivity">

     <com.example.coder.mycustomview.MyCustomView
         android:layout_width="100dp"
         android:layout_height="100dp"
         android:background="#FF0000"
         custom:shape="@string/shape_circle"/>

 </RelativeLayout>

我们自定义的属性已经用上去了,但此时我们运行的时候发现还是方形的,错了吗?没有,因为我们还没有对我们的属性进行相应的处理。

实现自定义属性

  1. 在我们自定义的 MyCustomView 中添加属性 shape,并实现它的 set 和 get 方法
  2. 解析我们自定义的属性,当通过 activity_main.xml 创建 View 时,所有的属性都已保存到 AttributeSet 中,我们可以从 AttributeSet 中获取我们想要的属性及属性值,但是从 AttributeSet 获取属性及属性值有俩个缺点:
  1. Resource references within attribute values are not resolved
  2. Styles are not applied

因此我们通常不会通过 AttributeSet 获取我们想要的属性及属性值,而是通过 obtainStyledAttributes() 方法,使用如下:

import android.content.Context;
 import android.content.res.TypedArray;
 import android.util.AttributeSet;
 import android.view.View;

 public class MyCustomView extends View{
     private String shape;

     public MyCustomView(Context context, AttributeSet attrs) {
         super(context, attrs);

         TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyCustomView, 0, 0);

         try {
             shape = typedArray.getString(R.styleable.MyCustomView_shape);
         } finally {
             typedArray.recycle();
         }
     }

     public String getShape() {
         return shape;
     }

     public void setShape(String shape) {
         this.shape = shape;
     }
 }

至此,属性具备,只欠绘画形状了。

  1. 绘画形状,涉及到绘画,我们第一步肯定要重写 onDraw()方法了,在绘画之前我们还需要一个画笔 Paint,分配资源最好不要在 onDraw() 方法中,因此我们定义一个 init() 方法来初始化我们在绘画过程中需要分配的资源:
package com.example.coder.mycustomview;

 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
 import android.util.AttributeSet;
 import android.view.View;

 public class MyCustomView extends View{
     private String shape;
     private Paint paint;

     public MyCustomView(Context context, AttributeSet attrs) {
         super(context, attrs);

         TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyCustomView, 0, 0);

         try {
             shape = typedArray.getString(R.styleable.MyCustomView_shape);
         } finally {
             typedArray.recycle();
         }

         init();
     }

     private void init() {
         paint = new Paint();
         paint.setColor(Color.BLUE);
         paint.setStyle(Paint.Style.FILL);
     }

     @Override
     protected void onDraw(Canvas canvas) {
         super.onDraw(canvas);

         if (shape == null) {
             // draw rectangle for default
             canvas.drawRect(getLeft(), getTop(), getRight(), getBottom(), paint);
         }else if (shape.equals(getResources().getString(R.string.shape_circle))) {
             canvas.drawCircle((getLeft() + getRight()) / 2, (getTop()+ getBottom()) / 2, (getWidth() > getHeight() ? getHeight(): getWidth()) / 2, paint);
         } else {
             // unknow shape, or rectangle
             canvas.drawRect(getLeft(), getTop(), getRight(), getBottom(), paint);
         }
     }

     public String getShape() {
         return shape;
     }

     public void setShape(String shape) {
         this.shape = shape;
     }
 }

这里默认使用蓝色填充我们的形状,左图为指定形状为圆形,有图为为指定形状时默认为矩形

</article>
</div>