打造“高逼格”Android应用——自定义View的学习之路(一)

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

View的绘图流程

<一>、目录

<二>、概述

<三>、Activity的组成结构

<四>、View的绘图流程

4.1、测量组件的大小

4.2、确定子组件的位置

4.3、绘制组件


<二>概述

  一枚开发小菜鸟,一直在做简单的页面逻辑渲染,一颗上进的心就这样慢慢淹没了,因为六月和七月在找工作和适应新工作,所以博客停了一段时间。新公司的UI是个妹子哎,程序员懂得的,这种比大熊猫还要稀奇的物种,所以不敢总是麻烦妹子要图标啊,在开发中需要用到的UI,能代码实现的坚决去麻烦妹子给绘图,不能用代码实现的也去麻烦妹子给绘图(不然怎么促进工作情意…..),有这种想法的,只能说活该你单身,你懂的,一个男生需要表现的异常强大,UI妹子给出原形页面,一切图形都用代码实现,哥就是这么拽,咋地了。哈哈,装逼结束,正式步入正题,在开发中我们会遇到一个特殊的炫酷UI界面,使用Android原生组件无法满足需求,这就需要自己绘制组件,来满足开发中需求,打造出高逼格的APP。
  自定义View是一个相当庞大的知识模块,包含很多知识点(绘图、位图运算、公式计算、布局、动画…)一直走在学习自定义View的路上,加上最近在网上找到一些资料,所以打算将这一块好好整理整理,形成一个专栏,大家一起来学习,写的不好之处还请多指教,不接受任何批评,因为知识点比较多,所以我会一期一期的出博客,感兴趣的猿可以加个关注。
  Android中组件必须是View的直接子类或间接子类,其中View有一个名为ViewGroup的子类,用于定义容器组件类(FrameLayout、LinearLayout都是ViewGroup的子类),二者的职责定义非常清晰,如果组件中还有子组件,就一定是从ViewGroup类继承,否则从View类继承。


<三>Activity的组成结构

  Activity代表一个窗口,这里的“窗口”由Activity的成员mWindow来表示的,mWindow本质上是一个PhoneWindow对象,PhoneWindow继承Window抽象类,负责窗口的管理。但是PhoneWindow并不是用来呈现界面效果,呈现界面由PhoneWindow管理的DecorView对象来完成,DecorView类是FrameLayout的子类,也是整个View树的“根”。DecorView由三部分组成:ActionBar、标题区、内容区。首先我们先来个“煎蛋”的自定义标题栏,认识一下View的关系。
 example1:自定义标题栏(应用场景-APP顶部标题栏)
 第一步:布局 activity_title.xml
 

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:padding="10dp"
    android:background="@color/colorPrimaryDark">

    <ImageView
        android:id="@+id/activity_title_back_img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/icon_back"
        android:layout_centerVertical="true"/>

    <TextView
        android:id="@+id/activity_title_middle_title_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="这是标题"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:textColor="#ffffff"
        android:textSize="20sp"/>

    <ImageView
        android:id="@+id/activity_title_right_img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/icon_calendar"
        android:layout_alignParentRight="true"
        android:layout_marginRight="10dp"
        android:layout_centerVertical="true"
        android:visibility="gone"/>

    <TextView
        android:id="@+id/activity_title_right_title_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="日期"
        android:textColor="#ffffff"
        android:layout_alignParentRight="true"
        android:layout_marginRight="10dp"
        android:layout_centerVertical="true"
        android:textSize="20sp"/>

</RelativeLayout>

第二步:Java代码:TitleLayout.java

import android.app.Activity;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

/**
 * Created by LJZ on 18/8/12.
 * QQ:85481455
 */

public class TitleLayout extends RelativeLayout{

    public TitleLayout(Context context, AttributeSet attrs) {
        super(context);
        LayoutInflater.from(context).inflate(R.layout.activity_title,this);
        ImageView backImg = (ImageView) findViewById(R.id.activity_title_back_img);
        TextView middleTitleTv = (TextView) findViewById(R.id.activity_title_middle_title_tv);
        backImg.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                ((Activity) getContext()).finish();

            }
        });
    }
}

ok,这样我们就简单封装了一个标题栏了。看看使用了吧
这里写图片描述

  喽,我们只需要引入一个TitleLayout标签即可,就能完成一个标题栏。当然这个Demo是用来描绘Activity的绘制的。大致可以看出,先绘制最外层的LinearLayout, 然后是include(这里理解为视图层),然后是我们自己定义的View。

这里写图片描述

  图就是丑了点, 没办法找不到免费的画图软件,求兄弟助攻, 来个地址:
从图上我们可以看出
1、Activity类似于一个框架,位于最顶层,负责容器生命周期,窗口通过Window来管理。
2、Window负责窗口管理(实际是子类PhoneWindow),窗口的绘制贺渲染交给来DecorView
3、DecorView是View的根,我们绘制渲染的XML实际由contentParent的管理
重点说明:重点说明:重点说明:
  PhoneWindow类海关联一个名为mWindowMnaager的WindowManager对象,WindowManager会绘制一个ViewRootImpl对象来和WindowManagerService进行沟通,WindowManagerService能获取触摸事件,键盘事件、轨迹事件。并通过ViewRootImpl将事件分发给各个Activity。
看图说话
这里写图片描述


<四>、View的绘图流程

  上面提到,ViewRootImpl负责Activity整个GUI的绘制,而绘制是从ViewRootImpl 的performTraversals()方法开始的,该方法使用private修饰,控制着View的绘制流程,禁止被重写,成年也不行。
这里写图片描述
我们按照流程图一步一步的找到performTraversals()方法,看他执行来什么,多达1300行的代码量看起来真的很费劲,这里我主要抽取我看见的重要方法。这里写图片描述
第一进行测量,WindowManager会遍历所有XML标签中控件的大小。并做出判断大于0.0f
这里写图片描述

这个条件是不是很眼熟,对的,就是在上面进行测量的时候的判断,当测量完成后,windowManager会给每个空间进行定位,当定位完成后,就可以进行绘制了。


4.1、测量View的大小

  performMeasure()方法负责组件自身尺寸的测量,在layout布局中,我们需要对每一个控件设置layout_width和layout_height属性,属性值由三种:充满父容器,包裹自适应,指定数值。充满父容器和制定数值是不需要再进行计算的,传过来的就是父容器自己计算好的尺寸,当使用包裹自适应需要获取内容进行尺寸计算。

这里写图片描述
谷歌大哥官方小哥的测量方法,只有一行代码,对象mView是View的根视图,代码中调用了measure方法,这个方法在自定义View中是必须重写的。
  这有点要说明的是,如果测量的是容器的尺寸,而容器尺寸又依赖于子控件的大小,所以就必须先测量子控件的大小,不然所获取的宽度和高度永远是0,白版APP。看见鸿阳大神的博客,发现一个有趣的错误,Android中调用的measure()方法获取的值,只是一个参考值,并不一定起最后作用,控件展示的大小由setFrame()方法决定。这个待验证。


4.2、确定子组件的位置

  performLayout()方法用于缺额定子组件的位置,所以,这个方法只针对ViewGroup容器类。作为容器,必须为容器中的子View精确定义位置和大小。
这里写图片描述

代码中的host是View树中的根视图(DecorView),也就是最外层的容器,容器的位置安排在坐标原点,大小默认为match_parent,所有的定位其实交给了layout()方法
这里写图片描述

这里写图片描述

 在layout()方法中,在定位之前如果需要重新测量组件的大小,则先调用 onMeasure()方法,接下来执行 setOpticalFrame()或 setFrame()方法确定自身的位置与大小,此时只是保存了相关的值,与具体的绘制无关。随后,onLayout()方法被调用,该方法是空方法,如下:

  /**
     * Called from layout when this view should
     * assign a size and position to each of its children.
     *
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * @param changed This is a new size or position for this view
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     */
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

  onLayout()方法在这里的作用是当前组件为容器时,负责定位容器中的子组件。这其实是一个递归的过程,如果子组件也是一个容器,该容器依然负责它的子组件的定位。依此类推,直到所有的组件都定位完成为止。也就是说:“从顶层的DecorView开始定位,像多米罗骨牌一样从上往下驱动,最后每一个组件都放到它对应该出现的位置上。”onLayout()方法和上节的 onMeasure()方法一样,是为开发人员预留的功能扩展接口,自定义容器时,该方法必须重写。


4.3、绘制组件

preformDraw()方法执行执行组件的绘制功能,组件的绘制是一个十分复杂的过程。不仅仅绘制组件本身,还要绘制背景、滚动条,好消息是每一个组件只需要负责自身的绘制。而且一般来说,容器组件不需要绘制,ViewGroup已经做了大量的工作。通过源码整理出的绘图流程如下:

private void performDraw() {

        ......
        final boolean fullRedrawNeeded = mFullRedrawNeeded;
        mFullRedrawNeeded = false;

        mIsDrawing = true;
         .......
        try {
            draw(fullRedrawNeeded);
        } finally {
            mIsDrawing = false;
          ......
        }
    ......
    }

  在performDraw()方法中调用 draw()方法

private void draw(boolean fullRedrawNeeded) {

    ......
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                    return;
                }
    ......
}

