您当前的位置:首页 > 互联网教程

再学一遍android:fitsSystemWindows属性

发布时间:2025-05-12 09:27:57    发布人:远客网络

再学一遍android:fitsSystemWindows属性

一、再学一遍android:fitsSystemWindows属性

1、熟悉android:fitsSystemWindows属性的用途,但它在实现沉浸式状态栏效果时的不稳定性常常让人困惑。该属性主要用于调整布局内容与系统状态栏的交互,以实现更为流畅的视觉体验。本文旨在深入探讨这一属性的工作原理与实现方法。

2、沉浸式状态栏效果,即让应用程序内容延伸到状态栏背后,以提升视觉连贯性。这通常通过将状态栏颜色设置为透明,并结合android:fitsSystemWindows属性来实现。然而,属性本身并不直接打开或关闭沉浸式效果,而是一个触发条件。实际效果依赖于布局结构以及与属性关联的代码逻辑。

3、通过代码示例,我们可以直观理解这一过程。首先,将系统状态栏颜色设为透明,然后在根布局中设置android:fitsSystemWindows属性。若直接在FrameLayout布局上应用此属性,内容仍不会延伸到状态栏区域。但将布局结构更改为CoordinatorLayout,属性便开始发挥作用,布局内容得以延伸,实现沉浸式状态栏效果。

4、CoordinatorLayout处理android:fitsSystemWindows属性的关键在于其内部逻辑。当布局设置此属性时,它会对布局的内边距(insets)进行调整,并调用系统UI可见性设置代码。这一过程确保了布局内容的合理布局,避免被状态栏遮挡,特别是对于可交互控件。

5、对于不希望布局内容延伸到状态栏区域的特定控件,如图像,可以通过在CoordinatorLayout外部包裹CollapsingToolbarLayout,并设置其android:fitsSystemWindows属性来实现。这样,图像等子控件将仅在布局内显示,不会影响沉浸式状态栏效果。

6、在项目限制下,无法使用特定布局时,可手动调用系统UI可见性设置函数实现沉浸式状态栏效果。通过监听WindowInsets变化事件,并根据状态栏高度调整布局内控件的位置,可以解决可交互控件被状态栏遮挡的问题。

7、本文通过详细的代码示例与解释,阐述了android:fitsSystemWindows属性的原理、使用方法以及在实现沉浸式状态栏效果时的注意事项。通过深入理解这一属性,开发者可以更加灵活地应用到实际项目中,提升用户界面的视觉效果与用户体验。

二、如何实现android沉浸式状态栏

有些手机是强制改变通知栏颜色的,比如魅族,苹果。但是目前主要还是通过代码作出自己想要的效果。

Android 4.4之前,即使我们打开手机app,我们还总是能看到系统顶部那条黑乎乎的通知栏,这样会使得app稍显突兀。于是Android 4.4开始,便引入了Translucent System Bar的新特性,用于弥补系统通知栏突兀之处。

状态栏透明后,你可以选择设置其颜色或者显示背景图片。效果如下

Android4.4和5.0的实现方式不同。这里简单介绍一种

主要的操作都在style.xml和 AndroidManifest.xml中,Activity里面没有任何涉及到Translucent System Bar设置的代码,所以可以忽略不看。

要在Activity中使用 Translucent System Bar特性,首先需要到AndroidManifest中为指定的Activity设置Theme。但我们不能直接在values/style.xml去定义Theme,因为改特性仅兼容 Android 4.4开始的平台,所以直接在values/style.xml声明引入,工程会报错。有些开发者朋友会在代码中去判断SDK的版本,然后再用代码设置Theme。虽然同样可以实现效果,但个人并不推荐。我采取的方法是建立多个SDK版本的values文件夹,系统会根据SDK的版本选择合适的Theme进行设置。我的工程里面有values、values-v19、values-v21。

1、在values、values-v19、values-v21的style.xml都设置一个 Translucent System Bar风格的Theme

<stylename="ImageTranslucentTheme"parent="AppTheme">

<!--在Android4.4之前的版本上运行,直接跟随系统主题-->

</style>

values-v19/style.xml

<stylename="ImageTranslucentTheme"parent="Theme.AppCompat.Light.DarkActionBar">

<itemname="android:windowTranslucentStatus">true</item>

<itemname="android:windowTranslucentNavigation">true</item>

</style>

values-v21/style.xml

<stylename="ImageTranslucentTheme"parent="Theme.AppCompat.Light.DarkActionBar">

<itemname="android:windowTranslucentStatus">false</item>

