咱们都知道,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.
原理都是一样,咱们能够依据自己的需求挑选。