切换语言

获取 string.xml 里的字段时,可以用下面的 getAttachBaseContext 获取对应语言的 context
集成 tinker 热更适配要拿到对的上下文

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123

    /**
     * 在 BaseActivity 里 attachBaseContext 使用
     * 设置语言后跳转到启动模式 singleTask 的页面
     * 并设置 intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 就能做到应用内切换
     */
    override fun attachBaseContext(newBase: Context?) {
        //注意 androidx 语言失效问题 这么写有问题
        //super.attachBaseContext(newBase?.let { AppUtilsKtx.getAttachBaseContext(it) })
        val context = AppUtilsKtx.getAttachBaseContext(newBase)
        val configuration = context.resources.configuration
        // 此处的ContextThemeWrapper是androidx.appcompat.view包下的
        // 你也可以使用android.view.ContextThemeWrapper,但是使用该对象最低只兼容到API 17
        // 所以使用 androidx.appcompat.view.ContextThemeWrapper省心
        val wrappedContext = object : ContextThemeWrapper(context, R.style.Theme_AppCompat_Empty) {
            override fun applyOverrideConfiguration(overrideConfiguration: Configuration?) {
                if (overrideConfiguration != null) {
                    overrideConfiguration.setTo(configuration)
                }
                super.applyOverrideConfiguration(overrideConfiguration)
            }
        }
        super.attachBaseContext(wrappedContext)
    }

    class AppUtilsKtx {

        companion object {

            /**
             * 代码中注意有些地方需要用这个 Context 比如在自定义 View 的构造函数或者使用 app 的 Context 获取 string 资源时
             */
            fun getAttachBaseContext(context: Context): Context {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    return setAppLanguageApi24(context)
                } else {
                    setAppLanguage(context)
                }
                return context
            }

            /**
             * 设置应用语言
             */
            @Suppress("DEPRECATION")
            fun setAppLanguage(context: Context) {
                val resources = context.resources
                val displayMetrics = resources.displayMetrics
                val configuration = resources.configuration
                // 获取当前语言,默认设置跟随系统
                val locale = getAppLocale()
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                    configuration.setLocale(locale);
                } else {
                    configuration.locale = locale;
                }
                resources.updateConfiguration(configuration, displayMetrics)
            }

            /**
             * 兼容 7.0 及以上
             */
            @TargetApi(Build.VERSION_CODES.N)
            private fun setAppLanguageApi24(context: Context): Context {
                val locale = getAppLocale()
                val resource = context.resources
                val configuration = resource.configuration
                configuration.setLocale(locale)
                configuration.setLocales(LocaleList(locale))
                return context.createConfigurationContext(configuration)
            }

            /**
             * 获取 App 当前语言
             */
            private fun getAppLocale() =
                when (MMKV.defaultMMKV().decodeString(CHOOSE_LANGUAGE, "")) {
                    "" -> {
                        getSystemLocale()
                    }
                    Locale.ENGLISH.language -> {
                        Locale.ENGLISH
                    }
                    Locale.CHINA.language -> {
                        Locale.CHINA
                    }
                    //很多语言在 Locale 中没有变量,可以直接用字符串
                    "ar" -> {
                        Locale("ar")
                    }
                    else -> Locale.ENGLISH
                }

            /**
             * 获取当前语言,如未包含则默认英文
             */
            private fun getSystemLocale(): Locale {
                val systemLocale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    LocaleList.getDefault()[0]
                } else {
                    Locale.getDefault()
                }
                return when (systemLocale.language) {
                    Locale.ENGLISH.language -> {
                        Locale.ENGLISH
                    }
                    Locale.CHINA.language -> {
                        Locale.CHINA
                    }
                    Locale("ar").language -> {
                        Locale("ar")
                    }
                    else -> {
                        Locale.ENGLISH
                    }
                }
            }

            fun isAr(): Boolean {
                return LANGUAGE_ARABIA == MMKV.defaultMMKV().decodeString(CHOOSE_LANGUAGE, "")
            }
        }
    }

string

strings.xml 中,点击右上角的 Open editor 可以总览编辑,点击左上角地球图标添加新语言,还可以筛选未翻译
中文(中国):values-zh-rCN 中文(台湾):values-zh-rTW , zh 代表语言 rCN 代表地区

1
2
//设置不可翻译
<string name="app_name" translatable="false">AppName</string>

RTL

