美文网首页
Android横竖屏切换调研报告

Android横竖屏切换调研报告

作者: 晴天忆雨 | 来源:发表于2019-10-23 10:50 被阅读0次

    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)方法恢复之前保留的状态.
    在此过程中,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的创建与更新时机.

    相关文章

      网友评论

          本文标题:Android横竖屏切换调研报告

          本文链接:https://www.haomeiwen.com/subject/mobhvctx.html