Android: Блокировка текущей ориентации экрана

Одной из важных фич мобильных читалок книг является возможность быстрой блокировки текущей ориентации экрана. Вроде нет ничего сложного. Однако и тут есть свои подводные камни. При разработке Reedy мы в этом убедились.

Итак, что нужно для того, чтобы заблокировать текущую ориентацию:
1. Определить её (с этим как раз часто возникает путаница);
2. Зафиксировать с помощью метода setRequestedOrientation().

Разберём по порядку.

1. Определяем текущую ориентацию экрана

В общем случае всё просто:

val orientation = getResources().getConfiguration().orientation

Однако данный способ подойдёт только для каких-то не слишком важных участков приложения. Например, когда выполнение кода связано с подгружаемыми ресурсами, которые разные для портретного и ландшафтного режимов. Хотя и в этом случае я бы не рискнул полностью полагаться на этот способ, так как, по заверению товарища из Apphance, на некоторых устройствах это поле иногда работает неверно, и порой даже возвращает ORIENTATION_SQUARE на заведомо неквадратных девайсах. Поэтому он рекомендует (а я полностью поддерживаю) определять ориентацию, явно проверяя соотношение ширины и высоты дисплея.

На Kotlin это может выглядеть примерно так:

val Activity.displaySize: Pair<Int, Int> get() {
	val display = getWindowManager().getDefaultDisplay()
	return if (Build.VERSION.SDK_INT >= 13) {
		val point = Point()
		display.getSize(point)
		point.x to point.y
	}
	else display.getWidth() to display.getHeight()
}

val Activity.displayOrientation: Int get() {
	val (width, height) = displaySize
	return if (width > height)
		Configuration.ORIENTATION_LANDSCAPE
	else
		Configuration.ORIENTATION_PORTRAIT
}

Но этого по-прежнему недостаточно. Ведь физически ориентаций может быть 4 штуки: портретная, ландшафтная, и ещё две, когда пользователь держит телефон в портретном или ландшафтном режиме, но «вверх ногами». Поэтому чтобы верно зафиксировать текущую ориентацию дисплея, нужно вызвать метод setRequestedOrientation(), передав ему одно из четырёх значений:
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE

Следовательно нам необходимо получать именно угол наклона устройства:

val Activity.displayRotation: Int
	get() = getWindowManager().getDefaultDisplay().getRotation()

А дальше, казалось бы, простой switch/case и готово! Но дело в том, что getRotation() возвращает вращение экрана относительно так называемой «натуральной» ориентации устройства (natural orientation), а оно разное для разных девайсов. Хотя в целом всё логично: для телефонов натуральное положение — портретное, для планшетов — ландшафтное. Именно в этих положениях getRotation() будет возвращать ROTATION_0:

Android Natural Screen Rotation & Orientation on Phones and Tablets

Если же rotation равен 90 или 180 в портретном режиме, либо 270 или 180 в ландшафтном, то это свидетельствует, что телефон перевёрнут «вверх ногами». Таким образом, чтобы верно определить текущее положение девайса, нужно учитывать и orientation, и rotation:

val Activity.displayRotationType: Int get() {
	val rotation = displayRotation

	if (displayOrientation == Configuration.ORIENTATION_LANDSCAPE) {
		if (rotation == Surface.ROTATION_270 || rotation == Surface.ROTATION_180)
			return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE

		return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
	}

	else if (rotation == Surface.ROTATION_180 || rotation == Surface.ROTATION_90)
		return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT

	return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}

2. Фиксируем ориентацию

Вышеописанный хитрый метод, возвращает константу ActivityInfo. Всё что нужно, чтобы заблокировать ориентацию — передать данную константу в метод setRequestedOrientation() текущего активити. Чтобы разблокировать — передать в него же константу ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED.

// lock
setRequestedOrientation(displayRotationType)

// unlock
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)

Снаружи всё выглядит красиво и аккуратно. Даже не скажешь, что внутри столько логики.