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而言非常重要,可以跳到用户之前阅读的位置,当然也会带来类似我遇到的困扰.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348