Android的自定义View深入解析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qqchenjian318/article/details/56678242

前言

自定义View是每个Android开发人员,都必备的技能。当SDK提供的常规控件如TextView、Button等没法满足我们日常开发需求时候,就需要我们进行View的自定义。本文就从View的绘制过程、自定义View的分类、自定义View的自定义属性、Canvas的简单使用、View的事件分发体系、View的滑动冲突等几个方面,简单讲解一下,如何自定义一个我们想要的View。

一、View的绘制过程

    在我们进行自定义View之前,需要简单了解一下,一个View是如何被绘制出来的。
    首先View体系中有两个概念,View和ViewGroup,View是所有UI组件的基类,ViewGroup也是View派生出来的子类。然后,我们再讲三个东西,WindowManager、DecorView、ViewRoot。通过这三个东西,我们就能对View的底层原理有个大概的了解了

WindowManager
是Android的GUI中非常重要的一环,他连接了手机屏幕和View,负责将View显示在屏幕上,以及将屏幕接受到的各个事件,如点击,滑动等,传给界面最上层的DecorView,然后通过View的事件分发体系,分发到各个View中去。
DecorView
是整个手机屏幕最顶级的View,代表了整个屏幕,包含了三个部分,通知栏、标题栏、内容显示栏,一般情况下它内部有一个竖直方向的LinerLayout,其分成了两个部分,一个是上面的标题栏,另一个就是下面的内容显示栏content。我们是给一个activity设置布局时,setContentView()方法,其实就是将我们的布局添加到了content中。
ViewRoot
连接DecorView和WindowManager的纽带,负责他们两个的交互。比如把WindowManger收到的事件传递给View等。

最后,我们来说说View的绘制过程,
View的绘制过程是从ViewRoot的peformTraversals方法发起的,该方法会分别调用测量宽高、确认在父容器中的位置、和绘制在屏幕上这三个过程。
View的measure方法,完成对View宽高的测量,然后会调用onMeasure,对它所有的子控件进行宽高的测量
View的layout方法,确认控件在父容器中的摆放位置,会调用onLayout方法,对它所有的子控件进行layout过程
View的draw方法,最终将控件绘制在屏幕上,它也会对它所有的子控件进行绘制
经过上面的三个步骤,我们就完成了View的绘制过程。

A、View的measure过程
该过程大致就是,通过解析得到的含测量模式的宽度和高度,该值由父容器的MeasureSpec和自身的LayoutParam来共同决定,根据测量模式的不同,对View的宽和高进行不同的赋值。
首先,解释下几种模式UNSPECIFIED、AT_MOST、EXACTLY :
如果是UNSPECIFIED模式,表示父容器没有对该view进行任何限制,即要多大给多大
如果是AT_MOST模式(最大化),父容器指定了一个可用大小,View的大小不能大于这个值
如果是EXACTLY 模式(精确模式),父容器已经检测出View所需要的精确大小。最终View大小就是这个值

View的测量模式和具体赋值可根据下表得来
<Image_1>
(注:第一列为View的LayoutParam属性,第一个行父容器的SpecMode)

上表可总结如下:
当View采用固定宽高时,不管父容器的模式是什么,View的MeasureSize都是精确模式,并且其大小遵循Layoutparams中的大小
当View的宽高是match_parent时,如果父容器的模式是精确模式,那么View也是精确模式并且大小是父容器的剩余空间,如果父容器
是最大化模式,那么View也是最大化模式,并且大小不能超过父容器的剩余空间
当View的宽高是wrap_content时,不管父容器的模式是精确还是最大化,View的模式总是最大化并且大小不能超过超过父容器的剩余
空间
备注:UNSPECIFIED模式,主要用于系统内部多少Measure的情形,一般来说我们不需要关注此模式

B、View的layout过程
该过程的作用就是确定View在父容器中的摆放位置,当ViewGroup确定了自己的摆放位置之后,会通过onMeasure方法依次确认遍历所有子控件,并且通过layout方法确认子控件的摆放位置。但是需要注意的就是layout过程需要考虑View的属性,比如如果是一个LinearLayout,就需要考虑是水平的还是竖直的,还有pading和margin值等。

C、View的draw过程
View的draw过程(View的draw方法),就是讲界面绘制在屏幕上面,它遵循以下几步:
1、绘制背景  background.draws(canvas)
2、绘制自己  onDraw
3、绘制children  dispathDraw
通过调用dispathDraw,依次对子控件进行draw
4、绘制装饰 onDrawForeground 
一个控件可能包括了滚动条等装饰,该方法就是绘制View的装饰物的。
如果我们需要自定义控件的话,就可以在draw过程中,通过Canvas(画板)、Paint(画笔)等进行绘制 。

二、自定义View的分类

在上面,我们讲解了一个View是如何绘制出来的,第二部分我们讲解一下自定义View的分类。其分类方式有很多,个人比较倾向于以下这种。
 A、继承View重写onDraw方法
