问题背景

【Android】Tencent IM Demo学习Activity使用语言切换的适配整个App(采用广播)_android studio


在对Tentcent的IM Demo 进行操作的过程中,想了解一下如何对整个的语言环境进行配置。如上图所示。

基本源码

首先会将该语言切换的Activity上面的所有代码post出来,但是如果想直接看详解可以直接跳过该部分。

该Activity的所有代码

package com.tencent.qcloud.tim.demo.login;

import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.RecyclerView;

import com.tencent.qcloud.tim.demo.R;
import com.tencent.qcloud.tuicore.TUIThemeManager;
import com.tencent.qcloud.tuicore.component.CustomLinearLayoutManager;
import com.tencent.qcloud.tuicore.component.TitleBarLayout;
import com.tencent.qcloud.tuicore.component.activities.BaseLightActivity;
import com.tencent.qcloud.tuicore.component.interfaces.ITitleBarLayout;
import com.tencent.qcloud.tuicore.util.TUIBuild;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
 * 语言选择;这里根据语言的切换进行自适应,使用android的四大组件之一,广播broadcast进行app的全局告知
 */
public class LanguageSelectActivity extends BaseLightActivity {

    public static final String LANGUAGE = "language";
    public static final String DEMO_LANGUAGE_CHANGED_ACTION = "demoLanguageChangedAction";

    private OnItemClickListener onItemClickListener;
    private TitleBarLayout titleBarLayout;//tencent 自带的一个组件
    private RecyclerView recyclerView;
    private final Map<String, String> languageMap = new HashMap<>();
    private final List<String> languageList = new ArrayList<>();
    private SelectAdapter adapter;
    private String currentLanguage;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_theme_language_select);
        titleBarLayout = findViewById(R.id.demo_select_title_bar);
        recyclerView = findViewById(R.id.theme_recycler_view);
        titleBarLayout.setTitle(getResources().getString(R.string.demo_language_title), ITitleBarLayout.Position.MIDDLE);
        titleBarLayout.getLeftGroup().setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });

        currentLanguage = TUIThemeManager.getInstance().getCurrentLanguage();
        if (TextUtils.isEmpty(currentLanguage)) {
            Locale locale;
            if (TUIBuild.getVersionInt() < Build.VERSION_CODES.N) {
                locale = getResources().getConfiguration().locale;
            } else {
                locale = getResources().getConfiguration().getLocales().get(0);
            }
            currentLanguage = locale.getLanguage();
        }
        adapter = new SelectAdapter();
        initData();

        recyclerView.setAdapter(adapter);
        recyclerView.setLayoutManager(new CustomLinearLayoutManager(this));
        DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);
        dividerItemDecoration.setDrawable(getResources().getDrawable(R.drawable.core_list_divider));
        recyclerView.addItemDecoration(dividerItemDecoration);

        onItemClickListener = new OnItemClickListener() {
            @Override
            public void onClick(String language) {
                if (TextUtils.equals(currentLanguage, language)) {
                    return;
                } else {
                    currentLanguage = language;
                }
                int index = TextUtils.equals(language, "zh") ? 0 : 1;
                adapter.setSelectedItem(index);
                adapter.notifyDataSetChanged();
                TUIThemeManager.getInstance().changeLanguage(LanguageSelectActivity.this, currentLanguage);
                changeTitleLanguage();
                //广播提示
                notifyLanguageChanged();
            }
        };
    }

    private void notifyLanguageChanged() {
        Intent intent = new Intent();
        intent.setAction(DEMO_LANGUAGE_CHANGED_ACTION);
        intent.putExtra(LANGUAGE, currentLanguage);
        LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
    }

    private void changeTitleLanguage() {
        titleBarLayout.setTitle(getResources().getString(R.string.demo_language_title), ITitleBarLayout.Position.MIDDLE);
    }

    private void initData() {
        String simplifiedChinese = "简体中文";
        String english = "English";
        languageList.add(simplifiedChinese);
        languageMap.put(simplifiedChinese, "zh");
        languageList.add(english);
        languageMap.put(english, "en");
        if (TextUtils.equals(currentLanguage, "zh")) {
            adapter.setSelectedItem(0);
        } else {
            adapter.setSelectedItem(1);
        }
    }

    public static void startSelectLanguage(Activity activity) {
        Intent intent = new Intent(activity, LanguageSelectActivity.class);
        activity.startActivity(intent);
    }


    class SelectAdapter extends RecyclerView.Adapter<SelectAdapter.SelectViewHolder> {
        int selectedItem = -1;

        public void setSelectedItem(int selectedItem) {
            this.selectedItem = selectedItem;
        }

        @NonNull
        @Override
        public SelectAdapter.SelectViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(LanguageSelectActivity.this).inflate(R.layout.core_select_item_layout,parent, false);
            return new SelectAdapter.SelectViewHolder(view);
        }

        @Override
        public void onBindViewHolder(@NonNull SelectAdapter.SelectViewHolder holder, int position) {
            String language = languageList.get(position);
            holder.name.setText(language);
            if (selectedItem == position) {
                holder.selectedIcon.setVisibility(View.VISIBLE);
            } else {
                holder.selectedIcon.setVisibility(View.GONE);
            }
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    onItemClickListener.onClick(languageMap.get(language));
                }
            });
        }

        @Override
        public int getItemCount() {
            return languageMap.size();
        }

        class SelectViewHolder extends RecyclerView.ViewHolder {
            TextView name;
            ImageView selectedIcon;
            public SelectViewHolder(@NonNull View itemView) {
                super(itemView);
                name = itemView.findViewById(com.tencent.qcloud.tuicore.R.id.name);
                selectedIcon = itemView.findViewById(com.tencent.qcloud.tuicore.R.id.selected_icon);
            }
        }
    }

    public interface OnItemClickListener {
        void onClick(String language);
    }
}

