具体自定义控件

view的自定义小结

2019-08-06  本文已影响0人  芝林_e8cf

自定义View

为什么要自定义View?

---既然goole已经为我们提供了很多原生的view,我们为什么还要自定义view呢?主要是Android系统内置的View无法实现我们的需求,我们需要针对我们的业务需求定制我们想要的View。同时自定义View对于一个Android开发者来说是必须掌握的知识点,也是Android开发进阶的必经之路。

如何实现view的自定义?

自定义View的最基本的三个方法分别是: onMeasure()、onLayout()、onDraw();

View在Activity中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout和draw。

测量:onMeasure()决定View的大小;

布局:onLayout()决定View在ViewGroup中的位置;

绘制:onDraw()决定绘制这个View。

自定义控件又分为自定义View和自定义ViewGroup,自定义View只需要重写onMeasure()和onDraw()即可,而自定义ViewGroup则只需要重写onMeasure()和onLayout()。

自定义View原理是Android开发者必须了解的基础,基础掌握了我们才能进行下一步的学习

基础的学习思路:

View的分类

类别解释特点

单一视图即一个View,如TextView不包含子View

视图组即多个View组成的ViewGroup,如LinearLayout包含子View

View类简介:

          View类是Android中各种组件的基类,如View是ViewGroup基类。

          Android中的UI组件都由View、ViewGroup组成。

          View的构造函数:共有4个,具体如下:

--------------自定义view必须重写至少一个构造

// 如果View是在Java代码里面new的,则调用第一个构造函数

publicCarsonView(Contextcontext) {

super(context);

   }

// 如果View是在.xml里声明的,则调用第二个构造函数

// 自定义属性是从AttributeSet参数传进来的

publicCarsonView(Contextcontext,AttributeSetattrs) {

super(context,attrs);

   }

// 不会自动调用

// 一般是在第二个构造函数里主动调用

// 如View有style属性时

publicCarsonView(Contextcontext,AttributeSetattrs,intdefStyleAttr) {

super(context,attrs,defStyleAttr);

   }

//API21之后才使用

// 不会自动调用

// 一般是在第二个构造函数里主动调用

// 如View有style属性时

publicCarsonView(Contextcontext,AttributeSetattrs,intdefStyleAttr,intdefStyleRes) {

super(context,attrs,defStyleAttr,defStyleRes);

   }

3.View视图结构

对于多View的视图,结构是树形结构:最顶层是ViewGroup,ViewGroup下可能有多个ViewGroup或View,如下图:

一定要记住:无论是measure过程、layout过程还是draw过程,永远都是从View树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个View树中各个View,最终确定整个View树的相关属性。

Android坐标系

Android的坐标系定义为:

屏幕的左上角为坐标原点

向右为x轴增大方向

向下为y轴增大方向

View位置(坐标)描述

View的位置由4个顶点决定的(如下A、B、C、D):

注意:View的位置是相对于父控件而言的

Top:子View上边界到父view上边界的距离

Left:子View左边界到父view左边界的距离

Bottom:子View下边距到父View上边界的距离

Right:子View右边界到父view左边界的距离

位置获取方式

View的位置是通过view.getxxx()函数进行获取:(以Top为例)

// 获取Top位置

public final int getTop() {  

   return mTop;  

}  

// 其余如下:

  getLeft();      //获取子View左上角距父View左侧的距离

  getBottom();    //获取子View右下角距父View顶部的距离

  getRight();     //获取子View右下角距父View左侧的距离

Android的角度(angle)与弧度(radian)

自定义View实际上是将一些简单的形状通过计算,从而组合到一起形成的效果。

这会涉及到画布的相关操作(旋转)、正余弦函数计算等,即会涉及到角度(angle)与弧度(radian)的相关知识

角度和弧度都是描述角的一种度量单位,区别如下图:

在默认的屏幕坐标系中角度增大方向为顺时针。

注:在常见的数学坐标系中角度增大方向为逆时针

Android中颜色相关内容:

  Android中的颜色相关内容包括颜色模式,创建颜色的方式,以及颜色的混合模式等。

  Android支持的颜色模式: 

以ARGB8888为例介绍颜色定义:

在java中定义颜色:

//java中使用Color类定义颜色

int color = Color.GRAY;     //灰色

//Color类是使用ARGB值进行表示

int color = Color.argb(127, 255, 0, 0);   //半透明红色

int color = 0xaaff0000;                   //带有透明度的红色