<itemname="android:windowTranslucentNavigation">true</item>

<!--Android5.x开始需要把颜色设置透明,否则导航栏会呈现系统默认的浅灰色-->

<itemname="android:statusBarColor">@android:color/transparent</item>

</style>

上面需要注意的地方是,无论你在哪个SDK版本的values目录下,设置了主题,都应该在最基本的values下设置一个同名的主题。这样才能确保你的app能够正常运行在 Android 4.4以下的设备。否则,肯定会报找不到Theme的错误。

上面需要注意的地方是,无论你在哪个SDK版本的values目录下,设置了主题,都应该在最基本的values下设置一个同名的主题。这样才能确保你的app能够正常运行在 Android 4.4以下的设备。否则,肯定会报找不到Theme的错误。

2、在AndroidManifest.xml中对指定Activity的theme进行设置

android:name=".ui.ImageTranslucentBarActivity"

android:label="@string/image_translucent_bar"

android:theme="@style/ImageTranslucentTheme"/>

3、在Activity的布局文件中设置背景图片,同时,需要把android:fitsSystemWindows设置为true

3、在Activity的布局文件中设置背景图片,同时,需要把android:fitsSystemWindows设置为true

activity_image_translucent_bar.xml

<?xmlversion="1.0"encoding="utf-8"?>

<RelativeLayoutxmlns:android="

android:layout_width="match_parent"

android:layout_height="match_parent"

android:background="@mipmap/env_bg"

android:fitsSystemWindows="true">

</RelativeLayout>

到此,第一种实现方式完成,系统的整个导航栏都融入了app的界面中,背景图片填满了整个屏幕,看起来舒服很多。设置android:fitsSystemWindows这个属性时需要注意。

到此,第一种实现方式完成,系统的整个导航栏都融入了app的界面中,背景图片填满了整个屏幕,看起来舒服很多。设置android:fitsSystemWindows这个属性时需要注意。

通知栏的沉浸式体验,推荐和Material Design配合使用,

@android:style/Theme.Material(dark version)

@android:style/Theme.Material.Light(light version)

@android:style/Theme.Material.Light.DarkActionBar

Theme.AppCompat.Light.DarkActionBar

我们可以根据我们的app的风格,去定制Color Palette(调色板),重点有以下几个属性:

<!--Baseapplicationtheme.-->

<stylename="AppBaseTheme"parent="Theme.AppCompat">

<!--customizethecolorpalette-->

<itemname="colorPrimary">@color/material_blue_500</item>

<itemname="colorPrimaryDark">@color/material_blue_700</item>

<itemname="colorAccent">@color/material_green_A200</item>

</resources>

colorPrimary对应ActionBar的颜色。

colorPrimary对应ActionBar的颜色。

colorPrimaryDark对应状态栏的颜色

colorAccent对应EditText编辑时、RadioButton选中、CheckBox等选中时的颜色。

于5.0以下的设备,目前colorPrimaryDark无法去个性化状态栏的颜色;底部的navagationBar可能也不一样。

另外ActionBar,被推荐使用ToolBar来代替。

<?xmlversion="1.0"encoding="utf-8"?><resources>

<colorname="md_red_50_color_code">#fde0dc</color>

<colorname="md_red_100_color_code">#f9bdbb</color>

<colorname="md_red_200_color_code">#f69988</color>

<colorname="md_red_300_color_code">#f36c60</color>

<colorname="md_red_400_color_code">#e84e40</color>

<colorname="md_red_500_color_code">#e51c23</color>

<colorname="md_red_600_color_code">#dd191d</color>

<colorname="md_red_700_color_code">#d01716</color>

<colorname="md_red_800_color_code">#c41411</color>

<colorname="md_red_900_color_code">#b0120a</color>

<colorname="md_red_a100_color_code">#ff7997</color>

<colorname="md_red_a200_color_code">#ff5177</color>

<colorname="md_red_a400_color_code">#ff2d6f</color>

<colorname="md_red_a700_color_code">#e00032</color>

<colorname="md_pink_50_color_code">#fce4ec</color>

<colorname="md_pink_100_color_code">#f8bbd0</color>

<colorname="md_pink_200_color_code">#f48fb1</color>

<colorname="md_pink_300_color_code">#f06292</color>

<colorname="md_pink_400_color_code">#ec407a</color>

<colorname="md_pink_500_color_code">#e91e63</color>

<colorname="md_pink_600_color_code">#d81b60</color>

<colorname="md_pink_700_color_code">#c2185b</color>

<colorname="md_pink_800_color_code">#ad1457</color>

