PS:本篇文章大多数翻译自github上一篇英文文章!

总所周知,安卓UI是基于View(屏幕上的单一节点)和ViewGroup(屏幕上节点的集合),在android中有很多widgets和layouts可以用于创建UI界面,比如最常见的View有Button,TextView等等,而最常见的布局也有RelativeLayout,LinearLayout等。

在一些应用中我们不得不自定义View去满足我们的需求,自定义View可以继承一个View或者已存在的子类去创建我们自己的自定义View,甚至可以用SurfaceView去做更复杂的绘图。

创建一个自定义View的一般步骤是继承View或者其子类,重写一些方法比如onDraw,onMeasure,onLayout,onTouchEvent,然后再activity中使用我们的自定义View。

我们主要是通过以下五个方面创建一个自定义View
1,绘图,通过重写onDraw方法控制View在屏幕上的渲染效果
2,交互,通过重写onTouchEvent方法或者使用手势来控制用户的交互
3,测量,通过重写onMeasure方法来对控件进行测量
4,属性,可以通过xml自定义控件的属性,然后通过TypedArray来进行使用
5,状态的保存,为了避免配置改变时丢失View状态,通过重写onSaveInstanceState,onRestoreInstanceState方法来保存和恢复状态

可能这样说比较笼统,我们通过一个例子来进一步了解,假设我们需要一个View允许用户选择不同的形状,而这个控件只会显示一些简单的形状,比如正方形,圆形,三角形,通过点击图形能够在不同形状之间切换。先看下效果图,不断点击进行切换。

一、定义自定义View的类。
为了创建点击可切换的形状的自定义View,我们继承View,编写构造方法。实现三个构造方法,最终调用三个参数的构造方法。

public class CustomView extends View {    public CustomView(Context context) {        this(context, null);    }    public CustomView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public CustomView(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);    }}

二、把自定义View加入到Layout中。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" >    <cn.edu.zafu.view.CustomView  android:id="@+id/customview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" /></RelativeLayout>

三、定义自定义属性。
一个良好的自定义控件应该是能通过xml进行控制的,所以我们需要考虑一下我们的自定义View的哪些属性需要被提取到xml中,比如,我们应该可以让用户选择图形的颜色,是否显示图形的名称等。我们可以通过下面的代码在xml中进行配置

  <cn.edu.zafu.view.CustomView  xmlns:app="http://schemas.android.com/apk/res/cn.edu.zafu.view" app:displayShapeName="true" app:shapeColor="#7f0000" />

为了能够使用图形的颜色和图形显示的名字的属性,我们应该新建res/values/attrs.xml文件,在里面定义这些属性

<?xml version="1.0" encoding="utf-8"?><resources>   <declare-styleable name="CustomView">       <attr name="shapeColor" format="color" />       <attr name="displayShapeName" format="boolean" />   </declare-styleable></resources>

注意上述代码,我们为每一个attr节点都写了name属性和format属性,format是属性的数据结构,合法的值包括string, color, dimension, boolean, integer, float, enum等

一旦我们定义了自定义属性,我们就可以在xml文件里进行使用,唯一的区别就是我们自定义属性的命名空间是不同的,我们需要在布局的根节点上或者自定义View上定义命名空间,然后才能使用自定义属性。这里我直接在View上定义命名空间,完全可以把命名空间提取到根布局上。

四、应用自定义属性。
现在我们已经通过xml设定了自定义属性shapeColor和displayShapeName,我们需要在构造方法中提取到这些属性。为了提取属性,我们使用TypedArray类和obtainStyledAttributes方法。

