屏幕适配

部分内容转载
作者:Alan_兰哥
链接:https://www.jianshu.com/p/0d61f9dffb14

屏幕尺寸、屏幕分辨率、屏幕像素密度

屏幕尺寸:屏幕对角线长度,单位是英寸,我们常说的多少多少寸,比如4.7存手机、5.7存手机,指的就是这个。
屏幕分辨率:如 1920×1080,是指在手机屏幕的像素点的个数,单位是px,1px = 1 像素点,一般是纵向像素 × 横向像素,意味着高有 1920 个像素点,宽有 1080 个像素点。
屏幕像素密度:是指每英寸上的像素点数,单位是 dpi(dotper inch)。像素密度和屏幕尺寸和屏幕分辨率有关,它是由对角线的像素点数除以屏幕的大小得到的,关系如下


pingmu0.png

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:就是我们常说的像素


pingmu1.png

pingmu2.png

屏幕分辨率限定符(不推荐)

屏幕分辨率限定符适配需要在 res 文件夹下创建各种屏幕分辨率对应的 values-xxx 文件夹,如下图:


pingmu3.png

然后根据一个基准分辨率,例如基准分辨率为 1280x720,将宽度分成 720 份,取值为 1px~720px,将高度分成 1280 份,取值为 1px~1280px,生成各种分辨率对应的 dimens.xml 文件。如下分别为分辨率 1280x720 与 1920x1080 所对应的横向dimens.xml 文件:


pingmu4.PNG

假设设计图上的一个控件的宽度为 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 文件:


pingmu5.PNG

可以通过ScreenMatch生成

需要自己安装ScreenMatch插件,第一次生成,会生成两个文件
将screenMatch_example_dimens拷贝到value目录,并且重命名成dimens;
screenMatch.properties文件中我们可以设置匹配哪些宽带的屏幕、忽略哪些以及匹配 哪个项目等等。
然后我再做一次生成操作就可以生成了各种swXXXdp目录了
最后使用android:layout_width="@dimen/x720"


pingmu6.PNG

pingmu7.PNG

pingmu8.PNG

平板适配

新建layout-sw720dp目录,里面添加layout布局文件。 使用平板打开APP自动切换。

也可以使用最小宽度符以及下面的适配 。

常见适配方案

布局适配

  1. 避免写死控件尺寸,使用wrap_content, match_parent
  2. LinearLayout xxx:weight="0.5“
  3. RelativeLayout xxx:layout_centerInParent="true" ...
  4. ContraintLayout xxx:layout_constraintLeft_toLeftOf="parent"...
    相对定位,可以设置偏移量0-1百分比
    角度定位
    边距margin分显隐性,就是不显示的时候也存在
    可以设置宽高比,横向纵向线可以设置百分比,可以设置占位view(不绘制),
    组成链式可以设置权重和链式样式,
    Barrier设置屏障,约束关联view一侧效果
    Group,一起控制显隐性
    Optimizer对二次测量进行优化
  5. Percent-support-lib xxx:layout_widthPercent="30%" ...宽占父布局宽度多少,
    LayoutParams中属性的获取
    onMeasure中,改变params.width为百分比计算结果,测量
    如果测量值过小且设置的w/h是wrap_content,重新测量

图片资源适配

  1. .9图或者SVG图实现缩放
  2. 备用位图匹配不同分辨率

动态运行时加载

(关于小分辨率手机看到比大分辨率手机大,跟像素点大小有关,可以参照权重处理,
其实权重比也有问题,就是屏幕分辩率带来的问题,不同屏幕其分辨率比值是不一样的,导致大分辨率上是长方形 ,小分辨率上是正方形)

自定义 分辨率布局

设计稿有标准尺寸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

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="PercentLayout">
        <attr name="widthPercent" format="fraction" />
        <attr name="heightPercent" format="fraction" />
        <attr name="marginLeftPercent" format="fraction" />
        <attr name="marginRightPercent" format="fraction" />
        <attr name="marginTopPercent" format="fraction" />
        <attr name="marginBottomPercent" format="fraction" />
    </declare-styleable>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<com.dn_alan.myapplication.percent.PercentLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#f00"
        app:heightPercent="5%"
        app:widthPercent="90%"
        app:marginLeftPercent="30%"
        app:marginRightPercent="30%"
        app:marginTopPercent="1%"
        tools:ignore="MissingPrefix" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/textView"
        android:background="#f00"
        app:heightPercent="30%"
        app:marginBottomPercent="30%"
        app:marginLeftPercent="30%"
        app:marginRightPercent="30%"
        app:marginTopPercent="30%"
        app:widthPercent="30%"
        tools:ignore="MissingPrefix" />


</com.dn_alan.myapplication.percent.PercentLayout>
像素密度布局(像素密度包含长宽,所以和之前分辨率布局类似但是也有不一样的地方)

重点就是改变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默认的适配刘海屏策略是这样的

  1. 如果应用未适配刘海屏,需要系统对全屏显示的应用界面做特殊移动处理(竖屏下移处理,横屏右移处),因此此处出现了黑边的现象;如果应用页面布局不能做到自适应,就会出现布局问题;如果应用布局能够做到自适应,也会有黑边无法全屏显示的体验问题
    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);
}

从图中可以看出,这个并不是完美的适配方案


pingmu11.png

第二种样式样式: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);
}
pingmu12.png

可以看出显示效果和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使用刘海屏了,只不过状态栏那边是白色


pingmu13.png
//设置页面延伸到刘海区显示
        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)。

<application>
    <meta-data android:name="android.max_aspect" android:value="ratio_float" />
</application>

若开发者没有声明该属性,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 横屏配置
        //<meta-data
        // android:name="notch.config"
        // android:value="portrait|landscape"/>
        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<Rect> 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;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,812评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,626评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,144评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,052评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,925评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,035评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,461评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,150评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,413评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,501评论 2 307
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,277评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,159评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,528评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,868评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,143评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,407评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,615评论 2 335

推荐阅读更多精彩内容