Android 4.1.1(API 级别 16)不支持 android:supportsRtl=“true” , start 和 end
右键项目选择 Refactor > Add Right to Left Support
如果 targetSdkVersion 为 17 或更高选中 Replace Left/Right Properties with Start/End
如果 targetSdkVersion 为 16 或更低,选中 Generate -v17 Versions 复选框

可以添加 layout-ldrtl((layout-direction-right-to-left) ) drawable-ldrtl-xxhdpi 来放对应资源

ViewPager 不支持 RTL ViewPager2 支持

除了布局文件,代码中还需注意 Gravity.LEFT , leftMargin , setMargins() , setPadding()。 setPaddingRelative() 对比 setPadding() 支持 start/end

Android 4.4(API 版本 19)支持使用 android:autoMirrored=“true”

判断是否 rtl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
        fun isRtl(): Boolean {
            return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL
        }
        
        fun shouldUseLayoutRtl(view: View): Boolean {
            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                View.LAYOUT_DIRECTION_RTL == view.layoutDirection
            } else {
                false
            }
        }

在 layout.xml 中点击地球图标,选中 ar 布局也会镜像

RelativeLayout 中的子 View 使用 android:layout_centerInParent=“true” 导致看不见子 View 因为父 View 使用了 wrap_content 没有确定大小,要么指定大小,要么在父 View 中使用 android:layoutDirection=“ltr”

TextView 设置 android:inputType=“textPassword” 时对齐有问题,需要设置 android:textAlignment=“viewStart”

开发者选项中可以启用强制使用从右到左的布局方向

TextView 和 EditView 全局设置 style

为了适配 Rtl 可以全局设置属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

    <style name="AppBaseTheme" parent="Theme.AppCompat.Light.NoActionBar">
        ···
        <item name="editTextStyle">@style/EditTextStyle.Alignment</item>
        <item name="android:textViewStyle">@style/TextViewStyle.TextDirection</item>
    </style>

    <style name="EditTextStyle.Alignment" parent="@android:style/Widget.EditText">
        <item name="android:textAlignment">viewStart</item>
        <item name="android:textDirection">locale</item>
    </style>

    <style name="TextViewStyle.TextDirection" parent="android:Widget.TextView">
        <item name="android:textDirection">locale</item>
        <item name="android:textAlignment">viewStart</item>
    </style>

自定义 View 的 style 不生效时,可能需要用到 ContextThemeWrapper 设置view的style

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//在构造函数中使用 ContextThemeWrapper
public class StrokeTextView extends AppCompatTextView {
    private Float strokeWidth = 0.5F;

    public StrokeTextView(Context context) {
        this(new ContextThemeWrapper(context,R.style.TextViewStyle_TextDirection), null);
    }

    public StrokeTextView(Context context, AttributeSet attrs) {
        this(new ContextThemeWrapper(context,R.style.TextViewStyle_TextDirection), attrs, 0);
    }

    public StrokeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(new ContextThemeWrapper(context,R.style.TextViewStyle_TextDirection), attrs, defStyleAttr);
        TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.StrokeTextView, defStyleAttr, R.style.TextViewStyle_TextDirection);
        strokeWidth = arr.getFloat(R.styleable.StrokeTextView_text_stroke_width, strokeWidth);
        arr.recycle();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        Paint paint = getPaint();
        paint.setStrokeWidth(strokeWidth);
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        super.onDraw(canvas);
    }
}

other

问题注意

多语言切换在Androidx失效

时间格式问题

使用 Translations Editor 本地化界面

支持不同的语言和文化

以前 ViewPager 在预加载时 Fragment 的生命周期就会走到 onResume , 现在做懒加载数据只要在 onResume 里做就可以了

RecyclerView 使用 GridLayoutManager 时适配 rtl ,写个继承 GridLayoutManager 的类,并重写 isLayoutRTL ,或者直接如下

1
2
3
4
5
6
GridLayoutManager lm = new GridLayoutManager(this, 2) {
    @Override
    protected boolean isLayoutRTL() {
        return true;
    }
};

图片 rtl 反转通过 android:autoMirrored=“true” 或者 drawable-ldrtl 资源或者设置 scaleX 或 rotationY 然后通过加载不同的 integer 来反转

1
2
3
4
5
android:scaleX="-1"

android:rotationY="@integer/rotation"
<integer name="rotation">0</integer>
<integer name="rotation">180</integer>