public class CustomView extends View {    private int shapeColor;    private boolean displayShapeName;    public CustomView(Context context) {        this(context, null);    }    public CustomView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public CustomView(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);        setupAttributes(attrs);    }    private void setupAttributes(AttributeSet attrs) {        // 提取自定义属性到TypedArray对象中        TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs,                R.styleable.CustomView, 0, 0);        // 将属性赋值给成员变量        try {            shapeColor = a.getColor(R.styleable.CustomView_shapeColor,                    Color.BLACK);            displayShapeName = a.getBoolean(                    R.styleable.CustomView_displayShapeName, false);        } finally {            // TypedArray对象是共享的必须被重复利用。            a.recycle();        }    }}

五、增加属性的getter和setter方法

public boolean isDisplayingShapeName() {    return displayShapeName;  }  public void setDisplayingShapeName(boolean state) {    this.displayShapeName = state;    invalidate();//重绘    requestLayout();  }  public int getShapeColor() {    return shapeColor;  }  public void setShapeColor(int color) {    this.shapeColor = color;    invalidate();    requestLayout();  }

注意以上代码,当View的属性发生改变时我们需要进行重绘和重新布局,为了保证正常进行,请确保调用了invalidate和requestLayout方法。

六、绘制图形
接下来,让我们开始真正使用自定义属性(颜色,是否显示图形名)进行图形的绘制。所有的View的绘制发生在onDraw方法里,我们使用其参数Canvas将图形绘制到View上,现在我们绘制一个正方形。

public class CustomView extends View {    private int shapeWidth = 100;    private int shapeHeight = 100;    private int textXOffset = 0;    private int textYOffset = 30;    private Paint paintShape;    private int currentShapeIndex = 0;    public CustomView(Context context) {        this(context, null);    }    public CustomView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public CustomView(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);        setupAttributes(attrs);        setupPaint();    }    private void setupPaint() {        paintShape = new Paint();        paintShape.setStyle(Style.FILL);        paintShape.setColor(shapeColor);        paintShape.setTextSize(30);    }}

以上代码会绘制我们定义的颜色的图形,如果显示图形名,其图形名也会被显示,效果图就跟上面的gif图片里的正方形一样。

七、计算尺寸
为了按照用户定义的宽度高度进行绘制,我们需要重写onMeasure方法进行View的测量,该方法决定了View的宽度和高度。我们定义的View的宽度和高度由我们的形状和形状名字共同决定。我们先看下onMeasure的代码。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        // 简单定义文本边距        int textPadding = 10;        int contentWidth = shapeWidth;        // 使用测量模式获得宽度        int minw = contentWidth + getPaddingLeft() + getPaddingRight();        int w = resolveSizeAndState(minw, widthMeasureSpec, 0);        // 同宽度        int minh = shapeHeight + getPaddingBottom() + getPaddingTop();        //如果现实图形名,则加上文字高度        if (displayShapeName) {            minh += textYOffset + textPadding;        }        int h = resolveSizeAndState(minh, heightMeasureSpec, 0);        // 测量完成后必须调用setMeasuredDimension方法        // 之后可以通过getMeasuredWidth 和 getMeasuredHeight 方法取出高度和宽度        setMeasuredDimension(w, h);    }

注意以上计算要将View的内边距计算进去然后再计算整个宽度高度,并且最后必须调用setMeasuredDimension方法设置宽度和高度,resolveSizeAndState() 方法将返回一个合适的尺寸,只要将测量模式和我们计算的宽度高度传进去即可。该方法在API11开始出现,低于该版本将无法使用该方法,这里我抽取android的源码供参考。

/** * Utility to reconcile a desired size and state, with constraints imposed * by a MeasureSpec. Will take the desired size, unless a different size * is imposed by the constraints. The returned value is a compound integer, * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the resulting * size is smaller than the size the view wants to be. * * @param size How big the view wants to be * @param measureSpec Constraints imposed by the parent * @return Size information bit mask as defined by * {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}. */    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {        int result = size;        int specMode = MeasureSpec.getMode(measureSpec);        int specSize =  MeasureSpec.getSize(measureSpec);        switch (specMode) {        case MeasureSpec.UNSPECIFIED:            result = size;            break;        case MeasureSpec.AT_MOST:            if (specSize < size) {                result = specSize | MEASURED_STATE_TOO_SMALL;            } else {                result = size;            }            break;        case MeasureSpec.EXACTLY:            result = specSize;            break;        }        return result | (childMeasuredState&MEASURED_STATE_MASK);    }

该方法里设计到了两处位运算,暂时还没搞懂这两处位运算有什么作用,如果有清除的还请帮忙解释下作用。

八、在不同图形之间进行切换
现在我们已经绘制了正方形,但是我们想让view在我们点击它的时候切换图形,现在我们给它加入事件,我们重写onTouchEvent方法即可