UI布局文件xml

这里的xml文件其实特简单,tencent他那里自己构建一个uicore的kit类,随时随用;代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:background="#F2F3F5"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.tencent.qcloud.tuicore.component.TitleBarLayout
        android:id="@+id/demo_select_title_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/theme_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

整体的效果如下所示:

【Android】Tencent IM Demo学习Activity使用语言切换的适配整个App(采用广播)_android_02

这会就简单处理,只有两个的语言切换,但是这足够满足基本的Internationalism的需求。
启用了Item的列表对应,在后续的代码解析中将会深入研究。

代码解析

资源准备

在UI上需要确定好相关的ID位置以及在语言设置的方向上需要相关的数据结构作为处理构建的关键。

//广播需要的Intent的以及action名称
    public static final String LANGUAGE = "language";
    public static final String DEMO_LANGUAGE_CHANGED_ACTION = "demoLanguageChangedAction";
    //在本类中自定义一个接口并实现
    private OnItemClickListener onItemClickListener;
    private TitleBarLayout titleBarLayout;//tencent 自带的一个组件
    private RecyclerView recyclerView;
    //语言的设置
    private final Map<String, String> languageMap = new HashMap<>();
    private final List<String> languageList = new ArrayList<>();
    private SelectAdapter adapter;
    private String currentLanguage;

以上都是一些基本的资源准备,特别强调的是这里的数据结构设置,我认为可以再简化一些,当然这是后话。一般来说,这种有点极端的性能节省在现在已经不是问题了。毕竟摩尔定律放在这里。

特定的Create周期

在Activity中OnCreate 是起步的一个周期点,那么在这个节点就得将相关的资源准备到位,如针对Tencent自带的TitleBarLayout就得将其进行适配对应,其源码就不放上来了,主要是了解使用方式,这个靠自己进行封装也可以实现;

//Activity的创建初始化,第一要素就是对UI进行适配
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_theme_language_select);
        titleBarLayout = findViewById(R.id.demo_select_title_bar);
        recyclerView = findViewById(R.id.theme_recycler_view);
        titleBarLayout.setTitle(getResources().getString(R.string.demo_language_title), ITitleBarLayout.Position.MIDDLE);
        titleBarLayout.getLeftGroup().setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });

以及对相关的文本资源的初始化:

