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的測量,佈局,繪製流程,入口在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所做的事情了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章