Activity在配置变更后刷新UI的思路及初步实现

2,603 阅读4分钟

1. 场景

在使用Activity时候,当切换深色模式,调整系统字体大小等操作后,默认情况会触发Activity的重建.

有一些场景,需要避免Activity的重建:
比如Activity和后台Service已经绑定,要保证后台Service一直运行;
比如当前展示了Dialog,要保证系统配置变更后,Dialog依然展示;
比如Activity要恢复UI需要保存大量的数据;

2. 如何实现系统配置变更后,Activity不重建情况下实现UI自动刷新

1. 一般实现方式

  1. 在Manifest对应的Activity中声明 android:configChanges属性.
android:configChanges="locale***"
  1. 在Activity的 onConfigurationChanged 回调中,执行对应控件的UI刷新逻辑
@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    getResources().updateConfiguration(newConfig, getResources().getDisplayMetrics());
    //刷新指定控件
    v.setLayoutDirection(newConfig.getLayoutDirection());
    v.setTextLocale(Locale.getDefault());
    v.setTextSize(TypedValue.COMPLEX_UNIT_SP,16);
    v.setText(R.string.s1);
}

2. 上述实现方式的问题

  1. 最直观看,代码量太大.比如界面中100个控件,则Activity代码会有巨大的增加.
  2. 并不是所有系统配置的变更都需要更新UI.在进行UI刷新前,应该进行校验.
  3. 有些UI控件要刷新UI,比如TextView的文字颜色是1个selector,直接引用R.color.tc是不够的,需要转换成对应的ColorStateList实例后设置才有效.这样会导致代码进一步膨胀.

3. 如何一定程度上解决上述问题

  1. 将UI刷新前的校验逻辑抽取出来,不在Activity中书写.
  2. 使用映射工具,将不同的View实例和其对应的UI属性进行保存.
  3. 其他

3. 工具类初步实现/更多控件类型待补充

1. SuperWeakReference .用于持有指定View实例,作为key存储于对应映射

import android.view.View;

import java.lang.ref.WeakReference;

public class SuperWeakReference extends WeakReference<View> {
    private View currView;

    public SuperWeakReference(View referent) {
        super(referent);
        this.currView = referent;
    }

    /**
     * 重写equals,用于HashMap的get,contains等方法
     *
     * @param obj
     * @return
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        } else if (obj == null) {
            return false;
        } else if (!(obj instanceof SuperWeakReference)) {
            return false;
        } else {
            SuperWeakReference ori = (SuperWeakReference) obj;
            return ori.get() != null && ori.get() == currView;
        }
    }

    /**
     * 重写hashCode,用于HashMap的get,contains等方法
     *
     * @return
     */
    @Override
    public int hashCode() {
        return (int) (currView == null ? -1L : currView.getId());
    }
}

2. ViewStateManager . View及其UI属性管理器.用于不同View实例UI属性的保存及配置变更后UI刷新.

import android.app.Activity;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.view.View;
import android.widget.TextView;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

public class ViewStateManager {
    private final int DEFAULT = -1;
    //持有Activity实例的弱引用
    private WeakReference<Activity> context;
    //Activity初始配置值
    private boolean isRtl = false;
    private float fontScale;
    private Locale locale;
    private boolean isDarkMode = false;
    private Map<SuperWeakReference, ViewState> viewStateContainer = new HashMap<SuperWeakReference, ViewState>();

    public ViewStateManager(Activity activity) {
        this.context = new WeakReference<>(activity);
        initActivityConfig(activity);
    }

