【自定义View】Android视图绘制流程完全解析

news/2024/7/11 1:59:25 标签: canvas, view
views" class="markdown_views prism-atom-one-dark">

Android中的任何一个布局、任何一个控件其实都是直接或间接继承自View的,如TextView、Button、ImageView、ListView等。这些控件虽然是Android系统本身就提供好的,我们只需要拿过来使用就可以了,但你知道它们是怎样被绘制到屏幕上的吗?

要知道,任何一个视图都不可能凭空突然出现在屏幕上,它们都是要经过非常科学的绘制流程后才能显示出来的。每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw(),下面我们逐个对这三个阶段展开进行探讨

一. onMeasure()

measure是测量的意思,那么onMeasure()方法顾名思义就是用于测量视图的大小

onMeasure()方法是可以重写的,也就是说,如果你不想使用系统默认的测量方式,可以按照自己的意愿进行定制,比如:

public class MyView extends View {  

    ......  

    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
        setMeasuredDimension(200, 200);  
    }  

}  

这样的话就把View默认的测量流程覆盖掉了,不管在布局文件中定义MyView这个视图的大小是多少,最终在界面上显示的大小都将会是200*200

需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0

视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在XML文件中指定视图的大小,然后视图本身会对最终的大小进行拍板

二. onLayout()

measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置

这里我们尝试自定义一个布局,借此来更深刻地理解onLayout()的过程

自定义的这个布局目标很简单,只要能够包含一个子视图,并且让子视图正常显示出来就可以了。那么就给这个布局起名叫做SimpleLayout吧,代码如下所示:

public class SimpleLayout extends ViewGroup{


    public SimpleLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if(getChildCount()>0){
            View childView = getChildAt(0);
            measureChild(childView,widthMeasureSpec,widthMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        if(getChildCount()>0){
            View childView = getChildAt(0);
            childView.layout(0,0,childView.getMeasuredWidth(),childView.getMeasuredHeight());
        }
    }
}

你已经知道,onMeasure()方法会在onLayout()方法之前调用,因此这里在onMeasure()方法中判断SimpleLayout中是否有包含一个子视图,如果有的话就调用measureChild()方法来测量出子视图的大小

接着在onLayout()方法中同样判断SimpleLayout是否有包含一个子视图,然后调用这个子视图的layout()方法来确定它在SimpleLayout布局中的位置,这里传入的四个参数依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分别代表着子视图在SimpleLayout中左上右下四个点的坐标。其中,调用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中测量出的宽和高

这样就已经把SimpleLayout这个布局定义好了,下面就是在XML文件中使用它了

<com.example.a00xiaoyugmailcom.myapplication.SimpleLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"/>

</com.example.a00xiaoyugmailcom.myapplication.SimpleLayout>

可以看到,我们能够像使用普通的布局文件一样使用SimpleLayout,只是注意它只能包含一个子视图,多余的子视图会被舍弃掉。这里SimpleLayout中包含了一个ImageView,并且ImageView的宽高都是wrap_content。现在运行一下程序

这里写图片描述

ImageView成功已经显示出来了,并且显示的位置也正是我们所期望的。如果你想改变ImageView显示的位置,只需要改变childView.layout()方法的四个参数就行了

在onLayout()过程结束后,我们就可以调用getWidth()方法和getHeight()方法来获取视图的宽高了。说到这里,我相信很多朋友长久以来都会有一个疑问,getWidth()方法和getMeasureWidth()方法到底有什么区别呢?它们的值好像永远都是相同的。其实它们的值之所以会相同基本都是因为布局设计者的编码习惯非常好,实际上它们之间的差别还是挺大的

首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的

观察SimpleLayout中onLayout()方法的代码,这里给子视图的layout()方法传入的四个参数分别是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),因此getWidth()方法得到的值就是childView.getMeasuredWidth() - 0 = childView.getMeasuredWidth() ,所以此时getWidth()方法和getMeasuredWidth() 得到的值就是相同的,但如果你将onLayout()方法中的代码进行如下修改:

@Override  
protected void onLayout(boolean changed, int l, int t, int r, int b) {  
    if (getChildCount() > 0) {  
        View childView = getChildAt(0);  
        childView.layout(0, 0, 200, 200);  
    }  
}  

这样getWidth()方法得到的值就是200 - 0 = 200,不会再和getMeasuredWidth()的值相同了。当然这种做法充分不尊重measure()过程计算出的结果,通常情况下是不推荐这么写的。getHeight()与getMeasureHeight()方法之间的关系同上,就不再重复分析了

三. onDraw()

measure和layout的过程都结束后,接下来就进入到draw的过程了。同样,根据名字你就能够判断出,在这里才真正地开始对视图进行绘制