在xml文件中定义颜色:

<?xml version="1.0" encoding="utf-8"?>

<resources>

   //定义了红色(没有alpha(透明)通道)

   <color name="red">#ff0000</color>

   //定义了蓝色(没有alpha(透明)通道)

   <color name="green">#00ff00</color>

</resources>

在java文件中引用xml中定义的颜色:

//方法1

int color = getResources().getColor(R.color.mycolor);

//方法2(API 23及以上)

int color = getColor(R.color.myColor);

在xml文件(layout或style)中引用或者创建颜色:

<!--在style文件中引用-->

  <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">

      <item name="colorPrimary">@color/red</item>

  </style>

<!--在layout文件中引用在/res/values/color.xml中定义的颜色-->

android:background="@color/red"    

<!--在layout文件中创建并使用颜色-->

android:background="#ff0000"

有了以上的基础,接下来我们就可以来学习自定义view的具体过程了;

首先是Measure过程

OnMeasure()方法是自定义控件中非常重要的一个方法,下面我们来系统的学习,由浅至深来Measure的过程

Measure的作用作用:

测量View的宽/高:

1.在某些情况下,需要多次测量(measure)才能确定View最终的宽/高;

2.在这种情况下measure过程后得到的宽/高可能是不准确的;

3.建议在layout过程中onLayout()去获取最终的宽/高

准备的基础

在了解measure 过程前,我们需要先了解measure过程中传递尺寸(宽 / 高测量值)的两个类:

ViewGroup.LayoutParams (View 自身的布局参数)

MeasureSpecs 类(父视图对子视图的测量要求)

2.1  ViewGroup.LayoutParams:

这个类我们很常见,用来指定视图的高度(height)和宽度(width)等布局参数。可通过以下参数进行指定:

fill_parent :     即一个View,如TextView

match_parent:    与fill_parent相同,用于Android 2.3及之后版本

wrap_content :    自适应大小,强制性地使视图扩展以便显示其全部内容(含 padding )

具体的应用如下图:

android:layout_weight="wrap_content"   //自适应大小  

android:layout_weight="match_parent"   //与父视图等高  

android:layout_weight="fill_parent"    //与父视图等高  

android:layout_weight="100dip"         //精确设置高度值为 100dip

ViewGroup 的子类有其对应的 ViewGroup.LayoutParams 子类

1.ViewGroup 的子类包括RelativeLayout、LinearLayout等;

2.如 RelativeLayout的 ViewGroup.LayoutParams 的子类是RelativeLayoutParams。

构造函数

构造函数是View的入口,可以用于初始化一些的内容,和获取自定义属性。