<colorname="md_pink_900_color_code">#880e4f</color>

<colorname="md_pink_a100_color_code">#ff80ab</color>

<colorname="md_pink_a200_color_code">#ff4081</color>

<colorname="md_pink_a400_color_code">#f50057</color>

<colorname="md_pink_a700_color_code">#c51162</color>

<colorname="md_deep_purple_50_color_code">#ede7f6</color>

<colorname="md_deep_purple_100_color_code">#d1c4e9</color>

<colorname="md_deep_purple_200_color_code">#b39ddb</color>

<colorname="md_deep_purple_300_color_code">#9575cd</color>

<colorname="md_deep_purple_400_color_code">#7e57c2</color>

<colorname="md_deep_purple_500_color_code">#673ab7</color>

<colorname="md_deep_purple_600_color_code">#5e35b1</color>

<colorname="md_deep_purple_700_color_code">#512da8</color>

<colorname="md_deep_purple_800_color_code">#4527a0</color>

<colorname="md_deep_purple_900_color_code">#311b92</color>

<colorname="md_deep_purple_a100_color_code">#b388ff</color>

<colorname="md_deep_purple_a200_color_code">#7c4dff</color>

<colorname="md_deep_purple_a400_color_code">#651fff</color>

<colorname="md_deep_purple_a700_color_code">#6200ea</color>

<colorname="md_yellow_50_color_code">#fffde7</color>

<colorname="md_yellow_100_color_code">#fff9c4</color>

<colorname="md_yellow_200_color_code">#fff59d</color>

<colorname="md_yellow_300_color_code">#fff176</color>

<colorname="md_yellow_400_color_code">#ffee58</color>

<colorname="md_yellow_500_color_code">#ffeb3b</color>

<colorname="md_yellow_600_color_code">#fdd835</color>

<colorname="md_yellow_700_color_code">#fbc02d</color>

<colorname="md_yellow_800_color_code">#f9a825</color>

<colorname="md_yellow_900_color_code">#f57f17</color>

<colorname="md_yellow_a100_color_code">#ffff8d</color>

<colorname="md_yellow_a200_color_code">#ffff00</color>

<colorname="md_yellow_a400_color_code">#ffea00</color>

<colorname="md_yellow_a700_color_code">#ffd600</color>

<colorname="md_orange_50_color_code">#fff3e0</color>

<colorname="md_orange_100_color_code">#ffe0b2</color>

<colorname="md_orange_200_color_code">#ffcc80</color>

<colorname="md_orange_300_color_code">#ffb74d</color>

<colorname="md_orange_400_color_code">#ffa726</color>

<colorname="md_orange_500_color_code">#ff9800</color>

<colorname="md_orange_600_color_code">#fb8c00</color>

<colorname="md_orange_700_color_code">#f57c00</color>

<colorname="md_orange_800_color_code">#ef6c00</color>

<colorname="md_orange_900_color_code">#e65100</color>

<colorname="md_orange_a100_color_code">#ffd180</color>

<colorname="md_orange_a200_color_code">#ffab40</color>

<colorname="md_orange_a400_color_code">#ff9100</color>

<colorname="md_orange_a700_color_code">#ff6d00</color>

<!--...............................-->

<colorname="md_grey_50_color_code">#fafafa</color>

<colorname="md_grey_100_color_code">#f5f5f5</color>

<colorname="md_grey_200_color_code">#eeeeee</color>

<colorname="md_grey_300_color_code">#e0e0e0</color>

<colorname="md_grey_400_color_code">#bdbdbd</color>

<colorname="md_grey_500_color_code">#9e9e9e</color>

<colorname="md_grey_600_color_code">#757575</color>

<colorname="md_grey_700_color_code">#616161</color>

<colorname="md_grey_800_color_code">#424242</color>

<colorname="md_grey_900_color_code">#212121</color>

<colorname="md_black_color_code">#000000</color>

<colorname="md_white_color_code">#ffffff</color>

<colorname="md_blue_grey_50_color_code">#eceff1</color>

<colorname="md_blue_grey_100_color_code">#cfd8dc</color>

<colorname="md_blue_grey_200_color_code">#b0bec5</color>

<colorname="md_blue_grey_300_color_code">#90a4ae</color>

<colorname="md_blue_grey_400_color_code">#78909c</color>

<colorname="md_blue_grey_500_color_code">#607d8b</color>

<colorname="md_blue_grey_600_color_code">#546e7a</color>

<colorname="md_blue_grey_700_color_code">#455a64</color>

<colorname="md_blue_grey_800_color_code">#37474f</color>

