Android 之自定义 View 的死亡三部曲之(Draw)

本贴最后更新于 2966 天前,其中的信息可能已经沧海桑田

前言

  • 大家好!本次我们将继续学习 Android 之自定义 View 的死亡三部曲中的最后一部(Draw):画出最真实的自己

  • 在此之前,我们在 Android 之自定义 View 的死亡三部曲之(Measure) 中分析了 View 测测量过程,获得了 View 的三围数据-测量后获得高和宽,在 Android 之自定义 View 的死亡三部曲之(Layout) 中分析了 View 的测量过程,经过测量后,我们就能拿到 View 的 left、top、right、bottom 四个点的值。那么我们剩下最后一步,将我的的 View 绘制出来。

  • Ok,这次我们依然是以 ViewRootImpl 的 performTraversals 方法起点。

    private void performTraversals() { ... if (!mStopped) { //1、获取顶层布局的childWidthMeasureSpec int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); //2、获取顶层布局的childHeightMeasureSpec int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); //3、测量开始测量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } } if (didLayout) { //4、执行布局方法 performLayout(lp, desiredWindowWidth, desiredWindowHeight); ... } if (!cancelDraw && !newSurface) { ... //5、开始绘制了哦 performDraw(); } } ... }
  • 这次我们分析到 performDraw 方法了。好的,我们一起来看下 performDraw 里面的代码吧,我只保留来于本次分析相关的关键代码。

    private void performDraw() { ...... //1、fullRedrawNeeded这个变量标识了本次绘制是否需要完全重新绘制 final boolean fullRedrawNeeded = mFullRedrawNeeded; try { //2、此处调用了ViewRootImpl的draw方法 draw(fullRedrawNeeded); } finally { mIsDrawing = false; Trace.traceEnd(Trace.TRACE_TAG_VIEW); } ...... }
  • 看 1 处,既然有完全绘制,当然也会有局部绘制了,这样做是为了提高性能

  • OK,我们看下 draw 这个方法里面的代码

    private void draw(boolean fullRedrawNeeded) { ...... //1、获得dirty,也就是我们要绘制的区域 final Rect dirty = mDirty; if (mSurfaceHolder != null) { // The app owns the surface, we won't draw. dirty.setEmpty(); if (animating) { if (mScroller != null) { mScroller.abortAnimation(); } disposeResizeBuffer(); } return; } //2、判断是否需要完全绘制 if (fullRedrawNeeded) { mAttachInfo.mIgnoreDirtyState = true; //3、需要完全绘制时,将dirty的值设置为神歌屏幕的大小 dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f)); } ...... //3、调用drawSoftware进行绘制 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { return; } }
  • 从上面的代码分析中,我们看到,最后时通过调用 drawSoftware 进行绘制,那么我们看下 drawSoftware 方法的代码

    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) { //1、哈哈,看到了canvas,是不是感觉里绘制越来越近了 final Canvas canvas; try { //2、取出绘制区域的四个位置的值 final int left = dirty.left; final int top = dirty.top; final int right = dirty.right; final int bottom = dirty.bottom; //3、传入我们的绘制区域,创建一个被锁定了绘制区域的canvas canvas = mSurface.lockCanvas(dirty); // The dirty rectangle can be modified by Surface.lockCanvas() //noinspection ConstantConditions if (left != dirty.left || top != dirty.top || right != dirty.right || bottom != dirty.bottom) { attachInfo.mIgnoreDirtyState = true; } //4、设置画布的密度 canvas.setDensity(mDensity); } try { if (!canvas.isOpaque() || yoff != 0 || xoff != 0) { //5、清除画布的颜色 canvas.drawColor(0, PorterDuff.Mode.CLEAR); } dirty.setEmpty(); mIsAnimating = false; attachInfo.mDrawingTime = SystemClock.uptimeMillis(); mView.mPrivateFlags |= View.PFLAG_DRAWN; try { //7、设置画布的偏离值 canvas.translate(-xoff, -yoff); if (mTranslator != null) { mTranslator.translateCanvas(canvas); } canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0); attachInfo.mSetIgnoreDirtyState = false; //8、调用mView大的draw方法开始绘制 mView.draw(canvas); } } return true; }
  • Ok,我们分析到第 8 步知道,最终调用了 mView 的 draw 开始绘制了,而 mView 也就是 DecorView,我们前面分析过 DecorView 是一个 FrameLayout,而 FrameLayout 并没有重现 draw 方法,ViewGroup 也没有重写,所以,我们直接看 View 的 draw 方法,代码有点长,但是思路分清晰,官方给出的解释也是非常清晰的

    @CallSuper public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; //(1)、dirtyOpaque标识了当前View是否时透明的 final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */ //上面的解释大知识,绘制过程中有一系列的步骤,但是有几个是必须要执行的 //1、绘制背景2、如果有需要,在可以先保存当前canvas的层级数据,3、绘制View的内容4、绘制View的子类5、如果又需要,在退出此次绘制时恢复之前的canvas的层级数据 //6、绘制一些装饰的效果 // Step 1, draw the background, if needed int saveCount; //(2)、透明时不需要绘制背景 if (!dirtyOpaque) { //(3)、不透明时,绘制背景 drawBackground(canvas); } //他说可以跳过第2步和第5步,说明第2和第5步时很重要 // skip step 2 & 5 if possible (common case) final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content //(4)、如果不透明,绘制View的内容 if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children //(5)将canvas传递给childView,将绘制事件传递下去 dispatchDraw(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // we're done... return; } ...... }
  • OK,第 2 步和第 5 步是保存 canves 状态和恢复的操作,我们这次就分析其他步骤就好

  • 首先,我们看第 1 步,在 View 非透明情况下,执行背景的绘制操作

    private void drawBackground(Canvas canvas) { final Drawable background = mBackground; //1、背景为null当然是直接返回了 if (background == null) { return; } //2、确认背景的边界值 setBackgroundBounds(); // Attempt to use a display list if requested. if (canvas.isHardwareAccelerated() && mAttachInfo != null && mAttachInfo.mHardwareRenderer != null) { mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode); final RenderNode renderNode = mBackgroundRenderNode; if (renderNode != null && renderNode.isValid()) { setBackgroundRenderNodeProperties(renderNode); ((DisplayListCanvas) canvas).drawRenderNode(renderNode); return; } } //3、获取当前的scrollX和scrollY的值 final int scrollX = mScrollX; final int scrollY = mScrollY; if ((scrollX | scrollY) == 0) { //此时没有滚动,开始绘制背景 background.draw(canvas); } else { //正在滚动,移动canvas后绘制 canvas.translate(scrollX, scrollY); background.draw(canvas); canvas.translate(-scrollX, -scrollY); } }
  • 从上面的分析,我有又个意外的发现,当 scrllX 或者 scrollY 的值不为 0 时,先使 canvas 偏移后在绘制,这就是为什么如果我们是使用 Scoller 来实现 View 的滑动时,实际上移动的是 View 的可视区域,而不是 View 本身

  • 我们看下 setBackgroundBounds 里面是如何确认背景边界的

    void setBackgroundBounds() { if (mBackgroundSizeChanged && mBackground != null) { //1、直接根据layout中获得的四个位置的值直接确定 mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop); mBackgroundSizeChanged = false; rebuildOutline(); } }
  • 介绍完绘制背景,我们接下来分析绘制内容部分,我们看 onDraw 方法,没错,又是空的,因为这是我们在自定义 View 的时候需要自己去实现的

    protected void onDraw(Canvas canvas) { }
  • OK,那我们看下一步,传递绘制事件给 child 们

  • 我们先看 View 的 dispatchDraw,没错,还是空的,View 就是最原始的了,哪里有 child 嘛。

    protected void dispatchDraw(Canvas canvas) { }
  • 那么我们来看 ViewGroup 中的吧,源码优点长,我保留于本次分析相关就好

    @Override protected void dispatchDraw(Canvas canvas) { boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode); //1、获取child的数据 final int childrenCount = mChildrenCount; final View[] children = mChildren; int flags = mGroupFlags; ...... for (int i = 0; i < childrenCount; i++) { ...... final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { //2、调用drawChild传递canvas、child进去绘制child more |= drawChild(canvas, child, drawingTime); } } ...... }
  • ok,重点是 drawChild 这个方法,我们看下 drawChild 里面做什么操作

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
  • 里面直接调用了 child 的 draw 方法。不过这个方法跟我们前面的分析的 draw 有点区别哦,没错,参数个数不同,那么我们看下到底却别在哪呢,这个方法的代码很长,我截取关键代码

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { ...... if (!drawingWithDrawingCache) { if (drawingWithRenderNode) { mPrivateFlags &= ~PFLAG_DIRTY_MASK; ((DisplayListCanvas) canvas).drawRenderNode(renderNode); } else { if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { mPrivateFlags &= ~PFLAG_DIRTY_MASK; dispatchDraw(canvas); } else { // 1、这里调用子View的draw方法,并将调整好的canvas传进去 draw(canvas); } } } else if (cache != null) // 2、如果是cache模式,则利用cache mPrivateFlags &= ~PFLAG_DIRTY_MASK; if (layerType == LAYER_TYPE_NONE) { Paint cachePaint = parent.mCachePaint; if (cachePaint == null) { cachePaint = new Paint(); cachePaint.setDither(false); parent.mCachePaint = cachePaint; } cachePaint.setAlpha((int) (alpha * 255)); canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint); } else { int layerPaintAlpha = mLayerPaint.getAlpha(); mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha)); canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint); mLayerPaint.setAlpha(layerPaintAlpha); } } ...... }
  • 上面主要做的事情就是,如果有 cache,就利用 cache 进行绘制,没有则直接调用 View 的 draw 方法。然后根据前面的分析,最终会调用个个 View 的 onDraw 进行绘制操作

  • 接下来到了第六部,绘制装饰物(例如 recyclerView 的滚动条),OK,我们来看下 onDrawForeground 方法

    public void onDrawForeground(Canvas canvas) { //1、绘制滚动指示器 onDrawScrollIndicators(canvas); //2、绘制滚动条 onDrawScrollBars(canvas); final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null; if (foreground != null) { if (mForegroundInfo.mBoundsChanged) { mForegroundInfo.mBoundsChanged = false; final Rect selfBounds = mForegroundInfo.mSelfBounds; final Rect overlayBounds = mForegroundInfo.mOverlayBounds; if (mForegroundInfo.mInsidePadding) { selfBounds.set(0, 0, getWidth(), getHeight()); } else { selfBounds.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); } final int ld = getLayoutDirection(); Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(), foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld); foreground.setBounds(overlayBounds); } //绘制foreground foreground.draw(canvas); } }
  • 通过以上的分析,我们就把 View 的 Draw 分析完了,


总结:好吧直接上一个收集的时序图

123.png

  • Android

    Android 是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。2005 年由 Google 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

    336 引用 • 324 回帖 • 1 关注
  • View
    11 引用 • 2 回帖
  • 学习

    “梦想从学习开始,事业从实践起步” —— 习近平

    172 引用 • 540 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    284 引用 • 248 回帖
  • 安全

    安全永远都不是一个小问题。

    199 引用 • 818 回帖
  • Swift

    Swift 是苹果于 2014 年 WWDC(苹果开发者大会)发布的开发语言,可与 Objective-C 共同运行于 Mac OS 和 iOS 平台,用于搭建基于苹果平台的应用程序。

    34 引用 • 37 回帖 • 558 关注
  • HTML

    HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

    108 引用 • 295 回帖 • 3 关注
  • 外包

    有空闲时间是接外包好呢还是学习好呢?

    26 引用 • 233 回帖 • 2 关注
  • OneNote
    1 引用 • 3 回帖 • 1 关注
  • 创造

    你创造的作品可能会帮助到很多人,如果是开源项目的话就更赞了!

    186 引用 • 1021 回帖
  • 导航

    各种网址链接、内容导航。

    45 引用 • 177 回帖 • 1 关注
  • Postman

    Postman 是一款简单好用的 HTTP API 调试工具。

    4 引用 • 3 回帖
  • GitLab

    GitLab 是利用 Ruby 一个开源的版本管理系统,实现一个自托管的 Git 项目仓库,可通过 Web 界面操作公开或私有项目。

    46 引用 • 72 回帖 • 1 关注
  • SQLServer

    SQL Server 是由 [微软] 开发和推广的关系数据库管理系统(DBMS),它最初是由 微软、Sybase 和 Ashton-Tate 三家公司共同开发的,并于 1988 年推出了第一个 OS/2 版本。

    21 引用 • 31 回帖 • 2 关注
  • Laravel

    Laravel 是一套简洁、优雅的 PHP Web 开发框架。它采用 MVC 设计,是一款崇尚开发效率的全栈框架。

    19 引用 • 23 回帖 • 741 关注
  • 服务器

    服务器,也称伺服器,是提供计算服务的设备。由于服务器需要响应服务请求,并进行处理,因此一般来说服务器应具备承担服务并且保障服务的能力。

    125 引用 • 585 回帖
  • C++

    C++ 是在 C 语言的基础上开发的一种通用编程语言,应用广泛。C++ 支持多种编程范式,面向对象编程、泛型编程和过程化编程。

    108 引用 • 153 回帖
  • 小薇

    小薇是一个用 Java 写的 QQ 聊天机器人 Web 服务,可以用于社群互动。

    由于 Smart QQ 从 2019 年 1 月 1 日起停止服务,所以该项目也已经停止维护了!

    35 引用 • 468 回帖 • 762 关注
  • webpack

    webpack 是一个用于前端开发的模块加载器和打包工具,它能把各种资源,例如 JS、CSS(less/sass)、图片等都作为模块来使用和处理。

    42 引用 • 130 回帖 • 252 关注
  • Elasticsearch

    Elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

    117 引用 • 99 回帖 • 199 关注
  • SMTP

    SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。

    4 引用 • 18 回帖 • 640 关注
  • V2Ray
    1 引用 • 15 回帖 • 3 关注
  • 心情

    心是产生任何想法的源泉,心本体会陷入到对自己本体不能理解的状态中,因为心能产生任何想法,不能分出对错,不能分出自己。

    59 引用 • 369 回帖
  • Log4j

    Log4j 是 Apache 开源的一款使用广泛的 Java 日志组件。

    20 引用 • 18 回帖 • 37 关注
  • Vue.js

    Vue.js(读音 /vju ː/,类似于 view)是一个构建数据驱动的 Web 界面库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

    268 引用 • 666 回帖 • 1 关注
  • sts
    2 引用 • 2 回帖 • 244 关注
  • 智能合约

    智能合约(Smart contract)是一种旨在以信息化方式传播、验证或执行合同的计算机协议。智能合约允许在没有第三方的情况下进行可信交易,这些交易可追踪且不可逆转。智能合约概念于 1994 年由 Nick Szabo 首次提出。

    1 引用 • 11 回帖
  • SOHO

    为成为自由职业者在家办公而努力吧!

    7 引用 • 55 回帖 • 5 关注
  • 书籍

    宋真宗赵恒曾经说过:“书中自有黄金屋,书中自有颜如玉。”

    84 引用 • 414 回帖
  • Office

    Office 现已更名为 Microsoft 365. Microsoft 365 将高级 Office 应用(如 Word、Excel 和 PowerPoint)与 1 TB 的 OneDrive 云存储空间、高级安全性等结合在一起,可帮助你在任何设备上完成操作。

    5 引用 • 34 回帖