Android :安卓学习笔记之 Android View绘制 的简单理解和使用
Posted JMW1407
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android :安卓学习笔记之 Android View绘制 的简单理解和使用相关的知识,希望对你有一定的参考价值。
android View的简单理解和使用
Android View
1、什么是View?
在Android中,什么是View?
- View是Android中所有控件的基类,不管是简单的TextView,Button还是复杂的LinearLayout和ListView,它们的共同基类都是View;
- View是一种界面层的控件的一种抽象,它代表了一个控件,除了View还有ViewGroup,从名字来看ViewGroup可以翻译为控件组,即一组View;
- 在Android中,ViewGroup也继承了View,
这就意味着View可以是单个控件,也可以是由多个控件组成的一组控件
2、View的位置参数
View和位置主要由它的四个顶点来决定,分别对应View的四个属性:top、left、right、bottom,
- top是左上角纵坐标
- left是左上角横坐标
- right是右下角横坐标
- bottom是右下角纵坐标
对应如图所示:
根据上图我们可以得到View的宽高和坐标的关系;
width = right - left;
hight = bottom - top;
如何得到View的这四个参数呢?
left = getLeft();
right = getRight();
top = getTop();
bottom = getBottom();
注:从Android 3.0开始,View增加了几个额外的参数:x,y,translationX和translationY,
- xy是View左上角的坐标
translationX
和translationY
是View左上角相对于父容器的偏移量。
这几个参数也是相对于父容器的坐标;这几个参数的换算关系如下:
x = left + translationX;
y = top + translationY;
3、UI管理系统的层级关系
PhoneWindow
:Android系统中最基本的窗口系统,继承自Windows类
,负责管理界面显示以及事件响应。它是Activity与View系统交互的接口DecorView
:PhoneWindow中的起始节点View,继承于View类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个FrameLayoutViewRoot
:Activtiy启动时创建,负责管理、布局、渲染窗口UI等等
ViewRoot与DecorView
ViewRoot
是连接WIndowManager
和DecorVIew
的纽带。View的三大流程(measure layout draw
)都是通过ViewRoot
完成的。- 在ActivityThread中,Activity创建后,
DecorView
会被添加到Window
中,同时创建ViewRootImpl
,然后将DecorView
与ViewRootImpl
建立关联(通过ViewRoot的setView方法)。
3.1、ViewRoot、DecorView、Window和Activity的关系
“activity,window,View 三者之间的关系是什么?”
- 1、
window
是activity
里的一个实例变量,本质是一个接口,唯一的实现类是PhoneWindow
。 - 2、
activity
的setContentView
方法实际上是就是交给phonewindow
去做的。window
和View
的关系可以类比为显示器和显示的内容。 - 3、每个
activity
都有一个“显示器”window
,“显示的内容”就是DecorView
。这个“显示器”定义了一些方法来决定如何显示内容。比如setTitleColor
setTitle
是设置导航栏的颜色和title
,setAllowReturnTransitionOverlap
设置进/出场动画等等。
所以 window 是 activity 的一个成员变量,window 和 View 是“显示器”和“显示内容”的关系。
3.1.1、 ViewRoot
// 在主线程中,Activity对象被创建后:
// 1. 自动将DecorView添加到Window中 & 创建ViewRootImpll对象
root = new ViewRootImpl(view.getContent(),display);
// 3. 将ViewRootImpll对象与DecorView建立关联
root.setView(view,wparams,panelParentView)
ViewRootImpl 创建的时机
- 在
ActivityThread.handleResumeActivity
里创建的
3.1.2、 DecorView
DecorView 的创建时机?
调用链是 Activity.setContentView -> PhoneWindow.setContentView -> installDecor
- 顶层View,即 Android 视图树的根节点;同时也是 FrameLayout 的子类
- 显示 & 加载布局。View层的事件都先经过DecorView,再传递到View
内含1个竖直方向的LinearLayout,分为2部分:
- 上 = 标题栏(titlebar)
- 下 = 内容栏(content)
在Activity
中通过setContentView
()所设置的布局文件其实是被加到内容栏之中的,成为其唯一子View = id
为content
的FrameLayout
中
// 在代码中可通过content得到对应加载的布局
// 1. 得到content
ViewGroup content = (ViewGroup)findViewById(android.R.id.content);
// 2. 得到设置的View
ViewGroup rootView = (ViewGroup) content.getChildAt(0);
3.1.2.1、 DecorView创建和显示
1、DecorView的创建
- 1、创建
Window
抽象类的子类PhoneWindow
类的实例对象; - 2、为
PhoneWindow
类对象设置WindowManager
对象; - 3、为
PhoneWindow
类对象创建1个DecroView
类对象(根据所选的主题样式增加); - 4、为
DecroView
类对象中的content
增加Activity
中设置的布局文件。
此时,DecorView(即顶层View)已创建和添加Activity中设置的布局文件中,但目前仍未显示出来,即不可见。
2、DecorView的显示
- 1、将
DecorView
对象添加到WindowManager
中; - 2、创建
ViewRootImpl
对象; - 3、
WindowManager
将DecorView
对象交给ViewRootImpl
对象; - 4、
ViewRootImpl
对象通过Handler
向主线程发送了一条触发遍历操作的消息:performTraversals
();该方法用于执行View
的绘制流程(measure、layout、draw)
。ViewRootImpl
对象中接收的各种变化(如来自WmS的窗口属性变化、来自控件树的尺寸变化、重绘请求等都引发performTraversals()的调用及完成相关处理,并最终显示到可见的Activity中。
从上面的结论可以看出:
- 一次次performTraversals()的调用驱动着控件树有条不紊的工作;
- 一旦此方法无法正常执行,整个控件树都将处于僵死状态;
- 因此performTraversals()可以说是ViewRootImpl类对象的核心逻辑。而performTraversals()的后续逻辑,则是View绘制的三大流程:
测量流程(measure)、布局流程(layout)、绘制流程(draw)
。
3.1.3、Window
3.1.4、 Activity
4、View绘制过程
- 1、
measure
决定了View的宽高,measure完成后可以通过getMeasureWidth/getMeasureHeight
获取测量后的宽高; - 2、
layout
决定了View四个顶点的坐标和实际View的宽高,可调
用getLeft/getTop/getRight/getBottom/getWidth/getHeight
获取对应属性; - 3、
draw
决定了View的显示,draw完成后View的内容才显示在屏幕上。
4.0、首次 View 的绘制流程是在什么时候触发的?
- 在
ActivityThread.handleResumeActivity
里触发的 onResume
结束后
ActivityThread.handleResumeActivity
里会调用 wm.addView
来添加 DecorView
,wm
是 WindowManagerImpl
// WindowManagerImpl
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params)
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
// WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow)
// 这里的 view 就是 DecorView
// ...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock)
// ...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try
root.setView(view, wparams, panelParentView);
catch (RuntimeException e)
// ViewRootImpl.setView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView)
requestLayout();
最终通过 WindowManagerImpl.addView
-> WindowManagerGlobal.addView
-> ViewRootImpl.setView
-> ViewRootImpl.requestLayout
就触发了第一次 View 的绘制。
拓展:RootViewImpl、WindowManager、WindowManagerGlobal调用关系
4.1、Measure
View会先做一次测量,算出自己需要占用多大的面积。View的Measure过程给我们暴露了一个接口onMeasure,方法的定义是这样的
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
View类已经提供了一个基本的onMeasure实现
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
public static int getDefaultSize(int size, int measureSpec)
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode)
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
return result;
其中invoke
了setMeasuredDimension
()方法,设置了measure过程中View
的宽高,getSuggestedMinimumWidth
()返回View
的最小Width
,Height
也有对应的方法。
MeasureSpec
类是View
类的一个内部静态类
,它定义了三个常量UNSPECIFIED
、AT_MOST
、EXACTLY
,其实我们可以这样理解它,它们分别对应LayoutParams中match_parent
、wrap_content
、xxxdp
。我们可以重写onMeasure
来重新定义View的宽高。
UNSPECIDIED
父容器不对view有任何限制,要多大有多大,一般用于系统内部测量。AT_MOST
对应的是LayoutParams的wrap_content,父容器指定了可用的大小。EXACTLY
对应的是LayoutParams的match_parent和具体数值(xxxdp),表示父容器已经检测出view的大小。
自定义View 测量过程(Measure)
Android中View测量之MeasureSpec
1、单一view
2、viewgroup
4.2、Layout
Layout过程对于View类非常简单,同样View给我们暴露了onLayout方法
protected void onLayout(boolean changed, int left, int top, int right, int bottom)
因为我们现在讨论的是View,没有子View需要排列,所以这一步其实我们不需要做额外的工作。插一句,对ViewGroup
类,onLayout
方法中,我们需要将所有子View的大小宽高设置好
1、单一View的layout过程解析如下:
2、对于视图组ViewGroup的布局流程(Layout)流程
ViewGroup 和 View 同样拥有方法:layout()、onLayout(),但二者应用场景是不一样的:
- 一开始计算
ViewGroup
位置时,调用的是ViewGroup
的layout
()和onLayout
(); - 当开始遍历子
View
及计算子View位置时,调用的是子View
的layout
()和onLayout
(),类似于单一View
的layout
过程。
总结:
4.3、Draw
Draw过程,就是在canvas上画出我们需要的View样式。同样View给我们暴露了onDraw方法
protected void onDraw(Canvas canvas)
默认View类的onDraw没有一行代码,但是提供给我们了一张空白的画布,举个例子,就像一张画卷一样,我们就是画家,能画出什么样的效果,完全取决我们。
1、单一View的draw过程解析如下:即 只需绘制View自身
2、ViewGroup的draw过程如下
4.4、三个比较重要的方法
requestLayout: View重新调用一次layout过程。
invalidate: View重新调用一次draw过程
forceLayout: 标识View在下一次重绘,需要重新调用layout过程。
4.4、自定义属性
整个View的绘制流程我们已经介绍完了,还有一个很重要的知识,自定义控件属性
,我们都知道View已经有一些基本的属性,比如layout_width
,layout_height
,background
等,我们往往需要定义自己的属性,那么具体可以这么做。
- 1.在
values
文件夹下,打开attrs.xml
,其实这个文件名称可以是任意的,写在这里更规范一点,表示里面放的全是view的属性。 - 2.因为我们下面的实例会用到2个长度,一个颜色值的属性,所以我们这里先创建3个属性。
<declare-styleable name="rainbowbar">
<attr name="rainbowbar_hspace" format="dimension"></attr>
<attr name="rainbowbar_vspace" format="dimension"></attr>
<attr name="rainbowbar_color" format="color"></attr>
</declare-styleable>
4.5、实现一个比较简单的Google彩虹进度条。
为了简单起见,这里我只用一种颜色
public class RainbowBar extends View
//progress bar color
int barColor = Color.parseColor("#1E88E5");
//every bar segment width 宽度
int hSpace = Utils.dpToPx(80, getResources());
//every bar segment height 高度
int vSpace = Utils.dpToPx(4, getResources());
//space among bars 间隙
int space = Utils.dpToPx(10, getResources());
float startX = 0;
// delta表示每次矩形框移动量
float delta = 10f;
Paint mPaint;
public RainbowBar(Context context)
super(context);
public RainbowBar(Context context, AttributeSet attrs)
this(context, attrs, 0);
public RainbowBar(Context context, AttributeSet attrs, int defStyleAttr)
super(context, attrs, defStyleAttr);
//read custom attrs
TypedArray t = context.obtainStyledAttributes(attrs,
R.styleable.rainbowbar, 0, 0);
hSpace = t.getDimensionPixelSize(R.styleable.rainbowbar_rainbowbar_hspace, hSpace);
vSpace = t.getDimensionPixelOffset(R.styleable.rainbowbar_rainbowbar_vspace, vSpace);
barColor = t.getColor(R.styleable.rainbowbar_rainbowbar_color, barColor);
t.recycle(); // we should always recycle after used
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(barColor);
mPaint.setStrokeWidth(vSpace);
.......
View有了三个构造方法需要我们重写,这里介绍下三个方法会被调用的场景,
- 第一个方法,一般我们这样使用时会被调用,
View view = new View(context)
; - 第二个方法,当我们在xml布局文件中使用View时,会在inflate布局时被调用,
<View
layout_width="match_parent"
layout_height="match_parent"/>
- 第三个方法,跟第二种类似,但是增加style属性设置,这时inflater布局时会调用第三个构造方法。
<View
style="@styles/MyCustomStyle"
layout_width="match_parent"
layout_height="match_parent"/>
上面大家可能会感觉到有点困惑的是,我把初始化读取自定义属性hspace
,vspace
,和barcolor
的代码写在第三个构造方法里面,但是我RainbowBar
在线性布局中没有加style
属性(),那按照我们上面的解释,inflate布局时应该会invoke第二个构造方法啊,但是我们在第二个构造方法里面调用了第三个构造方法,this(context, attrs, 0)
; 所以在第三个构造方法中读取自定义属性,没有问题,这是一点小细节,避免代码冗余-,-
Draw
因为我们这里不用关注measrue和layout过程,直接重写onDraw方法即可。
//draw be invoke numbers.
int index = 0;
@Override
protected void onDraw(Canvas canvas)
super.onDraw(canvas);
//get screen width
//这部分是用来计算当前的坐标起始点有没有在屏幕外面, delta表示每次矩形框移动量
float sw = this.getMeasuredWidth();
if (startX >= sw + (hSpace + space) - (sw % (hSpace + space)))
startX = 0;
else
startX += delta;
float start = startX;
// draw latter parse
//从当前起始点位置开始绘制矩形框,直到当前的起点坐标在屏幕外了
while (start < sw)
canvas.drawLine(start, 5, start + hSpace, 5, mPaint);
start += (hSpace + space);
start = startX - space - hSpace;
// draw front parse
//先判断每次偏移后初始坐标是否到屏幕的左边缘外了,没有则绘制矩形框,达到和初始坐标点右边的连接起来
while (start >= -hSpace)
canvas.drawLine(start, 5, start + hSpace, 5, mPaint);
start -= (hSpace + space);
if (index >= 700000)
index = 0;
invalidate();
//布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_marginTop="40dp"
android:orientation="vertical" >
<com.sw.demo.widget.RainbowBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:rainbowbar_color="@android:color/holo_blue_bright"
app:rainbowbar_hspace="80dp"
app:rainbowbar_vspace="10dp"
></com.sw.demo.widget.RainbowBar>
</LinearLayout>
其实就是调用canvas
的drawLine
方法,然后每次将draw
的起点向前推进,在方法的结尾,我们调用了invalidate
方法,上面我们已经说明了,这个方法会让View重新调用onDraw
方法,所以就达到我们的进度条一直在向前绘制的效果。
参考
1、Android View详解
2、教你搞定Android自定义View
3、每日一问(十九)请描述一下View的绘制流程
4、安卓View理解总结
5、Carson带你学Android:一文梳理自定义View工作流程
6、【朝花夕拾】Android自定义View篇之(一)View绘制流程
7、【Android面试】View的绘制流程
8、Android面试:从12个View绘制流程大厂面试真题入手,带你全面理解View绘制流程
9、Android面试官:“来给我讲讲View绘制?”
以上是关于Android :安卓学习笔记之 Android View绘制 的简单理解和使用的主要内容,如果未能解决你的问题,请参考以下文章
Android :安卓学习笔记之 Android View 的基础知识和冲突事件处理