Android Activity/Window/View/的background

前言

Activity/Window/View 的background,平時接觸最多的就是View的background,Activity的background次之,最后用的較少的是Window的background,這三者有什么關聯、區別呢?
通過本篇文章,你將了解到:

1、View background 原理與使用
2、Window background 原理與使用
3、Activity background 原理與使用
4、常用背景設置

1、View background 原理與使用

先看個簡單的圖示:


image.png

一般來說,View展示區域分為兩個部分:
1、背景
2、內容

本篇重點分析背景繪制,內容繪制請移步:Android 自定義View之Draw過程(上)

在平時的運用中,你是否思考過兩個問題:

1、為什么內容區域能遮住背景區域
2、為什么View.scrollTo(xx)只能移動內容

先看第一個問題
來看看如何繪制View的背景:
熟知的View.draw(xx)方法如下:

#View.java
public void draw(Canvas canvas) {
        ...
        //繪制背景-------------(1)
        drawBackground(canvas);
        
        ...
        if (!verticalEdges && !horizontalEdges) {
            //--------------(2)
            
            //繪制自身內容
            onDraw(canvas);
            
            //繪制子布局
            dispatchDraw(canvas);
            ...
            //前景、高亮等
            return;
        }
        ...
    }

從上面(1)、(2)點可以看出,先繪制背景,再繪制內容,因此內容區域會遮住部分背景區域。

再看第二個問題
主要是drawBackground(canvas)方法:

#View.java
    private void drawBackground(Canvas canvas) {
        //背景Drawable
        final Drawable background = mBackground;
        if (background == null) {
            //沒有背景,無需繪制
            return;
        }

        //設置背景Drawable,并設置其尺寸
        setBackgroundBounds();

        //支持硬件加速
        if (canvas.isHardwareAccelerated() && mAttachInfo != null
                && mAttachInfo.mThreadedRenderer != null) {
            //繪制背景,并返回Drawable ------------------(1)
            mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

            final RenderNode renderNode = mBackgroundRenderNode;
            if (renderNode != null && renderNode.hasDisplayList()) {
                //繪制完成
                setBackgroundRenderNodeProperties(renderNode);
                //和Canvas關聯起來,也就是將繪制好的背景掛到Canvas上
                ((RecordingCanvas) canvas).drawRenderNode(renderNode);
                return;
            }
        }

        //軟件繪制
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            //沒有偏移,直接繪制
            background.draw(canvas);
        } else {
            //現將canvas平移回來 ---------------(2)
            canvas.translate(scrollX, scrollY);
            //繪制
            background.draw(canvas);
            //再平移回去
            canvas.translate(-scrollX, -scrollY);
        }
    }

上面標注了兩個重點:
(1)
真正繪制背景的地方:

#View.java
    private RenderNode getDrawableRenderNode(Drawable drawable, RenderNode renderNode) {
        if (renderNode == null) {
            //創建renderNode
            renderNode = RenderNode.create(drawable.getClass().getName(),
                    new ViewAnimationHostBridge(this));
            renderNode.setUsageHint(RenderNode.USAGE_BACKGROUND);
        }

        //獲取尺寸,之前已經設置過
        final Rect bounds = drawable.getBounds();
        final int width = bounds.width();
        final int height = bounds.height();
        //獲取專門繪制背景的Canvas
        final RecordingCanvas canvas = renderNode.beginRecording(width, height);
        
        //平移
        canvas.translate(-bounds.left, -bounds.top);

        try {
            //繪制背景
            drawable.draw(canvas);
        } finally {
            //結束繪制,將displayList記錄到renderNode
            renderNode.endRecording();
        }
        
        ...
        return renderNode;
    }

可以看出,生成新的Canvas,用該Canvas繪制背景,并將繪制記錄到背景的renderNode里。
(2)
你可能已經發現了,此處為什么對Canvas進行平移?
對于軟件繪制來說,從RootView傳遞下來的Canvas是同一個,也就是說整個ViewTree都共用一個Canvas。對于View的繪制,其方法調用順序如下:

draw(x)->dispatchDraw(x)->child.draw(x1,x2,x3)->child.draw(x)

在child.draw(x1,x2,x3)方法里,判斷是否需要進行內容移動(mScrollX = 0 || mScrollY != 0),如果需要則移動Canvas,如下:

canvas.translate(-mScrollX, -mScrollY)

注意此處是取反了。
此時canvas已經被移動了,當調用到child.draw(xx)時候,就是上面分析的draw(xx)方法:
1、先繪制背景
2、繪制內容

繪制背景的時候將Canvas平移回來,再繪制背景,最后平移回去。再繪制內容的時候Canvas沒變,依然了平移了(-mScrollX, -mScrollY),因此內容繪制的時候就會平移,而繪制背景的時候不變,這就回答了第二個問題。

