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
所做的事情了。