Handle reader cutout setting with Insets to support Android 15+ (#2640)

This commit is contained in:
AntsyLich
2025-11-02 22:28:25 +06:00
committed by GitHub
parent 38b1bd7383
commit 0e0b6d9283
6 changed files with 48 additions and 44 deletions

View File

@@ -19,7 +19,6 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
### Changed ### Changed
- Increased default concurrent page downloads to 5 ([@AntsyLich](https://github.com/AntsyLich)) ([#2637](https://github.com/mihonapp/mihon/pull/2637)) - Increased default concurrent page downloads to 5 ([@AntsyLich](https://github.com/AntsyLich)) ([#2637](https://github.com/mihonapp/mihon/pull/2637))
- Hide "Show content in cutout area" reader setting on Android 15+ as it's not supported ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908))
### Improved ### Improved
- Spoofing of `X-Requested-With` header to support newer WebView versions ([@Guzmazow](https://github.com/Guzmazow)) ([#2491](https://github.com/mihonapp/mihon/pull/2491)) - Spoofing of `X-Requested-With` header to support newer WebView versions ([@Guzmazow](https://github.com/Guzmazow)) ([#2491](https://github.com/mihonapp/mihon/pull/2491))

View File

@@ -101,7 +101,7 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_fullscreen), title = stringResource(MR.strings.pref_fullscreen),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
preference = readerPreferences.cutoutShort(), preference = readerPreferences.drawUnderCutout(),
title = stringResource(MR.strings.pref_cutout_short), title = stringResource(MR.strings.pref_cutout_short),
enabled = LocalView.current.hasDisplayCutout() && fullscreen, enabled = LocalView.current.hasDisplayCutout() && fullscreen,
), ),

View File

@@ -1,12 +1,12 @@
package eu.kanade.presentation.reader.settings package eu.kanade.presentation.reader.settings
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalView
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.util.system.hasDisplayCutout import eu.kanade.tachiyomi.util.system.hasDisplayCutout
@@ -67,10 +67,10 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
) )
val isFullscreen by screenModel.preferences.fullscreen().collectAsState() val isFullscreen by screenModel.preferences.fullscreen().collectAsState()
if (LocalView.current.hasDisplayCutout() && isFullscreen) { if (LocalActivity.current?.hasDisplayCutout() == true && isFullscreen) {
CheckboxItem( CheckboxItem(
label = stringResource(MR.strings.pref_cutout_short), label = stringResource(MR.strings.pref_cutout_short),
pref = screenModel.preferences.cutoutShort(), pref = screenModel.preferences.drawUnderCutout(),
) )
} }

View File

