View绘制的一问一答

深入理解View绘制流程

最近从网上收集了一些 View 绘制的面试题来做练习,算是温故而知新了

1.View 的绘制流程是从哪里开始的?哪个过程结束后,我们才能看到 View 的显示?

View 是个抽象类,它在内部封装了测量(measure),布局(layout)和绘制(draw)的方法,但是以上方法的触发都是交给上层 ViewRootImpl 来的,View 整体的绘制流程如下:

  • ViewRootImpl(它处理一些全局的View事件)
    • -> performTraversals()
    • -> performMeasure()
      • -> View/ViewGroup measure()
        • -> View/ViewGroup onMeasure()
    • -> performLayout()
      • -> View/ViewGroup layout()
        • -> View/ViewGroup onLayout()
    • -> perfromDraw()
      • -> View/ViewGroup draw()
        • -> View/ViewGroup onDraw()

可以看出,从 ViewRootImplperformTraversals 方法开始,主要流程可以总结为
measurelayoutdraw 这个三个流程,draw 流程结束以后就可以在屏幕上看到 View 的内容了

2. View 的测试方法为什么会给多次调用? View 在什么情况下 getMeasuredWidht/Height() 和 getWidht/Height(),结果是不一致?

View 的测量方法会多次调用是因为前后的测量结果可能并不一致,比分说 LinearLayout 的权重,View 的第一次测量结果和最终的测量结果肯定是不一样的;

ViewgetMeasuredWidht/Height()getWidht/Height() ,它们大多数情况下是一致的,但是在 1 种情况下是例外的,那就是在 layout 方法中重新设置 View 的位置大小,从而改变 View 的宽高,因为最终确定 View 的实际尺寸和位置信息的就是 setFrame 方法 ,如下所示:

@Override
public void layout(int l, int t, int r, int b) {
    int width = r - l;
    int height = b - t;
    int size = Math.min(width, height);
    super.layout(l, t, l + size, t + size);
}
3. View 的 MeasureSpec 是什么?它是由谁决定的?

MeasureSpec 是 View 的一个内部类,它是由父布局传递给子布局的测量要求(可理解为测量规则)以及自己的 Layoutparams 来决定的

一个 MeasureSpec 对象里面有 尺寸大小测量规则 两组元素,MeasureSpec 使用一个32位 的 int 来存储这两组信息,头2位为模式,后30位为尺寸

它常用的有三个方法:

//根据提供的测量值(格式)提取模式
static int getMode(int measureSpec)
//根据提供的测量值(格式)提取大小值(这个大小也就是我们通常所说的大小)
static int getSize(int measureSpec)
//根据提供的大小值和模式创建一个测量值(32位的int)
static int makeMeasureSpec(int size,int mode)

Mode(模式)的取值总有3个:

  • MeasureSpec.EXACTLY:该值表示 View 必须使用其父 ViewGroup 指定的尺寸,以 widthMeasureSpec 为例,如果其 mode 值是 EXACTLY,控件大小就是它在 xml 里面设置的大小或其他动态配置的具体大小值
  • MeasureSpec.AT_MOST:该值表示 View 最大可以取其父 ViewGroup 给其指定的尺寸,例如父 ViewGroup 的宽度为 300,那么表示 View 能取的最大的宽度就是 300
  • MeasureSpec.UNSPECIFIED:该值表示 View 的 父 ViewGroup 没有给 View 在尺寸上设置限制条件,这种情况下 View 可以忽略 measureSpec 中的 size,View 可以取自己想要的值作为量算的尺寸,常用在 clip 上面
4.自定义 View 中 如果 onMeasure 方法没有对 wrap_content 做处理,会发生什么?为什么?怎么解决?

首先 View 设置为 wrap_content 的话,对应的 mode 为 MeasureSpec.AT_MOST

如果没有对 wrap_content 做处理 ,那即使你在 xml 里设置为 wrap_content 的话,其效果也和 match_parent 相同,我们可以从分析 View 默认的 onMeasure 方法代码:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //核心方法getDefaultSize
    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;
}

