咱们都知道,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.

原理都是一样,咱们能够依据自己的需求挑选。