新增判断折叠屏方法

参考相关:

手机是否是折叠屏设备

通过手机型号判断

private static boolean isHuaweiFold(Context context) {
        if (!DeviceTypeUtils.INSTANCE.isHuaweiPhone()) {
            return false;
        }

        return context.getPackageManager().hasSystemFeature("com.huawei.hardware.sensor.posture");
    }

    private static boolean isOppoFoldPhone(Context context) throws Throwable {
        boolean isFold = false;
        boolean isVerticalFold = false;
        Class<?> cls = Class.forName("com.oplus.content.OplusFeatureConfigManager");

        Method instance = cls.getMethod("getInstance");
        Object configManager = instance.invoke(null);

        Method hasFeature = cls.getDeclaredMethod("hasFeature", String.class);
        Object object = hasFeature.invoke(configManager, FEATURE_FOLD);
        if (object instanceof Boolean) {
            isFold = (boolean) object;
        }

        Object object2 = hasFeature.invoke(configManager, FEATURE_VERTICAL_FOLD);
        if (object2 instanceof Boolean) {
            isVerticalFold = (boolean) object2;
        }

        // 是横向折叠屏的同时,不是竖向折叠屏
        return isFold && !isVerticalFold;
    }

通过设备属性判断

这里的判断,首次是不准的,请做存储

WindowManagerConfig.Companion.getInstance().isUnFoldStatus()

具体代码:

package com.haohua.demo

import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import com.yunzhijia.logsdk.YZJLog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

class WindowManagerConfig private constructor(){

    var uiStatus: MutableLiveData<FoldState> = MutableLiveData<FoldState>()

    private object SingletonHolder {
        val instance = WindowManagerConfig()
    }

    companion object {
        val instance = SingletonHolder.instance
    }

    // 是否折叠状态,正常手机状态
    fun isNormalPhoneStatus(): Boolean {
        return uiStatus.value == FoldState.NORMAL_PHONE
    }

    fun isUnFoldStatus(): Boolean {
        return !isNormalPhoneStatus()
//        return uiStatus.value == FoldState.FLAT || uiStatus.value == FoldState.HALF_OPENED
    }

    fun register(activity: AppCompatActivity) {
        activity.lifecycleScope.launch(Dispatchers.Main) {
            activity.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED)
            {
                WindowInfoTracker.getOrCreate(activity)
                    .windowLayoutInfo(activity)
                    .collect { newLayoutInfo ->
                        uiStatus.value = getCurrentFoldState(newLayoutInfo)
                        YZJLog.d("im-fold", "折叠屏状态变化 = ${uiStatus.value}, act = ${activity.javaClass.simpleName}")
                    }
            }
        }
    }

    private fun getCurrentFoldState(layoutInfo: WindowLayoutInfo): FoldState {
        for (displayFeature in layoutInfo.displayFeatures) {

            val foldFeature = displayFeature as? FoldingFeature

            foldFeature?.let {
                return when (it.state) {
                    FoldingFeature.State.FLAT -> {
                        FoldState.FLAT
                    }

                    FoldingFeature.State.HALF_OPENED -> {
                        FoldState.HALF_OPENED
                    }

                    else -> {
                        FoldState.NORMAL_PHONE
                    }
                }
            }
        }
        return FoldState.NORMAL_PHONE

    }

    class FoldState private constructor(private val description: String) {

        override fun toString(): String {
            return description
        }

        public companion object {

            @JvmField
            val FLAT: FoldState = FoldState("FLAT")

            @JvmField
            val HALF_OPENED: FoldState = FoldState("HALF_OPENED")

            @JvmField
            val NORMAL_PHONE: FoldState = FoldState("NORMAL_PHONE")
        }
    }

}

参考文案:

折叠屏资料
target api 升级到29的官方改动
https://developer.android.com/about/versions/10/behavior-changes-10?hl=zh-cn#foldables

折叠屏适配介绍文档
https://developer.android.com/guide/topics/ui/foldables?hl=zh-cn

