AndroidQ 图形系统(9)SurfaceView实现原理之设置透明区域
SurfaceView概述
SurfaceView
是一种特殊的View,它可以并且应该在子线程进行UI绘制,它具有独立于应用程序之外的surface
,主要用来处理复杂,耗时的UI绘制,如视频播放,camera预览,游戏等,SurfaceView
的默认Z-order低于应用程序主窗口,为APPLICATION_MEDIA_SUBLAYER = -2
,意味着SurfaceView
其实默认就是用来播放视频的,可以通过setZOrderMediaOverlay
或者setZOrderOnTop
方法进行修改,SurfaceView
采用设置透明区域的方式显示出自己,调用的方法是requestTransparentRegion
方法,后面我们会依次分析SurfaceView
的创建,设置透明区域,及其内部Surface
的创建,以及绘制的原理。
我们先来看看一段代码:
MainActivity.java
package com.example.surfaceviewdemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class MainActivity extends AppCompatActivity {
private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSurfaceView = findViewById(R.id.surfaceView);
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(new MyHolderCallback());
}
class MyHolderCallback implements SurfaceHolder.Callback{
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.d("dongjiao","surfaceCreated....");
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.d("dongjiao","surfaceChanged....");
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d("dongjiao","surfaceDestroyed....");
}
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="200dp"
android:layout_height="200dp"/>
</LinearLayout>
运行之后:
这是一个最简单的SurfaceView
的用法,我们什么也没画,仅仅将它创建出来,SurfaceHolder
用于控制SurfaceView
生命周期并且回调,我们先暂时不管SurfaceView
如何进行绘制的,现在来研究它是如何创建出来的,并且创建过程都做了什么。
SurfaceView
再怎么特殊它也是一个View,可以像对待其他任何 View 一样对其进行操作,我们知道一个Activity启动之后会经历View的测量,布局,绘制流程,入口在ViewRootImpl
的performTraversals
方法中,SurfaceView
和普通View一样也会经历这个流程,但不会走绘制,为何?来看SurfaceView
构造方法中的一句关键代码:
public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mRenderNode.addPositionUpdateListener(mPositionListener);
setWillNotDraw(true);
}
所以你会发现就算重写了SurfaceView
的onDraw方法也不会调用,我们接着来看performTraversals
这个方法。
performTraversals
这个方法代码相当的多,我们只关注两个View生命周期回调方法
private void performTraversals() {
final View host = mView;
......
if (mFirst) {
...
host.dispatchAttachedToWindow(mAttachInfo, 0);
...
}
...
if (viewVisibilityChanged) {
...
host.dispatchWindowVisibilityChanged(viewVisibility);
...
}
.....
performMeasure();
...
performLayout();
...
performDraw();
...
}
host代表当前Activity的顶层视图DecorView
,当一个View附加到其父视图时提供给该View的一组窗口信息就是mAttachInfo
,会传递给每一个View。
DecorView
和其父类FrameLayout
都没有重写dispatchAttachedToWindow
方法,我们来看ViewGroup
的这个方法:
dispatchAttachedToWindow
@Override
@UnsupportedAppUsage
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
super.dispatchAttachedToWindow(info, visibility);
mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}
final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
for (int i = 0; i < transientCount; ++i) {
View view = mTransientViews.get(i);
view.dispatchAttachedToWindow(info,
combineVisibility(visibility, view.getVisibility()));
}
}
这个方法中遍历子View,依次调用其dispatchAttachedToWindow
方法,这种模式在View和ViewGroup中非常常见,一层一层的遍历View树。
SurfaceView
中并没有重写dispatchAttachedToWindow
方法,接着来看View的此方法。
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
if (mOverlay != null) {
mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
}
mWindowAttachCount++;
.....
onAttachedToWindow();
......
// Send onVisibilityChanged directly instead of dispatchVisibilityChanged.
// As all views in the subtree will already receive dispatchAttachedToWindow
// traversing the subtree again here is not desired.
onVisibilityChanged(this, visibility);
......
}
这里面又调用了两个我们常用的回调onAttachedToWindow
,onVisibilityChanged
,SurfaceView
中没有重写onVisibilityChanged
方法,但是重写了onAttachedToWindow
方法,我们接着就来看看这个方法:
SurfaceView.onAttachedToWindow
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewRootImpl().addWindowStoppedCallback(this);
mWindowStopped = false;
mViewVisibility = getVisibility() == VISIBLE;
updateRequestedVisibility();
mAttachedToWindow = true;
mParent.requestTransparentRegion(SurfaceView.this);
if (!mGlobalListenersAdded) {
ViewTreeObserver observer = getViewTreeObserver();
observer.addOnScrollChangedListener(mScrollChangedListener);
observer.addOnPreDrawListener(mDrawListener);
mGlobalListenersAdded = true;
}
}
此方法中会添加一些监听器,暂时不去看,
updateRequestedVisibility很简单,就是更新mRequestedVisible
的值,最重要的还是requestTransparentRegion
方法,因为SurfaceView
默认层级在主窗口之下,为了能够可见需要设置透明区域,就是依靠requestTransparentRegion
方法实现的。
mParent.requestTransparentRegion
mParent代表当前SurfaceView
父容器,其实不管父容器是LinearLayout,FrameLayout还是其他,他们都没有重写requestTransparentRegion
方法,所以最终都是调用到ViewGroup的requestTransparentRegion
方法:
@Override
public void requestTransparentRegion(View child) {
if (child != null) {
child.mPrivateFlags |= View.PFLAG_REQUEST_TRANSPARENT_REGIONS;
if (mParent != null) {
mParent.requestTransparentRegion(this);
}
}
}
这里的child就是SurfaceView
,首先会给SurfaceView
设置Flag为PFLAG_REQUEST_TRANSPARENT_REGIONS
,然后调用SurfaceView
的父容器的mParent的requestTransparentRegion
方法,mParent会一直向上寻找父容器,直到找到顶层视图DecorView
的mParent—>ViewRootImpl,所以最终其实是调用到ViewRootImpl的requestTransparentRegion
方法:
ViewRootImpl.requestTransparentRegion
@Override
public void requestTransparentRegion(View child) {
// the test below should not fail unless someone is messing with us
checkThread();
if (mView == child) {
mView.mPrivateFlags |= View.PFLAG_REQUEST_TRANSPARENT_REGIONS;
// Need to make sure we re-evaluate the window attributes next
// time around, to ensure the window has the correct format.
mWindowAttributesChanged = true;
mWindowAttributesChangesFlag = 0;
requestLayout();
}
}
首先会检查线程,这个操作必须在主线程,接着如果是正常调用的话,child就是DecorView了,给DecorView的mPrivateFlags设置为PFLAG_REQUEST_TRANSPARENT_REGIONS
,修改mWindowAttributesChanged和mWindowAttributesChangesFlag并调用requestLayout()
方法触发UI重绘,但重绘并不是立即执行的,而是将绘制的Runnable放入队列,需要等待下次Vsync到来才能执行,最终执行的就是performTraversals
方法,注意,这次的performTraversals
是第二次,第一次的我们还没分析完,第一次回调SurfaceView
的onAttachedToWindow
方法,此方法中设置的透明区域,第二次为什么不会再走到onAttachedToWindow
方法去,这是因为performTraversals
中有个开关mFirst
,我们接着来看第二次performTraversals
中设置透明区域的代码逻辑:
performTraversals
private void performTraversals() {
final View host = mView;
......
if (mFirst) {
...
host.dispatchAttachedToWindow(mAttachInfo, 0);
...
}
...
if (viewVisibilityChanged) {
...
host.dispatchWindowVisibilityChanged(viewVisibility);
...
}
.....
performMeasure();
...
if (didLayout) {
performLayout(lp, mWidth, mHeight);
// By this point all views have been sized and positioned
// We can compute the transparent area
if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
// start out transparent
// TODO: AVOID THAT CALL BY CACHING THE RESULT?
host.getLocationInWindow(mTmpLocation);
mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],
mTmpLocation[0] + host.mRight - host.mLeft,
mTmpLocation[1] + host.mBottom - host.mTop);
host.gatherTransparentRegion(mTransparentRegion);
if (mTranslator != null) {
mTranslator.translateRegionInWindowToScreen(mTransparentRegion);
}
if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {
mPreviousTransparentRegion.set(mTransparentRegion);
mFullRedrawNeeded = true;
// reconfigure window manager
try {
mWindowSession.setTransparentRegion(mWindow, mTransparentRegion);
} catch (RemoteException e) {
}
}
}
...
performDraw();
...
}
设置透明区域是在Layout之后执行的,必须要等到整个视图树的所有View位置确定之后才能知道要设置的透明区域在什么位置.
接着DecorView的flag为PFLAG_REQUEST_TRANSPARENT_REGIONS
则开始透明区域的设置,其实下面这两个方法合起来就是将mTransparentRegion
设置为当前DecorView的占据的区域:
host.getLocationInWindow(mTmpLocation);
mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],
mTmpLocation[0] + host.mRight - host.mLeft,
mTmpLocation[1] + host.mBottom - host.mTop);
mTmpLocation
是一个大小为2的int数组,这个数组会传递到View中去,接收调用getLocationInWindow
方法的View的mLeft
和mTop
返回,这里即是DecorView的mLeft
和mTop
,这两个值其实就是DecorView左上角点的坐标,所以getLocationInWindow
方法其实就是计算View的左上角坐标并以int数组返回,然后mTransparentRegion
就以DecorView的左上右下四个点创建了一个矩形区域作为初始化透明区域,接着调用了DecorView的gatherTransparentRegion
方法:
gatherTransparentRegion
@Override
public boolean gatherTransparentRegion(Region region) {
boolean statusOpaque = gatherTransparentRegion(mStatusColorViewState, region);
boolean navOpaque = gatherTransparentRegion(mNavigationColorViewState, region);
boolean decorOpaque = super.gatherTransparentRegion(region);
// combine bools after computation, so each method above always executes
return statusOpaque || navOpaque || decorOpaque;
}
可以很明显看出DecorView的gatherTransparentRegion
方法根本没有具体实现,最终都会调到ViewGroup或者View中,前面说过这种模式在ViewGroup或者View非常多,其实就是遍历视图树,ViewGroup中果然又是遍历子View,对于flag为PFLAG_REQUEST_TRANSPARENT_REGIONS
的ViewGroup则调用所有子View的gatherTransparentRegion
方法:
@Override
public boolean gatherTransparentRegion(Region region) {
final boolean meOpaque = (mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) == 0;
if (meOpaque && region == null) {
// The caller doesn't care about the region, so stop now.
return true;
}
super.gatherTransparentRegion(region);
.....
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) {
if (!child.gatherTransparentRegion(region)) {
noneOfTheChildrenAreTransparent = false;
}
}
}
...
}
return meOpaque || noneOfTheChildrenAreTransparent;
}
在遍历子View之前首先调用了super.gatherTransparentRegion
所以还得来看下View的gatherTransparentRegion
方法:
@UnsupportedAppUsage
public boolean gatherTransparentRegion(Region region) {
final AttachInfo attachInfo = mAttachInfo;
if (region != null && attachInfo != null) {
final int pflags = mPrivateFlags;
if ((pflags & PFLAG_SKIP_DRAW) == 0) {
final int[] location = attachInfo.mTransparentLocation;
getLocationInWindow(location);
int shadowOffset = getZ() > 0 ? (int) getZ() : 0;
region.op(location[0] - shadowOffset, location[1] - shadowOffset,
location[0] + mRight - mLeft + shadowOffset,
location[1] + mBottom - mTop + (shadowOffset * 3), Region.Op.DIFFERENCE);
} else {
if (mBackground != null && mBackground.getOpacity() != PixelFormat.TRANSPARENT) {
applyDrawableToTransparentRegion(mBackground, region);
}
if (mForegroundInfo != null && mForegroundInfo.mDrawable != null
&& mForegroundInfo.mDrawable.getOpacity() != PixelFormat.TRANSPARENT) {
// Similarly, we remove the foreground drawable's non-transparent parts.
applyDrawableToTransparentRegion(mForegroundInfo.mDrawable, region);
}
if (mDefaultFocusHighlight != null
&& mDefaultFocusHighlight.getOpacity() != PixelFormat.TRANSPARENT) {
// Similarly, we remove the default focus highlight's non-transparent parts.
applyDrawableToTransparentRegion(mDefaultFocusHighlight, region);
}
}
}
return true;
}
这里面有一个很重要的值,就是mPrivateFlags,如果flag被设置为了PFLAG_SKIP_DRAW
代表这个View不需要绘制,什么情况会被设置为PFLAG_SKIP_DRAW
,通常是调用setWillNotDraw(true)
设置的,SurfaceView
的构造方法中就是调用了此方法,如果是一个需要绘制的View,则会将这个View所占据的矩形区域从mTransparentRegion
透明区域中裁掉,裁剪的方式是Region.Op.DIFFERENCE
,因为一开始从ViewRootImpl传递过来的mTransparentRegion
是等于DecorView的大小,这里DecorView是需要绘制的,所以相当于整个DecorView没有收集到透明区域,接着对于View不需要绘制的情况例如SurfaceView
,首先如果此View的mBackground
不为空并且mBackground
不为透明,则会调用applyDrawableToTransparentRegion
方法同样会将此View所占据的矩形区域从region
中裁剪掉,代表此View不会在主窗口上设置透明区域,applyDrawableToTransparentRegion
中裁剪格式很有几种:Region.Op.UNION
,Region.Op.INTERSECT
,Region.Op.DIFFERENCE
,会根据具体条件来选择,关于裁剪格式借用博客https://blog.csdn.net/eyishion/article/details/53728913的一张图:
到SurfaceView
之前整个View树还没有收集到透明区域,这是因为其他View默认都是需要绘制的,所以他们都从mTransparentRegion
中裁剪掉了。
接着我们需要看看SurfaceView
中关于gatherTransparentRegion
的具体实现:
SurfaceView.gatherTransparentRegion
@Override
public boolean gatherTransparentRegion(Region region) {
if (isAboveParent() || !mDrawFinished) {
return super.gatherTransparentRegion(region);
}
boolean opaque = true;
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
// this view draws, remove it from the transparent region
opaque = super.gatherTransparentRegion(region);
} else if (region != null) {
int w = getWidth();
int h = getHeight();
if (w>0 && h>0) {
getLocationInWindow(mLocation);
// otherwise, punch a hole in the whole hierarchy
int l = mLocation[0];
int t = mLocation[1];
region.op(l, t, l+w, t+h, Region.Op.UNION);
}
}
if (PixelFormat.formatHasAlpha(mRequestedFormat)) {
opaque = false;
}
return opaque;
}
首先如果isAboveParent
为true,代表着SurfaceView
的mSubLayer
大于0,默认SurfaceView
的mSubLayer
是等于APPLICATION_MEDIA_SUBLAYER
为-2的,所以isAboveParent
默认值为false,可通过setZOrderOnTop
或者setZOrderMediaOverlay
修改,mDrawFinished
代表已经完成绘制,因为设置透明区域是通过requestLayout
请求下一个Vsync执行的,所以上一个Vsync已经执行完绘制流程了,mDrawFinished
为true,所以默认情况下不会进这个判断,接着判断SurfaceView
是否需要绘制,即flag是否为PFLAG_SKIP_DRAW
,默认情况下不会走进这里,除非手动调用setWillNotDraw(false)
,综上,默认情况下不会调用父类View的gatherTransparentRegion
方法,接着获取SurfaceView
的宽高,调用getLocationInWindow
获取SurfaceView
左上角坐标放入mLocation中,并裁剪出当前SurfaceView
的矩形区域放入mTransparentRegion
中,最后还会判断当前正在处理的SurfaceView
的格式是否设置有透明值,如果有则将opaque设置为false并返回。
最后回到performTraversals
,mTransparentRegion
有了之后会设置给mPreviousTransparentRegion
,避免在透明区域没有改动时重复调用,接着通过
mWindowSession.setTransparentRegion(mWindow, mTransparentRegion)将mTransparentRegion
传给WMS,最终传递到SurfaceFlinger
中去修改当前Activity的surface的透明区域。
到此SurfaceView
的onAttachedToWindow
方法中设置透明区域已经全部分析完毕了。接下来需要看SurfaceView
的另一个回调方法onWindowVisibilityChanged
所做的事情了。
本文地址:https://blog.csdn.net/qq_34211365/article/details/107045611
上一篇: Android 添加白名单实现保活
下一篇: 中国少年地子体验基地