likes
comments
collection
share

Android 切换主题时如何恢复 Dialog?

作者站长头像
站长
· 阅读数 5

我们都知道,Android 在横竖屏切换、主题切换、语言等操作时,系统会 finish Activity ,然后重建,这样便可以重新加载配置变更后的资源。

如果你只有 Activity 的内容需要展示,那这样处理是没有问题的,但是如果界面在点击操作之后打开一个 Dialog,那在配置改变后这个 Dialog 还会在么?答案是不一定,我们来看看展示 Dialog 有几种方式。

Dilog#show()

这可能是大家比较常用的方法,创建一个 Dialog ,然后调用其 show 方法,就像这样。

class MainActivity : AppCompatActivity() {  
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        setContentView(R.layout.activity_main)  
        
        findViewById<View>(R.id.tvDialog).setOnClickListener {  
            AlertDialog.Builder(this)  
            .setView(R.layout.test_dialog)  
            .show()  
        }  
    }  
}

每次点击按钮会创建一个新的 Dialog 对象,然后调用 show 方法展示。我们来看看配置改变后,Dialog 的表现是怎样的。

Android 切换主题时如何恢复 Dialog?

通过视频我们可以看到,在切换横竖屏或主题时,Dialog 都没有恢复。这是因为Dialog#show这种方式是开发者自己管理 Dialog,所以在恢复 Activity 时,Activity 是不知道需要恢复 Dialog 的。那怎么让 Activity 知道当前展示了 Dialog 呢?那就需要用到下面的方式。

Activity#showDialog()

先来看看此方法的注释

Show a dialog managed by this activity. A call to onCreateDialog(int, Bundle) will be made with the same id the first time this is called for a given id. From thereafter, the dialog will be automatically saved and restored. If you are targeting Build.VERSION_CODES.HONEYCOMB or later, consider instead using a DialogFragment instead. Each time a dialog is shown, onPrepareDialog(int, Dialog, Bundle) will be made to provide an opportunity to do any timely preparation.

简单来说这个方法会让 Activity 来管理需要展示的 Dialog,会跟 onCreateDialog(int, Bundle)成对出现,并且会保存这个 Dialog,在重复调用Activity#showDialog()时不会重复创建 Dialog 对象。Activity 自己管理 Dialog?那就能恢复了吗?我们来试试。

override fun onCreate(savedInstanceState: Bundle?) {  
    super.onCreate(savedInstanceState)  
    setContentView(R.layout.activity_main)  

    findViewById<View>(R.id.tvDialog).setOnClickListener {  
        showDialog(100) //自定义 id  
    }  
}  
  
override fun onCreateDialog(id: Int): Dialog? {  
    if(id == 100){ // id 与 showDialog 匹配  
        return AlertDialog.Builder(this)  
        .setView(R.layout.test_dialog)  
        .create()  
    }  
    return super.onCreateDialog(id)  
}

代码很简单,调用 Activity#showDialog(int id)方法,然后重写 Activity#onCreateDialog(id:Int),匹配两边的 id 就可以了。我们来看看效果。

Android 切换主题时如何恢复 Dialog?

我们可以看到,确实切换主题后 Dialog 是恢复了的,不过还有个问题,就是这个 ScrollView 的状态没有恢复,滑动的位置被还原了,难道我们需要手动记住滑动的 position 然后再恢复?是的,不过这个操作 Android 已经替我们做了,我们需要做的就是给需要恢复的组件指定一个 id 就行。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:app="http://schemas.android.com/apk/res-auto"  
    android:layout_width="wrap_content"  
    android:layout_height="200dp">  
  
    <ScrollView  
        android:id="@+id/scrollView"  
        android:layout_width="match_parent"  
        android:layout_height="300dp"  
        android:scrollbars="vertical"  
        android:scrollbarSize="10dp"  
        android:background="@color/primary_background">  

        <androidx.constraintlayout.widget.ConstraintLayout  
            android:layout_width="match_parent"  
            android:layout_height="wrap_content">  

            <TextView  
                android:id="@+id/tvContent"  
                android:layout_width="match_parent"  
                android:layout_height="wrap_content"  
                android:layout_marginStart="8dp"  
                android:layout_marginTop="8dp"  
                android:layout_marginEnd="8dp"  
                android:text="@string/test_content"  
                android:textAlignment="center"  
                android:textSize="30sp"  
                android:textColor="@color/primary_text"  
                app:layout_constraintBottom_toBottomOf="parent"  
                app:layout_constraintTop_toTopOf="parent" />  

        </androidx.constraintlayout.widget.ConstraintLayout>  
    </ScrollView>  
</FrameLayout>

刚刚 ScrollView 标签是没有 id 的,现在我们加了一个 id 再看看效果。

Android 切换主题时如何恢复 Dialog?

是不是很方便?这是什么原理呢?主要是两个方法,如下:

public void saveHierarchyState(SparseArray<Parcelable> container) {  
    dispatchSaveInstanceState(container);  
}
 
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {  
    if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {  
        mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;  
        Parcelable state = onSaveInstanceState();  
        if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {  
            throw new IllegalStateException(  
            "Derived class did not call super.onSaveInstanceState()");  
        }  
        if (state != null) {  
            // Log.i("View", "Freezing #" + Integer.toHexString(mID)  
            // + ": " + state);  
            container.put(mID, state);  
        }  
    }  
}
 
public void restoreHierarchyState(SparseArray<Parcelable> container) {  
    dispatchRestoreInstanceState(container);  
}

protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {  
    if (mID != NO_ID) {  
        Parcelable state = container.get(mID);  
        if (state != null) {  
            // Log.i("View", "Restoreing #" + Integer.toHexString(mID)  
            // + ": " + state);  
            mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;  
            onRestoreInstanceState(state);  
            if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {  
                throw new IllegalStateException(  
                "Derived class did not call super.onRestoreInstanceState()");  
            }  
        }  
    }  
}

在 Actvity 执行 onSaveInstance 时,会保存 View 的层级状态,View 的 id 为 key,状态为 value,这样的一个SparseArray,View 的状态是在 View 的 onSaveInstance 方法生成的,所以,如果 View 没有重写 onSaveInstance时,就算指定了 id 也不会被恢复。我们来看看 ScrollView#onSaveInstance做了什么工作。

protected Parcelable onSaveInstanceState() {  
    if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {  
        // Some old apps reused IDs in ways they shouldn't have.  
        // Don't break them, but they don't get scroll state restoration.  
        return super.onSaveInstanceState();  
    }  
    Parcelable superState = super.onSaveInstanceState();  
    SavedState ss = new SavedState(superState);  
    ss.scrollPosition = mScrollY;  
    return ss;  
}

ss.scrollPosition = mScrollY关键代码就是这一句,保存了 scrollPosition,恢复的逻辑就是在onRestoreInstance大家可以自己看看,逻辑比较简单,我这边就不列了。

Activity 如何恢复 Dialog?

配置变化后的恢复都会依赖onSaveInstanceonRestoreInstance,Dialog 也不例外,不过 Dialog 这两个流程都依赖 Activity,我们来完整过一遍 onSaveInstance 的流程,saveInstanceActivity#performSaveInstanceState开始.

Activity.java

/**  
* The hook for {@link ActivityThread} to save the state of this activity.  
*  
* Calls {@link #onSaveInstanceState(android.os.Bundle)}  
* and {@link #saveManagedDialogs(android.os.Bundle)}.  
*  
* @param outState The bundle to save the state to.  
*/  
final void performSaveInstanceState(@NonNull Bundle outState) {  
    dispatchActivityPreSaveInstanceState(outState);  
    onSaveInstanceState(outState);  
    saveManagedDialogs(outState);  
    mActivityTransitionState.saveState(outState);  
    storeHasCurrentPermissionRequest(outState);  
    if (DEBUG_LIFECYCLE) Slog.v(TAG, "onSaveInstanceState " + this + ": " + outState);  
    dispatchActivityPostSaveInstanceState(outState);  
}

/**  
* Save the state of any managed dialogs.  
*  
* @param outState place to store the saved state.  
*/  
@UnsupportedAppUsage  
private void saveManagedDialogs(Bundle outState) {  
    if (mManagedDialogs == null) {  
        return;  
    }  
    final int numDialogs = mManagedDialogs.size();  
    if (numDialogs == 0) {  
        return;  
    }  
    Bundle dialogState = new Bundle();  
    int[] ids = new int[mManagedDialogs.size()];  
    // save each dialog's bundle, gather the ids  
    for (int i = 0; i < numDialogs; i++) {  
        final int key = mManagedDialogs.keyAt(i);  
        ids[i] = key;  
        final ManagedDialog md = mManagedDialogs.valueAt(i);  
        dialogState.putBundle(savedDialogKeyFor(key), md.mDialog.onSaveInstanceState());  
        if (md.mArgs != null) {  
            dialogState.putBundle(savedDialogArgsKeyFor(key), md.mArgs);  
        }  
    }  
    dialogState.putIntArray(SAVED_DIALOG_IDS_KEY, ids);  
    outState.putBundle(SAVED_DIALOGS_TAG, dialogState);  
}

saveManagedDialogs这个方法就是处理 Dialog 的流程,我们可以看到它会调用 md.mDialog.onSaveInstanceState(),来保存 Dialog 的状态,而这个md.mDialog就是在showDialog时保存的

public final boolean showDialog(int id, Bundle args) {  
    if (mManagedDialogs == null) {  
        mManagedDialogs = new SparseArray<ManagedDialog>();  
    }  
    ManagedDialog md = mManagedDialogs.get(id);  
    if (md == null) {  
        md = new ManagedDialog();  
        md.mDialog = createDialog(id, null, args);  
        if (md.mDialog == null) {  
            return false;  
        }  
        mManagedDialogs.put(id, md);  
    }  
    md.mArgs = args;  
    onPrepareDialog(id, md.mDialog, args);  
    md.mDialog.show();  
    return true;  
}

这样流程就能串起来了吧,用Activity#showDialog关联 Activity 与 Dialog,在 Activity onSaveInstance 时会调用 Dialog#onSaveInstance保存状态,而不管在 Activity 或 Dialog 的 onSaveInstance 里都会执行View#saveHierarchyState来保存视图层级状态,这样不管是 Activity 还是 Dialog 亦或是 View 便都可以恢复啦。

不过以上描述的恢复,恢复的都是 Android 原生数据,如果你需要恢复业务数据,那就需要自己保存啦,不过 Google 也为我们提供了解决方案,就是 Jetpack ViewModel,对吧?

这样通过 ViewModel 和 SaveInstance 就可以恢复所有业务和视图状态了!

总结

到这边,关于如何恢复 Dialog 的主要内容就分享完了,需要多说一句的是,Activity#showDialog方法已被标记为废弃。

Use the new DialogFragment class with FragmentManager instead; this is also available on older platforms through the Android compatibility package.

原理都是一样,大家可以根据自己的需要选择。