Android 切换主题时如何恢复 Dialog?
我们都知道,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 的表现是怎样的。
通过视频我们可以看到,在切换横竖屏或主题时,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 就可以了。我们来看看效果。
我们可以看到,确实切换主题后 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 再看看效果。
是不是很方便?这是什么原理呢?主要是两个方法,如下:
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?
配置变化后的恢复都会依赖onSaveInstance
和onRestoreInstance
,Dialog 也不例外,不过 Dialog 这两个流程都依赖 Activity,我们来完整过一遍 onSaveInstance
的流程,saveInstance
从 Activity#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.
原理都是一样,大家可以根据自己的需要选择。
来源:juejin.cn/post/7246293244636004409