    /**
     * 初始化当前系统配置
     *
     * @param activity
     */
    private void initActivityConfig(Activity activity) {
        Configuration configuration = activity.getResources().getConfiguration();
        isRtl = configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
        fontScale = configuration.fontScale;
        locale = configuration.locale;
        isDarkMode = (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
    }

    /**
     * 根据View实例获取其关联的ViewState实例
     *
     * @param view
     * @return
     */
    private ViewState gainViewState(View view) {
        SuperWeakReference reference = new SuperWeakReference(view);
        if (viewStateContainer.containsKey(reference)) {
            return viewStateContainer.get(reference);
        }
        ViewState viewState = new ViewState();
        viewStateContainer.put(reference, viewState);
        return viewState;
    }

    /**
     * 检查持有的Activity实例是否处于'可操作'状态
     *
     * @return
     */
    public boolean activityIsAlive() {
        try {
            return context != null && context.get() != null && !context.get().isFinishing() && !context.get().isDestroyed();
        } catch (Exception e) {
            return false;
        }
    }

    public void setTextSize(int viewId, int resId) {
        if (activityIsAlive()) {
            View view = context.get().findViewById(viewId);
            if (view != null && view instanceof TextView) {
                setTextSize((TextView) view, resId);
            }
        }
    }

    public void setTextSize(TextView textView, int resId) {
        if (activityIsAlive()) {
            float textSize = DisplayUtils.px2sp(textView.getContext(), textView.getResources().getDimensionPixelSize(resId));
            textView.setTextSize(textSize);
            gainViewState(textView).textSize = textSize;
        }
    }

    public void setTextColor(int viewId, int resId) {
        if (activityIsAlive()) {
            View view = context.get().findViewById(viewId);
            if (view != null && view instanceof TextView) {
                setTextColor((TextView) view, resId);
            }
        }
    }

    public void setTextColor(TextView textView, int resId) {
        if (activityIsAlive()) {
            ColorStateList colorStateList = textView.getResources().getColorStateList(resId);
            textView.setTextColor(colorStateList);
            gainViewState(textView).textColor = resId;
        }
    }

    public void setText(int viewId, int resId) {
        if (activityIsAlive()) {
            View view = context.get().findViewById(viewId);
            if (view != null && view instanceof TextView) {
                setText((TextView) view, resId);
            }
        }
    }

    public void setText(TextView textView, int resId) {
        if (activityIsAlive()) {
            textView.setText(resId);
            gainViewState(textView).text = resId;
        }
    }

    /**
     * 设置指定TextView的文字尺寸,颜色,显示内容, 并缓存各属性对应的资源ID/值
     *
     * @param viewId         待设置的TextView实例对应的ID
     * @param textSizeResId  dimen资源ID
     * @param textColorResId color或selector资源ID
     * @param textResId      string资源ID
     */
    public void setTextAttrs(int viewId, int textSizeResId, int textColorResId, int textResId) {
        if (activityIsAlive()) {
            View view = context.get().findViewById(viewId);
            if (view != null && view instanceof TextView) {
                setTextAttrs((TextView) view, textSizeResId, textColorResId, textResId);
            }
        }
    }

    /**
     * 设置指定TextView的文字尺寸,颜色,显示内容, 并缓存各属性对应的资源ID/值
     *
     * @param textView       待设置的TextView实例
     * @param textSizeResId  dimen资源ID
     * @param textColorResId color或selector资源ID
     * @param textResId      string资源ID
     */
    public void setTextAttrs(TextView textView, int textSizeResId, int textColorResId, int textResId) {
        setTextSize(textView, textSizeResId);
        setTextColor(textView, textColorResId);
        setText(textView, textResId);
    }

    /**
     * 通过之前该TextView实例缓存过的属性,刷新其UI
     *
     * @param textView
     * @param viewState
     */
    private void refreshTextView(TextView textView, ViewState viewState) {
        textView.setTextLocale(locale);
        if (viewState.textSize > 0) {
            textView.setTextSize(viewState.textSize);
        }
        if (viewState.textColor > 0) {
            ColorStateList colorStateList = textView.getResources().getColorStateList(viewState.textColor);
            textView.setTextColor(colorStateList);
        }
        if (viewState.text > 0) {
            textView.setText(viewState.text);
        }
    }

    /**
     * 通过之前该View实例缓存过的属性,刷新其UI
     *
     * @param view
     * @param viewState
     */
    private void refreshView(View view, ViewState viewState) {
        if (view instanceof TextView) {
            refreshTextView((TextView) view, viewState);
        }
    }

    /**
     * Activity配置发生变更,需要刷新所有保存过的View的UI
     */
    private void refreshUiAsConfigChange() {
        if (activityIsAlive() && !viewStateContainer.isEmpty()) {
            Set<Map.Entry<SuperWeakReference, ViewState>> entrySet = viewStateContainer.entrySet();
            for (Map.Entry<SuperWeakReference, ViewState> item : entrySet) {
                if (item.getKey() != null && item.getKey().get() != null && item.getValue() != null) {
                    refreshView(item.getKey().get(), item.getValue());
                }
            }
        }
    }

    /**
     * 用于随时释放资源
     */
    public void release() {
        viewStateContainer.clear();
        context.clear();
    }

    /**
     * 用于Activity销毁时释放资源
     */
    public void onDestroy() {
        release();
    }

    /**
     * 在Activity的onConfigurationChanged中调用
     * @param newConfig
     */
    public void onConfigurationChanged(Configuration newConfig) {
        if (activityIsAlive()) {
            Resources resources = this.context.get().getResources();
            resources.updateConfiguration(newConfig, resources.getDisplayMetrics());
            boolean currIsRtl = resources.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
            float currFontScale = resources.getConfiguration().fontScale;
            boolean currDarkMode = (resources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
            if (isRtl == currIsRtl && fontScale == currFontScale && Locale.getDefault().equals(locale) && isDarkMode == currDarkMode) {
                //系统配置虽然发生变更,但和UI刷新对应的配置未改变,无需刷新View的UI
                return;
            }
            isRtl = currIsRtl;
            fontScale = currFontScale;
            locale = Locale.getDefault();
            isDarkMode = currDarkMode;
            //刷新所有保存过的View的UI
            refreshUiAsConfigChange();
        }
    }

    /**
     * 和指定的View实例进行关联,缓存指定View实例的UI相关属性
     */
    private class ViewState {
        /*TextView相关*/
        //以sp为单位的文字大小值
        private float textSize = DEFAULT;
        //文字颜色关联的颜色ID
        private int textColor = DEFAULT;
        //文字内容关联的字符串ID
        private int text = DEFAULT;
    }
}

3. 目前实现方式的局限

  1. 在MVVM架构模式下不方便使用
  2. 不能使用View原始方法设置属性,需要经过ViewStateManager实例.有一定习惯成本.

4. 工具类使用步骤

1. Activity声明android:configChanges属性.

2. Activity中创建ViewStateManager实例并使用.

public class T1Activity extends BaseActivity {
    private TextView tv1,tv2,tv3;
    private ViewStateManager viewStateManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_t1);
        tv1 = findViewById(R.id.tv1);
        tv2 = findViewById(R.id.tv2);
        tv3 = findViewById(R.id.tv3);
        viewStateManager = new ViewStateManager(this);
        viewStateManager.setTextAttrs(tv1,R.dimen.textSize1,R.color.tc1,R.string.s1);
        viewStateManager.setTextAttrs(tv2,R.dimen.textSize2,R.color.tc2,R.string.s2);
        viewStateManager.setTextColor(tv3,R.color.cs1);
        viewStateManager.setText(tv3,R.string.s3);
    }

    @Override
    public void onConfigurationChanged(@NonNull Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        //调用ViewStateManager实例的onConfigurationChanged方法,尝试刷新多个View的UI
        viewStateManager.onConfigurationChanged(newConfig);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //调用ViewStateManager实例的onDestroy方法,释放资源
        viewStateManager.onDestroy();
    }
}
<dimen name="textSize1">24sp</dimen>
<dimen name="textSize2">28sp</dimen>

values及values-night中颜色不同:
<color name="cs1">#0000FF</color>
<color name="cs1">#FF0000</color>

R.color.tc1:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/cc1" android:state_pressed="false"/>
    <item android:color="@color/cc2" android:state_pressed="true"/>
</selector>

R.color.tc2:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/cc3" android:state_pressed="false"/>
    <item android:color="@color/cc4" android:state_pressed="true"/>
</selector>

不同语言下各字符串的翻译:
<string name="s1">S1</string>
<string name="s2">S2</string>
<string name="s3">S3</string>

<string name="s1">مطلوب إلى الحفلة</string>
<string name="s2">م لشغي</string>
<string name="s3">رجىi-i</string>