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左上角的坐标
  • translationXtranslationY是View左上角相对于父容器的偏移量。

这几个参数也是相对于父容器的坐标;这几个参数的换算关系如下:

x = left + translationX;
y = top + translationY;


自定义View基础必知必会!

3、UI管理系统的层级关系


  • PhoneWindow:Android系统中最基本的窗口系统,继承自Windows类,负责管理界面显示以及事件响应。它是Activity与View系统交互的接口
  • DecorView:PhoneWindow中的起始节点View,继承于View类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个FrameLayout
  • ViewRoot:Activtiy启动时创建,负责管理、布局、渲染窗口UI等等

ViewRoot与DecorView

  • ViewRoot是连接WIndowManagerDecorVIew的纽带。View的三大流程(measure layout draw)都是通过ViewRoot完成的。
  • 在ActivityThread中,Activity创建后,DecorView会被添加到Window中,同时创建ViewRootImpl,然后将DecorViewViewRootImpl建立关联(通过ViewRoot的setView方法)。

3.1、ViewRoot、DecorView、Window和Activity的关系


“activity,window,View 三者之间的关系是什么?”

  • 1、windowactivity 里的一个实例变量,本质是一个接口,唯一的实现类是 PhoneWindow
  • 2、activitysetContentView 方法实际上是就是交给 phonewindow 去做的。windowView的关系可以类比为显示器和显示的内容。
  • 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 = idcontentFrameLayout
// 在代码中可通过content得到对应加载的布局

// 1. 得到content
ViewGroup content = (ViewGroup)findViewById(android.R.id.content);
// 2. 得到设置的View
ViewGroup rootView = (ViewGroup) content.getChildAt(0);
3.1.2.1、 DecorView创建和显示

自定义View绘制准备-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、WindowManagerDecorView对象交给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 的绘制流程是在什么时候触发的?

View何时被绘制

  • ActivityThread.handleResumeActivity 里触发的
  • onResume 结束后

ActivityThread.handleResumeActivity 里会调用 wm.addView 来添加 DecorViewwmWindowManagerImpl

// 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;
  

其中invokesetMeasuredDimension()方法,设置了measure过程中View的宽高,getSuggestedMinimumWidth()返回View的最小WidthHeight也有对应的方法。

MeasureSpec类是View类的一个内部静态类,它定义了三个常量UNSPECIFIEDAT_MOSTEXACTLY,其实我们可以这样理解它,它们分别对应LayoutParams中match_parentwrap_contentxxxdp。我们可以重写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位置时,调用的是ViewGrouplayout()和onLayout();
  • 当开始遍历子View及计算子View位置时,调用的是子Viewlayout()和onLayout(),类似于单一Viewlayout过程。

总结:

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_widthlayout_heightbackground等,我们往往需要定义自己的属性,那么具体可以这么做。

  • 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"/>

上面大家可能会感觉到有点困惑的是,我把初始化读取自定义属性hspacevspace,和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>

其实就是调用canvasdrawLine方法,然后每次将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 的基础知识和冲突事件处理

Android :安卓学习笔记之 Android View 的基础知识和冲突事件处理

Android :安卓学习笔记之事件内存泄露 的简单理解

Android:安卓学习笔记之共享元素的简单理解和使用

Android:安卓学习笔记之共享元素的简单理解和使用

Android:安卓学习笔记之共享元素的简单理解和使用