currentLanguage = TUIThemeManager.getInstance().getCurrentLanguage();
        if (TextUtils.isEmpty(currentLanguage)) {
            Locale locale;
            if (TUIBuild.getVersionInt() < Build.VERSION_CODES.N) {
                locale = getResources().getConfiguration().locale;
            } else {
                locale = getResources().getConfiguration().getLocales().get(0);
            }
            currentLanguage = locale.getLanguage();
        }

请注意这里Locale,之前也是不常接触的东西;该对象是在Android中对语言环境进行切换的重要对象。之后需要得本地的语言环境资源即可使用以下的代码复用:

Locale locale;
            if (TUIBuild.getVersionInt() < Build.VERSION_CODES.N) {
                locale = getResources().getConfiguration().locale;
            } else {
                locale = getResources().getConfiguration().getLocales().get(0);
            }
            currentLanguage = locale.getLanguage();

【Android】Tencent IM Demo学习Activity使用语言切换的适配整个App(采用广播)_ide_03

private void initData() {
        String simplifiedChinese = "简体中文";
        String english = "English";
        languageList.add(simplifiedChinese);
        languageMap.put(simplifiedChinese, "zh");
        languageList.add(english);
        languageMap.put(english, "en");
        if (TextUtils.equals(currentLanguage, "zh")) {
            adapter.setSelectedItem(0);
        } else {
            adapter.setSelectedItem(1);
        }
    }

核心-语言切换

使用语言切换的选择,使用到最常用的RecyclerView;这里需要肯定要用holderAdapter:
首先展示这个内部类Adapter和内部类的内部类(是不是隔这套娃呢?非也,看到下面就清楚了):

// 在该activity中自带的适配器,基本的常规操作,详细可以参考之前的RecyclerView 的使用
    class SelectAdapter extends RecyclerView.Adapter<SelectAdapter.SelectViewHolder> {
        int selectedItem = -1;

        public void setSelectedItem(int selectedItem) {
            this.selectedItem = selectedItem;
        }

        @NonNull
        @Override
        public SelectAdapter.SelectViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            // 视图加载然后反馈出来给适配器
            View view = LayoutInflater.from(LanguageSelectActivity.this).inflate(R.layout.core_select_item_layout,parent, false);
            return new SelectAdapter.SelectViewHolder(view);
        }

        @Override
        public void onBindViewHolder(@NonNull SelectAdapter.SelectViewHolder holder, int position) {
            //绑定一些数据
            String language = languageList.get(position);
            holder.name.setText(language);
            //对于点击的时候的UI变动的监听
            if (selectedItem == position) {
                holder.selectedIcon.setVisibility(View.VISIBLE);
            } else {
                holder.selectedIcon.setVisibility(View.GONE);
            }
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    onItemClickListener.onClick(languageMap.get(language));
                }
            });
        }

        @Override
        public int getItemCount() {
            return languageMap.size();
        }
        //内部类,对视图进行承接的确定实际的item
        class SelectViewHolder extends RecyclerView.ViewHolder {
            TextView name;
            ImageView selectedIcon;
            public SelectViewHolder(@NonNull View itemView) {
                super(itemView);
                name = itemView.findViewById(com.tencent.qcloud.tuicore.R.id.name);
                selectedIcon = itemView.findViewById(com.tencent.qcloud.tuicore.R.id.selected_icon);
            }
        }
    }

这里就会跟特定的UI挂钩,其xml代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="horizontal"
    android:layout_gravity="center"
    android:layout_width="match_parent"
    android:background="@color/white"
    android:paddingLeft="15.36dp"
    android:paddingRight="15.36dp"
    android:layout_height="46.08dp">

    <TextView
        android:id="@+id/name"
        android:layout_weight="1"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="15.36sp"
        android:textColor="#444444"
        tools:text="选项1"/>

    <ImageView
        android:id="@+id/selected_icon"
        android:background="?attr/core_selected_icon"
        android:layout_gravity="center"
        android:visibility="gone"
        android:layout_width="21dp"
        android:layout_height="21dp"
        tools:visibility="visible"/>

</LinearLayout>

如图所示

