1. 场景
在使用Activity时候,当切换深色模式,调整系统字体大小等操作后,默认情况会触发Activity的重建.
有一些场景,需要避免Activity的重建:
比如Activity和后台Service已经绑定,要保证后台Service一直运行;
比如当前展示了Dialog,要保证系统配置变更后,Dialog依然展示;
比如Activity要恢复UI需要保存大量的数据;
2. 如何实现系统配置变更后,Activity不重建情况下实现UI自动刷新
1. 一般实现方式
- 在Manifest对应的Activity中声明 android:configChanges属性.
android:configChanges="locale***"
- 在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. 上述实现方式的问题
- 最直观看,代码量太大.比如界面中100个控件,则Activity代码会有巨大的增加.
- 并不是所有系统配置的变更都需要更新UI.在进行UI刷新前,应该进行校验.
- 有些UI控件要刷新UI,比如TextView的文字颜色是1个selector,直接引用R.color.tc是不够的,需要转换成对应的ColorStateList实例后设置才有效.这样会导致代码进一步膨胀.
3. 如何一定程度上解决上述问题
- 将UI刷新前的校验逻辑抽取出来,不在Activity中书写.
- 使用映射工具,将不同的View实例和其对应的UI属性进行保存.
- 其他
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. 目前实现方式的局限
- 在MVVM架构模式下不方便使用
- 不能使用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>