横竖屏适配最好的方式还是判断设备是平板或手机,进入app时固定展示横屏或者竖屏,设置android:screenOrientation="locked"禁止在app内切换横竖屏。无论什么框架都能适用,且不需要考虑什么兼容。
为了方便横竖屏切换需使用ViewModel和DataBinding双向绑定来控制界面控件的数据和状态,将界面文案、点击事件、是否可见等等存放在ViewModel中,否则需要在Bundle存取大量数据来保证横竖屏切换后界面状态和数据。
横竖屏涉及到Activity和Fragment的复用重建、旧Fragment的关联状态、生命周期的管理避免如网络请求开启线程导致Activity无法释放及请求完成继续操作界面但界面已经销毁导致闪退、界面展示如按钮状态输入文本的内容保存、横屏界面布局和竖屏界面的切换。需要有大局观来考虑各种问题,实现比较复杂。
方法一:保留Activity横竖屏切换重建,Fragment重建,使用mvvm框架实现双向绑定,使得切换后界面显示数据保持一致。缺点就是每次横竖屏切换都会生成新对象,需要做好内存管理。
方法二:Activity不旋转,一个Fragment在同一个布局文件中写两套布局即有横屏又有竖屏界面,区分横竖屏展示。在Fragment的onConfigurationChanged方法中实现横竖屏布局的隐藏和展示。布局中控件状态和文案通过vm来控制,让两个布局中对应控件有相同状态。也可拆分为多个部分,根据横竖屏动态改变控件在布局中的位置。优点是横竖屏共用一个Activity/Fragment对象,缺点是一个xml文件实现两个布局可能内容比较多有上千行。
<activity
android:name="com.slaoren.mvvmexamp.ui.MvvmDemo2Activity"
android:configChanges="orientation|screenSize" 设置不重建activity
android:screenOrientation="user" />
//MvvmDemo2Activity展示的Fragment
class MvvmConfigurationChangedFragment:BaseFragment<FragmentConfigurationChangedBinding, DemoViewModel>() {
override fun getLayoutId(): Int {
return R.layout.fragment_configuration_changed
}
override fun setView() {
mBinding.vm = mViewModel
//首次进入可以根据横竖屏判断展示什么界面,这里直接展示竖屏
mBinding.clPortrait.visibility = View.VISIBLE
mBinding.clLandscape.visibility = View.GONE
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
//横竖屏切换后在此方法中判断展示什么界面
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
Toast.makeText(requireContext(), "竖屏", Toast.LENGTH_SHORT).show()
mBinding.clPortrait.visibility = View.VISIBLE
mBinding.clLandscape.visibility = View.GONE
}else{
Toast.makeText(requireContext(), "横屏", Toast.LENGTH_SHORT).show()
mBinding.clPortrait.visibility = View.GONE
mBinding.clLandscape.visibility = View.VISIBLE
}
}
}
布局文件fragment_configuration_changed.xml,在FrameLayout内放置了两个ConstraintLayout,对应横屏展示内容竖屏展示内容,可以根据需要修改实现横竖屏布局
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="com.slaoren.mvvmexamp.vm.DemoViewModel" />
</data>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/clPortrait"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvTimePortrait"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_margin="10dp"
android:background="#333333"
android:gravity="center"
android:onClick="@{()->vm.clickTime()}"
android:text="@={vm.time}"
android:textColor="#ffffff"
android:textSize="25sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/tvPortrait"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Portrait"
android:textColor="#333333"
android:textSize="25sp"
app:layout_constraintTop_toBottomOf="@id/tvTimePortrait"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/clLandscape"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvTimeLandscape"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="200dp"
android:background="#333333"
android:gravity="center"
android:onClick="@{()->vm.clickTime()}"
android:text="@={vm.time}"
android:textColor="#ffffff"
android:textSize="25sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/tvLandscape"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Landscape"
android:textColor="#333333"
android:textSize="25sp"
app:layout_constraintTop_toBottomOf="@id/tvTimeLandscape"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</layout>
方法三:Activity不旋转,一个Fragment分别实例化横屏和竖屏两个对象,或者写两个Fragment类分别展示横屏和竖屏的布局。在Activity的onConfigurationChanged方法中实现横竖屏Fragment的对象的切换。通过mvvm和Databinding双向绑定,实现两个Fragment对象间的数据同步。Fragment需要在layout-land、layout中实现两个xml布局对应横竖屏展示。对比方法一减少了创建对象的个数,只创建一个Activity对象,最多一个Fragment只创建两个对象。
<activity
android:name="com.slaoren.mvvmexamp.ui.MvvmDemo3Activity"
android:configChanges="orientation|screenSize" 设置不重建activity
android:screenOrientation="user" />
//在MvvmDemo3Activity的方法onConfigurationChanged中根据当前横竖屏状态,切换对应Fragment对象。另外有多个界面展示有不同Fragment时需要考虑记录下当前展示哪个界面的Fragment。
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
Log.d("test", "onConfigurationChanged:"+newConfig.orientation)
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE){
Toast.makeText(this, "横屏", Toast.LENGTH_SHORT).show()
changeFragment("landscapeFragment")
}else{
Toast.makeText(this, "竖屏", Toast.LENGTH_SHORT).show()
changeFragment("portraitFragment")
}
}
//切换Fragment,首次展示时创建Fragment实例,并隐藏不需要展示的Fragment
private fun changeFragment(tag: String) {
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
for (fragmentManagerFragment in fragmentManager.fragments) {
fragmentTransaction.hide(fragmentManagerFragment)
}
var fragment = fragmentManager.findFragmentByTag(tag)
if (fragment != null) {
fragmentTransaction.show(fragment)
} else {
fragment = when(tag){
"landscapeFragment"->MvvmDemoFragment()//只展示layout-land布局
"portraitFragment"->MvvmDemoFragment()//只展示layout布局
//其他Fragment界面横竖屏对象
else -> null
}
if (fragment != null) {
fragmentTransaction.add(R.id.frameLayout, fragment, tag)
}
}
fragmentTransaction.commitNow()
}
Fragment相关
class MvvmDemoFragment:BaseFragment<FragmentMvvmDemoBinding, DemoViewModel>() {
override fun getLayoutId(): Int {
return R.layout.fragment_mvvm_demo
}
override fun setView() {
mBinding.vm = mViewModel
}
}
系统会根据当前手机横竖屏状态读取对应layout或layout-land中的fragment_mvvm_demo.xml文件,注意两个xml文件的具体实现为横屏展示和竖屏展示