【Android】Tencent IM Demo学习Activity使用语言切换的适配整个App(采用广播)_android studio_04

这些基本上就是Recycler View的常用套路就不用深究了,套路模版来的。

对ItemView之间进行装饰

这一点是我之前没有接触过的点,在使用线性布局的情况下可以对每个itemView之间进行修饰;

recyclerView.setAdapter(adapter);
        recyclerView.setLayoutManager(new CustomLinearLayoutManager(this));
        //DividerItemDecoration是应用在recyclerView中的每个item之间的装饰
        DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);
        dividerItemDecoration.setDrawable(getResources().getDrawable(R.drawable.core_list_divider));
        recyclerView.addItemDecoration(dividerItemDecoration);

这个CustomLinearLayoutManager其实是自定义的线性布局管理器来的,没什么本质差别。而且这里有一个特点就是使用addItemDecoration之前没怎么使用过的修饰方法;

查看源码可以知道其使用方式:

【Android】Tencent IM Demo学习Activity使用语言切换的适配整个App(采用广播)_学习_05

DividerItemDecoration is a RecyclerView.ItemDecoration that can be used as a divider between items of a LinearLayoutManager. It supports both HORIZONTAL and VERTICAL orientations.
mDividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(),
mLayoutManager.getOrientation());
recyclerView.addItemDecoration(mDividerItemDecoration);

大意就是用来进行配置区分边界的。

Item点击自定义监听

在该Activity中是采用构建一个监听接口而不是在adapter中进行if-else判断(一般我这菜鸡会直接在里面进行判断写法);参考人家这样拆分出来就灵活多了,不过耦合就有点强,孰优孰劣得看实际而定。

public interface OnItemClickListener {
       void onClick(String language);
   }

    onItemClickListener = new OnItemClickListener() {
           @Override
           public void onClick(String language) {
               if (TextUtils.equals(currentLanguage, language)) {
                   return;
               } else {
                   currentLanguage = language;
               }
               //这里之后肯定要修改的,因为如果多个语言选择呢?index绝对不止两个
               int index = TextUtils.equals(language, "zh") ? 0 : 1;
               adapter.setSelectedItem(index);
               adapter.notifyDataSetChanged();
               TUIThemeManager.getInstance().changeLanguage(LanguageSelectActivity.this, currentLanguage);
               //本地先修改activity的东西,就不需要再重新唤醒该activity的生命周期
               changeTitleLanguage();
               //广播提示
               notifyLanguageChanged();
           }
       };

一旦其中的语言发生变化就会触发广播的操作和UI的变化。

例如在上一个Activity中就会有一个注册广播专门用来看语言的变化:代码就不copy。

【Android】Tencent IM Demo学习Activity使用语言切换的适配整个App(采用广播)_java_06

Title的UI变化和语言变化广播

private void notifyLanguageChanged() {
        Intent intent = new Intent();
        intent.setAction(DEMO_LANGUAGE_CHANGED_ACTION);
        intent.putExtra(LANGUAGE, currentLanguage);
        LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
    }

    private void changeTitleLanguage() {
        titleBarLayout.setTitle(getResources().getString(R.string.demo_language_title), ITitleBarLayout.Position.MIDDLE);
    }

广播层面的好理解,无非就是正常的上下文串接起来也是常规的广播使用方法。
那么changeTitleLanguage文本变化是怎么实现变化的?
其实这个是全局的TUIThemeManager实现对locale的更换,读取的文件资源就会改变,那么重读之后自然调用的资源就不一样。

TUIThemeManager.getInstance().changeLanguage(LanguageSelectActivity.this, currentLanguage);

在这个全局调用的管理类中相关的代码就copy如下以供参考

configuration.locale = locale;
        if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            configuration.setLocale(locale);
        }
        resources.updateConfiguration(configuration, null);

小结

总的来说,在这个demo中学习到一个很重要的思想:全局使用的资源一定要单领出来处理,至于什么样的资源需要全局处理,得看app所需要什么。考虑到这个项目采用的shared Preference,性能技术能用,但是还是比较老了,可以使用鹅厂自己提供的开源MMKV进行优化。