Android中所谓的视图残留
最近的一个项目,要实现下图所示的界面显示以及功能:
2017-12-02 20-41-06屏幕截图.png
经过搜索,知道可以通过TextView+SpannableString+ClickableSpan来实现:
具体如下:
mCheckNet = (TextView) mNetErrorPromptContainerBeforeReady.findViewById(R.id.check_net);
final String checkNetText = getString(R.string.net_error_hint_before_ready);
final String clickableCheckNetText = getString(R.string.clickable_check_hint);
final int totalLength = checkNetText.length();
final int start = checkNetText.indexOf(clickableCheckNetText);
final int end = start + clickableCheckNetText.length();
SpannableStringBuilder ssb = new SpannableStringBuilder(checkNetText);
final int normalSize = getResources().getDimensionPixelSize(R.dimen.normal_check_net_size);
final int bigSize = getResources().getDimensionPixelSize(R.dimen.clickable_check_net_size);
ssb.setSpan(new TextAppearanceSpan(null, 0, normalSize, null, null), 0, start - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new TextAppearanceSpan(null, 0, normalSize, null, null), end + 1, totalLength - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new TextAppearanceSpan(null, Typeface.BOLD, bigSize, null, null), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
//设置检查点击事件
ssb.setSpan(new CheckNetClickListener(getContext()), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mCheckNet.setText(ssb);
mCheckNet.setMovementMethod(LinkMovementMethod.getInstance());
private static class CheckNetClickListener extends ClickableSpan {
private Context mContext = null;
public CheckNetClickListener(Context context) {
mContext = context;
}
@Override
public void onClick(View widget) {
// 启动设置网络 Activity
Intent intentNet = new Intent(mContext, WifiListActivity.class);
intentNet.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intentNet);
}
@Override
public void updateDrawState(TextPaint ds) {
// 下划线
ds.setUnderlineText(true);
}
}
适配了所有系统语言,包括中文,阿拉伯语,俄罗斯语,西班牙语,德语,日语,韩语,英语等,一切非常顺利.
我也觉得差不多了,高高兴兴拿去给领导演示,被打脸了!!!
问题是拿去的时候显示的是中文,显示的内容如上图所示,然后我HOME键退出当前界面,切换系统语言到日语.
结果: 依然还是上图所示,而且点击 检查, 应用崩溃了. 赶紧拿回去分析原因.
主要是这个问题也不是必现的,所以一时半会儿也没有头绪.
因为我们这个布局是 Activity + ViewPager + Fragment 实现的.
有一点就是在切换系统语言之后Activity是会被重启的.意思是它的生命周期会重新从onCreate开始.
所以下一意识就怀疑是Fragment没有被及时回,造成老的Fragment依然压在新生成的Fragment之上,一顿调试没有进展.
然后因为这个是新加的功能,修改之处就是以上贴出的代码片段.最后发现去除以下片段:
//设置检查点击事件
ssb.setSpan(new CheckNetClickListener(getContext()), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
问题就不能再被复现了.于是怀疑是不是ClickableSpan导致Fragment没有被回收,然后网上一搜,还真有类似的案例,仔细看下来,跟我这个不是一个问题.
然后搜索视图残留,还真有挺多.
主要是在切换系统语言的时候,Activity会被销毁,然后它的ViewState会被保存起来.但是给出的解决方案说实在不人性,主要是以下:
第一种:
@Override
public void onSaveInstanceState(Bundle outState) {
// 注释掉,就可以
// super.onSaveInstanceState(outState, outPersistentState);
}
第二种:
主动调用FragmentMananer去移除Fragment.
这里我说下第一种,的确问题没有了,但是也带来非常不友好的体验: ListView实现界面滑动到中间后,切换语言之后,界面又重新跳到顶端.
不知道咋整了.
已经凌晨了,无奈下班打车回家了.
第二天要出版本了,都考虑摒弃ClickableSpan了.
但这时候有新发现, 大家应该知道Activity有以下方法:
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
}
同样Fragment以下方法:
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
}
于是我把savedInstanceState打印出来,对比正常和出问题时的log:
正常log:
11-30 04:58:30.672 D/Translator(15708): onViewStateRestored: LostFragment{28bcd0c #0 id=0x7f0800f8 android:switcher:2131230968:1},
savedInstanceState: Bundle[{android:view_state={2131230754=android.view.AbsSavedState$1@efa6742,
2131230755=android.view.AbsSavedState$1@efa6742,
2131230757=android.view.AbsSavedState$1@efa6742,
2131230775=android.view.AbsSavedState$1@efa6742, ...
...
2131230922=android.support.v7.widget.RecyclerView$SavedState@1482655, 2131230946=android.view.AbsSavedState$1@efa6742,
2131230950=android.view.AbsSavedState$1@efa6742,
出问题时候
11-30 04:59:03.030 D/Translator(15708): onViewStateRestored: LostFragment{759c4b2 #0 id=0x7f0800f8 android:switcher:2131230968:1},
savedInstanceState: Bundle[{android:view_state={2131230754=android.view.AbsSavedState$1@efa6742,
2131230755=android.view.AbsSavedState$1@efa6742, 2131230757=android.view.AbsSavedState$1@efa6742,
// 异样的地方
2131230775=TextView.SavedState{ed32c03 start=30 end=40 text=Netzwerk, nicht bereit, Bitte überprüfen sie die WLAN - konfiguration Oder Sim - karte.},
2131230802=android.view.AbsSavedState$1@efa6742,
...
2131230950=android.view.AbsSavedState$1@efa6742, 2131230951=android.view.AbsSavedState$1@efa6742}}]
正常:2131230755=android.view.AbsSavedState$1@efa6742
出问题: 2131230775=TextView.SavedState{ed32c03 start=30 end=40 text=Netzwerk, nicht bereit, Bitte überprüfen sie die WLAN - konfiguration Oder Sim - karte.}
2131230755这个数字你一看应该就大概猜到它是什么了,转成16进制: 0x7f080037. 没错它就是R.id.check_net的值.
我们看下savedInstanceState
savedInstanceState: Bundle[{android:view_state={
2131230754=android.view.AbsSavedState$1@efa6742, 2131230755=android.view.AbsSavedState$1@efa6742, 2131230757=android.view.AbsSavedState$1@efa6742,
2131230775=TextView.SavedState{ed32c03 start=30 end=40 text=Netzwerk, nicht bereit, Bitte überprüfen sie die WLAN - konfiguration Oder Sim - karte.},
2131230802=android.view.AbsSavedState$1@efa6742,
2131230821=android.view.AbsSavedState$1@efa6742,
...
2131230950=android.view.AbsSavedState$1@efa6742, 2131230951=android.view.AbsSavedState$1@efa6742}}]
2017-12-02 21-34-32屏幕截图.png
这个是key值, 它的值是mSavedViewState
public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner {
...
SparseArray<Parcelable> mSavedViewState;
所以问题原因清楚了:
onViewStateRestored 回调中给TextView设置了savedInstanceState中保存的值,因为onViewStateRestored是在onViewCreated之后执行的.
解决方法如下:
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (savedInstanceState != null) {
SparseArray sparseArray = savedInstanceState.getSparseParcelableArray("android:view_state");
if (sparseArray != null) {
Object savedState = sparseArray.get(R.id.check_net);
if (savedState != null && savedState instanceof TextView.SavedState) {
/////////////////////////////////////////////////
// 重新设置TextView的内容//////
////////////////////////////////////////////////
}
}
}
}
所谓的视图残留其实正是Android的视图内容恢复机制.
这个对于ListView而言非常重要,可以跳到用户之前阅读的位置,当然也会带来类似我遇到的困扰.