Android Orientation 的坑
最近在做 Bigo Live 直播间的横屏适配,横屏和竖屏下会有一些状态的差异。但我们的应用在横屏下,切换后台再回来后,发现一些状态显示不对。
猜想问题出现在了横竖屏状态判断上,先看下代码:
1
2
3public boolean isOrientationPortrait() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
}
通过调试发现应用切后台之后, isOrientationPortrait()
会返回 true,于是换上另一种横竖屏判断:
1
2
3public boolean isOrientationPortrait() {
return getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
}
这两种方式的区别在于 Configuration.orientation
拿到的是设备的方向,而 getRequestedOrientation()
拿到的是 Activity 请求的方向(通过AndroidManifes.xml
中设置或者通过 setRequestOrientation()
改变)。
修改之后,应用处于后台时方向能判断正常,但一些后台时 inflate
的 View 却不正常或是抛出异常。于是写了一个 Demo
测试一下:Gist。1
2
3
4
5
6
7
8
9
10
11V/MainActivity: ⇢ onPause()
D/MainActivity: getRequestedOrientation: landscape
D/MainActivity: Configuration.orientation: landscape
D/MainActivity: Layout View: landscape
V/MainActivity: ⇠ onPause [2ms]
V/MainActivity: ⇢ onStop()
D/MainActivity: getRequestedOrientation: landscape
D/MainActivity: Configuration.orientation: portrait
D/MainActivity: Layout View: portrait
V/MainActivity: ⇠ onStop [2ms]
从中可以发现,从应用横屏状态切到后台 Configuration.orientation
会在 onStop()
发生改变,而 LayoutInflater#inflate()
加载的 View 取决于Configuration.orientation
,与 getRequestedOrientation()
无关。
验证
由 LayoutInflater#inflate()
可以找到布局文件路径是在 Resources#loadXmlResourceParser()
中拿到的:
1
2
3
4
5
6
7
8
9
10
11XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
// ...
} finally {
releaseTempTypedValue(value);
}
}
ResourcesImpl#getValue()
:
1
2
3
4
5
6
7
8void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
throws NotFoundException {
boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
if (found) {
return;
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
}
AssetsManager#getResourceValue()
会调用 AssetsManager#loadResourceValue()
,这是一个 Native 方法,那么 Native 层是怎样获取方向的呢?ResourcesImpl#updateConfiguration()
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public void updateConfiguration(Configuration config, DisplayMetrics metrics,
CompatibilityInfo compat) {
// ...
mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc,
adjustLanguageTag(mConfiguration.getLocales().get(0).toLanguageTag()),
mConfiguration.orientation,
mConfiguration.touchscreen,
mConfiguration.densityDpi, mConfiguration.keyboard,
keyboardHidden, mConfiguration.navigation, width, height,
mConfiguration.smallestScreenWidthDp,
mConfiguration.screenWidthDp, mConfiguration.screenHeightDp,
mConfiguration.screenLayout, mConfiguration.uiMode,
Build.VERSION.RESOURCES_SDK_INT);
// ...
}
AssetsManager#setConfiguration()
是 Native 层的方法,由此可以得出结论,Native 找到布局文件路径是通过 Configuration.orientation 来判断方向的,所以应用后台时会加载竖屏的资源。
解决
横竖屏的判断:
可以通过给 View 设置 Tag 或者判断某个 View 是否存在,来判断加载的是哪个状态的布局文件:1
2
3
4
5
6public boolean isOrientationPortrait() {
if (mRootView == null) {
return getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
}
return "port" .equals(mRootView.getTag());
}后台时资源的加载:
如果你的应用处于横屏状态, 尽量不要在应用后台时加载与屏幕方向有关的资源,如果非要加载可以采取以下方法:- 将资源文件改为不同的名称(记得要都放到没有land标识的文件夹下,否则会出现 Resources#NotFoundException),然后根据方向判断加载哪一个。
- 通过反射方式修改 Native 层的屏幕方向:
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/**
* 指定屏幕方向来加载资源
* @author linroid <linroid@gmail.com>
* @since 09/11/2016
*/
public class OrientationResourceLoader {
public static void load(Activity activity, Callback callback) {
load(activity, activity.getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, callback);
}
public static void load(Context context, boolean isPortrait, @NonNull Callback callback) {
Resources resources = context.getResources();
if (isPortrait || context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
callback.onLoad(context, resources);
return;
}
try {
Method updateConfiguration = resources.getClass()
.getMethod("updateConfiguration", Configuration.class, DisplayMetrics.class);
Configuration configuration = new Configuration(resources.getConfiguration());
DisplayMetrics displayMetrics = resources.getDisplayMetrics();
configuration.orientation = Configuration.ORIENTATION_LANDSCAPE;
updateConfiguration.invoke(resources, configuration, displayMetrics);
callback.onLoad(context, resources);
configuration.orientation = Configuration.ORIENTATION_PORTRAIT;
updateConfiguration.invoke(resources, resources.getConfiguration(), displayMetrics);
} catch (Exception error) {
error.printStackTrace();
callback.onLoad(context, resources);
}
}
public interface Callback {
void onLoad(Context context, Resources resources);
}
}