// View的构造函数有四种重载

   public DIY_View(Context context){

       super(context);

   }

   public DIY_View(Context context,AttributeSet attrs){

       super(context, attrs);

   }

   public DIY_View(Context context,AttributeSet attrs,int defStyleAttr ){

       super(context, attrs,defStyleAttr);

// 第三个参数:默认Style

// 默认Style:指在当前Application或Activity所用的Theme中的默认Style

// 且只有在明确调用的时候

2.2      MeasureSpec

2.21  定义:测量规格也可理解为是测量View的依据

MeasureSpec的类型分为两种:

即每个MeasureSpec代表了一组宽度和高度的测量规格

2.22  作用:决定了一个view的大小(宽/高)

2.23  组成:如下图

其中,Mode模式共分为三类:

UNSPECIFIED模式():unspecified  (未指明的;未详细说明的)

EXACTLY模式:exactly  (恰好地;正是;精确地;正确地)

AT_MOST模式:at most:至多

具体说明如下图:

2.2.4  MeasureSpec类的使用

MeasureSpec 、Mode 和Size都封装在View类中的一个内部类里 - MeasureSpec类。

MeasureSpec类通过使用二进制,将mode和size打包成一个int值来减少对象内存分配,用一个变量携带两个数据(size,mode),并提供了打包和解包的方法

2.2.6  MeasureSpec值的确定:

子View的MeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。

如下图:

关于getChildMeasureSpec()里对于子View的测量模式和大小的判断逻辑有点复杂;

别担心,我已经帮大家总结好。具体子View的测量模式和大小请看下表:

规律总结:(以子View为标准,横向观察)

当子View采用具体数值(dp / px)时无论父容器的测量模式是什么,子View的测量模式都是EXACTLY且大小等于设置的具体数值;

当子View采用match_parent时子View的测量模式与父容器的测量模式一致若测量模式为EXACTLY,则子View的大小为父容器的剩余空间;若测量模式为AT_MOST,则子View的大小不超过父容器的剩余空间

当子View采用wrap_parent时无论父容器的测量模式是什么,子View的测量模式都是AT_MOST且大小不超过父容器的剩余空间。

UNSPECIFIED模式:由于适用于系统内部多次measure情况,很少用到,故此处不讨论

注:区别于顶级View(即DecorView)的计算逻辑

onMeasure()方法中常用的方法:

1.getChildCount():获取子View的数量;

2.getChildAt(i):获取第i个子控件;

3.subView.getLayoutParams().width/height:设置或获取子控件的宽或高;

4.measureChild(child, widthMeasureSpec, heightMeasureSpec):测量子View的宽高;

5.child.getMeasuredHeight/width():执行完measureChild()方法后就可以通过这种方式获取子View的宽高值;

6.getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;

7.setMeasuredDimension(width, height):重新设置控件的宽高。如果写了这句代码,就需要删除“super. onMeasure(widthMeasureSpec, heightMeasureSpec);”这行代码。

注意:onMeasure()方法可能被调用多次,这是因为控件中的内容或子View可能对分配给自己的空间“不满意”,因此向父空间申请重新分配空间。

接下来是layout过程:

主要说onLayout()这个方法:

onLayout()方法负责布局,大多数情况是在自定义ViewGroup中才会重写,主要用来确定子View在这个布局空间中的摆放位置。 onLayout(boolean changed, int left, int top, int right, int bottom)方法有5个参数,其中changed表示这个控件是否有了新的尺寸或位置;left、top、right、bottom分别表示这个View相对于父布局的左/上/右/下方的位置。

以下是onLayout()方法中常用的方法:

1.getChildCount():获取子View的数量;

2.getChildAt(i):获取第i个子View

3.getWidth/Height():获取onMeasure()中返回的宽度和高度的测量值;

4.child.getLayoutParams():获取到子View的LayoutParams对象;

5.child.getMeasuredWidth/Height():获取onMeasure()方法中测量的子View的宽度和高度值;

6.getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;

7.child.layout(l, t, r, b):设置子View布局的上下左右边的坐标。

Draw过程:

onDraw()  方法负责绘制,即如果我们希望得到的效果在Android原生控件中没有现成的支持,那么我们就需要自己绘制我们的自定义控件的显示效果。要学习onDraw()方法,我们就需要学习在onDraw()方法中使用最多的两个类:PaintCanvas

注意:每次自定义View/ViewGroup时都会调用onDraw()方法。

Paint类

Paint 画笔对象,这个类中包含了如何绘制几何图形、文字和位图的样式和颜色信息,指定了如何绘制文本和图形。画笔对象有很多设置方法,大体上可以分为两类:图形绘制和文本绘制。

Paint类中有如下方法:

1、图形绘制:

1)  setArgb(int a, int r, int g, int b):设置绘制的颜色,a表示透明度,r、g、b表示颜色值;

2)  setAlpha(int a):设置绘制的图形的透明度;

3)  setColor(int color):设置绘制的颜色;

4)  setAntiAlias(boolean a):设置是否使用抗锯齿功能,抗锯齿功能会消耗较大资源,绘制图形的速度会减慢;

5)  setDither(boolean b):设置是否使用图像抖动处理,会使图像颜色更加平滑饱满,更加清晰;

6)  setFileterBitmap(Boolean b):设置是否在动画中滤掉Bitmap的优化,可以加快显示速度;

7)  setMaskFilter(MaskFilter mf):设置MaskFilter来实现滤镜的效果;

8)  setColorFilter(ColorFilter cf):设置颜色过滤器,可以在绘制颜色时实现不同颜色的变换效果;

9)  setPathEffect(PathEffect pe):设置绘制的路径的效果;

10) setShader(Shader s):设置Shader绘制各种渐变效果;

11) setShadowLayer(float r, int x, int y, int c):在图形下面设置阴影层,r为阴影角度,x和y为阴影在     x轴和y轴上的距离,c为阴影的颜色;

12) setStyle(Paint.Style s):设置画笔的样式:FILL实心;STROKE空心;FILL_OR_STROKE同时实心与空心;

13) setStrokeCap(Paint.Cap c):当设置画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式;

14) setStrokeJoin(Paint.Join j):设置绘制时各图形的结合方式;

15) setStrokeWidth(float w):当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度;

