学更好的别人,
做更好的自己。
——《微卡智享》
本文长度为5289字,预计阅读8分钟
前言
Vivo在4月11号发布的X Fold折叠屏手机,也是抢了好几周好总算拿到手了,既然已经有了折叠屏手机,做为一个开发者,当然也要研究下折叠屏的开发,本篇就先简单介绍一下折叠屏的开发及通过传感器来获取到铰链的折叠角度,针对折叠屏的适配,Android官方推出了Jetpack WindowManager,下一篇会专门介绍WindowManager的折叠屏适配开发。
实现效果
折叠屏开发

微卡智享
2018年8月Google发布Android 9.0,首次支持折叠屏功能,并推出了Android官方折叠屏适配指南。主要从以下几个方面进行适配:
-
应用连续性:处理配置变更
-
屏幕兼容性:resizeableActivity 与 maxAspectRatio
-
多窗口适配:多项恢复与专属资源访问
01
关于应用连续性
针对应用连续性,最核心的两点就是配置界面状态和处理配置变更。
配置界面状态,现在的Android APP开发只要用到了ViewModel,其实就不是问题,Activity重建时,系统会把上次销毁的Activity的内部ViewModelStore中的所有ViewModel传给新的Activity的ViewModelStore,新的Activity中可以从ViewModelStore中获取之前的ViewModel,流程图如下:
上图中可以看出Activity最终finished的时,会通过ViewModelStore的onCleared清空当前的ViewModel,当然横竖屏切换这种情况Activity在finished志之前已经将ViewModel传递给新的Activity的ViewModelStore中了,不会有问题。
而自行配置处理变更时,如果应用在特定配置变更期间无需更新资源,并且因性能限制您需要尽量避免 Activity 重启,则可声明 Activity 自行处理配置变更,从而阻止系统重启 Activity,例如文件代码所声明的 Activity 可同时处理屏幕方向变更和键盘可用性变更:
<activity android:name=".MyActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name">
现在,即便其中某个配置发生变化,MyActivity 也不会重启。但 MyActivity 会接收到对 onConfigurationChanged() 的调用消息。此方法会收到传递的 Configuration 对象,从而指定新设备配置。您可以通过读取 Configuration 中的字段确定新配置,然后通过更新界面所用资源进行适当的更改。调用此方法时,Activity 的 Resources 对象会相应地进行更新,并根据新配置返回资源,以便您在系统不重启 Activity 的情况下轻松重置界面元素。
通过onConfigurationChanged() 实现用于检查当前的设备方向
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// Checks the orientation of the screen
if (newConfig.orientation === Configuration.ORIENTATION_LANDSCAPE) {
Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show()
} else if (newConfig.orientation === Configuration.ORIENTATION_PORTRAIT) {
Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show()
}
}
02
resizeableActivity 与 maxAspectRatio
resizeableActivity可以让应用大小可调,设置 resizeableActivity=true,这个在Android的7.0默认为true了,这可以为应用可能遇到的任何设备类型和环境(例如可折叠设备、桌面模式或自由窗口)提供最大兼容性。请在分屏模式下,或使用可折叠模拟器测试应用行为。
如果应用设置 resizeableActivity=false,则会告知平台其不支持多窗口模式。系统可能仍会调整应用的大小或将其置于多窗口模式;但要实现兼容性,便需要对应用中的所有组件(包括应用的所有 Activity、Service 等)应用同一配置。在某些情况下,重大变更(例如,显示屏尺寸更改)可能会重启进程,而不会更改配置。
例如,以下 Activity 设置了 resizableActivity=false 以及 maxAspectRatio。设备展开时,系统会将应用置于兼容模式,以此保持 Activity 配置、大小和宽高比。
如果未设置 resizeableActivity,或将其设置为 true,系统会假定该应用完全支持多窗口并且可调整大小。
微卡智享
获取铰链角度
关于折叠屏,在Android Q 版本已经开始官方支持,本文仅针对Android 11版本上折叠屏的一些扩展支持做简单的介绍。
根据官网提供的Android 11的新特性介绍文档中,对折叠屏的支持和适配指导,有如下一段文字描述:
使用 Android 11,可以通过以下方法使运行在采用合页式屏幕配置的设备上的应用能够确定合页角度:
提供具有 TYPE_HINGE_ANGLE 的新传感器,以及新的 SensorEvent,
后者可以监控合页角度,并提供设备的两部分之间的角度测量值。
您可以使用这些原始测量值在用户操作设备时执行精细的动画显示。
尽管对于某些类型的应用(例如启动器和壁纸)而言,
知道确切的合页角度会很有用,但大多数应用都应该使用 Jetpack 窗口管理器库,
通过调用 DeviceState.getPosture() 检索设备状态。
或者,您的应用也可以调用 registerDeviceStateChangeCallback(),
以在 DeviceState 更改时收到通知,并在状态发生变化时做出响应。
当然在新的版本中DeviceState已经不再是公开的API了,下一篇会说到,今天我们主要就是看铰链的折叠角度,TYPE_HINGE_ANGLE 为 Android 11新增的传感器类型,专门用于处理折叠屏两部分屏幕之间的的角度相关。
代码展示