上邊僅僅針對軟件繪制回答了第二個問題,那么硬件加速繪制的時候為啥不需要平移Canvas呢?此處簡單說下結論:

硬件加速繪制的時候,每個View都有自己的Canvas,RenderNode,而相應的背景也有自己的Canvas,RenderNode,因此即使View的Canvas發生平移,也不會影響背景的Canvas,因此背景的Canvas無需針對mScrollX、mScrollY平移。

View繪制細節部分請移步:Android 自定義View之Draw過程(上)

用圖表示背景繪制過程:


image.png

以上是針對View background分析,通常來說我們設置背景只要給其指定Drawable就ok了。
不論是通過動態設置:

#View.java
public void setBackground(Drawable background){...}
public void setBackgroundColor(@ColorInt int color){...}
public void setBackgroundResource(@DrawableRes int resid){...}
...

還是通過xml靜態配置:

android:background="@color/colorGreen"
android:background="@drawable/test"
...

最終都是將生成的Drawable對象賦值給View的成員變量mBackground,最終在繪制的背景[drawBackground()]的時候使用該Drawable繪制。

2、Window background 原理與使用

若要設置Window背景,那么需要獲取Window對象,我們常用的用到Window對象的地方有兩個:Activity和Dialog。
Window是個抽象類,它的實現類是PhoneWindow,因此Activity和Dialog里Window指向實際上就是PhoneWindow對象。
獲取Window引用方式如下:

Activity.getWindow()
Dialog.getWindow()

來看看Window是如何設置背景的:

    #Window.java
    public abstract void setBackgroundDrawable(Drawable drawable);

    #PhoneWindow.java
    @Override
    public final void setBackgroundDrawable(Drawable drawable) {
        //mBackgroundDrawable 為記錄當前Window的背景
        if (drawable != mBackgroundDrawable) {
            //背景改變則需要重新設置
            mBackgroundDrawable = drawable;
            if (mDecor != null) {
                //mDecor 即為熟知的DecorView
                //此處調用DecorView方法
                mDecor.setWindowBackground(drawable);
                ...
            }
        }
    }

PhoneWindow重寫了Window里的setBackgroundDrawable(xx)方法。該方法里調用了DecorView的setWindowBackground(xx)。

#DecorView.java
    public void setWindowBackground(Drawable drawable) {
        if (mOriginalBackgroundDrawable != drawable) {
            mOriginalBackgroundDrawable = drawable;
            //賦值給View的成員變量mBackground,也就是給View設置背景
            updateBackgroundDrawable();
            
            //該處主要是判斷如果Window不是半透明(windowTranslucent=true),但是drawable有透明度,強制設置透明度=255
            if (drawable != null) {
                mResizingBackgroundDrawable = enforceNonTranslucentBackground(drawable,
                        mWindow.isTranslucent() || mWindow.isShowingWallpaper());
            } else {
                mResizingBackgroundDrawable = getResizingBackgroundDrawable(
                        mWindow.mBackgroundDrawable, mWindow.mBackgroundFallbackDrawable,
                        mWindow.isTranslucent() || mWindow.isShowingWallpaper());
            }
            if (mResizingBackgroundDrawable != null) {
                mResizingBackgroundDrawable.getPadding(mBackgroundPadding);
            } else {
                mBackgroundPadding.setEmpty();
            }
            drawableChanged();
        }
    }

可以看出給Window 設置背景最終反饋到DecorView上了。
以Activity為例,設置Activity Window的背景為綠色:

        ColorDrawable colorDrawable = new ColorDrawable();
        colorDrawable.setColor(Color.GREEN);
        getWindow().setBackgroundDrawable(colorDrawable);

效果如下:


image.png

方法調用流程:


image.png

3、Activity background 原理與使用

一般來說,我們會在Activity Theme里設置其背景:

    <style name="activitytheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="android:windowBackground">#0033ff</item>
    </style>

而在Activity onCreate(xx)里會調用setContentView(xx),而后調用PhoneWindow的generateLayout(xx)方法:

#PhoneWindow.java
    protected ViewGroup generateLayout(DecorView decor) {
        ...
        if (getContainer() == null) {
            if (mBackgroundDrawable == null) {
                ...
                //獲取theme里設置的背景
                if (a.hasValue(R.styleable.Window_windowBackground)) {
                    mBackgroundDrawable = a.getDrawable(R.styleable.Window_windowBackground);
                }
            }
            ...
        }
        ...

        if (getContainer() == null) {
            //設置DecorView背景
            mDecor.setWindowBackground(mBackgroundDrawable);
            ...
        }
        ...
    }

在Theme里設置的背景在此處被取出來,然后設置給DecorView背景。
嚴格上來說,Activity沒有所謂背景的說法,它的"背景"指的是Window的背景,只是為了方便沒有特意區分。
Activity有默認的背景,不同的主題取值不一樣,我這主題默認背景色:

<color name="background_material_light">@color/material_grey_50</color>
<color name="material_grey_50">#fffafafa</color>