集成文档 WindowManager
https://developer.android.com/jetpack/androidx/releases/window?hl=zh-cn#groovy

识别折叠屏状态的参数说明
https://developer.android.com/reference/kotlin/androidx/window/layout/FoldingFeature

官方提供的折叠屏的demo
https://github.com/android/user-interface-samples/tree/main/WindowManager

继承折叠屏的步骤说明
https://developer.android.com/codelabs/android-window-manager-dual-screen-foldables?hl=zh-cn#6

三星适配指南:
http://samsung.smarterapps.cn/index.php?app=home&mod=Public&act=help&type=11&id=92

Android 折叠屏技术发展与适配
https://blog.51cto.com/u_15375308/5071743

以下为老办法,不建议使用

获取手机是否是折叠屏

1、通过系统参数进行判断,是否是折叠屏手机

public static boolean isFoldDisplay(Context context) {
final String KEY = “config_lidControlsDisplayFold”;
int id = context.getResources().getIdentifier(KEY, “bool”, “android”);
if (id > 0) {
return context.getResources().getBoolean(id);
}
return false;
}

2、系统数据,进行判断

/**

  • 系统方法判断,做兼容处理

  • @param context
  • @return
    */
    public static boolean isFoldUIConfig(Context context) {
    try {
    // 系统方法不能兼容所有折叠屏手机,补偿,通过status进行判断
    UIConfig.Status status = ResponsiveUIConfig.getDefault(context).getUiStatus().getValue();
    // 由于正常手机和折叠状态,status值都是fold,所以,我们得以是否是折叠屏作为判断标准
    return status == UIConfig.Status.UNFOLD || status == UIConfig.Status.UNFOLDING;
    // 如果为true,存储起来,但是这个地方,无法调用到存储代码,心累,存储代码写在上层
    } catch (Exception e) {
    YZJLog.e(e.getMessage());
    return false;
    }
    }

3、如果是折叠屏,存到sp里面,下次获取该数据

代码略。

判断代码(仅参考):
public static boolean isRealFoldPhone(Context context) {
// foldPhone 如果有值,直接返回,每次只读取一次配置,不需要反复读取
if (isRealFoldPhone != null && isRealFoldPhone) {
return isRealFoldPhone;
}
if (context == null) {
return false;
}

// 1、通过系统参数进行判断,是否是折叠屏手机
isRealFoldPhone = isFoldDisplay(context);
if (!isRealFoldPhone) {
    boolean isFoldUiConfig = isFoldUIConfig(context);
    if (isFoldUiConfig) {
        isRealFoldPhone = true;
    } else {
        return false;
    }

}
YZJLog.d("im-fold", "是否是折叠屏:" + isRealFoldPhone);

// 3、status补偿也不能完全确定,在折叠的情况下,获取到的值是异常的,最后在sp里面,看是否有记录
if (isRealFoldPhone) {
    // 是折叠屏手机,先记录下来,后续使用,每次进记录一次
    if (FoldUIConfigUtils.isRealFoldPhone == null) {
        RouterManager.AppFold.getAppFoldService().setFoldPhone(true);
    }
    FoldUIConfigUtils.isFoldPhone = true;
} else {
    // 非折叠屏手机,并且未赋值,从sp里面读值,可以直接拦截,不调用底层方法,暂时需要考虑默认配置,不处理
    if (FoldUIConfigUtils.isFoldPhone == null) {
        FoldUIConfigUtils.isFoldPhone = RouterManager.AppFold.getAppFoldService().isFoldPhone(context instanceof Activity ? (Activity) context : null);
    }
}
return FoldUIConfigUtils.isFoldPhone;

}

判断是否是pad手机

1、区分出平板设备

ps:折叠屏默认是平板,也就是说,设备里面,是没有平板和折叠屏区分的,折叠屏是在平板的基础上,衍生而来的。

public static void initPadParam(Context context) {
        if (padFlag == 1 || padFlag == 2 || context == null) {
            return;
        }
        boolean ret = (context.getResources().getConfiguration().screenLayout
                & Configuration.SCREENLAYOUT_SIZE_MASK)
                >= Configuration.SCREENLAYOUT_SIZE_LARGE;
        padFlag = ret ? 1 : 2;
    }

