ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Android Activity/Window/View 的background

2021-10-06 12:00:01  阅读:155  来源: 互联网

标签:background 背景 Window 设置 Android 绘制 View


前言

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/material_grey_50
#fffafafa

那当要设置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

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

标签:background,背景,Window,设置,Android,绘制,View
来源: https://blog.csdn.net/wekajava/article/details/120623143

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有