1.简介
简单学习语言切换页面相关的功能
1.1.预览图
这里学习下第一个即可,也就是系统语言的修改。 布局如下
1.2.language_and_input.xml
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/language_settings">
<PreferenceCategory
android:key="languages_category"
android:title="@string/locale_picker_category_title">
<Preference
android:key="phone_language"
android:title="@string/phone_language"
android:fragment="com.android.settings.localepicker.LocaleListEditor" />
<Preference
android:key="apps_language"
android:title="@string/app_locales_picker_menu_title"
android:summary="@string/app_locale_picker_summary"
android:fragment="com.android.settings.applications.manageapplications.ManageApplications"
settings:controller="com.android.settings.applications.appinfo.ManageAppLocalePreferenceController">
<extra
android:name="classname"
android:value="com.android.settings.applications.appinfo.AppLocaleDetails" />
</Preference>
</PreferenceCategory>
<PreferenceCategory
android:key="keyboards_category"
android:title="@string/keyboard_and_input_methods_category">
<Preference
android:key="virtual_keyboard_pref"
android:title="@string/virtual_keyboard_category"
android:fragment="com.android.settings.inputmethod.AvailableVirtualKeyboardFragment"
settings:keywords="@string/keywords_virtual_keyboard"/>
<Preference
android:key="physical_keyboard_pref"
android:title="@string/physical_keyboard_title"
android:summary="@string/summary_placeholder"
android:fragment="com.android.settings.inputmethod.PhysicalKeyboardFragment"/>
</PreferenceCategory>
<PreferenceCategory
android:key="speech_category"
android:title="@string/speech_category_title">
<com.android.settings.widget.GearPreference
android:key="voice_input_settings"
android:title="@string/voice_input_settings_title"
android:fragment="com.android.settings.language.DefaultVoiceInputPicker" />
<Preference
android:key="tts_settings_summary"
android:title="@string/tts_settings_title"
android:fragment="com.android.settings.tts.TextToSpeechSettings"
settings:searchable="false"/>
</PreferenceCategory>
<PreferenceCategory
android:key="input_assistance_category"
android:title="@string/input_assistance">
<!-- Spell checker preference title, summary and fragment will be set programmatically. -->
<!-- Note: Mark this as persistent="false" to remove unnecessarily saved shared preference.
See: InputMethodAndSubtypeUtil.removeUnnecessaryNonPersistentPreference. -->
<Preference
android:key="spellcheckers_settings"
android:title="@string/spellcheckers_settings_title"
android:persistent="false"
android:fragment="com.android.settings.inputmethod.SpellCheckersSettings" />
<!-- User dictionary preference title and fragment will be set programmatically. -->
<Preference
android:key="key_user_dictionary_settings"
android:title="@string/user_dict_settings_title"
android:summary="@string/user_dict_settings_summary"
android:fragment="com.android.settings.inputmethod.UserDictionaryList"
settings:controller="com.android.settings.language.UserDictionaryPreferenceController" />
</PreferenceCategory>
<PreferenceCategory
android:key="pointer_category"
android:layout="@layout/preference_category_no_label">
<com.android.settings.PointerSpeedPreference
android:key="pointer_speed"
android:title="@string/pointer_speed"
android:dialogTitle="@string/pointer_speed" />
</PreferenceCategory>
<SwitchPreference
android:key="vibrate_input_devices"
android:title="@string/vibrate_input_devices"
android:summary="@string/vibrate_input_devices_summary"
settings:controller="com.android.settings.inputmethod.GameControllerPreferenceController" />
<com.android.settings.widget.WorkOnlyCategory
android:key="language_and_input_for_work_category"
android:title="@string/language_and_input_for_work_category_title"
settings:searchable="false">
<Preference
android:key="spellcheckers_settings_for_work_pref"
android:title="@string/spellcheckers_settings_for_work_title"
android:fragment="com.android.settings.inputmethod.SpellCheckersSettings"
settings:forWork="true"
settings:controller="com.android.settings.core.WorkPreferenceController" />
<Preference
android:key="user_dictionary_settings_for_work_pref"
android:title="@string/user_dict_settings_for_work_title"
android:fragment="com.android.settings.inputmethod.UserDictionaryList"
settings:forWork="true"
settings:controller="com.android.settings.inputmethod.SpellCheckerForWorkPreferenceController" />
</com.android.settings.widget.WorkOnlyCategory>
</PreferenceScreen>
2.LocaleListEditor
2.1.locale_order_list.xml
加载的布局如下,一个列表,底部有个按钮
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutDirection="locale"
android:textDirection="locale">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="true"
android:clipChildren="true"
android:orientation="vertical">
<com.android.settings.localepicker.LocaleRecyclerView
android:id="@+id/dragList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"/>
<Button
android:id="@+id/add_language"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeight"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:drawableStart="@drawable/ic_add_24dp"
android:drawablePadding="32dp"
android:textAlignment="textStart"
android:text="@string/add_a_language"
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:textAppearance="?android:attr/textAppearanceListItem"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
>1.效果图
2.2.onCreateView
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
LocaleStore.fillCache(this.getContext());
final List<LocaleStore.LocaleInfo> feedsList = getUserLocaleList();
//adapter
mAdapter = new LocaleDragAndDropAdapter(this.getContext(), feedsList);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstState) {
final View result = super.onCreateView(inflater, container, savedInstState);
//加载布局
final View myLayout = inflater.inflate(R.layout.locale_order_list, (ViewGroup) result);
configureDragAndDrop(myLayout);//补充1
return result;
}
>1.configureDragAndDrop
private void configureDragAndDrop(View view) {
final RecyclerView list = view.findViewById(R.id.dragList);
final LocaleLinearLayoutManager llm = new LocaleLinearLayoutManager(getContext(), mAdapter);
llm.setAutoMeasureEnabled(true);
list.setLayoutManager(llm);
list.setHasFixedSize(true);
mAdapter.setRecyclerView(list);
list.setAdapter(mAdapter);
//老代码是自定义的rv,里边重写的touch方法,这里是新代码
list.setOnTouchListener(this);//列表的触摸事件见2.3
mAddLanguage = view.findViewById(R.id.add_language);
mAddLanguage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//跳转添加语言页面
final Intent intent = new Intent(getActivity(),
LocalePickerWithRegionActivity.class);
//回调见补充2
startActivityForResult(intent, REQUEST_LOCALE_PICKER);
}
});
}
>2.onActivityResult
添加了新的语言的回调
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_LOCALE_PICKER && resultCode == Activity.RESULT_OK
&& data != null) {
final LocaleStore.LocaleInfo locale =
(LocaleStore.LocaleInfo) data.getSerializableExtra(
INTENT_LOCALE_KEY);
mAdapter.addLocale(locale);//见3.1刷新列表
updateVisibilityOfRemoveMenu();
}
super.onActivityResult(requestCode, resultCode, data);
}
2.3.onTouch
可以看到,点击列表就会进行数据更新,或者弹框提示语言不匹配
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP
|| event.getAction() == MotionEvent.ACTION_CANCEL) {
showConfirmDialog(false, mAdapter.getFeedItemList().get(0));
}
return false;
}
public void showConfirmDialog(boolean isFirstRemoved, LocaleStore.LocaleInfo localeInfo) {
//数据见3.2.2
Locale currentSystemLocale = LocalePicker.getLocales().get(0);
//判断列表的第一个数据是否和当前系统的一样,不一样给与提示
if (!localeInfo.getLocale().equals(currentSystemLocale)) {
final LocaleDialogFragment localeDialogFragment =
LocaleDialogFragment.newInstance();
Bundle args = new Bundle();
args.putInt(LocaleDialogFragment.ARG_DIALOG_TYPE, DIALOG_CONFIRM_SYSTEM_DEFAULT);
args.putSerializable(LocaleDialogFragment.ARG_TARGET_LOCALE,
isFirstRemoved ? LocaleStore.getLocaleInfo(currentSystemLocale) : localeInfo);
localeDialogFragment.setArguments(args);
localeDialogFragment.show(mFragmentManager, TAG_DIALOG_CONFIRM_SYSTEM_DEFAULT);
} else {
//一样的话进行更新,见3.4
mAdapter.doTheUpdate();
}
}
3.LocaleDragAndDropAdapter.java
3.1.addLocale
void addLocale(LocaleStore.LocaleInfo li) {
mFeedItemList.add(li);
notifyItemInserted(mFeedItemList.size() - 1);//刷新数据
doTheUpdate();//更新local,见补充1
}
>1.doTheUpdate
刷新数据,有4处调用。除了上边的addLocal,还有拖拽以后,
public void doTheUpdate() {
int count = mFeedItemList.size();
final Locale[] newList = new Locale[count];
for (int i = 0; i < count; i++) {
final LocaleStore.LocaleInfo li = mFeedItemList.get(i);
newList[i] = li.getLocale();
}
final LocaleList ll = new LocaleList(newList);
updateLocalesWhenAnimationStops(ll);//补充2
}
>2.updateLocalesWhenAnimationStops
public void updateLocalesWhenAnimationStops(final LocaleList localeList) {
if (localeList.equals(mLocalesToSetNext)) {
return;
}
// This will only update the Settings application to make things feel more responsive,
// the system will be updated later, when animation stopped.
//设置默认的localList
LocaleList.setDefault(localeList);
mLocalesToSetNext = localeList;
final RecyclerView.ItemAnimator itemAnimator = mParentView.getItemAnimator();
itemAnimator.isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
@Override
public void onAnimationsFinished() {
if (mLocalesToSetNext == null || mLocalesToSetNext.equals(mLocalesSetLast)) {
// All animations finished, but the locale list did not change
return;
}
//更新localList,参考3.2.1
LocalePicker.updateLocales(mLocalesToSetNext);
mLocalesSetLast = mLocalesToSetNext;
//更新快捷方式,语言变了,名字也变了,参考补充3
new ShortcutsUpdateTask(mContext).execute();
mLocalesToSetNext = null;
mNumberFormatter = NumberFormat.getNumberInstance(Locale.getDefault());
}
});
}
>3.ShortcutsUpdateTask
static final String SHORTCUT_ID_PREFIX = "component-shortcut-";
static final Intent SHORTCUT_PROBE = new Intent(Intent.ACTION_MAIN)
.addCategory("com.android.settings.SHORTCUT")
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
static final String SHORTCUT_ID_PREFIX = "component-shortcut-";
public Void doInBackground(Void... params) {
ShortcutManager sm = mContext.getSystemService(ShortcutManager.class);
PackageManager pm = mContext.getPackageManager();
List<ShortcutInfo> updates = new ArrayList<>();
//就是我们长按settings的图标,弹出的可选的快捷方式,你拖动到桌面以后这里就有数据了
//好像返回的数据只有settings的,其他app的快捷方式不算?
for (ShortcutInfo info : sm.getPinnedShortcuts()) {
//只处理特殊的id
if (!info.getId().startsWith(SHORTCUT_ID_PREFIX)) {
continue;
}
ComponentName cn = ComponentName.unflattenFromString(
info.getId().substring(SHORTCUT_ID_PREFIX.length()));
ResolveInfo ri = pm.resolveActivity(new Intent(SHORTCUT_PROBE).setComponent(cn), 0);
if (ri == null) {
continue;
}
updates.add(new ShortcutInfo.Builder(mContext, info.getId())
.setShortLabel(ri.loadLabel(pm)).build());
}
if (!updates.isEmpty()) {
sm.updateShortcuts(updates);
}
return null;
}
3.2.LocalePicker.java
>1.updateLocales
public static void updateLocales(LocaleList locales) {
if (locales != null) {
locales = removeExcludedLocales(locales);
}
try {
final IActivityManager am = ActivityManager.getService();
final Configuration config = new Configuration();
config.setLocales(locales);
config.userSetLocale = true;
am.updatePersistentConfigurationWithAttribution(config,
ActivityThread.currentOpPackageName(), null);
// Trigger the dirty bit for the Settings Provider.
BackupManager.dataChanged("com.android.providers.settings");
} catch (RemoteException e) {
// Intentionally left blank
}
}
>2.getLocales
public static LocaleList getLocales() {
try {
return ActivityManager.getService()
.getConfiguration().getLocales();
} catch (RemoteException e) {
return LocaleList.getDefault();
}
}
3.3.mItemTouchHelper
拖拽功能利用的是系统的ItemTouchHelper
public LocaleDragAndDropAdapter(Context context, List<LocaleStore.LocaleInfo> feedItemList) {
mFeedItemList = feedItemList;
mContext = context;
final float dragElevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8,
context.getResources().getDisplayMetrics());
mItemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0 /* no swipe */) {
public void setRecyclerView(RecyclerView rv) {
mParentView = rv;
mItemTouchHelper.attachToRecyclerView(rv);
}
3.4.doTheUpdate
public void doTheUpdate() {
//获取当前页面的local数据
int count = mFeedItemList.size();
final Locale[] newList = new Locale[count];
for (int i = 0; i < count; i++) {
final LocaleStore.LocaleInfo li = mFeedItemList.get(i);
newList[i] = li.getLocale();
}
//更新数据
final LocaleList ll = new LocaleList(newList);
updateLocalesWhenAnimationStops(ll);//见补充1
}
>1.updateLocalesWhenAnimationStops
public void updateLocalesWhenAnimationStops(final LocaleList localeList) {
if (localeList.equals(mLocalesToSetNext)) {
return;
}
// This will only update the Settings application to make things feel more responsive,
// the system will be updated later, when animation stopped.
LocaleList.setDefault(localeList);
mLocalesToSetNext = localeList;
final RecyclerView.ItemAnimator itemAnimator = mParentView.getItemAnimator();
itemAnimator.isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
@Override
public void onAnimationsFinished() {
if (mLocalesToSetNext == null || mLocalesToSetNext.equals(mLocalesSetLast)) {
// All animations finished, but the locale list did not change
return;
}
LocalePicker.updateLocales(mLocalesToSetNext);
mLocalesSetLast = mLocalesToSetNext;
new ShortcutsUpdateTask(mContext).execute();
mLocalesToSetNext = null;
mNumberFormatter = NumberFormat.getNumberInstance(Locale.getDefault());
}
});
}
3.5.整理
>1.settings里的代码
修改系统语言,最终就执行了下边的操作。
LocaleList locales = xxx;
LocaleList.setDefault(localeList);
final IActivityManager am = ActivityManager.getService();
final Configuration config = new Configuration();
config.setLocales(locales);
config.userSetLocale = true;//这个很重要
//见4.1
am.updatePersistentConfigurationWithAttribution(config,
ActivityThread.currentOpPackageName(), null);
>1.反射逻辑
另外需要是系统应用,并且有对应的权限
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
正常代码如下,用反射处理,毕竟settings里的代码里的类我们用不了
public static void change(Locale locale) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
try {
//开头这堆代码目的是为了保留旧的语言列表
LocaleList old = LocaleList.getDefault();
int size = old.size();
Locale[] newLocales = new Locale[size + 1];
newLocales[0] = locale;//把要改变的语言放在第一位
for (int i = 0; i < size; i++) {
newLocales[i + 1] = old.get(i);//旧语言放在后边。
}
//不用担心重复的语言数据,构造方法里会过滤的
LocaleList localeList = new LocaleList(newLocales);
Method method = ActivityManager.class.getDeclaredMethod("getService");
method.setAccessible(true);
Object obj = method.invoke(null);
method = obj.getClass().getDeclaredMethod("getConfiguration");
method.setAccessible(true);
final Configuration config = (Configuration) method.invoke(obj);
config.setLocales(localeList);
Field field = Configuration.class.getDeclaredField("userSetLocale");
field.setAccessible(true);
//这个很重要,可以刷新默认语言
field.set(config, true);
method = obj.getClass().getDeclaredMethod("updatePersistentConfiguration", Configuration.class);
method.setAccessible(true);
method.invoke(obj, config);
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.ActivityManagerService.java
public class ActivityManagerService extends IActivityManager.Stub
4.1.updatePersistentConfiguration
// public static final String CHANGE_CONFIGURATION = "android.permission.CHANGE_CONFIGURATION";
public void updatePersistentConfiguration(Configuration values) {
updatePersistentConfigurationWithAttribution(values,
Settings.getPackageNameForUid(mContext, Binder.getCallingUid()), null);
}
@Override
public void updatePersistentConfigurationWithAttribution(Configuration values,
String callingPackage, String callingAttributionTag) {
//权限检查
enforceCallingPermission(CHANGE_CONFIGURATION, "updatePersistentConfiguration()");
enforceWriteSettingsPermission("updatePersistentConfiguration()", callingPackage,
callingAttributionTag);
if (values == null) {
throw new NullPointerException("Configuration must not be null");
}
int userId = UserHandle.getCallingUserId();
//交给5.1处理
mActivityTaskManager.updatePersistentConfiguration(values, userId);
}
5.ActivityTaskManagerService.java
5.1.updatePersistentConfiguration
public void updatePersistentConfiguration(Configuration values, @UserIdInt int userId) {
final long origId = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
// Window configuration is unrelated to persistent configuration (e.g. font scale,
// locale). Unset it to avoid affecting the current display configuration.
values.windowConfiguration.setToDefaults();
//补充1
updateConfigurationLocked(values, null, false, true, userId,
false /* deferResume */);
}
} finally {
Binder.restoreCallingIdentity(origId);
}
}
>1.updateConfigurationLocked
boolean updateConfigurationLocked(Configuration values, ActivityRecord starting,
boolean initLocale, boolean persistent, int userId, boolean deferResume,
ActivityTaskManagerService.UpdateConfigurationResult result) {
int changes = 0;
boolean kept = true;
deferWindowLayout();
try {
if (values != null) {
//见5.2
changes = updateGlobalConfigurationLocked(values, initLocale, persistent, userId);
}
if (!deferResume) {
kept = ensureConfigAndVisibilityAfterUpdate(starting, changes);
}
} finally {
continueWindowLayout();
}
if (result != null) {
result.changes = changes;
result.activityRelaunched = !kept;
}
return kept;
}
5.2.updateGlobalConfigurationLocked
int updateGlobalConfigurationLocked(@NonNull Configuration values, boolean initLocale,
boolean persistent, int userId) {
//先获取当前的配置,见补充1
mTempConfig.setTo(getGlobalConfiguration());
//更新配置并返回修改项的flag
final int changes = mTempConfig.updateFrom(values);
if (changes == 0) {
//为0说明配置未发生变化
return 0;
}
if (!initLocale && !values.getLocales().isEmpty() && values.userSetLocale) {
final LocaleList locales = values.getLocales();
int bestLocaleIndex = 0;
if (locales.size() > 1) {
if (mSupportedSystemLocales == null) {
mSupportedSystemLocales = Resources.getSystem().getAssets().getLocales();
}
bestLocaleIndex = Math.max(0, locales.getFirstMatchIndex(mSupportedSystemLocales));
}
SystemProperties.set("persist.sys.locale",
locales.get(bestLocaleIndex).toLanguageTag());
LocaleList.setDefault(locales, bestLocaleIndex);
}
mTempConfig.seq = increaseConfigurationSeqLocked();
mUsageStatsInternal.reportConfigurationChange(mTempConfig, mAmInternal.getCurrentUserId());
// TODO: If our config changes, should we auto dismiss any currently showing dialogs?
updateShouldShowDialogsLocked(mTempConfig);
AttributeCache ac = AttributeCache.instance();
if (ac != null) {
ac.updateConfiguration(mTempConfig);
}
mTempConfig.seq = increaseConfigurationSeqLocked();
Slog.i(TAG, "Config changes=" + Integer.toHexString(changes) + " " + mTempConfig);
// TODO(multi-display): Update UsageEvents#Event to include displayId.
mUsageStatsInternal.reportConfigurationChange(mTempConfig, mAmInternal.getCurrentUserId());
// TODO: If our config changes, should we auto dismiss any currently showing dialogs?
updateShouldShowDialogsLocked(mTempConfig);
AttributeCache ac = AttributeCache.instance();
if (ac != null) {
ac.updateConfiguration(mTempConfig);
}
// Make sure all resources in our process are updated right now, so that anyone who is going
// to retrieve resource values after we return will be sure to get the new ones. This is
// especially important during boot, where the first config change needs to guarantee all
// resources have that config before following boot code is executed.
mSystemThread.applyConfigurationToResources(mTempConfig);
if (persistent && Settings.System.hasInterestingConfigurationChanges(changes)) {
final Message msg = PooledLambda.obtainMessage(
ActivityTaskManagerService::sendPutConfigurationForUserMsg,
this, userId, new Configuration(mTempConfig));
mH.sendMessage(msg);
}
SparseArray<WindowProcessController> pidMap = mProcessMap.getPidMap();
for (int i = pidMap.size() - 1; i >= 0; i--) {
final int pid = pidMap.keyAt(i);
final WindowProcessController app = pidMap.get(pid);
app.onConfigurationChanged(mTempConfig);
}
final Message msg = PooledLambda.obtainMessage(
ActivityManagerInternal::broadcastGlobalConfigurationChanged,
mAmInternal, changes, initLocale);
mH.sendMessage(msg);
// Update stored global config and notify everyone about the change.
mRootWindowContainer.onConfigurationChanged(mTempConfig);
return changes;
}
>1.getGlobalConfiguration
Configuration getGlobalConfiguration() {
// Return default configuration before mRootWindowContainer initialized, which happens
// while initializing process record for system, see {@link
// ActivityManagerService#setSystemProcess}.
return mRootWindowContainer != null ? mRootWindowContainer.getConfiguration()
: new Configuration();
}