最近在做 Bigo Live 直播间的横屏适配,横屏和竖屏下会有一些状态的差异。但我们的应用在横屏下,切换后台再回来后,发现一些状态显示不对。

  猜想问题出现在了横竖屏状态判断上,先看下代码:

1
2
3
public boolean isOrientationPortrait() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
}

  通过调试发现应用切后台之后, isOrientationPortrait() 会返回 true,于是换上另一种横竖屏判断:
1
2
3
public 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
11
V/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
11
XmlResourceParser 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
8
void 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
15
public 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
    6
    public 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);
    }
    }