<colorname="md_blue_grey_900_color_code">#263238</color>

三、android开发设置屏蔽录制

项目开发中,为了用户信息的安全,会有禁止页面被截屏、录屏的需求。

这类资料,在网上有很多,一般都是通过设置Activity的Flag解决,如:

//禁止页面被截屏、录屏getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);

这种设置可解决一般的防截屏、录屏的需求。

如果页面中有弹出Popupwindow,在录屏视频中的效果是:

但Popupwindow区域仍然是可以看到的

未设置FLAG_SECURE,录屏的效果,如下图(git图片中间的水印忽略):

设置了FLAG_SECURE之后,录屏的效果,如下图(git图片中间的水印忽略):

看到了上面的效果,我们可能会有疑问PopupWindow不像Dialog有自己的window对象,而是使用WindowManager.addView方法将View显示在Activity窗体上的。那么,Activity已经设置了FLAG_SECURE,为什么录屏时还能看到PopupWindow?

我们先通过getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);来分析下源码:

//window布局参数private final WindowManager.LayoutParams mWindowAttributes= new WindowManager.LayoutParams();//添加标识public void addFlags(int flags){

}//通过mWindowAttributes设置标识public void setFlags(int flags, int mask){ final WindowManager.LayoutParams attrs= getAttributes();

attrs.flags=(attrs.flags&~mask)|(flags&mask);

dispatchWindowAttributesChanged(attrs);

}//获得布局参数对象,即mWindowAttributespublic final WindowManager.LayoutParams getAttributes(){ return mWindowAttributes;

通过源码可以看到,设置window属性的源码非常简单,即:通过window里的布局参数对象mWindowAttributes设置标识即可。

//显示PopupWindowpublic void showAtLocation(View parent, int gravity, int x, int y){

mParentRootView= new WeakReference<>(parent.getRootView());

showAtLocation(parent.getWindowToken(), gravity, x, y);

}//显示PopupWindowpublic void showAtLocation(IBinder token, int gravity, int x, int y){ if(isShowing()|| mContentView== null){ return;

TransitionManager.endTransitions(mDecorView);

final WindowManager.LayoutParams p=createPopupLayoutParams(token);

}//创建Window布局参数对象protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token){ final WindowManager.LayoutParams p= new WindowManager.LayoutParams();

p.flags= computeFlags(p.flags);

p.softInputMode= mSoftInputMode;

p.windowAnimations= computeAnimationResource(); if(mBackground!= null){

p.format= mBackground.getOpacity();

p.format= PixelFormat.TRANSLUCENT;

p.height= mLastHeight= mHeightMode;

p.height= mLastHeight= mHeight;

p.width= mLastWidth= mWidthMode;

p.privateFlags= PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH

| PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;

p.setTitle("PopupWindow:"+ Integer.toHexString(hashCode())); return p;

}//将PopupWindow添加到Window上private void invokePopup(WindowManager.LayoutParams p){ if(mContext!= null){

p.packageName= mContext.getPackageName();

} final PopupDecorView decorView= mDecorView;

decorView.setFitsSystemWindows(mLayoutInsetDecor);

setLayoutDirectionFromAnchor();

