mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
* fix(mobile): keep Android split screen from restarting or squeezing the app One UI gives a split-screen pane its own density. Android recreated MainActivity on that change, and React Native seeds its display metrics from the application context, which keeps the full-screen density, so the pane was laid out at the wrong scale. A config plugin declares density in configChanges and re-seeds the metrics from the activity. * fix(mobile): catch React Native's rotation re-seed before the frame draws A one-shot listener added on configuration change missed a cold start or a 180-degree flip at a non-zero rotation, where React Native's root view re-seeds from the application context without a configuration change. * fix(mobile): publish re-seeded display metrics in one step Text measurement reads the holder off the UI thread, so build the pane-density screen metrics before publishing them.
104 lines
4.0 KiB
JavaScript
104 lines
4.0 KiB
JavaScript
const { AndroidConfig, withAndroidManifest, withMainActivity } = require('expo/config-plugins')
|
|
|
|
// Why: One UI gives a split-screen pane its own density (#36305). Without `density` in
|
|
// configChanges Android recreates MainActivity, closing the open session and orphaning the image
|
|
// picker. Keeping the activity is not enough: React Native seeds its DisplayMetricsHolder from the
|
|
// application context, which keeps the full-screen density, so the pane is laid out at the wrong
|
|
// scale. MainActivity re-seeds it from its own resources after React Native does.
|
|
const MARKER = 'orca: window density'
|
|
|
|
const CLASS_HEADER = 'class MainActivity : ReactActivity() {'
|
|
const IMPORTS = [
|
|
'android.content.res.Configuration',
|
|
'android.os.Bundle',
|
|
'android.util.DisplayMetrics',
|
|
'com.facebook.react.uimanager.DisplayMetricsHolder'
|
|
]
|
|
|
|
const METHODS = `
|
|
// ${MARKER}, generated by plugins/android-window-density.js.
|
|
// React Native re-seeds from the application context when it creates the root view (onCreate)
|
|
// and in onConfigurationChanged, so these run before the next measure. Its root view's layout
|
|
// listener also re-seeds whenever the rotation differs from the last one it saw, after measuring;
|
|
// the pre-draw check runs after every layout listener and asks for another pass, in which React
|
|
// Native re-sends the window size to JS.
|
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
|
super.onPostCreate(savedInstanceState)
|
|
window.decorView.viewTreeObserver.addOnPreDrawListener {
|
|
val held = DisplayMetricsHolder.getScreenDisplayMetrics().densityDpi
|
|
if (held != resources.displayMetrics.densityDpi) {
|
|
useWindowDensity()
|
|
window.decorView.requestLayout()
|
|
}
|
|
true
|
|
}
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
useWindowDensity()
|
|
}
|
|
|
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
super.onConfigurationChanged(newConfig)
|
|
useWindowDensity()
|
|
}
|
|
|
|
// React Native's initDisplayMetrics with the pane's density on the real display size, built before
|
|
// it is published because text measurement reads it off the UI thread.
|
|
@Suppress("DEPRECATION")
|
|
private fun useWindowDensity() {
|
|
val pane = resources.displayMetrics
|
|
val screen = DisplayMetrics().apply {
|
|
windowManager.defaultDisplay.getRealMetrics(this)
|
|
density = pane.density
|
|
densityDpi = pane.densityDpi
|
|
scaledDensity = pane.scaledDensity
|
|
}
|
|
DisplayMetricsHolder.setWindowDisplayMetrics(pane)
|
|
DisplayMetricsHolder.setScreenDisplayMetrics(screen)
|
|
}
|
|
`
|
|
|
|
function patchMainActivity(contents) {
|
|
if (contents.includes(MARKER)) {
|
|
return contents
|
|
}
|
|
// A changed template must fail prebuild rather than drop or double an override.
|
|
if (
|
|
!contents.includes(CLASS_HEADER) ||
|
|
!contents.trimEnd().endsWith('}') ||
|
|
/fun (onPostCreate|onResume|onConfigurationChanged)\(/.test(contents)
|
|
) {
|
|
throw new Error(
|
|
"android-window-density: Expo's MainActivity template changed; update the plugin before building."
|
|
)
|
|
}
|
|
const imports = IMPORTS.filter((name) => !contents.includes(`import ${name}\n`))
|
|
.map((name) => `import ${name}\n`)
|
|
.join('')
|
|
const body = contents.trimEnd().slice(0, -1).trimEnd()
|
|
return `${body.replace(/^(package [^\n]+\n)/, `$1${imports}`)}\n${METHODS}}\n`
|
|
}
|
|
|
|
module.exports = function withAndroidWindowDensity(config) {
|
|
config = withAndroidManifest(config, (cfg) => {
|
|
const activity = AndroidConfig.Manifest.getMainActivityOrThrow(cfg.modResults)
|
|
const changes = (activity.$['android:configChanges'] ?? '').split('|').filter(Boolean)
|
|
if (!changes.includes('density')) {
|
|
activity.$['android:configChanges'] = [...changes, 'density'].join('|')
|
|
}
|
|
return cfg
|
|
})
|
|
|
|
return withMainActivity(config, (cfg) => {
|
|
if (cfg.modResults.language !== 'kt') {
|
|
throw new Error(
|
|
`android-window-density: expected a Kotlin MainActivity, got ${cfg.modResults.language}`
|
|
)
|
|
}
|
|
cfg.modResults.contents = patchMainActivity(cfg.modResults.contents)
|
|
return cfg
|
|
})
|
|
}
|