这种方法主要用于实现一些不规则的效果,即某些效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则 的图形
这时候就需要通过绘制的方式来实现,即重写onDraw。
 B、继承ViewGroup派生特殊的Layout
这种方法主要用于实现自定义的布局,当某种效果看起来很像几个View组合做一起的时候,可以采用这种方 法来实现。采用这 种方式较复杂,需要
合适地处理ViewGroup的测量,布局这两个过程,并同时处理子元素的 测量和布局过程。
 C、继承特定的View
这种方法比较常见,一般是用于扩展某种已有的View的功能,比如TextView,这种方法比较容易实现
 D、继承特定的ViewGroup
这种方法也比较常见,指将已有的view控件,进行组合来实现想要的效果,比如在一个Linerlayout中
所有的自定义控件,都能划入这四类中来,比如做一个视频播放器,我们需要自定义一个控件MyPlayer,让他实现SurfaceView 控件,他就是属于第三
类,或者我们写一个带清除功能的EditText,我们可以通过一个EditText和一个ImageView进行组合的方式来 实现,这种属于第四种。

三、View的自定义属性

我们在xml中使用传统的控件时,可以很方便的设置这个View的一些属性
   <TextView
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_marginLeft="15dp"
         android:text="Hello World"
         android:textColor="#333333"
         android:textSize="15sp" />

如上代码,我们在xml中写了一个控件TextView,并且设置了它的属性如width、height、text、textSize等,那我们自己定义一个View,如何实现这种可以直接在xml布局中设置View属性呢。
其实自定义属性并不复杂,我们可以通过以下几个步骤,来完成View的自定义属性。

A、res文件夹下的values文件夹中下建立一个xml,比如attr.xml,然后在这个xml里面自定义一个属性集合,就是存放指定控件的自定义属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color"/>
        <attr name="fontColor" format="enum">
        <enum name="blue" value="1"/>
        <enum name="red" value="2"/>
   </attr>
    </declare-styleable>
</resources>

上述代码就创建了一个名为CircleView的自定义属性的集合,然后在这个集合里面创建我们想要设置的自定义属性项,比如circle_color 就定义了一个名
为circle_color,类型为color的自定义属性。这里的类型包括有reference(资源id)、dmension指尺寸、string、integer、color等, 上述代码也创建了一
个枚举类型的属性fontColor,开发者在设置xml属性的时候,可以在我们规定的范围内进行选择。

B、在我们的自定义View中,对这些属性进行读取和解析
public MyView(Context context, AttributeSet attrs) { 
        super(context, attrs); 
        //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签 
        //即属性集合的标签,在R文件中名称为R.styleable+name 
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView); 
        //第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称 
        //第二个参数为,如果没有设置这个属性,则设置的默认的值 
        mFontColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED); 
        //最后记得将TypedArray对象回收 
        a.recycle(); 
 } 

C、 然后就是在xml文件中使用自定义属性
首先为了可以使用自定义属性,我们需要添加schems声明。
 	xmlns:app="http://schemas.android.com/apk/res-auto"
这样我们才能知道去资源文件中找自定义好的属性集合,然后就可以使用自定义属性了
<com.example.lenovo.myapplication.MyView
        app:circle_color="#333333"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />
通过以上的步骤,我们就完成了一个View的自定义属性。怎么样?是不是很简单?

四、Canvas的简单使用

我们在自定义View的分类以及View的绘制过程中讲到了通过Canvas和Paint来绘制我们想要的View。这一部分,我们就简单来介绍一下Canvas在自定义View中的简单实用。
在自定义View的时候,我们可以通过draw方法获取到Canvas对象,所以不需要自己new对象了
 @Override
 protected void onDraw(Canvas canvas) {
        
 }
可以通过查看api的方法的知,canvas能绘制的对象有弧线(arcs)、填充颜色(argb和color)、Bitmap、圆(circle和oval)、点(point)、 线(line)、矩形(Rect)
、图片(Picture)、圆角矩形(RoundRect)、文本(text)、顶点(Vertices)、路径(Path)。我们可以通过组合这些对象 画出我们想要的图形出来。但是光有这
些还不够,当我们想操作Canvas进行位置转换时,可以通过它提供的(rorate、scale、 translate、skew(扭曲))等方法来完成一些操作。也可以通过
getMatrix方法直接操作矩阵(关于矩阵操作,就涉及到另一个知识点了 ,因为动画里面也会涉及到这部分的知识点,所以详见另一片笔记)。

做进行canvas的使用前,首先我们要知道RectF是做什么用的 RectF其实就是通过left、top、right、bottom四个坐标点来确定
一个矩形

下面就进行一些canvas的简单用法
A、比如画一个圆
@Override
    protected void onDraw(Canvas canvas) {
        Paint paint = new Paint();//new一个画笔对象,并且设置为蓝色
        paint.setColor(Color.BLUE);
        canvas.drawCircle(100,100,100,paint);//通过canvas画圆
    }
