1. android自定义View(8) 《绘制文本》
在Android中,我们经常会需要去绘制一些自己需要的控件,所以继承自View的自定义View就产生了。这篇文章主要介绍如何在我们的自定义View中绘制Text
先举例是为了更清楚的看到绘制text时的几个重要的基准线,上图运行结果中总共有5条线,它们从上至下分别是
这里参数很容易看懂,但是这个x,y分别意味着啥呢,它就是绘制文本的基准
除了baseline是我们定义的,其他的几个参考线都是通过我们设置的文字大小来变化的,所以我们在绘制时一定要记住我们绘制文字的基准线是baseline而不是我们的文字框的最底端,记住这一点在我们绘制文字居中是非常有用的。
今天的绘制文字就总结到这里吧,下篇再学别的有意思的~
2. Android自定义View(3) 《Canvas绘制简单的图形》
在Android中,我们经常会需要去绘制一些自己需要的控件,所以继承自View的自定义View就产生了。这篇文章主要介绍在View中的重要类,Canvas类。
Canvas是自定义View中几个最重要的类之一,可以理解为画布,但是我理解下来,这只是单一的图层,每次Canvas绘制时其实都是在canvas类所定义出的一个图层上进行绘制图形,一旦canvas变化,则图层也发生变化,图层变化之前绘制的结果不变,但之后的绘制结果会以新的图层为基准进行绘制
在这里绘制线条调用Canvas.drawLine(float startX, float startY, float stopX, float stopY,Paint paint)方法,参数分别如下
绘制矩形调用canvas.drawRect(RectF rect, Paint paint),矩形Rect的构造参数的四个参数分别为左边界,上边界,右边界,下边界的值,单位为px,RectF与Rect不同之处在于构造参数的类型一个全为float,一个全为int,运行结果如下
绘制椭圆主要调用drawOval(RectF oval, Paint paint),以一个矩形的边界来绘制一个椭圆,运行结果如下
绘制圆主要使用Canvas.drawCircle(float cx, float cy, float radius, Paint paint),
绘制Path的方法比较多,这里只简单示例绘制线条路径构建一个三角形的用法,后续会再详细介绍绘制别的类型路径的用法,该例运行结果如下
这里我们绘制了一段文字和一根线段这里是为了演示出Canvas中的文字的绘制方法只能实现水平方向上的居中显示,垂直方向上的垂直是需要我们再进行实现的,绘制文字有很多个,我们这里只简单调用了设置起始点的绘制方法,关于文字的绘制其实内容还是比较多的,这里我们就只简单的介绍,后续有时间会专门出一个文字绘制的文章,该示例运行结果如下
今天总结了Canvas中一些简单图形的绘制,下篇我们介绍Canvas中的一些其他内容。
3. Android - View 绘制流程
我们知道,在 Android 中,View 绘制主要包含 3 大流程:
Android 中,主要有两种视图: View 和 ViewGroup ,其中:
虽然 ViewGroup 继承于 View ,但是在 View 绘制三大流程中,某些流程需要区分 View 和 ViewGroup ,它们之间的操作并不完全相同,比如:
对 View 进行测量,主要包含两个步骤:
对于第一个步骤,即求取 View 的 MeasureSpec ,首先我们来看下 MeasureSpec 的源码定义:
MeasureSpec 是 View 的一个公有静态内部类,它是一个 32 位的 int 值,高 2 位表示 SpecMode(测量模式),低 30 位表示 SpecSize(测量尺寸/测量大小)。
MeasureSpec 将两个数据打包到一个 int 值上,可以减少对象内存分配,并且其提供了相应的工具方法可以很方便地让我们从一个 int 值中抽取出 View 的 SpecMode 和 SpecSize。
一个 MeasureSpec 表达的是:该 View 在该种测量模式(SpecMode)下对应的测量尺寸(SpecSize)。其中,SpecMode 有三种类型:
对 View 进行测量,最关键的一步就是计算得到 View 的 MeasureSpec ,子View 在创建时,可以指定不同的 LayoutParams (布局参数), LayoutParams 的源码主要内容如下所示:
其中:
LayoutParams 会受到父容器的 MeasureSpec 的影响,测量过程会依据两者之间的相互约束最终生成子View 的 MeasureSpec ,完成 View 的测量规格。
简而言之,View 的 MeasureSpec 受自身的 LayoutParams 和父容器的 MeasureSpec 共同决定( DecorView 的 MeasureSpec 是由自身的 LayoutParams 和屏幕尺寸共同决定,参考后文)。也因此,如果要求取子View 的 MeasureSpec ,那么首先就需要知道父容器的 MeasureSpec ,层层逆推而上,即最终就是需要知道顶层View(即 DecorView )的 MeasureSpec ,这样才能一层层传递下来,这整个过程需要结合 Activity 的启动过程进行分析。
我们知道,在 Android 中, Activity 是作为视图组件存在,主要就是在手机上显示视图界面,可以供用户操作, Activity 就是 Andorid 中与用户直接交互最多的系统组件。
Activity 的基本视图层次结构如下所示:
Activity 中,实际承载视图的组件是 Window (更具体来说为 PhoneWindow ),顶层View 是 DecorView ,它是一个 FrameLayout , DecorView 内部是一个 LinearLayout ,该 LinearLayout 由两部分组成(不同 Android 版本或主题稍有差异): TitleView 和 ContentView ,其中, TitleView 就是标题栏,也就是我们常说的 TitleBar 或 ActionBar , ContentView 就是内容栏,它也是一个 FrameLayout ,主要用于承载我们的自定义根布局,即当我们调用 setContentView(...) 时,其实就是把我们自定义的布局设置到该 ContentView 中。
当 Activity 启动完成后,最终就会渲染出上述层次结构的视图。
因此,如果我们要求取得到子View 的 MeasureSpec ,那么第一步就是求取得到顶层View(即 DecorView )的 MeasureSpec 。大致过程如下所示:
经过上述步骤求取得到 View 的 MeasureSpec 后,接下来就可以真正对 View 进行测量,求取 View 的最终测量宽/高:
Android 内部对视图进行测量的过程是由 View#measure(int, int) 方法负责的,但是对于 View 和 ViewGroup ,其具体测量过程有所差异。
因此,对于测量过程,我们分别对 View 和 ViewGroup 进行分析:
综上,无论是对 View 的测量还是 ViewGroup 的测量,都是由 View#measure(int widthMeasureSpec, int heightMeasureSpec) 方法负责,然后真正执行 View 测量的是 View 的 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法。
具体来说, View 直接在 onMeasure(...) 中测量并设置自己的最终测量宽/高。在默认测量情况下, View 的测量宽/高由其父容器的 MeasureSpec 和自身的 LayoutParams 共同决定,当 View 自身的测量模式为 LayoutParams.UNSPECIFIED 时,其测量宽/高为 android:minWidth / android:minHeight 和其背景宽/高之间的较大值,其余情况皆为自身 MeasureSpec 指定的测量尺寸。
而对于 ViewGroup 来说,由于布局特性的丰富性,只能自己手动覆写 onMeasure(...) 方法,实现自定义测量过程,但是总的思想都是先测量 子View 大小,最终才能确定自己的测量大小。
当确定了 View 的测量大小后,接下来就可以来确定 View 的布局位置了,也即将 View 放置到屏幕具体哪个位置。
View 的布局过程由 View#layout(...) 负责,其源码如下:
View#layout(...) 主要就做了两件事:
ViewGroup 的布局流程由 ViewGroup#layout(...) 负责,其源码如下:
可以看到, ViewGroup#layout(...) 最终也是通过 View#layout(...) 完成自身的布局过程,一个注意的点是, ViewGroup#layout(...) 是一个 final 方法,因此子类无法覆写该方法,主要是 ViewGroup#layout(...) 方法内部对子视图动画效果进行了相关设置。
由于 ViewGroup#layout(...) 内部最终调用的还是 View#layout(...) ,因此, ViewGroup#onLayout(...) 就会得到回调,用于处理 子View 的布局放置,其源码如下:
由于不同的 ViewGroup ,其布局特性不同,因此 ViewGroup#onLayout(...) 是一个抽象方法,交由 ViewGroup 子类依据自己的布局特性,摆放其 子View 的位置。
当 View 的测量大小,布局位置都确定后,就可以最终将该 View 绘制到屏幕上了。
View 的绘制过程由 View#draw(...) 方法负责,其源码如下:
其实注释已经写的很清楚了, View#draw(...) 主要做了以下 6 件事:
我们知道,在 Activity 启动过程中,会调用到 ActivityThread.handleResumeActivity(...) ,该方法就是 View 视图绘制的起始之处:
可以看到, ActivityThread.handleResumeActivity(...) 主要就是获取到当前 Activity 绑定的 ViewManager ,最后调用 ViewManager.addView(...) 方法将 DecorView 设置到 PhoneWindow 上,也即设置到当前 Activity 上。 ViewManager 是一个接口, WindowManager 继承 ViewManager ,而 WindowManagerImpl 实现了接口 WindowManager ,此处的 ViewManager.addView(...) 实际上调用的是 WindowManagerImpl.addView(...) ,源码如下所示:
WindowManagerImpl.addView(...) 内部转发到 WindowManagerGlobal.addView(...) :
在 WindowManagerGlobal.addView(...) 内部,会创建一个 ViewRootImpl 实例,然后调用 ViewRootImpl.setView(...) 将 ViewRootImpl 与 DecorView 关联到一起:
ViewRootImpl.setView(...) 内部首先关联了传递过来的 DecorView (通过属性 mView 指向 DecorView 即可建立关联),然后最终调用 requestLayout() ,而 requestLayout() 内部又会调用方法 scheleTraversals() :
ViewRootImpl.scheleTraversals() 内部主要做了两件事:
Choreographer.postCallback(...) 会申请一次 VSYNC 中断信号,当 VSYNC 信号到达时,便会回调 Choreographer.doFrame(...) 方法,内部会触发已经添加的回调任务, Choreographer 的回调任务有以下四种类型:
因此, ViewRootImpl.scheleTraversals(...) 内部通过 mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null) 发送的异步视图渲染消息就会得到回调,即回调 mTra