Android横竖屏切换调研报告
两种响应横竖屏切换的方式
- 不走生命周期,执行
onConfigurationChanged()
方法; - 走生命周期,重新创建
Activity
;
1. 不走生命周期的配置
1.1 AndroidManifest.xml
中,添加配置:
android:configChanges="orientation|screenSize"
1.2 重写onConfigurationChanged()
方法:
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// TODO:自己的逻辑
}
2. 走生命周期,重建Activity
该模式下,Activity
会执行正常的销毁,并调用onSaveInstanceState(Bundle outState)
保留销毁前的状态.
随后,重新创建Activity
,并调用onRestoreInstanceState(Bundle savedInstanceState)
方法恢复之前保留的状态.
在此过程中,Fragment
与View
的状态都会被保存及恢复.
提示:下文所有场景皆是在该模式下
3. View的状态保持及恢复过程
在横竖屏切换的过程中,系统会自动保持及恢复View的状态.
保持过程如下图:
恢复过程如下图:
View状态的恢复过程
3.1 自定义View的状态保存及恢复
在自定义View
中,如果存在需要保存及恢复的状态,可以通过重写View
中的protected Parcelable onSaveInstanceState()
与protected void onRestoreInstanceState(Parcelable state)
方法实现.
官方的一个demo如下:
class CollapsibleCard @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private var expanded = false
//保存状态
//重写了onSaveInstanceState,并保存私有字段
override fun onSaveInstanceState(): Parcelable {
val savedState = SavedState(super.onSaveInstanceState())
savedState.expanded = expanded
return savedState
}
//恢复状态
override fun onRestoreInstanceState(state: Parcelable?) {
if (state is SavedState) {
super.onRestoreInstanceState(state.superState)
if (expanded != state.expanded) {
toggleExpanded()
}
} else {
super.onRestoreInstanceState(state)
}
}
//扩展了BaseSavedState类,加入私有字段expanded
internal class SavedState : BaseSavedState {
var expanded = false
constructor(source: Parcel) : super(source) {
expanded = source.readByte().toInt() != 0
}
constructor(superState: Parcelable?) : super(superState)
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeByte((if (expanded) 1 else 0).toByte())
}
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<SavedState> {
override fun createFromParcel(source: Parcel): SavedState {
return SavedState(source)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
}
}
3.2 保存Adapter状态
如果需要保存Adapter
的状态,可以参考官方的demo的做法:
在初始化时,还原之前保存的状态.
提供onSaveInstanceState
方法,以便在Fragment
中调用.
internal class CodelabsAdapter(
private val codelabsActionsHandler: CodelabsActionsHandler,
private val tagViewPool: RecycledViewPool,
private val isMapEnabled: Boolean,
savedState: Bundle?
) : ListAdapter<Any, CodelabsViewHolder>(CodelabsDiffCallback) {
companion object {
private const val STATE_KEY_EXPANDED_IDS = "CodelabsAdapter:expandedIds"
}
private var expandedIds = mutableSetOf<String>()
init {
//在初始化时,恢复状态
savedState?.getStringArray(STATE_KEY_EXPANDED_IDS)?.let {
expandedIds.addAll(it)
}
}
fun onSaveInstanceState(state: Bundle) {
state.putStringArray(STATE_KEY_EXPANDED_IDS, expandedIds.toTypedArray())
}
}
在Fragment
的onSaveInstanceState
方法中,保存Adapter的状态.
在创建Adapter
时,传入savedInstanceState
class CodelabsFragment : MainNavigationFragment() {
private lateinit var codelabsAdapter: CodelabsAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//在构建方法中,传入了savedInstanceState
codelabsAdapter = CodelabsAdapter(
this,
tagRecycledViewPool,
mapFeatureEnabled,
savedInstanceState
)
}
//在onSaveInstanceState中保存adapter的状态
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
codelabsAdapter.onSaveInstanceState(outState)
}
}
ps:以上代码片段皆来自Google I/O 2019 App
4. ViewModel:摆脱生命周期的束缚
在Activity
及Fragment
生命周期中,如果不能正确地把握View
的创建及更新时机,会导致在横竖屏切换后,View
的状态错误.
下面是使用ViewModel
重构后的首页Fragment
,以极其简练的代码,确保了在横竖屏切换后卡片列表位置的一致.
4.1 定义SocketViewModel
class SocketViewModel : ViewModel() {
var data = MutableLiveData<Pair<List<Tab>, List<HomeCell>>>()
init {
Log.e("novelot.vm", "SocketViewModel::init ")
if (UserInfoManager.getInstance().isUserActivation) {
SocketManager.getInstance().addHomeRefreshListener(object : SocketService.HomeRefreshListener {
override fun refresh(msg: Any) {
Log.e("novelot.vm", "SocketViewModel::refresh ")
val grps = getColumnGrps(msg)
AppExecutors.getInstance().mainThread().execute {
data.value = DataConverter.toTabsAndCells(grps)
}
}
override fun requestRefresh() {
Log.e("novelot.vm", "SocketViewModel::requestRefresh ")
requestRefresh2()
}
override fun connectError() {
}
})
}
}
private fun getColumnGrps(msg: Any): List<ColumnGrp> {
return HomePlayerModel.getColumnGrps(msg)
}
private fun requestRefresh2() {
//zone
val zone = "mainPage"
//经纬度:默认北京
var lng = "116.23"
var lat = "39.54"
val location = LocationManager.getInstance().mapLocation
if (location != null) {
lng = location.longitude.toString()
lat = location.latitude.toString()
}
val msg = HashMap<String, String>()
msg.put("zone", zone)
msg.put("lng", lng)
msg.put("lat", lat)
msg.put("parentCode", "0")
msg.put("isRecursive", "1")
msg.putAll(CommonRequestParamsUtil.getCommonParams())
val gson = GsonBuilder().enableComplexMapKeySerialization().create()
var jsonObject: JSONObject? = null
try {
jsonObject = JSONObject(gson.toJson(msg))
} catch (e: JSONException) {
e.printStackTrace()
}
SocketManager.getInstance().requestRefresh(jsonObject)
}
}
在init
中对SocketManager
添加状态监听,并在获取到数据后,赋值给LiveData
.
LiveData
在发现数据改变后,会通知到监听者.
4.2 在Fragment中创建ViewModel及添加监听
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mSocketViewModel = ViewModelProviders.of(this).get(SocketViewModel.class);
//...省略其他逻辑
mSocketViewModel.getData().observe(this, new Observer<Pair<List<Tab>, List<HomeCell>>>() {
@Override
public void onChanged(@Nullable Pair<List<Tab>, List<HomeCell>> tabsAndCells) {
if (!ListUtil.isEmpty(tabsAndCells.first)) {
showTabs(tabsAndCells.first);
}
if (!ListUtil.isEmpty(tabsAndCells.second)) {
showContent(tabsAndCells.second);
}
}
});
}
在HorizontalHomePlayerFragment
的onViewCreated()
中,通过ViewModelProviders.of(this).get(SocketViewModel.class);
创建mSocketViewModel
,并通过mSocketViewModel.getData().observe()
对数据进行监听.
当SocketViewModel
获取到数据后,会通知到监听器,回调Observer
的onChanged
方法.
该代码在kradio的devOrientation分支上
4.3 分析
从代码上观察,并未发现对Fragment
或View
做任何处理,横竖屏切换中,所有保存及恢复逻辑都是系统默认的方式.ViewModel
唯一帮我们做的只是:理顺了正确的生命周期调用,正所谓无为而治.
5. 总结
对比两种横竖屏切换的方式:第二种方式,在横竖屏布局不同的需求下,具有天然的优势.结合使用ViewModel
及相关组件,将复杂易混的生命周期回调交给组件来处理,以极其简练的代码,使开发者从中解脱出来,同时,也理顺了View
的创建与更新时机.
网友评论