效果如下
<Image_2>

B、比如画一段弧形
@Override
    protected void onDraw(Canvas canvas) {
        Paint paint = new Paint();
        paint.setColor(Color.BLUE);
        RectF rectF = new RectF(0,0,100,100);//确定一个100 * 100的矩形
        canvas.drawArc(rectF,//在这个矩形范围内画一段圆弧
                0,//开始角度
                90,//扫过的角度
                true,//是否使用中心
                paint);
    }
效果如下
<Image_3>

C、通过path绘制一个五角星
    @Override
    protected void onDraw(Canvas canvas) {
        Paint paint = new Paint();
        paint.setColor(Color.BLUE);
        Path path = new Path();
        path.moveTo(131,5);
        path.lineTo(88.357f,63.725f);
        path.lineTo(19,42);
        path.lineTo(62.002f,100);
        path.lineTo(19,159);
        path.lineTo(88.357f,136.275f);
        path.lineTo(131,195);
        path.lineTo(131,122.419f);
        path.lineTo(200,100);
        path.lineTo(131,77.581f);
        path.lineTo(131,5);
        canvas.drawPath(path,paint);
    }
效果如下
<Image_4>

通过上述Canvas的介绍,我们就可以通过Canvas和Paint来绘制我们想要样子的自定义控件了。更多更复杂的用法,请大家自己去查询相关资料。

五、View的事件分发体系

View的事件分发是核心知识点,更是难点。所谓的事件就是指MotionEvent,当一个MotionEvent产生了之后,系统需要把这个事件传递给一个具体的
View,而这个传递的过程就是事件的分发。事件的分发由三个很重要的方法来完成:dispatchTouchEvent、onInterceptTouchEvent和on TouchEvent。

public boolean  dispatchTouchEvent(MotionEvent event):
该方法用来进行事件的分发,如果该事件能传递到当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatch
TouchEvent方法的影响,表示是否消耗该事件

public boolean onInterceptTouchEvent(MotionEvent event):
在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次
调用,返回结果表示
是否拦截当前事件。

public boolean onTouchEvent(MotionEvent  event):
在dispatichTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,
当前View无法再次接
收到事件
他们的关系可以用下列伪代码来体现
public boolean dispatchTouchEvent(MotionEvent event) {
        boolean consume = false;
        if (onInterceptTouchEvent(event)){
            consume = onTouchEvent(event);
        }else {
            consume = child.dispatchTouchEvent(event);
        }
        return consume;
  }
事件传递的大致规则如下:
对于一个根ViewGroup来说,点击事件产生后,首先会传给它,此时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的 onInterceptTouchEvent方法返回true,就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方 法就会被调用;如
果这个ViewGroup的oinInterceptTouchEvent方法返回false,就表示它不拦截当前事件,这时当前事件就会继续传 递给它的子元素,接着子元素的
dispatchToucEvent方法就会被调用,如此反复直到事件被最终处理。 如果根ViewGroup的所有子View都不处理事件,即onTouchEvent返回false,那么
它的父容器的onTouchEvent将会倍调用,以此类推 ,如果所有的View都不处理这个事件,那么这个事件将会最终传递给Activity。

一些关键点:
1、同一事件序列:是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序
列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。

2、ViewGroup默认不拦截任何事件,onInterceptTouchEvent默认返回false。

3、View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。

4、事件的传递过程总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法,可以在子
元素中干预父元素
的事件分发过程,但是ACTION_DOWN事件除外,如果DOWN事件被拦截了,那么该事件序列的其他事件都不会 再传递下去。

总结

到这里,本篇文件关于自定义View的内容,就讲解的差不多了。当然,本篇文章只是比较简单的从跟自定义View密切相关的几 个方面,对View体系进行
了一番讲解,如想自定义View必须先了解View的绘制过程,然后你得明白常见的自定义View是如何分类, 当我们对我们想要的自定义控件进行分类之后
,就能比较清晰的找到一个实现思路,比如是通过Canvas去画这个控件的样子、还是 通过控件的组合。然后,你的自定义控件肯定需要自己的自定义属
性,这样便于xml中直接使用。再然后,你得学会一点Canvas的简 单使用。这样便于你绘制自己想要的View的形状。再然后,你通过常用控件组装起自
己的自定义View时,你得知道点击、滑动等事件 是如何进行分发的。这样才能清楚的让所有子控件各司其职,不会发生错乱。

以上就是本文的全部内容,View的绘制、自定义View的分类、View的自定义属性、Canvas的简单使用、View的事件分发体系。 本文所有内容来自于自己
平时的总结以及其他人分享的经验,感谢所有无私奉献的同行。本文并没有对View的绘制、Canvas的使用 等内容进行更深入的解析,如果有这方面需求
的,请查阅相关内容。
因个人水平水平有限,难免有不足和错误之处,请大家指正。谢谢。。。


猜你喜欢

转载自blog.csdn.net/qqchenjian318/article/details/56678242