如果你去观察TextView、ImageView等类的源码,你会发现它们都有重写onDraw()这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。Canvas这个类的用法非常丰富,基本可以把它当成一块画布,在上面绘制任意的东西,那么我们就来尝试一下吧

这里简单起见,只是创建一个非常简单的视图,并且用Canvas随便绘制了一点东西,代码如下所示

public class MyView extends View{
    private Paint mPaint;
    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(Color.YELLOW);
        canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);
        mPaint.setColor(Color.BLUE);
        mPaint.setTextSize(50);
        String text = "hello world";
        canvas.drawText(text,0,getHeight()/2,mPaint);
    }
}

可以看到,我们创建了一个自定义的MyView继承自View,并在MyView的构造函数中创建了一个Paint对象
Paint就像是一个画笔一样,配合着Canvas就可以进行绘制了
这里我们的绘制逻辑比较简单,在onDraw()方法中先是把画笔设置成黄色,然后调用Canvas的drawRect()方法绘制一个矩形。然后在把画笔设置成蓝色,并调整了一下文字的大小,然后调用drawText()方法绘制了一段文字

就这么简单,一个自定义的视图就已经写好了,现在可以在XML中加入这个视图

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <com.example.a00xiaoyugmailcom.myapplication.MyView
        android:layout_width="200dp"
        android:layout_height="200dp" />

</LinearLayout>

这里写图片描述

当然了Canvas的用法还有很多很多,会单独写一些Canvas的文章,未完待续…….

总结自郭大神的博客
https://blog.csdn.net/guolin_blog/article/details/16330267


http://www.niftyadmin.cn/n/1817740.html

相关文章

12306拖库,三方软件别乱用!

昨天12306被拖库了&#xff0c;朋友圈都在传&#xff0c;有人不信&#xff0c;有人不以为然。1.什么叫拖库&#xff1f;拖库本来是数据库领域的术语&#xff0c;指从数据库中导出数据。到了黑客攻击泛滥的今天&#xff0c;它被用来指网站遭到入侵后&#xff0c;黑客窃取其数据库…

Python | 徒手画小猪佩奇,看完别说你不会。

今天闲来无事&#xff0c;用turtle画了个小猪佩奇&#xff0c;代码其实很简单的&#xff0c;就是烦了点。没必要每行代码都发出来&#xff0c;给大家看几个主要的函数好了。另外我给绝大多数代码都加上了注释&#xff0c;相同的代码我就不加了。首先需要初始化画笔的一些属性&a…

JavaEE下载文件名不显示中文的问题

我们在做JavaEE项目下载文件时&#xff0c;在我们熟悉的UTF-8编码下经常会发现文件名中文乱码、中文不显示等状况&#xff0c;此时&#xff0c;将文 件名改一下编码或许会解决这个烦恼&#xff1a; fileName new String(fileName.replace(" ", "_").getBy…

【达内课程】SQLite(一)创建数据库、创建数据表、增删改查

文章目录介绍创建数据库创建数据表增加数据/插入数据删除数据修改数据数据查询介绍 SQLite 是一种数据库存储数据的软件&#xff0c;是非数据库服务器软件。 SQLite 存储技术适用于存储有结构的数据&#xff0c;且可以存储大量的数据&#xff0c;在数据访问上也有非常高的效率…

MongoDB 2018 深圳年度大会 报告

先给各位道个歉&#xff0c;前几天一直在参加「MongoDB年度大会」每天晚上都是弄到很晚。第二天又要很早起来&#xff0c;时间抽不出时间写报告。今天早上顶着头皮起来写了。废话不多说&#xff0c;直接进入整体&#xff0c;本次年终盛会真的是干货多多&#xff0c;先给大家上一…

Jfrog Bintray如何删除版本库某一版本

进入项目列表 点击进入相应项目 点击Edit 点击左侧的Version List 点击相应版本编辑 点击Delete进行删除

Python | 异步IO,轻松管理10k+并发连接

异步操作在计算机软硬件体系中是一个普遍概念&#xff0c;根源在于参与协作的各实体处理速度上有明显差异。软件开发中遇到的多数情况是CPU与IO的速度不匹配&#xff0c;所以异步IO存在于各种编程框架中&#xff0c;客户端比如浏览器&#xff0c;服务端比如node.js。本文主要分…

软件架构师1

什么是架构&#xff1f;1、根据要解决的问题&#xff0c;对目标系统的边界进行界定&#xff1b;2、并对目标系统按某个原则的进行切分&#xff1b;3、根据2&#xff0c;使得这些部分之间能够进行有机的联系&#xff0c;合并组装成为一个整体&#xff0c;完成目标系统的所有工作…