mWindowManager.addView(decorView, p); if(mEnterTransition!= null){

decorView.requestEnterTransition(mEnterTransition);

通过PopupWindow的源码分析,我们不难看出,在调用showAtLocation时,会单独创建一个WindowManager.LayoutParams布局参数对象,用于显示PopupWindow,而该布局参数对象上并未设置任何防止截屏Flag。

原因既然找到了,那么如何处理呢?

再回头分析下Window的关键代码:

//通过mWindowAttributes设置标识public void setFlags(int flags, int mask){ final WindowManager.LayoutParams attrs= getAttributes();

attrs.flags=(attrs.flags&~mask)|(flags&mask);

dispatchWindowAttributesChanged(attrs);

其实只需要获得WindowManager.LayoutParams对象,再设置上flag即可。

但是PopupWindow并没有像Activity一样有直接获得window的方法,更别说设置Flag了。我们再分析下PopupWindow的源码:

//将PopupWindow添加到Window上private void invokePopup(WindowManager.LayoutParams p){ if(mContext!= null){

p.packageName= mContext.getPackageName();

final PopupDecorView decorView= mDecorView;

decorView.setFitsSystemWindows(mLayoutInsetDecor);

setLayoutDirectionFromAnchor();//添加View

mWindowManager.addView(decorView, p); if(mEnterTransition!= null){

decorView.requestEnterTransition(mEnterTransition);

我们调用showAtLocation,最终都会执行mWindowManager.addView(decorView, p);

那么是否可以在addView之前获取到WindowManager.LayoutParams呢?

答案很明显,默认是不可以的。因为PopupWindow并没有公开获取WindowManager.LayoutParams的方法,而且mWindowManager也是私有的。

我们可以通过hook的方式解决这个问题。我们先使用动态代理拦截PopupWindow类的addView方法,拿到WindowManager.LayoutParams对象,设置对应Flag,再反射获得mWindowManager对象去执行addView方法。

不过,通过hook的方式也有一定的风险,因为mWindowManager是私有对象,不像Public的API,谷歌后续升级Android版本不会考虑其兼容性,所以有可能后续Android版本中改了其名称,那么我们通过反射获得mWindowManager对象不就有问题了。不过从历代版本的Android源码去看,mWindowManager被改的几率不大,所以hook也是可以用的,我们尽量写代码时考虑上这种风险,避免以后出问题。

...... private WindowManager mWindowManager;

而addView方法是ViewManger接口的公共方法,我们可以放心使用。

public interface ViewManager{ public void addView(View view, ViewGroup.LayoutParams params); public void updateViewLayout(View view, ViewGroup.LayoutParams params); public void removeView(View view);

考虑到hook的可维护性和扩展性,我们将相关代码封装成一个独立的工具类吧。

package com.ccc.ddd.testpopupwindow.utils;

import android.view.WindowManager;

import android.widget.PopupWindow;

import java.lang.reflect.Field;

import java.lang.reflect.InvocationHandler;

import java.lang.reflect.Method;

import java.lang.reflect.Proxy;public class PopNoRecordProxy implements InvocationHandler{ private Object mWindowManager;//PopupWindow类的mWindowManager对象

public static PopNoRecordProxy instance(){ return new PopNoRecordProxy();

} public void noScreenRecord(PopupWindow popupWindow){ if(popupWindow== null){ return;

} try{//通过反射获得PopupWindow类的私有对象:mWindowManager

Field windowManagerField= PopupWindow.class.getDeclaredField("mWindowManager");

windowManagerField.setAccessible(true);

mWindowManager= windowManagerField.get(popupWindow); if(mWindowManager== null){ return;

}//创建WindowManager的动态代理对象proxy

Object proxy= Proxy.newProxyInstance(Handler.class.getClassLoader(), new Class[]{WindowManager.class}, this);//注入动态代理对象proxy(即:mWindowManager对象由proxy对象来代理)

windowManagerField.set(popupWindow, proxy);

} catch(IllegalAccessException e){

} catch(NoSuchFieldException e){

@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{ try{//拦截方法mWindowManager.addView(View view, ViewGroup.LayoutParams params);

if(method!= null&& method.getName()!= null&& method.getName().equals("addView")

&& args!= null&& args.length== 2){//获取WindowManager.LayoutParams,即:ViewGroup.LayoutParams

WindowManager.LayoutParams params=(WindowManager.LayoutParams) args[1];//禁止录屏

} return method.invoke(mWindowManager, args);

private void setNoScreenRecord(WindowManager.LayoutParams params){

setFlags(params, WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);

private void setAllowScreenRecord(WindowManager.LayoutParams params){

setFlags(params, 0, WindowManager.LayoutParams.FLAG_SECURE);

*设置WindowManager.LayoutParams flag属性(参考系统类Window.setFlags(int flags, int mask))

*@param params WindowManager.LayoutParams

*@param flags The new window flags(see WindowManager.LayoutParams).

*@param mask Which of the window flag bits to modify.

private void setFlags(WindowManager.LayoutParams params, int flags, int mask){ try{ if(params== null){ return;

} params.flags=(params.flags&~mask)|(flags& mask);

Popwindow禁止录屏工具类的使用,代码示例:

//正常项目中,该方法可改成工厂类

//正常项目中,也可自定义PopupWindow,在其类中设置禁止录屏

private PopupWindow createPopupWindow(View view, int width, int height){

PopupWindow popupWindow= new PopupWindow(view, width, height);//PopupWindow禁止录屏

PopNoRecordProxy.instance().noScreenRecord(popupWindow); return popupWindow;

View view= LayoutInflater.from(this).inflate(R.layout.pm1, null);

PopupWindow pw= createPopupWindow(view,ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);

pw1.showAtLocation(this.getWindow().getDecorView(), Gravity.BOTTOM| Gravity.RIGHT, PopConst.PopOffsetX, PopConst.PopOffsetY);