这个情况下的解决方案就是针对 MeasureSpec.AT_MOST 来做特殊处理,比如指定一个默认的宽高,当发现是 wrap_content 就设置这个默认宽高即可,如下所示:

 case MeasureSpec.AT_MOST:
        result = Math.min(1,specSize);
        break;
5.对普通的 View 来说, ViewGroup 在测量过程中对它做了哪些事情?

ViewGroup 在测量过程中做了以下的事情:

  1. 遍历每个⼦ View,用 measureChildWidthMargins()其他类似的方法来测量子 View
  2. 将子 View 的大小等参数配置保存到子 ViewLayoutParams 中用于进行布局
  3. 测量出所有子 View 的尺⼨后,计算出⾃⼰的尺寸,并⽤ setMeasuredDimension(width, height) 保存

下面以 FrameLayout 源码来配合解析:

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取 子View 的数量
        int count = getChildCount();
        //判断 FrameLayout 是否 wrap_content
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
				//第一次遍历全部的 子View
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //只要不是 Gone 状态,都要测量一次
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
              	//测量 子View
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
              	//得到 子View 的 LayoutParams
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //计算 FrameLayout 的最大宽高值和状态值
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                //true说明 FrameLayout 是 wrap_cotent
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        //把所有宽高中有设置 match_parent 属性的 子View 都记录下来
                        //因为只有 FrameLayout 最终宽高值确定后才能确定这些 子View 的宽高
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
        //加上padding值
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
        //对比有无设置View的最小值
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
        //对比设置的Drawable的宽高,如果有的话
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }
				//设置 FrameLayout 的最终宽高值
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
        //得到保存的 子View 数量
        //这部分有 match_parent 属性的 子View 需要重新测量自身宽高
        //此时 FrameLayout 的宽高已经确定下来了,可以得到它们真正的宽高了
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }
								//重新测量自己的宽高,得到确切的数据
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }
6.View 的 measure 方法和 onMeasure 方法有什么区别和关系

measure 方法使用了 final 来修饰,说明是不可修改的,onMeasure 方法则是可以让子类按需重写

为什么需要这样子,先看 measure 源码:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        //省去部分代码
        //获取key
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        //省去部分代码
        if (forceLayout || needsLayout) {
            //通过key获取缓存
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                //没有对应的缓存数据,执行onMeasure进行测量
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                //有缓存数据,不需要重新测量
                //直接使用缓存的宽高值
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
        //省去部分代码
        }
        //更新缓存
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL);
    }

可以看出,measure 主要是检测有无缓存数据,对比是否要重新测量,也就是调用 onMeasure 方法

onMeasure 方法是可以让子类重写的,我们重写一个空的 onMeasure 方法的话,运行就会看到:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
			//空方法
    }

在这里插入图片描述

直接报错了,不过现在我们也明白了,onMeasure 方法最终目标是为了调用 setMeasuredDimension 方法来确定 View 的宽高值

现在,我们就明白了:

  • measure 方法用于检测缓存数据,对比是否要重新测量
  • onMeasure 方法用于读取父布局的测量规则并按需求定制自己的测量规则,调用 setMeasuredDimension 确定自己的尺寸
7.ViewGroup 里面有重写 onMeasure 方法吗?为什么?

ViewGroup 默认是没有重写 onMeasure 的,重写 onMeasure 方法这个任务是交给 ViewGroup 的子类的

不同的 ViewGroup 子类(LinearLayout、FrameLayout 等),它们的布局要求往往都不一样,那 onMeasure 方法就交给他们自己重写好了

8.为什么在 Activity 的生命周期里无法获得测量宽高?有什么方法可以解决这个问题吗?

因为 View 的测量过程和 Activity 的生命周期没有任何关系

View 的测量绘制流程是在 Activity 进入了 onResume 之后才开始的,何时结束我们无法决定,但可以通过以下方法来监听或间接监听到 View 的测量流程结束了:

View 的 post 方法:

View 还没开始绘制时( ActivityonResume 方法还没回调之前 )就会用一个初始长度为 4 的数组缓存起来(Runnable 数量大于4时会进行扩容),ViewRootImpl 在初始化时创建了一个 View.AttchInfo 对象并绑定 ViewRootImplHandlerHandler 也用于发送 View 绘制相关的 msg