draw()方法又调用了 drawSoftware()方法

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

    final Canvas canvas;
    try {
            final int left = dirty.left;
            final int top = dirty.top;
            final int right = dirty.right;
            final int bottom = dirty.bottom;

            canvas = mSurface.lockCanvas(dirty);
            ........


    if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }
            ......

    mView.draw(canvas);
    ......
    surface.unlockCanvasAndPost(canvas);
    ......
}

如果说前面的代码倍感陌生,那么从 drawSoftware()开始,代码似乎越来越平易近人越来越接地气了。绘制组件是通过 Canvas类完成的,该类定义了若干个绘制图像的方案。通过 Paint类配置绘制参数,便能绘制出各种图案效果。为了提高绘图的性能,使用了 Surface技术,sureface提供了一套双缓存机制,能大大的加快绘图效率。而我们绘图是需要的 Canvas对象也由是 Surface创建的。

  drawSoftware()方法中调用了 mView的 draw()方法。前面说过,mView是 ACtivity界面中 View树的根(DecorView),也是一个容器(具体来说就是一个FrameLayout布局容器)。所以,我们来看看 FrameLayout类的 draw()方法源码:
  

public void draw(Canvas canvas) {
    super.draw(canvas);
    ......
    final Drawable foreground = mForeground;
    ......
    foreground.draw(canvas);
}

  FrameLayout类的 draw()方法做了两件事情,一是调用谷类的 draw()方法绘制自己;二是将前景位图画在 Canvas上,自然,super.draw(canvas)语句是我们关注重点,FrameLayout继承自 ViewGroup,遗憾的是 ViewGroup并没有重写 draw()方法,也就是说,ViewGroup的绘制完全重用了它的父类 View的 draw()方法。不过,ViewGroup中定义了一个名为 dispatchDraw()的方法。该方法在 View中定义,在 ViewGroup中实现。至于有什么用? 暂且不说,我们先扒开 View的 draw()方法源码看看:

public void draw(Canvas canvas) {

    ......
    drawBackground(Canvas canvas)

    ......
    if (!dirtyOpaque) onDraw(canvas);

    ......
    dispatchDraw(canvas);

    onDrawScrollBars(canvas);

    ......

}

View 类的 draw()方法是组件绘制的核心方法,主要做了下面几件事情:

绘制背景:background.draw(canvas)
绘制自己 :onDraw(canvas)
绘制子视图 :dispatchDraw(canvas);
绘制滚动条 :onDrawScrollBars(canvas);
  backgroud是一个 Drawable对象,直接绘制在 Canvas上,并且与组件要绘制的内容互补干扰。跟多时候,这个特征能被某些场景利用。比如后面的“刮刮乐”就是一个很好的范例。

  View 只是组件的抽象定义,它自己并不知道自己长神马样子。所以,View定义了一个空 onDraw(),如下 :

/**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }

和前面的 onMeasure() 与 onLayout()一样,onDraw()方法同样是预留给子类扩展的功能接口。用于绘制组件自身,组件的外观有该方法来决定。

dispatchDraw()方法也是一个空方法,如下:

/**
     * Called by draw to draw the child views. This may be overridden
     * by derived classes to gain control just before its children are drawn
     * (but after its own view has been drawn).
     * @param canvas the canvas on which to draw the view
     */
    protected void dispatchDraw(Canvas canvas) {

    }

  该方法服务容器组件,容器中的子组件必须通过 dispatchDraw()方法进行绘制。所以,View虽然没有实现该方法但是它的子类 ViewGroup实现了该方法。

protected void dispatchDraw(Canvas canvas) {
......
    final int count = mChildrenCount;
    final View[] children = mChildren;
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
        }
    }
......
}

  在 dispatchDraw()方法中,循环遍历每一个子组件,并用 drawChild()方法绘制子组件。而子组件有调用 View的 draw()方法绘制自己。

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

组件的绘制委员会寺一个递归的过程,说到底 Activity的 UI界面的根一定是容器,根容器绘制结束后开始绘制子组件。子组件如果是容器继续往下递归绘制,直到说有的组件正确绘制为止。否则直接将子组件绘制出来。

总体来说,UI界面的绘制从开始到结束要经历的几个过程:

测量组件大小,回调 onMeasure()方法
组件定位,回调 onLayout()方法
组件绘制,回调 onDraw()方法


前人栽树,后人乘凉。谢谢以下博主的优秀博客给出参考:
Android之Activity的框架原理分析:
https://blog.csdn.net/sauphy/article/details/50507562
Android中的ViewRootImpl类源码解析
https://blog.csdn.net/qianhaifeng2012/article/details/51737370

特别感谢株洲新程IT教育 李赞红老师的视频和博客,强烈推荐他的《Android自定义组件开发详解》这本书。

写篇博客不容易,如果有错误欢迎指出,要是打嘴炮,抱歉,概步接受;
后续会提供出GIT地址;
谢谢大家

阅读更多
换一批

没有更多推荐了,返回首页