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

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

前言

  • 大家好!本次我们将继续学习 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 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

    333 引用 • 323 回帖 • 72 关注
  • View
    11 引用 • 2 回帖
  • 学习

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

    161 引用 • 473 回帖

相关帖子

欢迎来到这里!

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

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

推荐标签 标签

  • WebClipper

    Web Clipper 是一款浏览器剪藏扩展,它可以帮助你把网页内容剪藏到本地。

    3 引用 • 9 回帖 • 2 关注
  • Sym

    Sym 是一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)系统平台。

    下一代的社区系统,为未来而构建

    523 引用 • 4581 回帖 • 692 关注
  • 反馈

    Communication channel for makers and users.

    123 引用 • 906 回帖 • 191 关注
  • Firefox

    Mozilla Firefox 中文俗称“火狐”(正式缩写为 Fx 或 fx,非正式缩写为 FF),是一个开源的网页浏览器,使用 Gecko 排版引擎,支持多种操作系统,如 Windows、OSX 及 Linux 等。

    7 引用 • 30 回帖 • 454 关注
  • 友情链接

    确认过眼神后的灵魂连接,站在链在!

    24 引用 • 373 回帖 • 2 关注
  • Shell

    Shell 脚本与 Windows/Dos 下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的。但是它比 Windows 下的批处理更强大,比用其他编程程序编辑的程序效率更高,因为它使用了 Linux/Unix 下的命令。

    122 引用 • 73 回帖
  • Redis

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

    284 引用 • 247 回帖 • 183 关注
  • CloudFoundry

    Cloud Foundry 是 VMware 推出的业界第一个开源 PaaS 云平台,它支持多种框架、语言、运行时环境、云平台及应用服务,使开发人员能够在几秒钟内进行应用程序的部署和扩展,无需担心任何基础架构的问题。

    5 引用 • 18 回帖 • 149 关注
  • uTools

    uTools 是一个极简、插件化、跨平台的现代桌面软件。通过自由选配丰富的插件,打造你得心应手的工具集合。

    5 引用 • 13 回帖 • 1 关注
  • wolai

    我来 wolai:不仅仅是未来的云端笔记!

    1 引用 • 11 回帖
  • H2

    H2 是一个开源的嵌入式数据库引擎,采用 Java 语言编写,不受平台的限制,同时 H2 提供了一个十分方便的 web 控制台用于操作和管理数据库内容。H2 还提供兼容模式,可以兼容一些主流的数据库,因此采用 H2 作为开发期的数据库非常方便。

    11 引用 • 54 回帖 • 642 关注
  • 自由行
  • IPFS

    IPFS(InterPlanetary File System,星际文件系统)是永久的、去中心化保存和共享文件的方法,这是一种内容可寻址、版本化、点对点超媒体的分布式协议。请浏览 IPFS 入门笔记了解更多细节。

    20 引用 • 245 回帖 • 229 关注
  • 支付宝

    支付宝是全球领先的独立第三方支付平台,致力于为广大用户提供安全快速的电子支付/网上支付/安全支付/手机支付体验,及转账收款/水电煤缴费/信用卡还款/AA 收款等生活服务应用。

    29 引用 • 347 回帖 • 1 关注
  • Netty

    Netty 是一个基于 NIO 的客户端-服务器编程框架,使用 Netty 可以让你快速、简单地开发出一个可维护、高性能的网络应用,例如实现了某种协议的客户、服务端应用。

    49 引用 • 33 回帖 • 21 关注
  • 负能量

    上帝为你关上了一扇门,然后就去睡觉了....努力不一定能成功,但不努力一定很轻松 (° ー °〃)

    85 引用 • 1201 回帖 • 455 关注
  • GitHub

    GitHub 于 2008 年上线,目前,除了 Git 代码仓库托管及基本的 Web 管理界面以外,还提供了订阅、讨论组、文本渲染、在线文件编辑器、协作图谱(报表)、代码片段分享(Gist)等功能。正因为这些功能所提供的便利,又经过长期的积累,GitHub 的用户活跃度很高,在开源世界里享有深远的声望,并形成了社交化编程文化(Social Coding)。

    207 引用 • 2031 回帖
  • 宕机

    宕机,多指一些网站、游戏、网络应用等服务器一种区别于正常运行的状态,也叫“Down 机”、“当机”或“死机”。宕机状态不仅仅是指服务器“挂掉了”、“死机了”状态,也包括服务器假死、停用、关闭等一些原因而导致出现的不能够正常运行的状态。

    13 引用 • 82 回帖 • 36 关注
  • Hadoop

    Hadoop 是由 Apache 基金会所开发的一个分布式系统基础架构。用户可以在不了解分布式底层细节的情况下,开发分布式程序。充分利用集群的威力进行高速运算和存储。

    82 引用 • 122 回帖 • 614 关注
  • 域名

    域名(Domain Name),简称域名、网域,是由一串用点分隔的名字组成的 Internet 上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位(有时也指地理位置)。

    43 引用 • 208 回帖
  • 机器学习

    机器学习(Machine Learning)是一门多领域交叉学科,涉及概率论、统计学、逼近论、凸分析、算法复杂度理论等多门学科。专门研究计算机怎样模拟或实现人类的学习行为,以获取新的知识或技能,重新组织已有的知识结构使之不断改善自身的性能。

    76 引用 • 37 回帖
  • CSS

    CSS(Cascading Style Sheet)“层叠样式表”是用于控制网页样式并允许将样式信息与网页内容分离的一种标记性语言。

    180 引用 • 447 回帖
  • SSL

    SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS 与 SSL 在传输层对网络连接进行加密。

    69 引用 • 190 回帖 • 493 关注
  • MongoDB

    MongoDB(来自于英文单词“Humongous”,中文含义为“庞大”)是一个基于分布式文件存储的数据库,由 C++ 语言编写。旨在为应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 JSON 的 BSON 格式,因此可以存储比较复杂的数据类型。

    90 引用 • 59 回帖 • 3 关注
  • V2Ray
    1 引用 • 15 回帖
  • Unity

    Unity 是由 Unity Technologies 开发的一个让开发者可以轻松创建诸如 2D、3D 多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

    25 引用 • 7 回帖 • 249 关注
  • Flume

    Flume 是一套分布式的、可靠的,可用于有效地收集、聚合和搬运大量日志数据的服务架构。

    9 引用 • 6 回帖 • 592 关注