最近从网上收集了一些 View 绘制的面试题来做练习,算是温故而知新了
1.View 的绘制流程是从哪里开始的?哪个过程结束后,我们才能看到 View 的显示?
View
是个抽象类,它在内部封装了测量(measure),布局(layout)和绘制(draw)的方法,但是以上方法的触发都是交给上层 ViewRootImpl
来的,View
整体的绘制流程如下:
- ViewRootImpl(它处理一些全局的View事件)
- -> performTraversals()
- -> performMeasure()
- -> View/ViewGroup measure()
- -> View/ViewGroup onMeasure()
- -> View/ViewGroup measure()
- -> performLayout()
- -> View/ViewGroup layout()
- -> View/ViewGroup onLayout()
- -> View/ViewGroup layout()
- -> perfromDraw()
- -> View/ViewGroup draw()
- -> View/ViewGroup onDraw()
- -> View/ViewGroup draw()
可以看出,从 ViewRootImpl
的 performTraversals
方法开始,主要流程可以总结为
measure,layout,draw 这个三个流程,draw 流程结束以后就可以在屏幕上看到 View
的内容了
2. View 的测试方法为什么会给多次调用? View 在什么情况下 getMeasuredWidht/Height() 和 getWidht/Height(),结果是不一致?
View
的测量方法会多次调用是因为前后的测量结果可能并不一致,比分说 LinearLayout
的权重,View
的第一次测量结果和最终的测量结果肯定是不一样的;
View
的 getMeasuredWidht/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
在测量过程中做了以下的事情:
- 遍历每个⼦
View
,用measureChildWidthMargins()
或其他类似的方法来测量子View
- 将子
View
的大小等参数配置保存到子View
的LayoutParams
中用于进行布局 - 测量出所有子
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
还没开始绘制时( Activity
的 onResume
方法还没回调之前 )就会用一个初始长度为 4 的数组缓存起来(Runnable 数量大于4时会进行扩容),ViewRootImpl
在初始化时创建了一个 View.AttchInfo
对象并绑定 ViewRootImpl
的 Handler
,该 Handler
也用于发送 View
绘制相关的 msg
;
在 ViewRootImpl
执行 performTraversals
方法时,会配置 View 的 AttchInfo 对象并且通过 View 的 dispatchAttachedToWindow 方法传入到 View 里面完成绑定,在该方法中会取出 View 缓存的 Runnable 并用 View.AttchInfo 的 Handler 进行 post,这样子就会加入到 MessageQueue
里面进行排队,等到这些缓存到 Runnable
执行时,主线程上面到 View
的绘制流程也就结束了,所以这时候 Looper
取出这些缓存的Runnable
时就可以拿到 View
的宽高
使用 ViewTreeObserver 的监听事件:
ViewTreeObserver
对象也是存储在 View
的 AttchInfo
对象中,在 ViewRootImpl
调用完 performMeasure
、performLayout
等方法后,通过 View
的 AttchInfo
调用它里面的 dispatch 系列的方法 ,如源码中的 mAttchInfo.mTreeObserver.dispatchOnPreDraw
回调 ViewTreeObserver
的 mOnPreDrawListeners
,从而实现对 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 的位置,View
和 ViewGroup
的 onLayout
方法都没有写,都是交给子类来制定布局规则的
10.View 的 draw 方法的绘制顺序是什么?
这个源码已经有相关的注释了:
翻译过来就是:
- 画出背景
- 如有必要,保存画布的图层以准备褪色
- 绘制视图的内容(可以通过设置标记位关闭该绘制)
- 绘制子 View
- 如有必要,绘制淡化边缘并恢复图层
- 绘制装饰(例如滚动条)
重点部分已经加粗
11.ViewGroup 为什么无法通过 onDraw 方法绘制自定义内容?如果我希望重写 onDraw 来在 ViewGroup 进行一些绘制操作,怎样才能看到效果?
View
的 setWillNotDraw
可以设置标记位跳过 onDraw
和 drawBackground
方法:
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
方法不执行 drawBackground
和 onDraw
方法的情况 ;
它们为什么可以解决这个情况,先看下 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);
//省去部分代码
}
这里就明白了设计标记位为何可以控制不执行 drawBackground
和 onDraw
方法了
接着去看下 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
这个属性,从而不执行 drawBackground
和 onDraw
了
那么如果 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
无法显示绘制内容是因为默认设置的标记位关闭执行了它的 drawBackground
和 onDraw
方法,如果希望显示 ViewGroup
的绘制内容,只要做到下面 2 点即可:
- 调用
View.setWillNotDraw(false)
- 为
ViewGroup
设置背景图
12.子线程为什么不可以更新 UI ?如果要在子线程更新 UI,应该怎么办?
如果在 UI 还没开始绘制的时候,我们可以在子线程修改配置来达到更新 UI 的效果,如 TextView
在 onCreate
方法中设置它的背景颜色,但是在 UI 开始绘制( Activity
进入了 onResume
)或者已经绘制完成的时候,就不能直接这么干了
更新 UI 时通常会调用到 View
的 2 个方法,requestLayout 和 invalidate ,我们分别看下这两个方法
requestLayout 方法:
View
的 requestLayout
方法它最终是调用到 ViewRootImpl
的 requestLayout
:
而 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了!
想知道原因,就要到源码去找;View
的 invalidate
方法最终调用的是 invalidateInternal
:
最终还是让 ViewParent 来执行 child 的重新绘制,ok,我们去看 ViewGroup
的 invalidateChild
方法,看他葫芦里面是什么药:
可以看到,如果开启了硬件加速( Android 4.0 开始默认时开启硬件加速的),就会去执行 onDescendantInvalidated
方法,一直到 ViewRootImpl
里面到 onDescendantInvalidated
方法:
可以看到越过了 checkThread
的检查来重新更新 UI ,因此可以在子线程刷新 UI 也就不奇怪了
View
的硬件加速只能在清单文件上面关闭(setLayerType(LAYER_TYPE_SOFTWARE, null);关闭得是 canvas 的硬件加速):
<activity
android:name=".MainActivity"
android:hardwareAccelerated="false" />
那么就会看到下面的结果:
总结:
由于 ViewRootIpml
的 checkThread
方法会检查当前 UI 修改是否主线程运行,所以一般情况下,我们无法使用子线程去刷新 UI ; 如果需要在子线程更新 UI,有以下的方法:
- 在
View
还没开始绘制时,我们可以在子线程修改 UI 的配置来达到子线程修改 UI 的效果 - 避免触发
View
的requestLayout
方法,启动硬件加速,可以通过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 个参数,作用如下:
- LAYER_TYPE_SOFTWARE:表示有一个 Bitmap 的 View Layer,绘制内容会先绘制到这个 Bitmap 上面,然后再绘制到屏幕上面,这时会关闭 Canvas 的硬件加速
- LAYER_TYPE_HARDWARE:如果当前 View 开启了硬件加速,那么将会有一个 OpenGL texture 的View Layer,交给 GPU 来进行渲染,如果 View 没有开启硬件加速,那么效果和 LAYER_TYPE_SOFTWARE 是一样的
- LAYER_TYPE_NONE:关闭 View Layer
为什么需要设置 View Layer (离屏缓冲区) 呢?很简单,因为有一些效果无法直接呈现在屏幕上面,要在屏幕之外做额外的处理预合成,最后将结果绘制到屏幕上面,才可以看到正确的画面
说到这里,你可能也明白了,上面的需要关闭硬件加速的 API ,与其说它们需要关闭硬件加速,不如说它们的效果比较特殊,需要先在一个 Bitmap 的 View Layer (离屏缓冲区) 上面做额外的处理预合成,才可以在屏幕上面正常显示了
不过 View Layer (离屏缓冲区) 实际上是一个耗费内存的操作,如非必要,还是别用了