@@ -19,7 +19,6 @@ import android.view.View
import android.view.View.LAYER_TYPE_HARDWARE import android.view.View.LAYER_TYPE_HARDWARE
import android.view.WindowManager import android.view.WindowManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -42,7 +41,6 @@ import androidx.core.graphics.Insets
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.transition.doOnEnd import androidx.core.transition.doOnEnd
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -79,18 +77,17 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import eu.kanade.tachiyomi.util.system.isNightMode import eu.kanade.tachiyomi.util.system.isNightMode
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setComposeContent import eu.kanade.tachiyomi.util.view.setComposeContent
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -159,7 +156,7 @@ class ReaderActivity : BaseActivity() {
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit) overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
} }
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT)) enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false window.isNavigationBarContrastEnforced = false
} }
@@ -252,7 +249,6 @@ class ReaderActivity : BaseActivity() {
private fun ReaderActivityBinding.setComposeOverlay(): Unit = composeOverlay.setComposeContent { private fun ReaderActivityBinding.setComposeOverlay(): Unit = composeOverlay.setComposeContent {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val showPageNumber by readerPreferences.showPageNumber().collectAsState() val showPageNumber by readerPreferences.showPageNumber().collectAsState()
val isFullscreen by readerPreferences.fullscreen().collectAsState()
val settingsScreenModel = remember { val settingsScreenModel = remember {
ReaderSettingsScreenModel( ReaderSettingsScreenModel(
readerState = viewModel.state, readerState = viewModel.state,
@@ -268,7 +264,7 @@ class ReaderActivity : BaseActivity() {
totalPages = state.totalPages, totalPages = state.totalPages,
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.then(if (isFullscreen) Modifier else Modifier.navigationBarsPadding()), .navigationBarsPadding(),
) )
} }
@@ -537,7 +533,7 @@ class ReaderActivity : BaseActivity() {
binding.viewerContainer.removeAllViews() binding.viewerContainer.removeAllViews()
} }
viewModel.onViewerLoaded(newViewer) viewModel.onViewerLoaded(newViewer)
updateViewerInset(readerPreferences.fullscreen().get()) updateViewerInset(readerPreferences.fullscreen().get(), readerPreferences.drawUnderCutout().get())
binding.viewerContainer.addView(newViewer.getView()) binding.viewerContainer.addView(newViewer.getView())
if (readerPreferences.showReadingMode().get()) { if (readerPreferences.showReadingMode().get()) {
@@ -780,22 +776,28 @@ class ReaderActivity : BaseActivity() {
/** /**
* Updates viewer inset depending on fullscreen reader preferences. * Updates viewer inset depending on fullscreen reader preferences.
*/ */
private fun updateViewerInset(fullscreen: Boolean) { private fun updateViewerInset(fullscreen: Boolean, drawUnderCutout: Boolean) {
val view = viewModel.state.value.viewer?.getView() ?: return val view = viewModel.state.value.viewer?.getView() ?: return
view.applyInsetsPadding(ViewCompat.getRootWindowInsets(view), fullscreen) view.applyInsetsPadding(ViewCompat.getRootWindowInsets(view), fullscreen, drawUnderCutout)
ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets -> ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
view.applyInsetsPadding(windowInsets, fullscreen) view.applyInsetsPadding(windowInsets, fullscreen, drawUnderCutout)
windowInsets windowInsets
} }
} }
private fun View.applyInsetsPadding(windowInsets: WindowInsetsCompat?, fullscreen: Boolean) { private fun View.applyInsetsPadding(
val insets = if (!fullscreen) { windowInsets: WindowInsetsCompat?,
windowInsets?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: Insets.NONE fullscreen: Boolean,
} else { drawUnderCutout: Boolean,
Insets.NONE ) {
val insets = when {
!fullscreen -> windowInsets?.getInsets(WindowInsetsCompat.Type.systemBars())
!drawUnderCutout -> windowInsets?.getInsets(WindowInsetsCompat.Type.displayCutout())
else -> null
} }
?: Insets.NONE
setPadding(insets.left, insets.top, insets.right, insets.bottom) setPadding(insets.left, insets.top, insets.right, insets.bottom)
} }
@@ -851,10 +853,6 @@ class ReaderActivity : BaseActivity() {
.onEach { setDisplayProfile(it) } .onEach { setDisplayProfile(it) }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
readerPreferences.cutoutShort().changes()
.onEach(::setCutoutShort)
.launchIn(lifecycleScope)
readerPreferences.keepScreenOn().changes() readerPreferences.keepScreenOn().changes()
.onEach(::setKeepScreenOn) .onEach(::setKeepScreenOn)
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
@@ -863,14 +861,21 @@ class ReaderActivity : BaseActivity() {
.onEach(::setCustomBrightness) .onEach(::setCustomBrightness)
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
merge(readerPreferences.grayscale().changes(), readerPreferences.invertedColors().changes()) combine(
.onEach { setLayerPaint(readerPreferences.grayscale().get(), readerPreferences.invertedColors().get()) } readerPreferences.grayscale().changes(),
readerPreferences.invertedColors().changes(),
) { grayscale, invertedColors -> grayscale to invertedColors }
.onEach { (grayscale, invertedColors) ->
setLayerPaint(grayscale, invertedColors)
}
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
readerPreferences.fullscreen().changes() combine(
.onEach { readerPreferences.fullscreen().changes(),
WindowCompat.setDecorFitsSystemWindows(window, !it) readerPreferences.drawUnderCutout().changes(),
updateViewerInset(it) ) { fullscreen, drawUnderCutout -> fullscreen to drawUnderCutout }
.onEach { (fullscreen, drawUnderCutout) ->
updateViewerInset(fullscreen, drawUnderCutout)
} }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
} }
@@ -905,15 +910,6 @@ class ReaderActivity : BaseActivity() {
} }
} }
private fun setCutoutShort(enabled: Boolean) {
if (!window.decorView.hasDisplayCutout()) return
window.attributes.layoutInDisplayCutoutMode = when (enabled) {
true -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
false -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
}
/** /**
* Sets the keep screen on mode according to [enabled]. * Sets the keep screen on mode according to [enabled].
*/ */

View File

@@ -31,7 +31,7 @@ class ReaderPreferences(
fun fullscreen() = preferenceStore.getBoolean("fullscreen", true) fun fullscreen() = preferenceStore.getBoolean("fullscreen", true)
fun cutoutShort() = preferenceStore.getBoolean("cutout_short", true) fun drawUnderCutout() = preferenceStore.getBoolean("cutout_short", true)
fun keepScreenOn() = preferenceStore.getBoolean("pref_keep_screen_on_key", false) fun keepScreenOn() = preferenceStore.getBoolean("pref_keep_screen_on_key", false)

View File

@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.util.system package eu.kanade.tachiyomi.util.system
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
@@ -57,11 +58,19 @@ fun Context.isNightMode(): Boolean {
/** /**
* Checks whether if the device has a display cutout (i.e. notch, camera cutout, etc.). * Checks whether if the device has a display cutout (i.e. notch, camera cutout, etc.).
* *
* Only relevant from Android 9 to Android 14. * Only works on Android 9+.
*/
fun Activity.hasDisplayCutout(): Boolean {
return window.decorView.hasDisplayCutout()
}
/**
* Checks whether if the device has a display cutout (i.e. notch, camera cutout, etc.).
*
* Only works on Android 9+.
*/ */
fun View.hasDisplayCutout(): Boolean { fun View.hasDisplayCutout(): Boolean {
return Build.VERSION.SDK_INT in Build.VERSION_CODES.P..Build.VERSION_CODES.UPSIDE_DOWN_CAKE && return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && rootWindowInsets?.displayCutout != null
rootWindowInsets?.displayCutout != null
} }
/** /**