Android横竖屏切换调研报告

2019-10-23  本文已影响0人  晴天忆雨

Android横竖屏切换调研报告

两种响应横竖屏切换的方式

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)方法恢复之前保留的状态.
在此过程中,FragmentView的状态都会被保存及恢复.

提示:下文所有场景皆是在该模式下

3. View的状态保持及恢复过程

在横竖屏切换的过程中,系统会自动保持及恢复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())
    }
}

FragmentonSaveInstanceState方法中,保存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:摆脱生命周期的束缚

ActivityFragment生命周期中,如果不能正确地把握View的创建及更新时机,会导致在横竖屏切换后,View的状态错误.

ViewModel的使用

下面是使用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在发现数据改变后,会通知到监听者.

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);
            }
        }
    });
}

HorizontalHomePlayerFragmentonViewCreated()中,通过ViewModelProviders.of(this).get(SocketViewModel.class);创建mSocketViewModel,并通过mSocketViewModel.getData().observe()对数据进行监听.
SocketViewModel获取到数据后,会通知到监听器,回调ObserveronChanged方法.

该代码在kradio的devOrientation分支上

4.3 分析

从代码上观察,并未发现对FragmentView做任何处理,横竖屏切换中,所有保存及恢复逻辑都是系统默认的方式.ViewModel唯一帮我们做的只是:理顺了正确的生命周期调用,正所谓无为而治.

5. 总结

对比两种横竖屏切换的方式:第二种方式,在横竖屏布局不同的需求下,具有天然的优势.结合使用ViewModel及相关组件,将复杂易混的生命周期回调交给组件来处理,以极其简练的代码,使开发者从中解脱出来,同时,也理顺了View的创建与更新时机.

上一篇下一篇

猜你喜欢

热点阅读