ViewRootImpl 执行 performTraversals 方法时,会配置 View 的 AttchInfo 对象并且通过 View 的 dispatchAttachedToWindow 方法传入到 View 里面完成绑定,在该方法中会取出 View 缓存的 Runnable 并用 View.AttchInfo 的 Handler 进行 post,这样子就会加入到 MessageQueue 里面进行排队,等到这些缓存到 Runnable 执行时,主线程上面到 View 的绘制流程也就结束了,所以这时候 Looper 取出这些缓存的Runnable 时就可以拿到 View 的宽高

使用 ViewTreeObserver 的监听事件:

ViewTreeObserver 对象也是存储在 ViewAttchInfo 对象中,ViewRootImpl 调用完 performMeasureperformLayout等方法后,通过 ViewAttchInfo 调用它里面的 dispatch 系列的方法 ,如源码中的 mAttchInfo.mTreeObserver.dispatchOnPreDraw 回调 ViewTreeObservermOnPreDrawListeners,从而实现对 View 的绘制流程的监听,因此我们可以通过它的监听事件来获取到 View 的宽高(所以请勿在这类监听中加入耗时操作)

总结:

简单来说,View.post 是通过 ViewRootImpl 的Hanlder 加入绘制任务的 MessageQueue 中,等待 View 的绘制完成时,顺利拿到 View 的宽高

ViewTreeObserver 则是一个观察者,在 ViewRootlmpl 执行完 performMeasure、performLayout等方法后进行相应的回调,从而可以拿到 View 的宽高

9.layout 和 onLayout 方法有什么区别?

layout 方法主要是使用 serFrame 方法来设置本身 View 的四个顶点的位置,只要这4个位置确定了,那么 View 的位置和宽高就固定了

layout 方法里面调用 serFrame 方法后就会调用 onLayout 来摆放子 View 的位置ViewViewGrouponLayout 方法都没有写,都是交给子类来制定布局规则的

10.View 的 draw 方法的绘制顺序是什么?

这个源码已经有相关的注释了:

在这里插入图片描述

翻译过来就是:

  1. 画出背景
  2. 如有必要,保存画布的图层以准备褪色
  3. 绘制视图的内容(可以通过设置标记位关闭该绘制)
  4. 绘制子 View
  5. 如有必要,绘制淡化边缘并恢复图层
  6. 绘制装饰(例如滚动条)

重点部分已经加粗

11.ViewGroup 为什么无法通过 onDraw 方法绘制自定义内容?如果我希望重写 onDraw 来在 ViewGroup 进行一些绘制操作,怎样才能看到效果?

ViewsetWillNotDraw 可以设置标记位跳过 onDrawdrawBackground 方法:

public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

然后我们去看下 ViewGroup 的初始化方法源码:

在这里插入图片描述

可以看到一家很熟悉的代码:

setFlags(WILL_NOT_DRAW, DRAW_MASK);

因此 ViewGroup 默认是跳过 onDraw 方法的,因为 ViewGroup 一般就是管理布局相关的事项,不处理自身的绘制有助于提高执行效率

解决方案也很简单,除了上面提到的 setWillNotDraw 方法设置为 false,那可以ViewGroup 设置背景来解决draw 方法不执行 drawBackgroundonDraw 方法的情况 ;

它们为什么可以解决这个情况,先看下 draw 方法的核心源码:

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    //通过标记位得出一个绘制的flag
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    //省去部分代码
    if (!dirtyOpaque) {
        //第一步,绘制背景
       drawBackground(canvas);
    }
   //省去部分代码
   //第三步,绘制自己内容,执行onDraw
   if (!dirtyOpaque){ 
     onDraw(canvas);
   }
   //第四步,绘制子View
   dispatchDraw(canvas);
  //省去部分代码
}

这里就明白了设计标记位为何可以控制不执行 drawBackgroundonDraw 方法了

接着去看下 setFlags 方法的源码:

void setFlags(int flags, int mask) {
  //......
  //存储新的标记位
  int old = mViewFlags;
  mViewFlags = (mViewFlags & ~mask) | (flags & mask);
  //......省去大量标记位计算的代码
  if ((changed & DRAW_MASK) != 0) {
      //mViewFlags如果设置了WILL_NOT_DRAW标志
      if ((mViewFlags & WILL_NOT_DRAW) != 0) {
           if (mBackground != null
               || mDefaultFocusHighlight != null
               || (mForegroundInfo != null && mForegroundInfo.mDrawable != null)) {
                   //如果当前 View 有背景,那么取消 mPrivateFlags 的 PFLAG_SKIP_DRAW 标志
                   mPrivateFlags &= ~PFLAG_SKIP_DRAW;
                } else {
                    mPrivateFlags |= PFLAG_SKIP_DRAW;
                }
            } else {
                mPrivateFlags &= ~PFLAG_SKIP_DRAW;
            }
          requestLayout();
          invalidate(true);
        }
  //......省去大量标记位计算的代码
}

核心代码已经标记出来了,也就是说ViewGroup 初始化时,如果设置了背景,那么 mPrivateFlags 这个标记位就无法设置 PFLAG_SKIP_DRAW 这个属性,从而不执行 drawBackgroundonDraw

那么如果 ViewGroup 已经结束了绘制的流程,能否通过设置背景图来显示自定义绘制内容呢?这个也是没有问题的,可以在 setBackgroundDrawable 方法看到下面的源码:

 public void setBackgroundDrawable(Drawable background) {
   //......省去部分代码
    if (background != null) {
      //......省去部分代码
      if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
           //取消 mPrivateFlags 的 PFLAG_SKIP_DRAW 标志
           mPrivateFlags &= ~PFLAG_SKIP_DRAW;
           requestLayout = true;
      }
    }else{
        mBackground = null;
        if ((mViewFlags & WILL_NOT_DRAW) != 0
             && (mDefaultFocusHighlight == null)
             && (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) {
               //恢复 mPrivateFlags 的 PFLAG_SKIP_DRAW 标志
               mPrivateFlags |= PFLAG_SKIP_DRAW;
        }
    }
   //......省去部分代码
 }

因此,我们可以随时通过给 ViewGroup 设置背景图,让 ViewGroup 显示我们自定义的绘制效果

总结如下:

ViewGroup 无法显示绘制内容是因为默认设置的标记位关闭执行了它的 drawBackgroundonDraw 方法,如果希望显示 ViewGroup 的绘制内容,只要做到下面 2 点即可:

  1. 调用 View.setWillNotDraw(false)
  2. ViewGroup 设置背景图
12.子线程为什么不可以更新 UI ?如果要在子线程更新 UI,应该怎么办?

如果在 UI 还没开始绘制的时候,我们可以在子线程修改配置来达到更新 UI 的效果,如 TextViewonCreate 方法中设置它的背景颜色,但是在 UI 开始绘制( Activity 进入了 onResume )或者已经绘制完成的时候,就不能直接这么干了

更新 UI 时通常会调用到 View 的 2 个方法,requestLayoutinvalidate ,我们分别看下这两个方法

requestLayout 方法:

ViewrequestLayout 方法它最终是调用到 ViewRootImplrequestLayout

在这里插入图片描述

checkThread 方法来检查更新 UI 的线程,如果不是主线程就会抛出异常闪退

在这里插入图片描述

invalidate 方法:

先来做个测试,它能否在子线程里面刷新 UI:

public class LifeView extends View {
     //.......
      public void test() {
          new Thread(new Runnable() {
              @Override
              public void run() {
                  mTitle = "hello world";
                  invalidate();
              }
          }).start();
      }
      //.......
      protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.e(TAG, "onDraw()");
        canvas.drawText(mTitle, 100, 100, mPaint);
     }
}

如果在 Activity 里面创建这个 View 并触发 test 方法的话,然后会惊讶发现,子线程调用 invalidate 居然成功更新UI了!

想知道原因,就要到源码去找;Viewinvalidate 方法最终调用的是 invalidateInternal
在这里插入图片描述

最终还是让 ViewParent 来执行 child 的重新绘制,ok,我们去看 ViewGroupinvalidateChild 方法,看他葫芦里面是什么药:

在这里插入图片描述