那當要設置Activity為透明,怎么做呢?直接設置其背景透明,你會發現并沒有達到效果,而是一片黑色。此時需要配合另一個屬性使用:

    <style name="activitytheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">#00000000</item>
    </style>

4、常用背景設置

從上可知,無論是Activity還是Window,設置它們背景的時候,最終都是設置了DecorView的背景。
我們知道,想要在屏幕上顯示View,實際上是需要將這個View添加到Window里。調用如下方法:

WindowManager.addView(View view, ViewGroup.LayoutParams params)

該view作為Window的RootView。
來看看一些常用的RootView:

  • Activity/Dialog 使用DecorView作為RootView
  • PopupWindow使用PopupDecorView(沒設置背景的時候)/PopupBackgroundView(有背景的時候)
  • 普通懸浮窗選擇任意的View作為RootView

設置背景的過程實際上就是設置RootView的背景

上面舉例說明了Activity背景設置,接下來闡述常用的彈框背景設置及其注意事項。

Dialog 背景設置

先看一個簡單的Demo

    private void showDialog(Context context) {
        Dialog dialog = new Dialog(context);
        FrameLayout frameLayout = new FrameLayout(context);
        TextView textView1 = new TextView(context);
        textView1.setText("hello");
        frameLayout.addView(textView1, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
//                frameLayout.setBackgroundColor(Color.RED);---------->(1)
        dialog.setContentView(frameLayout);
        dialog.getWindow().setLayout(800, 800);
        ColorDrawable colorDrawable = new ColorDrawable();
        colorDrawable.setColor(Color.TRANSPARENT);
//                dialog.getWindow().setBackgroundDrawable(colorDrawable);--------->(2)
        dialog.show();
    }

將TextView添加到FrameLayout,并將FrameLayout作為Dialog ContentView添加進去,效果如下:


image.png

可以看出,Dialog默認設置了一個背景,該背景是有圓角的矩形。
現在將注釋(1)打開:設置FrameLayout背景
效果如下:


image.png

發現圓角沒了,先搞清楚這個圓角背景到底是哪個的背景呢?
將(1)注釋掉,注釋(2)打開,并設置

colorDrawable.setColor(Color.RED);

效果如下:


image.png

平白無故外層多了黑色的區域。
修改背景顏色為透明:

colorDrawable.setColor(Color.TRANSPARENT);

再看效果時,發現整個Dialog都沒背景了。


image.png

在此基礎上,將注釋(1)打開,效果如下:


image.png

上面操作可能比較繞,實際上想表達的就是兩個:

1、Dialog默認背景是DecorView的背景(DecorView默認背景會設置4個方向的Padding,當去除默認背景后會發現ContentView區域變大了)
2、一般來說,不要將Dialog背景改為ColorDrawable類型,會有黑色背景。要么將背景變為透明,然后設置contentView的背景;要么將背景改指向Shape。

設置Dialog背景兩種方式:

//動態
    dialog.getWindow().setBackgroundDrawable(colorDrawable);

//靜態
//設置style
    <style name="myDialog">
        <item name="android:windowBackground">@android:color/transparent</item>
    </style>

PopupWindow 背景設置

PopupWindow沒有使用Window,也沒有使用DecorView作為RootView。

    private void showPopupWindow(Context context, View anchor) {
        TextView textView1 = new TextView(context);
        textView1.setText("heloo jj");
        PopupWindow popupWindow = new PopupWindow(textView1, 300, 300, true);
        ColorDrawable colorDrawable = new ColorDrawable();
//                colorDrawable.setColor(Color.GREEN);-------------->(1)
        popupWindow.setBackgroundDrawable(colorDrawable);
        popupWindow.showAsDropDown(anchor);
    }

運行上面的Demo:


image.png

可以看出,PopupWindow 沒有背景。
將注釋(1)打開,效果如下:


image.png

背景已添加上。
想表達的意思:

PopupWindow 如果沒有設置背景的話,那么背景會是透明的。
當設置PopupWindow背景時,會生成一個PopupBackgroundView 作為PopupWindow的RootView,而設置PopupWindow背景就是設置PopupBackgroundView的背景

設置PopupWindow背景兩種方式:

//動態
    popupWindow.setBackgroundDrawable(colorDrawable);

//靜態
//設置style
    <style name="myPopupWindow">
        <item name="android:popupBackground">@color/green</item>
    </style>

普通懸浮窗口 背景設置

如果是直接通過WindowManager.addView(View view, ViewGroup.LayoutParams params)添加彈窗。
需要設置RootView的背景,也就是上面方法的view的背景,否則背景將是黑色的。
關于懸浮窗已經分析過很多次了,更詳細的內容請移步:
Window/WindowManager 不可不知之事

本文基于Android 10.0

您若喜歡,請點贊、關注,您的鼓勵是我前進的動力

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容