微卡智享
整个代码我们直接用上一篇《Android MVI架构初探》的Demo,因为直接用的ViewModel,在屏幕Destory和Create中数据是一直保持的状态。
在MainActivity中加入了SensorManager和Sensor的定义,并写了SensorEventListener的监听事件
通过onResume和onPause来注册和取消监听。
在onCreate中获取到SensorManager和Sensor,其中Sensor记得是要改为Sensor.TYPE_HINGE_ANGLE。
package pers.vaccae.mvidemo.ui.view
import android.content.Context
import android.content.res.Configuration
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import pers.vaccae.mvidemo.R
import pers.vaccae.mvidemo.bean.CDrugs
import pers.vaccae.mvidemo.ui.adapter.DrugsAdapter
import pers.vaccae.mvidemo.ui.intent.ActionIntent
import pers.vaccae.mvidemo.ui.intent.ActionState
import pers.vaccae.mvidemo.ui.viewmodel.MainViewModel
class MainActivity : AppCompatActivity() {
private val TAG = "X Fold"
private val recyclerView: RecyclerView by lazy { findViewById(R.id.recycler_view) }
private val btncreate: Button by lazy { findViewById(R.id.btncreate) }
private val btnadd: Button by lazy { findViewById(R.id.btnadd) }
private val btndel: Button by lazy { findViewById(R.id.btndel) }
private lateinit var mainViewModel: MainViewModel
private lateinit var drugsAdapter: DrugsAdapter
//adapter的位置
private var adapterpos = -1
private var mSensorManager: SensorManager? = null
private var mSensor: Sensor? = null
private val mSensorEventListener = object : SensorEventListener {
override fun onSensorChanged(p0: SensorEvent?) {
//p0.values[0]: 测量的铰链角度,其值范围在0到360度之间
p0?.let {
Log.i(TAG, "当前铰链角度为:${it.values[0]}")
}
}
// 当传感器精度发生改变时回调该方法
override fun onAccuracyChanged(p0: Sensor?, p1: Int) {
p0?.let {
Log.i(TAG, "Sensor:${it.name}, value:$p1")
}
}
}
override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "onDestroy")
}
override fun onResume() {
super.onResume()
Log.i(TAG, "onResume")
//开启监听
mSensorManager?.let {
it.registerListener(mSensorEventListener, mSensor!!,
SensorManager.SENSOR_DELAY_NORMAL)
}
}
override fun onPause() {
super.onPause()
Log.i(TAG, "onPause")
// 取消监听
mSensorManager?.let {
it.unregisterListener(mSensorEventListener);
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.i(TAG, "onCreate")
// 获取传感器管理对象
mSensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
// 获取传感器的类型(TYPE_HINGE_ANGLE:铰链角度传感器)
mSensorManager?.let {
mSensor = it.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE);
}
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
drugsAdapter = DrugsAdapter(R.layout.rcl_item, mainViewModel.listDrugs)
drugsAdapter.setOnItemClickListener { baseQuickAdapter, view, i ->
adapterpos = i
}
val gridLayoutManager = GridLayoutManager(this, 3)
recyclerView.layoutManager = gridLayoutManager
recyclerView.adapter = drugsAdapter
//初始化ViewModel监听
observeViewModel()
btncreate.setOnClickListener {
lifecycleScope.launch {
mainViewModel.actionIntent.send(ActionIntent.LoadDrugs)
}
}
btnadd.setOnClickListener {
lifecycleScope.launch {
mainViewModel.actionIntent.send(ActionIntent.InsDrugs)
}
}
btndel.setOnClickListener {
lifecycleScope.launch {
Log.i("status", "$adapterpos")
val item = try {
drugsAdapter.getItem(adapterpos)
} catch (e: Exception) {
CDrugs()
}
mainViewModel.actionIntent.send(ActionIntent.DelDrugs(adapterpos, item))
}
}
}
private fun observeViewModel() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mainViewModel.state.collect {
when (it) {
is ActionState.Normal -> {
btncreate.isEnabled = true
btnadd.isEnabled = true
btndel.isEnabled = true
}
is ActionState.Loading -> {
btncreate.isEnabled = false
btncreate.isEnabled = false
btncreate.isEnabled = false
}
is ActionState.Drugs -> {
drugsAdapter.setList(it.drugs)
// drugsAdapter.setNewInstance(it.drugs)
}
is ActionState.Error -> {
Toast.makeText(this@MainActivity, it.msg, Toast.LENGTH_SHORT).show()
}
}
}
}
}
}
}
上面是MainActivity的全部代码,等下一章都完成后整个Demo会更新到Github和Gitee上,这样通过传感器获取折叠屏折叠角度的代码就完成了。
参考文章
https://developer.android.google.cn/guide/topics/ui/foldables?hl=zh-cn
https://blog.csdn.net/vitaviva/article/details/105246639
https://juejin.cn/post/7049705037525680164
https://blog.csdn.net/luzhenrong45/article/details/109632140
完
往期精彩回顾