可以看到,如果开启了硬件加速( Android 4.0 开始默认时开启硬件加速的),就会去执行 onDescendantInvalidated 方法,一直到 ViewRootImpl 里面到 onDescendantInvalidated 方法:

在这里插入图片描述

可以看到越过了 checkThread 的检查来重新更新 UI ,因此可以在子线程刷新 UI 也就不奇怪了

View 的硬件加速只能在清单文件上面关闭(setLayerType(LAYER_TYPE_SOFTWARE, null);关闭得是 canvas 的硬件加速):

<activity
    android:name=".MainActivity"
    android:hardwareAccelerated="false" />

那么就会看到下面的结果:
在这里插入图片描述
总结:

由于 ViewRootIpmlcheckThread 方法会检查当前 UI 修改是否主线程运行,所以一般情况下,我们无法使用子线程去刷新 UI ; 如果需要在子线程更新 UI,有以下的方法:

  1. View 还没开始绘制时,我们可以在子线程修改 UI 的配置来达到子线程修改 UI 的效果
  2. 避免触发 ViewrequestLayout 方法,启动硬件加速,可以通过 invalidate 方法来实现子线程更新 UI
13.硬件加速到底是什么?有什么好处和坏处?setLayerType() 到底是用来干什么的?

硬件加速指的是使用 GPU 来完成 CPU 绘制的计算工作,降低 CPU 的负担;

GPU 本来就干渲染的活的,自然可以加快 View 的绘制,这个就是好处;但是坏处就是 Canvas 和 Paint 某些 api 无法使用 GPU 来实现,这时候就需要关闭硬件加速了

侵权即删

上图不需要硬背,自定义 View 时检查一下就好了

Android 并没有提供 API 来给我们调用关闭硬件加速,我们可以在清单文件里面配置关闭 App 或 某个 Activity 的硬件加速, View 是否开启了硬件加速受到 application 和 它所在的 Activity 的影响,也就是说 View 层的硬件加速也是在清单文件上面关闭的,但是 View 里面提供了 setLayerType 用于关闭 Canvas 的硬件加速

public class LifeView extends View {
    
    public LifeView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }
  
    public LifeView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }
  
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.e(TAG, "view.isHardwareAccelerated() " + isHardwareAccelerated());
        Log.e(TAG, "canvas.isHardwareAccelerated() " + canvas.isHardwareAccelerated());
    }
}

View.isHardwareAccelerated 可以返回当前 View 是否开启了硬件加速,Canvas.isHardwareAccelerated 返回当前 Canvas 是否开启了硬件加速:

在这里插入图片描述

如果注释掉 setLayerType(LAYER_TYPE_SOFTWARE, null); 就会看到下面的结果:

在这里插入图片描述

那么 setLayerType 就只是用来关闭 Canvas 的硬件加速的吗?并不是,它其实是用来设置 View Layer (离屏缓冲区) 的,该方法有 3 个参数,作用如下:

  1. LAYER_TYPE_SOFTWARE:表示有一个 Bitmap 的 View Layer,绘制内容会先绘制到这个 Bitmap 上面,然后再绘制到屏幕上面,这时会关闭 Canvas 的硬件加速
  2. LAYER_TYPE_HARDWARE:如果当前 View 开启了硬件加速,那么将会有一个 OpenGL texture 的View Layer,交给 GPU 来进行渲染,如果 View 没有开启硬件加速,那么效果和 LAYER_TYPE_SOFTWARE 是一样的
  3. LAYER_TYPE_NONE:关闭 View Layer

为什么需要设置 View Layer (离屏缓冲区) 呢?很简单,因为有一些效果无法直接呈现在屏幕上面,要在屏幕之外做额外的处理预合成,最后将结果绘制到屏幕上面,才可以看到正确的画面

说到这里,你可能也明白了,上面的需要关闭硬件加速的 API ,与其说它们需要关闭硬件加速,不如说它们的效果比较特殊,需要先在一个 Bitmap 的 View Layer (离屏缓冲区) 上面做额外的处理预合成,才可以在屏幕上面正常显示了

不过 View Layer (离屏缓冲区) 实际上是一个耗费内存的操作,如非必要,还是别用了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值