  private String[] shapeValues = { "square", "circle", "triangle" };  private int currentShapeIndex = 0;  @Override  public boolean onTouchEvent(MotionEvent event) {    boolean result = super.onTouchEvent(event);    if (event.getAction() == MotionEvent.ACTION_DOWN) {      currentShapeIndex ++;      if (currentShapeIndex > (shapeValues.length - 1)) {        currentShapeIndex = 0;      }      postInvalidate();      return true;    }    return result;  }

现在无论什么时候点击view,都会选中对应的形状,当postInvalidate 方法被调用后就会进行重绘,现在我们更新onDraw代码,绘制不同的图形。

protected void onDraw(Canvas canvas) {    super.onDraw(canvas);    String shapeSelected = shapeValues[currentShapeIndex];    if (shapeSelected.equals("square")) {      canvas.drawRect(0, 0, shapeWidth, shapeHeight, paintShape);      textXOffset = 0;    } else if (shapeSelected.equals("circle")) {      canvas.drawCircle(shapeWidth / 2, shapeHeight / 2, shapeWidth / 2, paintShape);      textXOffset = 12;    } else if (shapeSelected.equals("triangle")) {      canvas.drawPath(getTrianglePath(), paintShape);      textXOffset = 0;    }    if (displayShapeName) {      canvas.drawText(shapeSelected, 0 + textXOffset, shapeHeight + textYOffset, paintShape);    }  }  protected Path getTrianglePath() {    Point p1 = new Point(0, shapeHeight), p2 = null, p3 = null;    p2 = new Point(p1.x + shapeWidth, p1.y);    p3 = new Point(p1.x + (shapeWidth / 2), p1.y - shapeHeight);    Path path = new Path();    path.moveTo(p1.x, p1.y);    path.lineTo(p2.x, p2.y);    path.lineTo(p3.x, p3.y);    return path;  }

现在我们点击view,每点击一次图形就会进行切换,其效果图就跟最初贴的gif图片一样。

九、完善控件
增加getter方法获得图形名

public String getSelectedShape() {    return shapeValues[currentShapeIndex];  }

现在在activity中,我们就可以通过getSelectedShape可以获取到图形名了。

十、状态的保存

当配置改变时,比如手机屏幕发生旋转,我们必须保存一些数据供从容保证view的状态不会发生改变。我们通过重写onSaveInstanceState和onRestoreInstanceState方法来保存和恢复数据。比如,在我们的view中,我们需啊哟保存的数据是当前是什么图形,可以通过保存数组的下标currentShapeIndex来实现。

 @Override  public Parcelable onSaveInstanceState() {    // 新建一个Bundle    Bundle bundle = new Bundle();    // 保存view基本的状态,调用父类方法即可    bundle.putParcelable("instanceState", super.onSaveInstanceState());    // 保存我们自己的数据    bundle.putInt("currentShapeIndex", this.currentShapeIndex);    // 当然还可以继续保存其他数据    // 返回bundle对象    return bundle;  }  @Override  public void onRestoreInstanceState(Parcelable state) {    // 判断该对象是否是我们保存的    if (state instanceof Bundle) {      Bundle bundle = (Bundle) state;      // 把我们自己的数据恢复      this.currentShapeIndex = bundle.getInt("currentShapeIndex");      // 可以继续恢复之前的其他数据      // 恢复view的基本状态      state = bundle.getParcelable("instanceState");    }    // 如果不是我们保存的对象,则直接调用父类的方法进行恢复    super.onRestoreInstanceState(state);  }

一旦我们定义这些保存和恢复的方法,我们就能够在配置发生改变时保存我们必要的数据。

好了,整个流程就大致这样,可能很多语句都会读上去不通,但是还是可以凑合看的,整个文章翻译后自己做过部分整理。希望可以给android刚入门的新手带来一些帮助,同时呢,大神勿喷。

源码下载

自定义View过程解析源代码下载

更多相关文章

  1. Android(安卓)Studio中配置AndroidAnnotations,遇到的问题及解决
  2. eclipse 上调试android的自带应用方法
  3. android实现横向滚动
  4. Android程序的签名保护及绕过方法
  5. 9.1、Android中得到新打开Activity 关闭后返回的数据
  6. Matrix详解_Matrix怎么用
  7. 如何在Android(安卓)或Linux 下,做Suspend /Resume 的Debug
  8. Android系列之GreenDao连表查询(二)
  9. Android(安卓)自定义DialogFragment 以及设置宽高

随机推荐

  1. android 多语言实现总结
  2. Android(安卓)两个可拖动的SeekBar 两点
  3. Android(二)HelloWorld,Android(上)
  4. 环境配置
  5. API 23 widget.AnalogClock——属性分析
  6. edittext底部输入
  7. Android(安卓)如何获取RadioGroup选中Rad
  8. android N0 屏蔽某个应用的通知
  9. 安卓开发问题记录
  10. ListView CheckBox点击事件