1. Android 13屏幕方向动态控制完全指南
作为一名在移动开发领域深耕多年的工程师,我经常遇到需要动态控制屏幕方向的需求。Android 13在这个领域带来了一些值得注意的变化和改进。今天我就来分享一套完整的动态设置屏幕方向的解决方案,涵盖从基础原理到高级技巧的全方位知识。
屏幕方向控制看似简单,实则暗藏玄机。正确的方向控制不仅能提升用户体验,还能避免各种奇怪的兼容性问题。特别是在Android 13上,Google对权限管理和后台行为做了不少调整,我们需要重新审视传统的实现方式。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Android屏幕方向基础原理
2.1 屏幕方向的核心概念
在Android系统中,屏幕方向(Orientation)主要通过Activity的screenOrientation属性来控制。这个属性决定了Activity如何响应设备的物理方向变化。常见的取值包括:
- portrait:固定竖屏
- landscape:固定横屏
- sensor:根据传感器自动旋转
- user:遵循用户系统设置
在代码中,我们通过Activity的setRequestedOrientation()方法来动态改变这个属性。这是实现动态方向控制的基础API。
2.2 Android 13的方向控制变化
Android 13在屏幕方向控制方面有几个重要变化:
- 权限要求更严格:现在要动态设置方向,需要在AndroidManifest.xml中声明CHANGE_CONFIGURATION权限
- 后台限制:应用在后台时,方向变更请求可能被系统忽略
- 多窗口适配:分屏模式下方向控制需要特殊处理
这些变化意味着我们需要调整传统的实现方式,特别是在权限申请和异常处理方面需要更加谨慎。
3. 动态设置方向的实现方案
3.1 基础实现代码
下面是一个在Android 13上动态设置屏幕方向的基础实现:
kotlin复制// 在AndroidManifest.xml中添加权限
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
// Kotlin实现代码
fun setScreenOrientation(activity: Activity, orientation: Int) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// Android 13及以上版本需要检查权限
if (checkSelfPermission(Manifest.permission.CHANGE_CONFIGURATION)
== PackageManager.PERMISSION_GRANTED) {
activity.requestedOrientation = orientation
} else {
// 处理权限缺失情况
requestPermissions(arrayOf(Manifest.permission.CHANGE_CONFIGURATION), REQUEST_CODE)
}
} else {
// 低版本直接设置
activity.requestedOrientation = orientation
}
} catch (e: Exception) {
Log.e("Orientation", "设置屏幕方向失败", e)
}
}
3.2 方向锁定的高级实现
有时我们需要更精细的控制,比如只在特定条件下锁定方向。下面是一个更完善的实现:
kotlin复制class OrientationManager private constructor(private val activity: Activity) {
companion object {
fun with(activity: Activity): OrientationManager {
return OrientationManager(activity)
}
}
private var originalOrientation = activity.requestedOrientation
fun lock(orientation: Int): OrientationManager {
activity.requestedOrientation = orientation
return this
}
fun unlock(): OrientationManager {
activity.requestedOrientation = originalOrientation
return this
}
fun restore() {
activity.requestedOrientation = originalOrientation
}
}
// 使用示例
OrientationManager.with(this)
.lock(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
// 恢复原始方向
OrientationManager.with(this).restore()
这个实现保存了原始方向,便于后续恢复,同时提供了流畅的API调用方式。
4. Android 13特殊场景处理
4.1 分屏模式下的方向控制
在分屏模式下,方向控制变得更加复杂。我们需要考虑以下几点:
- 检查当前是否处于分屏模式
- 分屏模式下可能需要禁用方向改变
- 监听分屏状态变化
实现代码示例:
kotlin复制// 检查是否分屏
fun isInMultiWindowMode(activity: Activity): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
activity.isInMultiWindowMode
} else {
false
}
}
// 分屏模式监听
private val multiWindowModeChangedListener =
OnMultiWindowModeChangedListener { isInMultiWindow ->
if (isInMultiWindow) {
// 分屏模式下禁用方向控制
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
} else {
// 退出分屏恢复控制
requestedOrientation = desiredOrientation
}
}
// 注册监听
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
registerMultiWindowModeChangedListener(multiWindowModeChangedListener)
}
}
4.2 权限处理的完整方案
Android 13要求显式声明CHANGE_CONFIGURATION权限。完整的权限处理应包括:
- Manifest声明
- 运行时检查
- 权限拒绝处理
- 兼容低版本
实现代码:
kotlin复制// 检查并请求权限
fun checkAndRequestOrientationPermission(activity: Activity): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
when {
ContextCompat.checkSelfPermission(
activity,
Manifest.permission.CHANGE_CONFIGURATION
) == PackageManager.PERMISSION_GRANTED -> {
true
}
activity.shouldShowRequestPermissionRationale(
Manifest.permission.CHANGE_CONFIGURATION
) -> {
// 解释为什么需要权限
showPermissionRationale(activity)
false
}
else -> {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.CHANGE_CONFIGURATION),
REQUEST_ORIENTATION_PERMISSION
)
false
}
}
} else {
// 低版本不需要权限
true
}
}
// 处理权限结果
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
REQUEST_ORIENTATION_PERMISSION -> {
if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限已授予
setDesiredOrientation()
} else {
// 权限被拒绝
handlePermissionDenied()
}
}
}
}
5. 常见问题与解决方案
5.1 方向设置不生效的排查步骤
在实际开发中,方向设置可能会遇到各种问题。以下是系统化的排查方法:
- 检查Android版本:确认是否处理了Android 13的特殊要求
- 验证权限:确保CHANGE_CONFIGURATION权限已声明和获取
- 检查Activity配置:查看AndroidManifest中是否有冲突的screenOrientation设置
- 观察生命周期:确保不在onPause或onStop后尝试修改方向
- 测试分屏模式:验证分屏情况下的行为是否符合预期
5.2 方向变化的平滑过渡
突然的方向变化可能导致UI闪烁。改善用户体验的技巧:
- 使用ViewTreeObserver监听布局变化
- 在方向变化前保存关键状态
- 考虑使用过渡动画
- 对于RecyclerView等复杂控件,保存滚动位置
实现示例:
kotlin复制// 保存RecyclerView状态
val recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState()
// 方向变化后恢复
recyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState)
// 添加过渡动画
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
window.enterTransition = Slide(Gravity.END)
}
5.3 方向锁定与视频播放
视频播放是方向控制的典型场景。最佳实践包括:
- 进入全屏时锁定为横屏
- 退出全屏恢复原始方向
- 处理中间状态(如下拉通知栏)
- 协调系统UI(导航栏、状态栏)的变化
完整实现:
kotlin复制class VideoPlayerActivity : AppCompatActivity() {
private var isFullscreen = false
private var originalOrientation = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
originalOrientation = requestedOrientation
videoView.setOnFullscreenListener { fullscreen ->
isFullscreen = fullscreen
if (fullscreen) {
// 进入全屏
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
hideSystemUI()
} else {
// 退出全屏
requestedOrientation = originalOrientation
showSystemUI()
}
}
}
private fun hideSystemUI() {
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
)
}
private fun showSystemUI() {
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
)
}
}
6. 性能优化与最佳实践
6.1 方向变化的性能影响
频繁的方向变化可能导致:
- Activity重建带来的性能开销
- 布局重新测量和绘制
- 资源重新加载
- 业务逻辑重复执行
优化策略:
- 在AndroidManifest中配置configChanges以减少重建
- 使用ViewModel保存数据
- 优化布局层次结构
- 延迟加载非必要资源
配置示例:
xml复制<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout"
android:screenOrientation="fullSensor" />
6.2 方向敏感的布局设计
为了更好适应方向变化,布局设计应考虑:
- 使用ConstraintLayout减少布局变化
- 为横竖屏提供不同的布局资源
- 使用尺寸限定符(如sw600dp)
- 避免硬编码尺寸
res/layout/activity_main.xml (竖屏默认布局)
res/layout-land/activity_main.xml (横屏布局)
6.3 方向变化的测试策略
全面的方向控制测试应包括:
- 基础功能测试:验证各方向设置是否生效
- 边界测试:快速连续切换方向
- 中断测试:在方向变化过程中打断(如来电)
- 兼容性测试:不同厂商ROM的表现
- 压力测试:长时间多方向切换
测试代码示例:
kotlin复制@Test
fun testOrientationChange() {
// 启动Activity
val scenario = ActivityScenario.launch(MainActivity::class.java)
// 验证初始方向
scenario.onActivity { activity ->
assertEquals(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, activity.requestedOrientation)
}
// 改变方向并验证
scenario.onActivity { activity ->
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
// 检查方向是否改变
scenario.onActivity { activity ->
assertEquals(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, activity.requestedOrientation)
}
// 模拟配置变化
scenario.recreate()
// 验证方向保持
scenario.onActivity { activity ->
assertEquals(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, activity.requestedOrientation)
}
}
7. 高级技巧与创新应用
7.1 自定义方向逻辑
有时标准的方向控制不能满足需求,我们可以实现更复杂的逻辑:
- 基于内容的方向控制(如阅读app根据段落长度决定方向)
- 基于设备传感器的精细控制
- 多显示器环境下的方向协调
- 游戏中的特殊方向处理
示例:基于加速度传感器的方向控制
kotlin复制class SensorOrientationController(private val activity: Activity) : SensorEventListener {
private val sensorManager by lazy {
activity.getSystemService(Context.SENSOR_SERVICE) as SensorManager
}
private val accelerometer by lazy {
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
}
fun start() {
sensorManager.registerListener(
this,
accelerometer,
SensorManager.SENSOR_DELAY_NORMAL
)
}
fun stop() {
sensorManager.unregisterListener(this)
}
override fun onSensorChanged(event: SensorEvent) {
val x = event.values[0]
val y = event.values[1]
val orientation = when {
abs(y) > abs(x) -> {
if (y > 0) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
}
else -> {
if (x > 0) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
else ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
}
}
activity.runOnUiThread {
activity.requestedOrientation = orientation
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
7.2 方向变化与Compose的集成
对于使用Jetpack Compose的项目,方向控制需要特别处理:
- 在Compose中监听方向变化
- 根据方向调整UI布局
- 保持状态不受方向变化影响
实现示例:
kotlin复制@Composable
fun OrientationAwareScreen() {
val configuration = LocalConfiguration.current
var orientation by remember { mutableStateOf(configuration.orientation) }
// 监听方向变化
DisposableEffect(Unit) {
val listener = object : ComponentCallbacks {
override fun onConfigurationChanged(newConfig: Configuration) {
orientation = newConfig.orientation
}
override fun onLowMemory() {}
}
LocalContext.current.applicationContext.registerComponentCallbacks(listener)
onDispose {
LocalContext.current.applicationContext.unregisterComponentCallbacks(listener)
}
}
// 根据方向显示不同布局
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
LandscapeLayout()
} else {
PortraitLayout()
}
}
@Composable
fun PortraitLayout() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("竖屏布局")
// 竖屏特有组件
}
}
@Composable
fun LandscapeLayout() {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text("横屏布局")
// 横屏特有组件
}
}
7.3 方向变化与CameraX的集成
在使用CameraX时,方向控制尤为重要:
- 协调预览方向与UI方向
- 处理图像捕获的方向
- 视频录制时的方向锁定
实现示例:
kotlin复制class CameraActivity : AppCompatActivity() {
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
// 设置方向监听
val orientationEventListener = object : OrientationEventListener(this) {
override fun onOrientationChanged(orientation: Int) {
// 根据设备方向调整相机
updateCameraOrientation(orientation)
}
}
if (orientationEventListener.canDetectOrientation()) {
orientationEventListener.enable()
} else {
// 设备不支持方向检测
orientationEventListener.disable()
}
}
private fun updateCameraOrientation(orientation: Int) {
val rotation = when (orientation) {
in 45..134 -> Surface.ROTATION_270 // 横屏反向
in 135..224 -> Surface.ROTATION_180 // 反向竖屏
in 225..314 -> Surface.ROTATION_90 // 横屏
else -> Surface.ROTATION_0 // 竖屏
}
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
cameraProvider.unbindAll()
val preview = Preview.Builder()
.setTargetRotation(rotation)
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val imageCapture = ImageCapture.Builder()
.setTargetRotation(rotation)
.build()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture
)
} catch (exc: Exception) {
Log.e("Camera", "绑定用例失败", exc)
}
}, ContextCompat.getMainExecutor(this))
}
}
8. 疑难问题深度解析
8.1 方向变化导致的Fragment重建问题
方向变化默认会导致Activity和Fragment重建。解决方案:
- 在Fragment中保存状态
- 使用setRetainInstance(已废弃,不推荐)
- 使用ViewModel保存数据
- 在onSaveInstanceState中保存关键状态
最佳实践:
kotlin复制class MyFragment : Fragment() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
// 恢复保存的状态
savedInstanceState?.let {
viewModel.restoreState(it)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
viewModel.saveState(outState)
}
// 其他Fragment代码
}
8.2 方向锁定与WebView的兼容性问题
WebView在方向锁定时可能出现问题:
- 内容布局错乱
- 视频全屏问题
- 键盘弹出问题
解决方案:
kotlin复制class WebViewActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = WebView(this).apply {
webChromeClient = object : WebChromeClient() {
override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
// WebView进入全屏时
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
super.onShowCustomView(view, callback)
}
override fun onHideCustomView() {
// WebView退出全屏时
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
super.onHideCustomView()
}
}
settings.javaScriptEnabled = true
loadUrl("https://example.com")
}
setContentView(webView)
}
override fun onBackPressed() {
if (webView.canGoBack()) {
webView.goBack()
} else {
super.onBackPressed()
}
}
}
8.3 方向变化与WindowInsets的协调处理
方向变化会影响WindowInsets(如状态栏、导航栏)的布局:
- 监听Insets变化
- 调整布局边距
- 处理不同方向的Insets差异
实现代码:
kotlin复制override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { view, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
// 根据当前方向应用不同的padding
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
view.updatePadding(
left = systemBars.left,
right = systemBars.right
)
} else {
view.updatePadding(
top = systemBars.top,
bottom = systemBars.bottom
)
}
WindowInsetsCompat.CONSUMED
}
}
9. 实战经验与性能调优
9.1 方向变化性能优化实战
在实际项目中优化方向变化性能的关键点:
- 减少布局层次:使用扁平化布局结构
- 延迟加载:非必要资源延后加载
- 视图缓存:复用已创建的视图
- 数据保持:使用ViewModel保存数据
优化后的Activity示例:
kotlin复制class OptimizedActivity : AppCompatActivity() {
private val viewModel: OptimizedViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 使用ViewStub延迟加载部分UI
val viewStub = findViewById<ViewStub>(R.id.heavyViewStub)
viewStub?.setOnInflateListener { _, inflated ->
// 视图膨胀完成后初始化
initHeavyViews(inflated)
}
// 只有第一次创建时加载
if (savedInstanceState == null) {
viewStub?.inflate()
}
// 从ViewModel恢复数据
viewModel.data.observe(this) { data ->
updateUI(data)
}
}
private fun initHeavyViews(view: View) {
// 初始化复杂视图
}
private fun updateUI(data: Data) {
// 更新UI
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// 只保存必要的最小状态
}
}
9.2 方向变化的内存管理
方向变化可能导致内存问题:
- 大图重复加载
- 缓存未清理
- 临时对象积累
解决方案:
- 使用内存缓存
- 及时释放资源
- 监控内存使用
内存监控实现:
kotlin复制class MemoryMonitor(private val activity: Activity) {
private val memoryInfo = ActivityManager.MemoryInfo()
private val activityManager by lazy {
activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
}
fun startMonitoring() {
val handler = Handler(Looper.getMainLooper())
val memoryCheckRunnable = object : Runnable {
override fun run() {
activityManager.getMemoryInfo(memoryInfo)
val availableMegs = memoryInfo.availMem / 0x100000L
val percentAvail = memoryInfo.availMem * 100 / memoryInfo.totalMem
Log.d("Memory", "可用内存: ${availableMegs}MB ($percentAvail%)")
if (memoryInfo.lowMemory) {
Log.w("Memory", "系统内存不足")
}
handler.postDelayed(this, 5000)
}
}
handler.post(memoryCheckRunnable)
}
}
9.3 方向变化的自动化测试
完善的自动化测试策略:
- 单元测试方向控制逻辑
- 仪器测试方向变化行为
- UI测试方向敏感的布局
测试示例:
kotlin复制@RunWith(AndroidJUnit4::class)
class OrientationTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testOrientationChange() {
// 初始为竖屏
activityRule.scenario.onActivity { activity ->
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
// 验证UI状态
onView(withId(R.id.title)).check(matches(isDisplayed()))
// 改为横屏
activityRule.scenario.onActivity { activity ->
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
// 验证UI适配
onView(withId(R.id.title)).check(matches(isDisplayed()))
// 模拟配置变化
activityRule.scenario.recreate()
// 验证状态保持
onView(withId(R.id.title)).check(matches(isDisplayed()))
}
@Test
fun testOrientationInFragment() {
// 初始方向
activityRule.scenario.onActivity { activity ->
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
// 添加Fragment
val fragmentArgs = Bundle()
val fragment = MyFragment().apply { arguments = fragmentArgs }
activityRule.scenario.onActivity { activity ->
activity.supportFragmentManager.beginTransaction()
.add(R.id.container, fragment)
.commitNow()
}
// 验证Fragment状态
onView(withId(R.id.fragmentView)).check(matches(isDisplayed()))
// 改变方向
activityRule.scenario.onActivity { activity ->
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
// 验证Fragment保持
onView(withId(R.id.fragmentView)).check(matches(isDisplayed()))
}
}
10. 未来展望与进阶方向
10.1 折叠屏设备的方向处理
随着折叠屏设备的普及,方向控制需要考虑更多因素:
- 铰链角度检测
- 屏幕比例变化
- 多显示区域协调
示例代码:
kotlin复制class FoldableActivity : AppCompatActivity() {
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) {}
override fun onDisplayRemoved(displayId: Int) {}
override fun onDisplayChanged(displayId: Int) {
updateLayoutForFoldable()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager
displayManager.registerDisplayListener(displayListener, null)
updateLayoutForFoldable()
}
private fun updateLayoutForFoldable() {
val windowMetrics = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(this)
val bounds = windowMetrics.bounds
val width = bounds.width()
val height = bounds.height()
if (width > height) {
// 横屏或展开状态
setupLandscapeLayout()
} else {
// 竖屏或折叠状态
setupPortraitLayout()
}
}
private fun setupLandscapeLayout() {
// 配置横屏布局
}
private fun setupPortraitLayout() {
// 配置竖屏布局
}
override fun onDestroy() {
super.onDestroy()
val displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager
displayManager.unregisterDisplayListener(displayListener)
}
}
10.2 多窗口模式的高级方向控制
在多窗口模式下,方向控制需要更精细的策略:
- 检测窗口大小变化
- 动态调整布局
- 协调多个Activity的方向
实现示例:
kotlin复制class MultiWindowActivity : AppCompatActivity() {
private val sizeChangeListener = OnConfigurationChangedListener { config ->
handleSizeChange(config)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 注册尺寸变化监听
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window.decorView.addOnConfigurationChangedListener(sizeChangeListener)
}
}
private fun handleSizeChange(config: Configuration) {
val isInMultiWindow = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
isInMultiWindowMode
} else {
false
}
if (isInMultiWindow) {
// 多窗口模式下的特殊处理
val widthDp = config.screenWidthDp
val heightDp = config.screenHeightDp
when {
widthDp < 300 -> setupCompactLayout()
heightDp < 300 -> setupCompactLayout()
else -> setupNormalLayout()
}
} else {
// 全屏模式
setupNormalLayout()
}
}
private fun setupCompactLayout() {
// 紧凑布局配置
}
private fun setupNormalLayout() {
// 正常布局配置
}
override fun onDestroy() {
super.onDestroy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window.decorView.removeOnConfigurationChangedListener(sizeChangeListener)
}
}
}
10.3 方向控制与MotionLayout的集成
使用MotionLayout实现流畅的方向过渡动画:
- 为不同方向创建不同的MotionScene
- 平滑过渡布局变化
- 协调方向变化与动画
示例实现:
xml复制<!-- res/xml/scene_portrait.xml -->
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android">
<Transition
android:id="@+id/transition"
app:constraintSetEnd="@+id/end">
<OnSwipe
app:touchAnchorId="@+id/button"
app:touchAnchorSide="right"
app:dragDirection="dragRight" />
</Transition>
<ConstraintSet android:id="@+id/start">
<!-- 竖屏初始约束 -->
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<!-- 竖屏结束约束 -->
</ConstraintSet>
</MotionScene>
<!-- res/xml/scene_landscape.xml -->
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 横屏MotionScene配置 -->
</MotionScene>
kotlin复制class MotionLayoutActivity : AppCompatActivity() {
private lateinit var motionLayout: MotionLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_motion)
motionLayout = findViewById(R.id.motionLayout)
// 初始加载竖屏场景
loadPortraitScene()
}
private fun loadPortraitScene() {
motionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
override fun onTransitionStarted(layout: MotionLayout?, startId: Int, endId: Int) {}
override fun onTransitionChange(layout: MotionLayout?, startId: Int, endId: Int, progress: Float) {}
override fun onTransitionCompleted(layout: MotionLayout?, currentId: Int) {
// 场景加载完成后移除监听
motionLayout.setTransitionListener(null)
}
override fun onTransitionTrigger(layout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) {}
})
motionLayout.loadLayoutDescription(R.xml.scene_portrait)
}
private fun loadLandscapeScene() {
motionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
override fun onTransitionStarted(layout: MotionLayout?, startId: Int, endId: Int) {}
override fun onTransitionChange(layout: MotionLayout?, startId: Int, endId: Int, progress: Float) {}
override fun onTransitionCompleted(layout: MotionLayout?, currentId: Int) {
motionLayout.setTransitionListener(null)
}
override fun onTransitionTrigger(layout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) {}
})
motionLayout.loadLayoutDescription(R.xml.scene_landscape)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
when (newConfig.orientation) {
Configuration.ORIENTATION_PORTRAIT -> loadPortraitScene()
Configuration.ORIENTATION_LANDSCAPE -> loadLandscapeScene()
}
}
}