2、oppo手机专门判断

//       平板 FEATURE_TABLET oplus.hardware.type.tablet
//       折叠 FEATURE_FOLD oplus.hardware.type.fold
        public static boolean isTablet(Context context) {
            if (BuildConfig.DEBUG) {
                YZJLog.d("im-device", "is pad = " + (context.getPackageManager().hasSystemFeature(FEATURE_TABLET)));
            }
            return context.getPackageManager().hasSystemFeature(FEATURE_TABLET);
        }

        public static boolean isFold(Context context) {
            if (BuildConfig.DEBUG) {
                YZJLog.d("im-device", "is pad = " + (context.getPackageManager().hasSystemFeature(FEATURE_FOLD)));
            }
            return context.getPackageManager().hasSystemFeature(FEATURE_FOLD);
        }

3、其他手机,通过设备宽度判断

//方式2:smallestDeviceWidth
    private static boolean isTableBySmallWidth(Context context) {
        Context applicationContext = context.getApplicationContext();
        if (applicationContext != null && applicationContext.getResources().getConfiguration().smallestScreenWidthDp >= 600) {
            return true;
        }
        return false;
    }

4、其他未验证方法,仅供参考

public static class HuaweiPad {

        // 方式1
//        public static boolean isTablet() {
//            return "tablet".equals(SystemPropertiesEx.get("ro.build.characteristics", ""));
//        }

        //方式2
        public static boolean isTablet(Context context) {
            return context != null && (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE;
        }

        public static Boolean isTablet3() {
//            return SystemProperties.getBoolean("lockscreen.rot_override", false);
//            return SystemProperties.get("lockscreen.rot_override", false);
            Boolean getBoolean = (Boolean) SystemProperties.get("getBoolean", Boolean.class, "lockscreen.rot_override", Boolean.class, false);
            return getBoolean;
        }

// 折叠屏判断
//        IS_FOLDABLE = (!SystemProperties.get("ro.config.hw_fold_disp").isEmpty() || !SystemProperties.get(HwFoldScreenState.DEBUG_HW_FOLD_DISP_PROP).isEmpty());
    }

    public static class SemPad {
//        平板判断:
        //方式1:screenLayout
        public static boolean isTablet(Context context) {
            return (context.getResources().getConfiguration().screenLayout & 15) >= 3;
        }

