背景
如同汉语里的他、她、它,英语里的 He、She、it,很多语言都存在依据性别、对象不同而造成的语法差异,甚至不仅限于名词,还涉及到形容词、动词等,复杂得多。
而这部分语言所涉及到的人群多达 30 亿之众,如果文本只使用通用的、中性的表述,则显得不够准确。假使不加区分,甚至对女性使用男性化的表述方式,则体验更为糟糕。
所以如果 App 的界面语言能正确反映用户的语法性别,则就可以提高用户好感度、互动度,达到更个性化、更自然的用户体验。
这便是 Android 14 推出的重要新特性:语法性别 Grammatical Gender。
语法性别管理 API
首先,Android 14 推出了针对语法性别的 API:GrammaticalInflectionManager
,其提供了针对单个 App 获取、设置性别偏好的入口。
-
getApplicationGrammaticalGender():获取语法性别偏好,返回的是 Configuration 类中的 int 常量,有这么几种类型:
- GRAMMATICAL_GENDER_NOT_SPECIFIED, 0:尚未指定性别偏好,将用默认的资源文本
- GRAMMATICAL_GENDER_NEUTRAL, 1:指定中性、客观的资源文本
- GRAMMATICAL_GENDER_FEMININE, 2:指定针对女性的资源文本
- GRAMMATICAL_GENDER_MASCULINE, 3:指定针对男性的资源文本
-
setRequestedApplicationGrammaticalGender():相对应的将上述常量类型动态设置到性别偏好
设置性别资源
反映语法性别的变化还得有依据语法性别配置的文本才行。而法语针对语法性别的差异比较典型,我们选择法语文本进行示例说明。
比如我们添加 res/values-fr-feminine 目录,在其 strings.xml 里添加针对女性的表述方式。
<resources>
...
<string name="example_string">Vous êtes abonnée à...</string>
</resources>
并添加 res/values-fr-masculine 目录,添加针对男性的表述方式。
<resources>
...
<string name="example_string">Vous êtes abonné à...</string>
</resources>
在 res/values-fr 下的 strings.xml 里添加针对中性、客观的表述方式。
<resources>
...
<string name="example_string">Abonnement à...activé</string>
</resources>
另外在 values/strings.xml 添加默认的英文表述。
<resources>
<string name="example_string">You are subscribed to our store service</string>
</resources>
动态设置性别偏好
然后我们添加个 TextView 来展示语法性别变化的效果,首先看一下默认 Gender 即 NOT_SPECIFIED 时的文本表述。
可以看到使用的是 values 下默认的英文表述。
然后,在 Button 点击里模拟各种性别偏好的动态更新。
class GenderActivity : AppCompatActivity() {
private lateinit var gIM: GrammaticalInflectionManager
private lateinit var binding: GenderLayoutBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("Gender", "onCreate()")
binding = GenderLayoutBinding.inflate(layoutInflater)
setContentView(binding.root)
gIM = getSystemService(GrammaticalInflectionManager::class.java)
updateCurrentGender()
binding.changeGender.setOnClickListener {
Log.d("Gender", "change Gender button tapped")
gIM.setRequestedApplicationGrammaticalGender(
gIM.applicationGrammaticalGender.nextGender()
)
updateCurrentGender()
}
}
private fun updateCurrentGender() {
gIM.applicationGrammaticalGender.genderToString().let {
Log.d("Gender", "current gender: $it")
binding.changeGender.text = "Grammatical gender:$it"
}
}
private fun Int.nextGender() =
when (this) {
Configuration.GRAMMATICAL_GENDER_NEUTRAL -> Configuration.GRAMMATICAL_GENDER_MASCULINE
Configuration.GRAMMATICAL_GENDER_MASCULINE -> Configuration.GRAMMATICAL_GENDER_FEMININE
Configuration.GRAMMATICAL_GENDER_FEMININE -> Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED
Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED -> Configuration.GRAMMATICAL_GENDER_NEUTRAL
else -> Configuration.GRAMMATICAL_GENDER_NEUTRAL
}
}
运行下看下变化效果,可以看到 Gender 在 NOT_SPECIFIED、NEUTRAL、MASCULINE 和 FEMININE 配置之间切换,TextView 的表述也相应地进行了更新。
Log 也展示了当前 Gender 和变化的记录:
ellisonchan@bogon AndroidUDemo % adb logcat -s Gender
06-16 17:47:33.160 D Gender : onCreate()
06-16 17:47:33.228 D Gender : current gender: Not specified
06-16 17:47:49.103 D Gender : change Gender button tapped
06-16 17:47:49.238 D Gender : onCreate()
06-16 17:47:49.261 D Gender : current gender: Neutral
06-16 17:47:49.325 D Gender : current gender: Neutral
06-16 17:47:55.771 D Gender : change Gender button tapped
06-16 17:47:55.845 D Gender : onCreate()
06-16 17:47:55.863 D Gender : current gender: Masculine
06-16 17:47:55.904 D Gender : current gender: Masculine
06-16 17:47:59.634 D Gender : change Gender button tapped
06-16 17:47:59.666 D Gender : onCreate()
06-16 17:47:59.683 D Gender : current gender: Feminine
06-16 17:47:59.742 D Gender : current gender: Feminine
06-16 17:48:01.735 D Gender : change Gender button tapped
06-16 17:48:01.773 D Gender : onCreate()
06-16 17:48:01.788 D Gender : current gender: Not specified
06-16 17:48:01.846 D Gender : current gender: Not specified
需要留意的是,上述效果需要将设备或 App 偏好语言设置成法语才可以看到效果。
还有个细节要注意,调用完 setRequestedApplicationGrammaticalGender 更新 Gender 后,通过 getApplicationGrammaticalGender() 的处理要稍微延迟一下,才能看到新的偏好,也可以理解,因为这个设置是 GrammaticalInflectionManager 系统服务通知的 App Context,这个过程是异步的。
class GenderActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
binding.changeGender.setOnClickListener {
...
gIM.setRequestedApplicationGrammaticalGender(
gIM.applicationGrammaticalGender.nextGender()
)
// 延迟一下再去获取最新 Gender 偏好
GlobalScope.launch(Dispatchers.Main) {
delay(50)
updateCurrentGender()
}
}
}
...
}
语法性别变更的重绘防止
上面的录屏、log 都可以看到 Gender 偏好变化之后 Activity 会发生重绘,因为它本质上也属于 Configuration 的范畴。和其他 Configuration change 一样,如有需要防止画面重启,可以在 Manifest 中配置。
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
...
<application...>
<activity
...
android:configChanges="grammaticalGender">
</activity>
</application>
</manifest>
可想而知,Activity 不自动刷新,改了 Gender TextView 也不会有效果了。
但好在该 Gender 偏好随着 onConfigurationChanged() 被传递了过来。
06-16 18:05:31.021 D Gender : onCreate()
06-16 18:05:31.021 D Gender : current gender: Not specified
06-16 18:06:33.023 D Gender : change Gender button tapped
06-16 18:06:33.229 D Gender : GenderActivity# onConfigurationChanged() new gender:Neutral
06-16 18:06:34.541 D Gender : change Gender button tapped
06-16 18:06:34.578 D Gender : GenderActivity# onConfigurationChanged() new gender:Masculine
...
我们可以选择使用 Configuration
中 getGrammaticalGender()
来获取新的 Gender 偏好,也可以使用上述的 GrammaticalInflectionManager
获取 API,两者得到的性别偏好结果是一致的。
class GenderActivity : AppCompatActivity() {
...
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
Log.d("Gender", "GenderActivity# onConfigurationChanged()" +
" new gender:${newConfig.grammaticalGender.genderToString()}"
)
}
}
既然可以拿到新的 Gender,那么让 TextView 被动刷新即可,而刷新的关键使用新的 Resources 获取当前 Gender Configuration 下的 text 去更新。
class GenderActivity : AppCompatActivity() {
...
override fun onConfigurationChanged(newConfig: Configuration) {
...
// Resources will be updated with new configuration
val newText = resources.getString(R.string.example_string)
Log.d("Gender", "onConfigurationChanged()" +
" new text:${resources.getString(R.string.example_string)}"
)
binding.textview.text = newText
}
}
可以看到这次只是 TextView 局部刷新,不会有全体的重绘、闪烁。
需要留意,如下两个方法是无效成功更新 Gender 效果的:
- invalidate(),因为它只是触发重新测量、布局和描画,text 内容并无变化
- dispatchConfigurationChanged(),因为 TextView 的 onConfigurationChanged() 只针对 Locale、Typeface 进行了配置刷新,没有针对 Gender 做更新
注意
即便在最新的 Release 版 AS 上,都是无法识别上述语法性别资源目录的,会发生编译失败:
AndroidUDemo/app/src/main/res/values-fr-feminine: Error: Invalid resource directory name
得升级到 Android Studio Giraffe Canary 7 或更高的版本,才能支持。
另外还得升级到最新的 AGP,笔者使用的版本组合是:Android Studio Hedgehog | 2023.1.1 Canary 7 + AGP 8.1.0-alpha07,供大家参考。
DEMO 源码
https://github.com/ellisonchan/AndroidUDemo
参考
- https://developer.android.com/about/versions/14/features/grammatical-inflection
- https://developer.android.com/studio/preview/features?hl=zh-cn#grammatical-inflection-api
- https://developer.android.google.cn/reference/android/app/GrammaticalInflectionManager
- https://androidstudio.googleblog.com/2023/02/android-studio-giraffe-canary-7-now.html