部分内容转载
作者:Alan_兰哥
链接:https://www.jianshu.com/p/0d61f9dffb14
屏幕尺寸、屏幕分辨率、屏幕像素密度
屏幕尺寸:屏幕对角线长度,单位是英寸,我们常说的多少多少寸,比如4.7存手机、5.7存手机,指的就是这个。
屏幕分辨率:如 1920×1080,是指在手机屏幕的像素点的个数,单位是px,1px = 1 像素点,一般是纵向像素 × 横向像素,意味着高有 1920 个像素点,宽有 1080 个像素点。
屏幕像素密度:是指每英寸上的像素点数,单位是 dpi(dotper inch)。像素密度和屏幕尺寸和屏幕分辨率有关,它是由对角线的像素点数除以屏幕的大小得到的,关系如下
dp、dip、dpi、sp、px
dp:是Android 特有的,意为密度无关像素,Google 发布的 BASELINE(基准线)为 160,以此为基准。
dip:Density Independent Pixels,同dp一个意思,目前废弃了,一般都写dp。
dpi:即为屏幕像素密度的单位
sp:Scale-IndependentPixels的缩写,可以根据文字大小首选项自动进行缩放。Google推荐我们使用12sp以上的大小,通常可以使用12sp,14sp,18sp,22sp,为避免精度损失,建议最好不要使用奇数和小数。
px:就是我们常说的像素
屏幕分辨率限定符(不推荐)
屏幕分辨率限定符适配需要在 res 文件夹下创建各种屏幕分辨率对应的 values-xxx 文件夹,如下图:
然后根据一个基准分辨率,例如基准分辨率为 1280x720,将宽度分成 720 份,取值为 1px~720px,将高度分成 1280 份,取值为 1px~1280px,生成各种分辨率对应的 dimens.xml 文件。如下分别为分辨率 1280x720 与 1920x1080 所对应的横向dimens.xml 文件:
假设设计图上的一个控件的宽度为 720px,那么布局中就写 android:layout_width="@dimen/x720" ,当运行程序的时候,系统会根据设备的分辨率去寻找对应的 dimens.xml 文件。例如运行在分辨率为 1280x720 的设备上,系统会自动找到对应的 values-1280x720 文件夹下的 lay_x.xml 文件,由上图可知 x720 对应的值为
720.px,可铺满该屏幕宽度。运行在分辨率为 1920x1080 的设备上,系统会自动找到对应的 values-1920x1080 文件夹下的 lay_x.xml 文件,由上图可知 x720 对应的值为 1080.0px,可铺满该屏幕宽度。这样就达到了屏幕适配的要求!
smallestWidth 限定符
smallestWidth 限定符适配原理与屏幕分辨率限定符适配原理一样,系统都是根据限定符去寻找对应的 dimens.xml 文件。例如程序运行在最小宽度为 360dp 的设备上,系统会自动找到对应的 values-sw360dp 文件夹下的 dimens.xml 文件。区别就在于屏幕分辨率限定符适配是拿 px 值等比例缩放,而 smallestWidth 限定符适配是拿 dp 值来等比缩放而已。需要注意的是“最小宽度”是不区分方向的,即无论是宽度还是高度,哪一边小就认为哪一边是“最小宽度”。如下分别为最小宽度为 360dp 与最小宽度为 640dp 所对应的 dimens.xml 文件:
可以通过ScreenMatch生成
需要自己安装ScreenMatch插件,第一次生成,会生成两个文件
将screenMatch_example_dimens拷贝到value目录,并且重命名成dimens;
screenMatch.properties文件中我们可以设置匹配哪些宽带的屏幕、忽略哪些以及匹配 哪个项目等等。
然后我再做一次生成操作就可以生成了各种swXXXdp目录了
最后使用android:layout_width="@dimen/x720"
平板适配
新建layout-sw720dp目录,里面添加layout布局文件。 使用平板打开APP自动切换。
也可以使用最小宽度符以及下面的适配 。
常见适配方案
布局适配
- 避免写死控件尺寸,使用wrap_content, match_parent
- LinearLayout xxx:weight="0.5“
- RelativeLayout xxx:layout_centerInParent="true" ...
- ContraintLayout xxx:layout_constraintLeft_toLeftOf="parent"...
相对定位,可以设置偏移量0-1百分比
角度定位
边距margin分显隐性,就是不显示的时候也存在
可以设置宽高比,横向纵向线可以设置百分比,可以设置占位view(不绘制),
组成链式可以设置权重和链式样式,
Barrier设置屏障,约束关联view一侧效果
Group,一起控制显隐性
Optimizer对二次测量进行优化 - Percent-support-lib xxx:layout_widthPercent="30%" ...宽占父布局宽度多少,
LayoutParams中属性的获取
onMeasure中,改变params.width为百分比计算结果,测量
如果测量值过小且设置的w/h是wrap_content,重新测量
图片资源适配
- .9图或者SVG图实现缩放
- 备用位图匹配不同分辨率
动态运行时加载
(关于小分辨率手机看到比大分辨率手机大,跟像素点大小有关,可以参照权重处理,
其实权重比也有问题,就是屏幕分辩率带来的问题,不同屏幕其分辨率比值是不一样的,导致大分辨率上是长方形 ,小分辨率上是正方形)
自定义 分辨率布局
设计稿有标准尺寸1920X1080(设计稿针对的屏幕尺寸)
那View控件宽高(设计稿view的宽高)*当前手机屏幕和标准尺寸(设计稿标准尺寸)的缩放比,得到view控件的真实值。
另外 margin也有计算。
最外层加一个自己的布局,在测量时完成上面的逻辑
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!flag) { //防止两次测量
flag = true;
//获取横竖方向等比
float scaleX = Utils.getInstance(getContext()).getHorizontalScale();
float scaleY = Utils.getInstance(getContext()).getVerticalScale();
//子View个数
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams params = (LayoutParams) child.getLayoutParams();
params.width = (int) (params.width * scaleX);
params.height = (int) (params.height * scaleY);
params.leftMargin = (int) (params.leftMargin * scaleX);
params.rightMargin = (int) (params.rightMargin * scaleX);
params.topMargin = (int) (params.topMargin * scaleY);
params.bottomMargin = (int) (params.bottomMargin * scaleY);
child.setPadding((int) (child.getPaddingLeft() * scaleX), (int) (child.getPaddingTop() * scaleY),
(int) (child.getPaddingRight() * scaleX), (int) (child.getPaddingBottom() * scaleY));
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public class Utils {
private static Utils utils;
private Context mContext;
//这里是设计稿参考宽高
private static final float STANDARD_WIDTH = 1080;
private static final float STANDARD_HEIGHT = 1920;
//这里是屏幕显示宽高
private int mDisplayWidth;
private int mDisplayHeight;
public Utils(Context context) {
mContext = context;
//获取屏幕宽高
if (mDisplayWidth == 0 || mDisplayHeight == 0) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (windowManager != null) {
//宽高获取
DisplayMetrics displayMetrics = new DisplayMetrics();
//如果不是NavigationBar沉浸式(不包含NavigationBar,如果屏幕有NavigationBar会比real小即真实分辨率小)
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
// windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);//真实屏幕宽高
//判断当前的横竖屏
if (displayMetrics.widthPixels > displayMetrics.heightPixels) {
//横屏
mDisplayWidth = displayMetrics.heightPixels;
mDisplayHeight = displayMetrics.widthPixels - getStatusBarHeight(context);
} else {
//竖屏
mDisplayWidth = displayMetrics.widthPixels;
mDisplayHeight = displayMetrics.heightPixels - getStatusBarHeight(context);
}
}
}
}
public static Utils getInstance(Context context) {
if (utils == null) {
utils = new Utils(context);
}
return utils;
}
//获取状态栏高度
public int getStatusBarHeight(Context context) {
int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resId > 0) {
return context.getResources().getDimensionPixelSize(resId);//获取具体的像素值
}
return 0;
}
//获取水平方向的缩放比例
public float getHorizontalScale() {
return mDisplayWidth / STANDARD_WIDTH;
}
//获取垂直方向的缩放比例
public float getVerticalScale() {
return mDisplayHeight / (STANDARD_HEIGHT - getStatusBarHeight(mContext));
}
}
自定义 百分比布局
和谷歌提供的百分比布局类似
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取父容器宽高
if (!flag) {
flag = true;
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//给子控件设置修改后的属性值
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
//获取子控件
View child = getChildAt(i);
//获取子控件LayoutParams
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
//判断子控件是否是百分比布局属性
if (checkLayoutParams(layoutParams)) {
//是
LayoutParams lp = (LayoutParams) layoutParams;
float widthPercent = lp.widthPercent;
float heightPercent = lp.heightPercent;
float marginLeftPercent = lp.marginLeftPercent;
float marginRightPercent = lp.marginRightPercent;
float marginTopPercent = lp.marginTopPercent;
float marginBottomPercent = lp.marginBottomPercent;
if (widthPercent > 0) {
lp.width = (int) (widthSize * widthPercent);
}
if (heightPercent > 0) {
lp.height = (int) (heightSize * heightPercent);
}
if (marginLeftPercent > 0) {
lp.leftMargin = (int) (widthSize * marginLeftPercent);
}
if (marginRightPercent > 0) {
lp.rightMargin = (int) (widthSize * marginRightPercent);
}
if (marginTopPercent > 0) {
lp.topMargin = (int) (heightSize * marginTopPercent);
}
if (marginBottomPercent > 0) {
lp.bottomMargin = (int) (heightSize * marginBottomPercent);
}
}
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
/**
* 1、创建自定义属性
* 2、在容器中去创建一个静态内部类LayoutParams
* 3、在LayoutParams构造方法中获取自定义属性
* 4、onMeasure中给子控件设置修改后的属性值
*/
public static class LayoutParams extends RelativeLayout.LayoutParams {
private float widthPercent;
private float heightPercent;
private float marginLeftPercent;
private float marginRightPercent;
private float marginTopPercent;
private float marginBottomPercent;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
//3、在LayoutParams构造方法中获取自定义属性 解析自定义属性
TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.PercentLayout);
widthPercent = typedArray.getFraction(R.styleable.PercentLayout_widthPercent, 1, 2, 0);
heightPercent = typedArray.getFraction(R.styleable.PercentLayout_heightPercent, 1, 2, 0);
marginLeftPercent = typedArray.getFraction(R.styleable.PercentLayout_marginLeftPercent, 1, 2, 0);
marginRightPercent = typedArray.getFraction(R.styleable.PercentLayout_marginRightPercent, 1, 2, 0);
marginTopPercent = typedArray.getFraction(R.styleable.PercentLayout_marginTopPercent, 1, 2, 0);
marginBottomPercent = typedArray.getFraction(R.styleable.PercentLayout_marginBottomPercent, 1, 2, 0);
typedArray.recycle();//回收
}
}
attrs
像素密度布局(像素密度包含长宽,所以和之前分辨率布局类似但是也有不一样的地方)
重点就是改变displayMetrics
dm.density = targetDensity; //(dpi/160) 后得到的值
dm.scaledDensity = targetScaleDensity;//字体缩放
dm.densityDpi = targetDensityDpi; //dpi
并注册组件监听,判断字体是否更改缩放,生成适配当前的缩放;
也可以 在application中注册activity声明周期回调
public class DensityUtils {
private static final float WIDTH = 360;//参考宽度(dp)
private static float appDensity;//表示屏幕密度
private static float appScaleDensity;//字体缩放比例,默认为appDensity
public static void setDensity(final Application application, Activity activity) {
//获取当前屏幕信息
DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
if (appDensity == 0) {
//初始化赋值
appDensity = displayMetrics.density;
appScaleDensity = displayMetrics.scaledDensity;
//监听字体变化
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
//字体发生更改,重新计算scaleDensity
if (newConfig != null && newConfig.fontScale > 0) {
appScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
//计算目标density scaledDensity
float targetDensity = displayMetrics.widthPixels / WIDTH;//1080/360=3;
float targetScaleDensity = targetDensity * (appScaleDensity / appDensity);
int targetDensityDpi = (int) (targetDensity * 160);
//替换Activity的值
//px = dp * (dpi / 160)
DisplayMetrics dm = activity.getResources().getDisplayMetrics();
dm.density = targetDensity; //(dpi/160) 后得到的值
dm.scaledDensity = targetScaleDensity;
dm.densityDpi = targetDensityDpi; //dpi
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// //设置density
DensityUtils.setDensity(getApplication(), this);
setContentView(R.layout.activity_density);
}
今日头条
也是改的这几个值
做了架构设计同时提供页面单独适配包括fragment和取消适配,
---->AutoSizeConfig.init ()
给了初始值放在获取MetaData过慢
mDesignWidthInDp = 1080;
mDesignHeightInDp = 1920;
getMetaData(application);
mActivityLifecycleCallbacks = new ActivityLifecycleCallbacksImpl(new WrapperAutoAdaptStrategy(strategy == null ? new DefaultAutoAdaptStrategy() : strategy));
application.registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
----->ActivityLifecycleCallbacksImpl.onActivityCreated()
1. 适配androidx或者v4注册fargment声明周期
((androidx.fragment.app.FragmentActivity) activity).getSupportFragmentManager().registerFragmentLifecycleCallbacks(mFragmentLifecycleCallbacksToAndroidx, true);
最终也是调用的applyAdapt方法只不过第一个参数是fagement自己
2. 调用设置方法
//Activity 中的 setContentView(View) 一定要在 super.onCreate(Bundle); 之后执行
if (mAutoAdaptStrategy != null) {
mAutoAdaptStrategy.applyAdapt(activity, activity);
}
----->DefaultAutoAdaptStrategy.applyAdapt(Object target, Activity activity)两个参数都传的当前acitivity
如果你有自定义就是走自定义没有走默认类DefaultAutoAdaptStrategy()
1. 检查是否开启了外部三方库的适配模式, 只要不主动调用 ExternalAdaptManager 的方法, 下面的代码就不会执行。里面还会判断是否实现取消接口,然后才走第三方。
2. target是不是实现了CancelAdapt,即取消,走AutoSize.cancelAdapt(activity),把值改回原来的还会走下面的方法
3. 如果 target 实现 CustomAdapt 接口表示该 target 想自定义一些用于适配的参数, 从而改变最终的适配效果AutoSize.autoConvertDensityOfCustomAdapt(activity, (CustomAdapt) target);--->autoConvertDensity(activity, sizeInDp, customAdapt.isBaseOnWidth())
4. 最终走AutoSize.autoConvertDensityOfGlobal(activity)
------>AutoSize.autoConvertDensityOfGlobal(activity)
基于高度还是宽度走不同方法
autoConvertDensityBaseOnWidth(activity, AutoSizeConfig.getInstance().getDesignWidthInDp());
autoConvertDensityBaseOnHeight(activity, AutoSizeConfig.getInstance().getDesignHeightInDp());
两者最终调用了autoConvertDensity(activity, designWidthInDp, true);第三个参数宽是true高是false
------>AutoSize.autoConvertDensity(Activity activity, float sizeInDp, boolean isBaseOnWidth)
根据传入对比值+设计大小+屏幕大小+字体放大比例以及MODE_MASK算出key
int key = Math.round((sizeInDp + subunitsDesignSize + screenSize) * AutoSizeConfig.getInstance().getInitScaledDensity()) & ~MODE_MASK;
isBaseOnWidth是true用屏幕宽比值算,否则用屏幕高比值算
targetDensity = AutoSizeConfig.getInstance().getScreenWidth() * 1.0f / sizeInDp;
根据targetDensity计算targetDensityDpi,targetScaledDensity,targetXdpi,targetScreenWidthDp,targetScreenHeightDp;
放入缓存方便下次取出
mCache.put(key, new DisplayMetricsInfo(targetDensity, targetDensityDpi, targetScaledDensity, targetXdpi, targetScreenWidthDp, targetScreenHeightDp));
setDensity(activity, targetDensity, targetDensityDpi, targetScaledDensity, targetXdpi);
setScreenSizeDp(activity, targetScreenWidthDp, targetScreenHeightDp);
------>1. AutoSize.setDensity()
兼容Miui获取Resources.class.getDeclaredField("mTmpMetrics"),一个是activity.getResources(),一个是getApplication().getResources();两者顺序是线activity再app
setDensity(DisplayMetrics displayMetrics, float density, int densityDpi, float scaledDensity, float xdpi)
设置三个值density,densityDpi,scaledDensity
if (AutoSizeConfig.getInstance().getUnitsManager().isSupportDP()) {
displayMetrics.density = density;
displayMetrics.densityDpi = densityDpi;
}
if (AutoSizeConfig.getInstance().getUnitsManager().isSupportSP()) {
displayMetrics.scaledDensity = scaledDensity;
}
如果有附属单位转换displayMetrics.xdpi的值
------->2. AutoSize.setScreenSizeDp(Configuration configuration, int screenWidthDp, int screenHeightDp)
得到activityConfiguration和appConfiguration修改screenWidthDp和screenHeightDp
Configuration activityConfiguration = activity.getResources().getConfiguration();
Configuration appConfiguration = AutoSizeConfig.getInstance().getApplication().getResources().getConfiguration()
刘海屏适配
系统级适配规则
Google默认的适配刘海屏策略是这样的
- 如果应用未适配刘海屏,需要系统对全屏显示的应用界面做特殊移动处理(竖屏下移处理,横屏右移处),因此此处出现了黑边的现象;如果应用页面布局不能做到自适应,就会出现布局问题;如果应用布局能够做到自适应,也会有黑边无法全屏显示的体验问题
2)如果有状态栏的App,则不受刘海屏的影响(有状态肯定不是全屏,那么就不会有下移的风险)
在Android P版本中,通过DisplayCutout 类,可以确定非功能区域(刘海屏)的位置和形状
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT:
只有当DisplayCutout完全包含在系统状态栏中时,才允许窗口延伸到DisplayCutout区域显示。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER:
该窗口决不允许与DisplayCutout区域重叠。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES :
该窗口始终允许延伸到屏幕短边上的DisplayCutout
第一种样式::LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);
WindowManager.LayoutParams lp = this.getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
this.getWindow().setAttributes(lp);
setContentView(R.layout.activity_android_p_demo_layout);
}
从图中可以看出,这个并不是完美的适配方案
第二种样式样式:LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
WindowManager.LayoutParams lp = this.getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
this.getWindow().setAttributes(lp);
setContentView(R.layout.activity_android_p_demo_layout);
}
可以看出显示效果和DEFAULT是一致的,LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER是不允许使用刘海屏区域,而LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT全屏窗口不允许使用刘海屏区域
第三种样式样式:LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
WindowManager.LayoutParams lp = this.getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
this.getWindow().setAttributes(lp);
setContentView(R.layout.activity_android_p_demo_layout);
}
可以看出LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES已经允许全屏app使用刘海屏了,只不过状态栏那边是白色
//设置页面延伸到刘海区显示
WindowManager.LayoutParams lp = mAc.getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(lp);
//使内容出现在status bar后边,如果要使用全屏的话再加上View.SYSTEM_UI_FLAG_FULLSCREEN
requestWindowFeature(Window.FEATURE_NO_TITLE);
View decorView = mAc.getWindow().getDecorView();
int systemUiVisibility = decorView.getSystemUiVisibility();
int flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN;
systemUiVisibility |= flags;
mAc.getWindow().getDecorView().setSystemUiVisibility(systemUiVisibility);
WindowInsets.getDisplayCutout() 来获得 DisplayCutout object,里面包含了几个有用的方法:
getBoundingRects():获取刘海 / Cutout 所在的矩形区域的位置,多个刘海则返回多个区域(单位:像素)。
getSafeInsetLeft() / getSafeInsetTop() / getSafeInsetRight() / getSafeInsetBottom() :返回安全区上下左右的偏移值(单位:像素)
套用小米的处理话来说
处理好同一页面,进入与退出全屏模式(fullscreen mode)的过渡
因为在默认模式 / LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 下,系统针对全屏与非全屏的页面,耳朵区的显示逻辑不一样。如果开发者没有处理好,容易出现页面可用区域跳变的问题。针对这种页面,我们建议开发者主动声明是否使用耳朵区,以避免跳变。
我们总结一下:
if (context instanceof AppCompatActivity) {
AppCompatActivity app = (AppCompatActivity) context;
if (app.getSupportActionBar() != null) {
app.getSupportActionBar().hide();
}
//LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
// 只有当DisplayCutout完全包含在系统状态栏中时,才允许窗口延伸到DisplayCutout区域显示。
//LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
//该窗口决不允许与DisplayCutout区域重叠,但是会把状态栏变成黑色,效果很差,建议这种情况使用DEFAULT。
//LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
//该窗口始终允许延伸到屏幕短边上的DisplayCutout区域。
//PS:如果需要应用的布局延伸到刘海区显示,需要设置SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN。
if (isUseImmersiveBars) {//是否使用沉浸式状态栏
app.getWindow().getDecorView().
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); //设置页面全屏显示
} else {
if (isUseCutout) {//是否使用刘海
app.getWindow().getDecorView().
setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
//设置页面全屏显示
} else {
app.getWindow().getDecorView().setSystemUiVisibility(0);
}
}
if (cutoutMode == -1) {//是否做刘海屏适配
cutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
}
WindowManager.LayoutParams lp = app.getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = cutoutMode;
//设置页面延伸到刘海区显示
app.getWindow().setAttributes(lp);
}
小米特殊处理:声明 Maximum Aspect Ratio
Android 标准接口中,支持应用声明其支持的最大屏幕高宽比(maximum aspect ratio)。具体声明如下,其中的 ratio_float 被定义为是高除以宽,以 16:9 为例,ratio_float = 16/9 = 1.778 (18:9则为2.0)。
若开发者没有声明该属性,ratio_float 的默认值为1.86,小于2.0,因此这类应用在全面屏手机上,默认不会全屏显示,屏幕底部会留黑。考虑到将有更多 19.5:9 甚至更长的手机出现,建议开发者声明 Maximum Aspect Ratio ≥ 2.2 或更多。值得一提的是,如果应用的 android:resizeableActivity 已经设置为 true,就不必设置 Maximum Aspect Ratio 了
Android Q
/**
* 小米删除刘海区域
*
* @param context context
*/
public static void clearExtraFlag(Context context) {
int flag = 0x00000100 | 0x00000200 | 0x00000400;
//0x00000100 开启配置
//0x00000200 竖屏配置
//0x00000400 横屏配置
//
if (context instanceof AppCompatActivity) {
AppCompatActivity app = (AppCompatActivity) context;
try {
Method method = Window.class.getMethod("clearExtraFlags",
int.class);
method.invoke(app.getWindow(), flag);
} catch (Exception e) {
Log.i(TAG, "addExtraFlags not found.");
}
}
}
/**
* 小米添加刘海区域
*
* @param context context
*/
public static void addExtraFlag(Context context) {
int flag = 0x00000100 | 0x00000200 | 0x00000400;
if (context instanceof AppCompatActivity) {
AppCompatActivity app = (AppCompatActivity) context;
try {
Method method = Window.class.getMethod("addExtraFlags",
int.class);
method.invoke(app.getWindow(), flag);
} catch (Exception e) {
Log.i(TAG, "addExtraFlags not found.");
}
}
}
/*刘海屏全屏显示FLAG*/
private static final int FLAG_NOTCH_SUPPORT = 0x00010000;
/**
* 设置应用窗口在华为刘海屏手机使用刘海区
*
* @param window 应用页面window对象
*/
public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
if (window == null) {
return;
}
WindowManager.LayoutParams layoutParams = window.getAttributes();
try {
Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
Constructor con = layoutParamsExCls.getConstructor(WindowManager.LayoutParams.class);
Object layoutParamsExObj = con.newInstance(layoutParams);
Method method = layoutParamsExCls.getMethod("addHwFlags", int.class);
method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
} catch (Exception e) {
Log.e(TAG, "other Exception");
}
}
/**
* 设置应用窗口在华为刘海屏手机使不用刘海区
*
* @param window 应用页面window对象
*/
public static void setNotFullScreenWindowLayoutInDisplayCutout(Window window) {
if (window == null) {
return;
}
WindowManager.LayoutParams layoutParams = window.getAttributes();
try {
Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
Constructor con = layoutParamsExCls.getConstructor(WindowManager.LayoutParams.class);
Object layoutParamsExObj = con.newInstance(layoutParams);
Method method = layoutParamsExCls.getMethod("clearHwFlags", int.class);
method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
} catch (Exception e) {
Log.e(TAG, "other Exception");
}
}
刘海屏判断
ANDROID P
if (context instanceof AppCompatActivity) {
AppCompatActivity app = (AppCompatActivity) context;
DisplayCutout cutout = app.getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
if (cutout == null) {
// listener可以无视
// 这里理解为无刘海
if (listener != null) {
listener.isHasCutout(false);
}
if (BuildConfig.DEBUG) {
Log.e(TAG, "cutout==null, is not notch screen");//通过cutout是否为null判断是否刘海屏手机
}
} else {
List rects = cutout.getBoundingRects();
if (rects == null || rects.size() == 0) {
// listener可以无视
// 这里理解为无刘海
listener.isHasCutout(false);
} else {
listener.isHasCutout(true);
// listener可以无视
// 这里理解为有刘海
//如需用到cutout信息,则使用
if (listener instanceof OnCutoutDetailListener) {
((OnCutoutDetailListener) listener).onCutout(cutout);
}
}
}
}
Android Q
各家方法判断
/**
* 判断用户是否开启了隐藏刘海区域
*
* @param context
* @return
*/
public static boolean isHideNotchScreen4Xiaomi(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return Settings.Global.getInt(context.getContentResolver(), "force_black", 0) == 1;
}else{
return false;
}
}
/**
* 判断小米是否有刘海屏
*
* @return 是否有刘海屏
*/
public static boolean hasNotchInScreenAtXiaomi() {
return SystemProperties.getint("ro.miui.notch", 0) == 1;
}
/**
* 华为手机是否有刘海屏
*
* @param context context
* @return 是否有刘海屏
*/
public static boolean hasNotchInScreenAtHuawei(Context context) {
boolean ret = false;
try {
ClassLoader cl = context.getClassLoader();
Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
ret = (boolean) get.invoke(HwNotchSizeUtil);
} catch (ClassNotFoundException e) {
Log.e(TAG, "hasNotchInScreen ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e(TAG, "hasNotchInScreen NoSuchMethodException");
} catch (Exception e) {
Log.e(TAG, "hasNotchInScreen Exception");
}
return ret;
}
/**
* 判断是否有刘海屏
*
* @param context context
* @return 是否有刘海屏
*/
private static final int NOTCH_IN_SCREEN_VOIO = 0x00000020;//是否有凹槽
public static boolean hasNotchInScreenAtVoio(Context context) {
boolean ret = false;
try {
ClassLoader cl = context.getClassLoader();
Class FtFeature = cl.loadClass("android.util.FtFeature");
Method get = FtFeature.getMethod("isFeatureSupport", int.class);
ret = (boolean) get.invoke(FtFeature, NOTCH_IN_SCREEN_VOIO);
} catch (ClassNotFoundException e) {
Log.e(TAG, "hasNotchInScreen ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e(TAG, "hasNotchInScreen NoSuchMethodException");
} catch (Exception e) {
Log.e(TAG, "hasNotchInScreen Exception");
} finally {
return ret;
}
}
/**
* 判断oppo是否有刘海屏
*
* @param context context
* @return 是否有刘海屏
*/
public static boolean hasNotchInScreenAtOppo(Context context) {
return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}
获取刘海高度
/**
* 获得小米刘海屏幕高度
*
* @param context context
* @return 小米屏幕高度
*/
public static int getNotchXiaomiHeight(Context context) {
int result = 0;
int resourceId = context.getResources().getIdentifier("notch_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
/**
* 获得小米刘海屏幕宽度
*
* @param context context
* @return 小米屏幕宽度
*/
public static int getNotchXiaomiWidth(Context context) {
int result = 0;
int resourceId = context.getResources().getIdentifier("notch_width", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
/**
* 获取华为刘海屏的刘海尺寸
*
* @param context context
* @return 刘海尺寸
*/
public static int[] getNotchSize4Huawei(Context context) {
int[] ret = new int[]{0, 0};
try {
ClassLoader cl = context.getClassLoader();
Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
Method get = HwNotchSizeUtil.getMethod("getNotchSize");
ret = (int[]) get.invoke(HwNotchSizeUtil);
} catch (ClassNotFoundException e) {
Log.e(TAG, "getNotchSize ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e(TAG, "getNotchSize NoSuchMethodException");
} catch (Exception e) {
Log.e(TAG, "getNotchSize Exception");
}
return ret;
}
状态栏(沉浸式两者:一种设置状态栏颜色或者透明,一种隐藏状态栏,因为全屏后刘海位置出现黑色所以才会刘海屏适配)
状态栏高度比刘海高 。所以如果需要移动,就移动状态栏高度。
关于状态栏介绍:https://www.jianshu.com/p/752f4551e134
/**
* 获取状态栏高度
*
* @param activity
* @return
*/
public static int getStatusBarHeight(Activity activity) {
int statusBarHeight = 0;
int resourceId = activity.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = activity.getResources().getDimensionPixelSize(resourceId);
}
return statusBarHeight;
}
/**
* 修改状态栏为全透明
*
* @param activity
*/
@TargetApi(19)
public static void transparencyBar(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = activity.getWindow();
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Window window = activity.getWindow();
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
}
/**
* 回复状态栏
* PS:如果一开始有沉浸式状态栏,然后再恢复初始化,那么状态栏会变成黑色背景
* 如用到此方法,可以做延迟操作改变自己想要的状态栏颜色。
*
* @param activity
*/
@TargetApi(19)
public static void restoreBar(Activity activity, int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = activity.getWindow();
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.getDecorView().setSystemUiVisibility(0);
window.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Window window = activity.getWindow();
window.setFlags(0,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
setStatusBarColor(activity, color);
}
/**
* 修改状态栏颜色,支持4.4以上版本
*
* @param activity
* @param colorId
*/
public static void setStatusBarColor(Activity activity, int colorId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = activity.getWindow();
// window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(activity.getResources().getColor(colorId));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
//使用SystemBarTint库使4.4版本状态栏变色,需要先将状态栏设置为透明
transparencyBar(activity);
SystemBarTintManager tintManager = new SystemBarTintManager(activity);
tintManager.setStatusBarTintEnabled(true);
tintManager.setStatusBarTintResource(colorId);
}
}
public static void setStatusBarColor(Activity activity, int colorId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = activity.getWindow();
// window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(activity.getResources().getColor(colorId));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
//使用SystemBarTint库使4.4版本状态栏变色,需要先将状态栏设置为透明
transparencyBar(activity);
SystemBarTintManager tintManager = new SystemBarTintManager(activity);
tintManager.setStatusBarTintEnabled(true);
tintManager.setStatusBarTintResource(colorId);
}
}
状态栏字色和图标浅黑色
/**
* 状态栏亮色模式,设置状态栏黑色文字、图标,
* 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android
*
* @param activity
* @return 1:MIUUI 2:Flyme 3:android6.0
*/
public static int statusBarLightMode(Activity activity) {
return statusBarLightMode(activity.getWindow());
}
/**
* 状态栏亮色模式,设置状态栏黑色文字、图标,
* 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android
*
* @return 1:MIUUI 2:Flyme 3:android6.0
*/
public static int statusBarLightMode(Window window) {
int result = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (MIUISetStatusBarLightMode(window, true)) {
result = 1;
} else if (FlymeSetStatusBarLightMode(window, true)) {
result = 2;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
result = 3;
}
}
return result;
}
/**
* 状态栏亮色模式,设置状态栏黑色文字、图标,
* 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android
*
* @param activity
* @return 1:MIUUI 2:Flyme 3:android6.0
*/
public static int statusBarDarkMode(Activity activity) {
int result = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (MIUISetStatusBarLightMode(activity.getWindow(), false)) {
result = 1;
} else if (FlymeSetStatusBarLightMode(activity.getWindow(), false)) {
result = 2;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.getWindow().getDecorView().
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
result = 3;
}
}
return result;
}
/**
* 设置状态栏图标为深色和魅族特定的文字风格
* 可以用来判断是否为Flyme用户
*
* @param window 需要设置的窗口
* @param dark 是否把状态栏文字及图标颜色设置为深色
* @return boolean 成功执行返回true
*/
public static boolean FlymeSetStatusBarLightMode(Window window, boolean dark) {
boolean result = false;
if (window != null) {
try {
WindowManager.LayoutParams lp = window.getAttributes();
Field darkFlag = WindowManager.LayoutParams.class
.getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
Field meizuFlags = WindowManager.LayoutParams.class
.getDeclaredField("meizuFlags");
darkFlag.setAccessible(true);
meizuFlags.setAccessible(true);
int bit = darkFlag.getInt(null);
int value = meizuFlags.getInt(lp);
if (dark) {
value |= bit;
} else {
value &= ~bit;
}
meizuFlags.setInt(lp, value);
window.setAttributes(lp);
result = true;
} catch (Exception e) {
}
}
return result;
}
/**
* 需要MIUIV6以上
*
* @param dark 是否把状态栏文字及图标颜色设置为深色
* @return boolean 成功执行返回true
*/
public static boolean MIUISetStatusBarLightMode(Window window, boolean dark) {
boolean result = false;
if (window != null) {
Class clazz = window.getClass();
try {
int darkModeFlag = 0;
Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
darkModeFlag = field.getInt(layoutParams);
Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
if (dark) {
extraFlagField.invoke(window, darkModeFlag, darkModeFlag);//状态栏透明且黑色字体
} else {
extraFlagField.invoke(window, 0, darkModeFlag);//清除黑色字体
}
result = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//开发版 7.7.13 及以后版本采用了系统API,旧方法无效但不会报错,所以两个方式都要加上
//SYSTEM_UI_FLAG_VISIBLE这个表示,不占据全屏,把状态栏会空出来,
if (dark) {
window.getDecorView().
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
} else {
window.getDecorView().
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
}
} catch (Exception e) {
}
}
return result;
}
华为:https://devcenter-test.huawei.com/consumer/cn/devservice/doc/50114
小米:https://dev.mi.com/console/doc/detail?pId=1293
Oppo:https://open.oppomobile.com/service/message/detail?id=61876
Vivo:https://dev.vivo.com.cn/documentCenter/doc/103
系统OS判断
参考https://www.jianshu.com/p/ba9347a5a05a
public class OSUtil {
private static final String TAG = "Rom";
public static final String ROM_MIUI = "MIUI";
public static final String ROM_EMUI = "EMUI";
public static final String ROM_FLYME = "FLYME";
public static final String ROM_OPPO = "OPPO";
public static final String ROM_SMARTISAN = "SMARTISAN";
public static final String ROM_VIVO = "VIVO";
public static final String ROM_QIKU = "QIKU";
private static final String KEY_VERSION_MIUI = "ro.miui.ui.version.name";
private static final String KEY_VERSION_EMUI = "ro.build.version.emui";
private static final String KEY_VERSION_OPPO = "ro.build.version.opporom";
private static final String KEY_VERSION_SMARTISAN = "ro.smartisan.version";
private static final String KEY_VERSION_VIVO = "ro.vivo.os.version";
private static String sName;
private static String sVersion;
public static boolean isEmui() {
return check(ROM_EMUI);
}
public static boolean isMiui() {
return check(ROM_MIUI);
}
public static boolean isVivo() {
return check(ROM_VIVO);
}
public static boolean isOppo() {
return check(ROM_OPPO);
}
public static boolean isFlyme() {
return check(ROM_FLYME);
}
public static boolean is360() {
return check(ROM_QIKU) || check("360");
}
public static boolean isSmartisan() {
return check(ROM_SMARTISAN);
}
public static String getName() {
if (sName == null) {
check("");
}
return sName;
}
public static String getVersion() {
if (sVersion == null) {
check("");
}
return sVersion;
}
public static boolean check(String rom) {
if (sName != null) {
return sName.equals(rom);
}
if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_MIUI))) {
sName = ROM_MIUI;
} else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_EMUI))) {
sName = ROM_EMUI;
} else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_OPPO))) {
sName = ROM_OPPO;
} else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_VIVO))) {
sName = ROM_VIVO;
} else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_SMARTISAN))) {
sName = ROM_SMARTISAN;
} else {
sVersion = Build.DISPLAY;
if (sVersion.toUpperCase().contains(ROM_FLYME)) {
sName = ROM_FLYME;
} else {
sVersion = Build.UNKNOWN;
sName = Build.MANUFACTURER.toUpperCase();
}
}
return sName.equals(rom);
}
public static String getProp(String name) {
String line = null;
BufferedReader input = null;
try {
Process p = Runtime.getRuntime().exec("getprop " + name);
input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);
line = input.readLine();
input.close();
} catch (IOException ex) {
Log.e(TAG, "Unable to read prop " + name, ex);
return null;
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return line;
}
}