android 自定义View过程解析

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过程解析源代码下载

时间: 2024-10-08 13:09:08

android 自定义View过程解析的相关文章

Android自定义View(二、深入解析自定义属性)

转载请标明出处: http://blog.csdn.net/xmxkf/article/details/51468648 本文出自:[openXu的博客] 目录: 为什么要自定义属性 怎样自定义属性 属性值的类型format 类中获取属性值 Attributeset和TypedArray以及declare-styleable ??在上一篇博客<Android自定义View(一.初体验)>中我们体验了自定义控件的基本流程: 继承View,覆盖构造方法 自定义属性 重写onMeasure方法测量宽

Android自定义View(三、深入解析控件测量onMeasure)

转载请标明出处: http://blog.csdn.net/xmxkf/article/details/51490283 本文出自:[openXu的博客] 目录: onMeasure什么时候会被调用 onMeasure方法执行流程 MeasureSpec类 从ViewGroup的onMeasure到View的onMeasure ViewGroup中三个测量子控件的方法 getChildMeasureSpec方法 View的onMeasure setMeasuredDimension ??在上一篇

Android自定义View(RollWeekView-炫酷的星期日期选择控件)

转载请标明出处: http://blog.csdn.net/xmxkf/article/details/53420889 本文出自:[openXu的博客] 目录: 1分析 2定义控件布局 3定义CustomWeekView 4重写onMeasure 5点击后执行动画 7重置预备控件 源码下载 ??最近收到一个自定义控件的需求,需要做一个日期选择控件,实现图如下: ???? ??一次展示一个星期的5天,中间放大的为当前选中的:如果点击了其中一个日期,比如星期五,那么整体向左滑动,并将星期五慢慢放大

android 自定义view 前的基础知识

本篇文章是自己自学自定义view前的准备,具体参考资料来自 Android LayoutInflater原理分析,带你一步步深入了解View(一) Android视图绘制流程完全解析,带你一步步深入了解View(二) Android视图状态及重绘流程分析,带你一步步深入了解View(三) Android自定义View的实现方法,带你一步步深入了解View(四) 这位大哥的系列博文,相当于自己看这些的一个思考吧. 一.首先学layoutInflater. 相信接触Android久一点的朋友对于La

【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象

前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/11039252.html]谢谢! 在上一篇文章[[朝花夕拾]Android自定义View篇之(五)Android事件分发机制(上)Touch三个重要方法的处理逻辑][下文简称(五),请先阅读完(五)再阅读本文],我们通过示例和log来分析了Android的事件分发机制.这些,我们只是看到了现象,如果要进一步了解事件分发机制,这是不够的,我们还需要透过现象看本质,去研究研究源码.本文将从源码(基

Android 自定义 View 详解

View 的绘制系列文章: Android View 绘制流程之 DecorView 与 ViewRootImpl Android View 的绘制流程之 Measure 过程详解 (一) Android View 的绘制流程之 Layout 和 Draw 过程详解 (二) Android View 的事件分发原理解析 对于 Android 开发者来说,原生控件往往无法满足要求,需要开发者自定义一些控件,因此,需要去了解自定义 view 的实现原理.这样即使碰到需要自定义控件的时候,也可以游刃有

Android自定义View(CustomCalendar-定制日历控件)

转载请标明出处: http://blog.csdn.net/xmxkf/article/details/54020386 本文出自:[openXu的博客] 目录: 1分析 2自定义属性 3onMeasure 4onDraw 绘制月份 绘制星期 绘制日期及任务 5事件处理 源码下载 ??应项目需求,需要做一个日历控件,效果图如下: ???? ??接到需求后,没有立即查找是否有相关开源日历控件可用.系统日历控件是否能满足 ,第一反应就是这个控件该怎么画?谁叫咱自定义控件技术牛逼呢O(∩_∩)O哈哈~

Android自定义View,你必须知道的几点

为什么我们觉得自定义View是学习Android的一道坎? 为什么那么多Android大神却认为自定义View又是如此的简单? 为什么google随便定义一个View都是上千行的代码? 以上这些问题,相信学Android的同学或多或少都有过这样的疑问. 那么,看完此文,希望对你们的疑惑有所帮助. 回到主题,自定义View ,需要掌握的几个点是什么呢? 我们先把自定义View细分一下,分为两种 1) 自定义ViewGroup 2) 自定义View 其实ViewGroup最终还是继承之View,当然

Android 自定义View合集

自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/Mr-XiaoLiang 自定义控件三部曲 http://blog.csdn.net/harvic880925?viewmode=contents Android 从0开始自定义控件之View基础知识与概念 http://blog.csdn.net/airsaid/article/details/5