很多时候系统自带的View满足不了设计的要求,就需要自定义View控件。

自定义View的方法:

  1. 继承View
  2. 继承特定的View(如Button)
  3. 继承ViewGroup
  4. 继承特定的ViewGroup(如LinearLayout)


我们先了解一下View的一些内容

一、View

1.LayoutInflater

我们可以使用LayoutInflater来加载布局。加载布局的任务通常都是在Activity中调用setContentView()方法来完成的。其实setContentView()方法的内部也是使用LayoutInflater来加载布局的。


LayoutInflater layoutInflater = getLayoutInflater().from(this);//获取到LayoutInflater的实例
View view = layoutInflater.inflate(resource,container,flase);//加载布局

2.动态添加View


mainLayout = (LinearLayout) findViewById(R.id.main_layout);
LayoutInflater layoutInflater = LayoutInflater.from(this);
View buttonLayout = layoutInflater.inflate(R.layout.button_layout, null);
mainLayout.addView(buttonLayout);


先是获取到了LayoutInflater的实例,然后调用它的inflate()方法来加载button_layout这个布局,最后调用LinearLayout的addView()方法将它添加到LinearLayout中。这样就把button_layout这个布局添加到主布局文件的LinearLayout中了。

inflate还有一个三个参数的方法:


inflate(int resource, ViewGroup root, boolean attachToRoot);

第一个参数就是要加载的布局id,第二个参数是指给该布局的外部再嵌套一层父布局,如果不需要就直接传null。那么第三个参数attachToRoot又是什么意思呢?这里我先将结论说一下吧,感兴趣的朋友可以再阅读一下源码。

  1. 如果root为null,attachToRoot将失去作用,设置任何值都没有意义。
  2. 如果root不为null,attachToRoot设为true,则会给加载的布局文件的指定一个父布局,即root。
  3.  如果root不为null,attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效。
  4.  在不设置attachToRoot参数的情况下,如果root不为null,attachToRoot参数默认为true。

注:在setContentView()方法中,Android会自动在布局文件的最外层再嵌套一个FrameLayout,所以layout_width和layout_height属性才会有效果。


二、View绘制过程

每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw()。一般继承View的需要重写onMeasure()和onDraw()方法,继承ViewGroup的需要重写onMeasure()和onLayout()方法。

下面介绍一下三个方法。

(1)onMeasure()

onMeasure()方法顾名思义就是用于测量视图的大小的。View系统的绘制流程会从ViewRoot的 performTraversals()方法中开始,在其内部调用View的measure()方法。measure()方法接收两个参 数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。
MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型,如下所示:
1. EXACTLY

一般是设置了明确的值或者是MATCH_PARENT。
2. AT_MOST

表示子布局限制在一个最大值内,一般为WARP_CONTENT。
3. UNSPECIFIED

表示子布局想要多大就多大(xxdp),很少使用。

看个例子


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Try for a width based on our minimum
   int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 1);
   // Whatever the width ends up being, ask for a height that would let the pie
   // get as big as it can
   int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, heightMeasureSpec, 0);
   setMeasuredDimension(w, h);//设置最后的实际测量结果
}


上面的代码有三个重要的事情需要注意:

  • 计算的过程有把view的padding考虑进去。这个在后面会提到,这部分是view所控制的。
  • 帮助方法resolveSizeAndState()是用来创建最终的宽高值的。这个方法会通过比较view的需求大小与spec值,返回一个合适的View.MeasureSpec值,并传递到onMeasure方法中。
  • onMeasure()没有返回值。它通过调用setMeasuredDimension()来获取结果。调用这个方法是强制执行的,如果你遗漏了这个方法,会出现运行时异常。如果调用了super. onMeasure(widthMeasureSpec,heightMeasureSpec)就可以不写setMeasuredDimension(),因为设置了默认值。这种情况下如果设置了wrap_content或padding将无效,因为默认情况下wrap_content将填充整个屏幕。

还需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。
由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在XML文件中指定视图的大小,然后视图本身会对最终的大小进行排版。

当View为ViewGroup时,可以通过调用measureChildren()方法来遍历计算每个子View的测量大小。
小结:View测量的大小是由父布局以及子View本身共同计算的;ViewGroup需要对子View的测量负责,提供widthMeasureSpec,heightMeasureSpec两个参数。

(2)onDraw()

onDraw()的参数是一个Canvas对象。Canvas类定义了绘制文本,线条,图像与许多其他图形的方法。你可以在onDraw方法里面使用那些方法来创建你的UI。
在你调用任何绘制方法之前,你需要创建一个Paint对象。

  • 绘制什么,由Canvas处理
  • 如何绘制,由Paint处理

例如Canvas提供绘制一条直线的方法,Paint提供直线颜色。Canvas提供绘制矩形的方法,Paint定义是否使用颜色填充。简单来说:Canvas定义你在屏幕上画的图形,而Paint定义颜色,样式,字体,所以在绘制之前,你需要创建一个或者多个Paint对象。

(3)onLayout()

用于给视图进行布局的,也就是确定视图的位置。
layout()方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。
getWidth()方法和getMeasureWidth()方法到底有什么区别呢?它们的值好像永远都是相同的。其实它们的值之所以会相同基本都是因为布局设计者的编码习惯非常好,实际上它们之间的差别还是挺大的。
首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才 能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而 getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。


三、View中三个比较重要的方法

  • requestLayout():View重新调用一次layout过程。
  • invalidate():View重新调用一次draw过程
  • forceLayout():标识View在下一次重绘,需要重新调用layout过程


四、自定义属性

为了添加一个内置的View到你的UI上,你需要通过XML属性来指定它的样式与行为。良好的自定义views可以通过XML添加和改变样式,为了让你的自定义的view也有如此的行为,你应该:

  • 为你的view在资源标签下定义自设的属性
  • 在你的XML layout中指定属性值
  • 在运行时获取属性值
  • 把获取到的属性值应用在你的view上

在res/values/attrs.xml文件下,定义自定义属性

<resources>
   <declare-styleable name="PieChart">
       <attr name="showText" format="boolean" />
       <attr name="labelPosition" format="enum">
           <enum name="left" value="0"/>
           <enum name="right" value="1"/>
       </attr>
   </declare-styleable>
</resources>


上面的代码声明了2个自设的属性,showText与labelPosition,它们都归属于PieChart的项目下的styleable实例。styleable实例的名字,通常与自定义的view名字一致。

一旦你定义了自设的属性,你可以在layout XML文件中使用它们,就像内置属性一样。唯一不同的是你自设的属性是归属于不同的命名空间。不是属于http://schemas.android.com/apk/res/android的命名空间,它们归属于http://schemas.android.com/apk/res/[your package name]。例如,下面演示了如何为PieChart使用上面定义的属性:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews">
 <com.example.customviews.charting.PieChart
     custom:showText="true"
     custom:labelPosition="left" />
</LinearLayout>


为了避免输入长串的namespace名字,示例上面使用了xmlns指令,这个指令可以指派custom作为http://schemas.android.com/apk/res/com.example.customviewsnamespace的别名。你也可以选择其他的别名作为你的namespace。

请注意,如果你的view是一个inner class,你必须指定这个view的outer class。同样的,如果PieChart有一个inner class叫做PieView。为了使用这个类中自设的属性,你应该使用com.example.customviews.charting.PieChart$PieView。

参考自:


http://android-doc.com/training/custom-views/index.html