当前位置: 移动技术网 > 移动技术>移动开发>Android > AndroidQ 图形系统(9)SurfaceView实现原理之设置透明区域

AndroidQ 图形系统(9)SurfaceView实现原理之设置透明区域

2020年07月08日  | 移动技术网移动技术  | 我要评论

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的测量,布局,绘制流程,入口在ViewRootImplperformTraversals方法中,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);
		......
    }

这里面又调用了两个我们常用的回调onAttachedToWindowonVisibilityChangedSurfaceView中没有重写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是第二次,第一次的我们还没分析完,第一次回调SurfaceViewonAttachedToWindow方法,此方法中设置的透明区域,第二次为什么不会再走到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的mLeftmTop返回,这里即是DecorView的mLeftmTop,这两个值其实就是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.UNIONRegion.Op.INTERSECTRegion.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,代表着SurfaceViewmSubLayer大于0,默认SurfaceViewmSubLayer是等于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并返回。

最后回到performTraversalsmTransparentRegion有了之后会设置给mPreviousTransparentRegion,避免在透明区域没有改动时重复调用,接着通过
mWindowSession.setTransparentRegion(mWindow, mTransparentRegion)将mTransparentRegion传给WMS,最终传递到SurfaceFlinger中去修改当前Activity的surface的透明区域。

到此SurfaceViewonAttachedToWindow方法中设置透明区域已经全部分析完毕了。接下来需要看SurfaceView的另一个回调方法onWindowVisibilityChanged所做的事情了。

本文地址:https://blog.csdn.net/qq_34211365/article/details/107045611

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网