        //方式2:smallestDeviceWidth
        public static boolean isTablet2(Context context) {
            Context applicationContext = context.getApplicationContext();
            if (applicationContext != null && applicationContext.getResources().getConfiguration().smallestScreenWidthDp >= 600) {
                return true;
            }
            return false;
        }

//        //方式3:ro.build.characteristics
//        private static boolean isTablet() {
//            String str = SemSystemProperties.get("ro.build.characteristics");
//            return str != null && str.contains("tablet");
//        }
    }

手机屏幕状态

多窗口模式

/**
     * 是否是多窗口模式
     * @param activity
     * @return
     */
    public static boolean isInMultiWindow(Activity activity) {
        boolean isInMultiWindowMode = false;
        if (activity != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
            isInMultiWindowMode = activity.isInMultiWindowMode();
        }
        return isInMultiWindowMode;
    }

屏幕方向

/**
     * 获取屏幕方向
     *
     * @param context
     * @return true: 竖屏 false: 横屏
     */
    public static boolean getScreenDirection(Context context) {
        DisplayMetrics d = context.getResources().getDisplayMetrics();
        if (d.heightPixels > d.widthPixels) {
            return true;
        }
        return false;
    }

平行视窗模式判断

String config = context.getResources().getConfiguration().toString();
boolean isInMagicWindow = config.contains("oplus-magic-windows");
context为Activity的context

说明:
部分Activity在平行视界下即使显示状态为全屏,其状态仍处于平行视界状态。

折叠屏适配

方案一:
activity + fragment 方式
方案二:
系统提供的方式
修改AndroidManifest文件
在“AndroidManifest.xml”文件的“application”中新增“meta-data”:

<meta-data android:name="EasyGoClient" android:value="true" />

新增Easygo.json配置文件
在“assets”目录下新建配置文件“easygo.json”,“easygo.json”文件模板和字段详细说明:

{
  "easyGoVersion": "1.0",
  "client": "com.teamtalk.ims",
  "logicEntities": [
    {
      "head": {
        "function": "magicwindow",
        "required": "true"
      },
      "body": {
        "mode": "0",
        "defaultDualActivities": {
          //主页面Activity,可以有多个,分号隔开
展开态时冷启动应用打开此页面时,系统在右屏自动启动relatedPage页面
          "mainPages": "com.kdweibo.android.ui.fragment.HomeMainFragmentActivity",
          "relatedPage": "com.yunzhijia.im.chat.ui.ChatActivity"
        },
        "Activities": [
          {
            "name": "com.kdweibo.android.ui.activity.StartActivity",
            "defaultFullScreen": "true"
          },
          {
            "name": "com.kdweibo.android.ui.fragment.HomeMainFragmentActivity",
            "lockSide": "primary"//Activity锁定方式,当前仅支持锁定在primary侧
primary:锁定在主界面那一侧,锁定后,另一侧启动新的Activity时不会轻易平推窗口过来,除非推过来的窗口也是primary锁定窗口(典型场景:直播购物场景,将直播Activity配置成锁定)。
          }
        ],
        "UX": {
          "supportDraggingToFullScreen": "true",
          "isDraggable": "true",//支持左右大小拖拽
        }
      }
    }
  ]
}

参数说明:

参数

限制

描述

easyGoVersion

1

协议版本,固定值为“1.0”

client

1

应用包名

logicEntities. head. function

1

调用组件名,固定值“magicwindow”

logicEntities. head. required

1

预留字段,固定值“true”

logicEntities.body.mode

1

基础分屏模式0:购物模式,activityPairs 节点不生效1:自定义模式(包括导航栏模式)

logicEntities.body. activityPairs

?

自定义模式参数,配置从 from 页面到 to 页面的分屏展示

logicEntities.body. activityPairs.from

*

触发分屏的源 Activity

logicEntities.body. activityPairs.to

*

触发分屏的目标 Activity,“”表示任意 Activity自定义模式:[{”from” :”com.xxx. ActivityA”, “to” :”com.xxx. ActivityB”}] 表示 A 上启动 B,触发分屏(A 左 B 右)导航栏模式:[{”from” :”com.xxx. ActivityA”, “to” :””}]

logicEntities.body. defaultDualActivities

?

应用冷启动默认打开首页双屏配置

logicEntities.body. defaultDualActivities.mainPages

1

主页面 Activity,可以有多个,分号隔开展开态时冷启动应用打开此页面时,系统在右屏自动启动 relatedPage 页面

logicEntities.body.defaultDualActivities.relatedPage

?

右屏默认展示页面 ActivitymainPages 和 relatedPage 只能配置 1 对,需要具体的 Activity 名,不支持通配符如: [{“mainPages”:“com.xxx.MainActivity”,“relatedPage”:“com.xxx.EmptyActivity”}]

logicEntities.body. transActivities

*

过渡页面列表如[ “com.xxx.ActivityD”,“com.xxx.ActivityE”,“com.xxx.ActivityF”]

logicEntities.body.Activities

?

应用关键 Activity 属性列表

logicEntities.body.Activities.name

1

Activity 组件名

logicEntities.body.Activities.defaultFullScreen

?

Activity 是否支持默认以全屏启动true:支持false:不支持默认为 false

logicEntities.body.Activities.lockSide


Activity 锁定方式,当前仅支持锁定在 primary 侧primary:锁定在主界面那一侧,锁定后,另一侧启动新的 Activity 时不会轻易平推窗口过来,除非推过来的窗口也是 primary 锁定窗口(典型场景:直播购物场景,将直播 Activity 配置成锁定)。