16) setXfermode(Xfermode m):设置图形重叠时的处理方式;

2、文本绘制:

1)  setTextAlign(Path.Align a):设置绘制的文本的对齐方式;

2)  setTextScaleX(float s):设置文本在X轴的缩放比例,可以实现文字的拉伸效果;

3)  setTextSize(float s):设置字号;

4)  setTextSkewX(float s):设置斜体文字,s是文字倾斜度;

5)  setTypeFace(TypeFace tf):设置字体风格,包括粗体、斜体等;

6)  setUnderlineText(boolean b):设置绘制的文本是否带有下划线效果;

7)  setStrikeThruText(boolean b):设置绘制的文本是否带有删除线效果;

8)  setFakeBoldText(boolean b):模拟实现粗体文字,如果设置在小字体上效果会非常差;

9)  setSubpixelText(boolean b):如果设置为true则有助于文本在LCD屏幕上显示效果;

Canvas类

Canvas  即画布,其上可以使用Paint画笔对象绘制很多东西。Canvas对象中可以绘制:

1)  drawArc():绘制圆弧;

2)  drawBitmap():绘制Bitmap图像;

3)  drawCircle():绘制圆圈;

4)  drawLine():绘制线条;

5)  drawOval():绘制椭圆;

6)  drawPath():绘制Path路径;

7)  drawPicture():绘制Picture图片;

8)  drawRect():绘制矩形;

9)  drawRoundRect():绘制圆角矩形;

10) drawText():绘制文本;

11) drawVertices():绘制顶点。

Canvas对象的其他方法:

1)  canvas.save():把当前绘制的图像保存起来,让后续的操作相当于是在一个新图层上绘制;

2)  canvas.restore():把当前画布调整到上一个save()之前的状态;

3)  canvas.translate(dx, dy):把当前画布的原点移到(dx, dy)点,后续操作都以(dx, dy)点作为参照;

4)  canvas.scale(x, y):将当前画布在水平方向上缩放x倍,竖直方向上缩放y倍;

5)  canvas.rotate(angle):将当前画布顺时针旋转angle度.

小结:绘制什么,由Canvas处理。          怎么去绘制,由Paint处理。

最后我们需要注意的是绘制顺序

Android 里面的绘制都是按顺序的,先绘制的内容会被后绘制的盖住。比如你在重叠的位置先画圆再画方,和先画方再画圆所呈现出来的结果肯定是不同的:

到底放在 super.onDraw() 上面还是下面?通常如果我们继承的是 View 的话,super.onDraw() 只是一个空实现,所以它的位置放在哪儿都没事,甚至直接不要也没事,但反正加上也没啥影响,尽量还是加上吧。由于 Android 的绘制顺序性,当你继承至已经有绘制的其他 View(比如 TextView)的时候,放在 super.onDraw() 上面就意味着绘制代码会被控件的原内容盖住。

dispatchDraw():绘制子 View 的方法

自定义绘制其实不止 onDraw() 一个方法。onDraw() 只是负责自身主体内容绘制的。而有的时候,你想要的遮盖关系无法通过 onDraw() 来实现,而是需要通过别的绘制方法。

例如,你继承了一个 LinearLayout,重写了它的 onDraw() 方法,在 super.onDraw() 中插入了你自己的绘制代码,使它能够在内部绘制一些斑点作为点缀:

看起来确实没有问题,但是你会发现,当你添加了子 View 之后,你的斑点不见了:

造成这种情况的原因是 Android 的绘制顺序:在绘制过程中,每一个 ViewGroup 会先调用自己的 onDraw() 来绘制完自己的主体之后再去绘制它的子 View。对于上面这个例子来说,就是你的 LinearLayout 会在绘制完斑点后再去绘制它的子 View。那么在子 View 绘制完成之后,先前绘制的斑点就被子 View 盖住了。具体来讲,这里说的「绘制子 View」是通过另一个绘制方法的调用来发生的,这个绘制方法叫做:dispatchDraw()。也就是说,在绘制过程中,每个 View 和 ViewGroup 都会先调用 onDraw() 方法来绘制主体,再调用 dispatchDraw() 方法来绘制子 View。

怎样才能让 LinearLayout 的绘制内容盖住子 View 呢?只要让它的绘制代码在子 View 的绘制之后再执行就好了。所以直接执行在 super.dispatchDraw() 的下面即可。

下面是绘制的流程图:

view自定义直通车
上一篇下一篇

猜你喜欢

热点阅读