I would like to read current screen orientation on Android using Jetpack Compose.
I already tried following in a #Composable:
val o = LocalActivity.current.requestedOrientation
val o = LocalContext.current.display.rotation
val o = LocalActivity.current.resources.configuration.orientation
but in vain.
Log.d("Orientation", "The Orientation $o")
Everytime, I rotate the orientation, log outputs on these remain the same. So they do not seem to change.
How can I read the screen orientation?
You could override onConfigurationChanged(Configuration) method of your activity.
To get this method called you also need to specify the changing in android:configChanges attribute in your manifest file.
See what and how to specify, kindly check this docs.
You can get it from BoxWithConstraints and compare the width with the height.
A simple example:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
//Our composable that gets the screen orientation
BoxWithConstraints {
val mode = remember { mutableStateOf("") }
mode.value = if (maxWidth < maxHeight) "Portrait" else "Landscape"
Text(text = "Now it is in ${mode.value} mode")
}
}
}
}
androidx.compose.ui.platform.LocalConfiguration.current.orientation
solved it for me.
Related
I am saving the size of a composable using the onSizeChanged modifier. I want to know what the size of the composable was during the previous configuration so that when the configuration changes, I can do a calculation with that size. However, I want to wait until my calculation finishes before I start saving the size again based on the new configuration.
Right now I'm passing the size to MyComposable. MyComposable is the only one who cares about what the size was during the last configuration (landscape or portrait), so I thought I would save the size within its scope. I have no idea when the configuration change happens, so I keep updating a variable oldSize whenever there is a new size to hopefully save the most recent value before the configuration changes.
Below is the code of how I manage the size within MyComposable. It seems to work, but is there a more straightforward way of doing this?
#Composable
fun MyComposable(
size: () -> IntSize,
) {
// save size across configurations
var oldSize by rememberSaveable(stateSaver = IntSizeSaver) {
mutableStateOf(IntSize.Zero)
}
var updateSize by remember { mutableStateOf(false) }
if (updateSize) {
LaunchedEffect(size()) { oldSize = size() }
}
LaunchedEffect(true) {
// do stuff with oldSize...
myFunction(oldSize)
// allow oldSize to be updated again
updateSize = true
}
}
I am guessing if I wanted to put some of this logic into some kind of composable function, it would look something like this. I used rememberUpdatedState as a guide.
#Composable
fun <T> rememberUpdatedStateSaveable(
newValue: T,
stateSaver: Saver<T, out Any>,
enableUpdates: Boolean
): State<T> = rememberSaveable(stateSaver = stateSaver) {
mutableStateOf(newValue)
}.apply { value = if (enableUpdates) newValue else value }
As Display methods are deprecated in Android 12, I am planning to use Jetpack's backward compatible WindowManager library succeeding Display.
However I am not sure whether I face an issue if I directly access the sizes of a screen in Activity like below:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val windowMetrics = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(this#WindowMetricsActivity)
val width = windowMetrics.bounds.width()
val height = windowMetrics.bounds.height()
}
Because Google's sample code insists using onConfigurationChanged method by using a utility container view like below:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Add a utility view to the container to hook into
// View.onConfigurationChanged.
// This is required for all activities, even those that don't
// handle configuration changes.
// We also can't use Activity.onConfigurationChanged, since there
// are situations where that won't be called when the configuration
// changes.
// View.onConfigurationChanged is called in those scenarios.
// https://issuetracker.google.com/202338815
container.addView(object : View(this) {
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
logCurrentWindowMetrics("Config.Change")
}
})
}
#SuppressLint("NotifyDataSetChanged")
private fun logCurrentWindowMetrics(tag: String) {
val windowMetrics = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(this#WindowMetricsActivity)
val width = windowMetrics.bounds.width()
val height = windowMetrics.bounds.height()
adapter.append(tag, "width: $width, height: $height")
runOnUiThread {
adapter.notifyDataSetChanged()
}
}
In our project, we directly access the sizes of screens and I am wondering how we migrate to accessing them after onConfigurationChanged invokes and emits the size values by using MAD skills.
Any approaches will be appreciated
I use the following method to get a screen size starting from Android 11 (API level 30):
fun getScreenSize(context: Context): Size {
val metrics: WindowMetrics = context.getSystemService(WindowManager::class.java).currentWindowMetrics
return Size(metrics.bounds.width(), metrics.bounds.height())
}
With jetpack compose navigation, I suppose it may become a single activity app. How to force orientation only for certain composables (screens)? Say if I want to force landscape only when playing video and not other screens? Without navigation, can declare orientation in manifest but with navigation where we can specify this, possibly, at the composable level?
You can get current activity from LocalContext, and then update requestedOrientation.
To set the desired orientation when the screen appears and bring it back when it disappears, use DisposableEffect:
#Composable
fun LockScreenOrientation(orientation: Int) {
val context = LocalContext.current
DisposableEffect(Unit) {
val activity = context.findActivity() ?: return#DisposableEffect onDispose {}
val originalOrientation = activity.requestedOrientation
activity.requestedOrientation = orientation
onDispose {
// restore original orientation when view disappears
activity.requestedOrientation = originalOrientation
}
}
}
fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
Usage:
#Composable
fun Screen() {
LockScreenOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
}
I am setting a navigation graph programmatically to set the start destination depending on some condition (for example, active session), but when I tested this with the "Don't keep activities" option enabled I faced the following bug.
When activity is just recreated and the app calls method NavController.setGraph, NavController forces restoring the Navigation back stack (from internal field mBackStackToRestore in onGraphCreated method) even if start destination is different than before so the user sees the wrong fragment.
Here is my MainActivity code:
class MainActivity : AppCompatActivity() {
lateinit var navController: NavController
lateinit var navHost: NavHostFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
log("fresh start = ${savedInstanceState == null}")
navHost = supportFragmentManager.findFragmentById(R.id.main_nav_host) as NavHostFragment
navController = navHost.navController
createGraph(App.instance.getValue())
}
private fun createGraph(bool: Boolean) {
Toast.makeText(this, "Is session active: $bool", Toast.LENGTH_SHORT).show()
log("one: ${R.id.fragment_one}, two: ${R.id.fragment_two}")
val graph =
if (bool) {
log("fragment one")
navController.navInflater.inflate(R.navigation.nav_graph).also {
it.startDestination = R.id.fragment_one
}
} else {
log("fragment two")
navController.navInflater.inflate(R.navigation.nav_graph).also {
it.startDestination = R.id.fragment_two
}
}
navController.setGraph(graph, null)
}
}
App code:
class App : Application() {
companion object {
lateinit var instance: App
}
private var someValue = true
override fun onCreate() {
super.onCreate()
instance = this
}
fun getValue(): Boolean {
val result = someValue
someValue = !someValue
return result
}
}
Fragment One and Two are just empty fragments.
How it looks like:
Repository with full code and more explanation available by link
My question: is it a Navigation library bug or I am doing something wrong? Maybe I am using a bad approach and there is a better one to achieve what I want?
As you tried in your repository, It comes from save/restoreInstanceState.
It means you set suit graph in onCreate via createGraph(App.instance.getValue()) and then fragmentManager in onRestoreInstanceState will override your configuration for NavHostFragment.
So you can set another another time the graph in onRestoreInstanceState. But it will not work because of this line and backstack is not empty. (I think this behavior may be a bug...)
Because of you're using a graph (R.navigation.nav_graph) for different situation and just change their startDestination, you can be sure after process death, used graph is your demand graph. So just override startDestination in onRestoreInstanceState.
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
if (codition) {
navController.graph.startDestination = R.id.fragment_one
} else {
navController.graph.startDestination = R.id.fragment_two
}
}
Looks like there is some wrong behaviour in the library and my approach wasn't 100% correct too. At least, there is the better one and it works well.
Because I am using the same graph and only changing the start destination, I can simply set that graph in onCreate of my activity and set some default start destination there. Then, in createGraph method, I can do the following:
// pop backStack while it is not empty
while (navController.currentBackStackEntry != null) {
navController.popBackStack()
}
// then just navigate to desired destination with additional arguments if needed
navController.navigate(destinationId, destinationBundle)
Suppose I have some activity with a jetpack-compose content
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ScrollableColumn(
modifier = Modifier
.fillMaxSize()
.border(4.dp, Color.Red)
) {
val (text, setText) = remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = setText,
label = {},
modifier = Modifier
.fillMaxWidth()
)
for (i in 0..100) {
Text("Item #$i")
}
}
}
}
}
If I were to launch this activity and focus on the TextField a software keyboard would pop up.
The interface, however, would not react to it. ScrollableColumn's bottom border (.border(4.dp, Color.Red)) would not be visible, as well as 100th item (Text("Item #$i")).
In other words, software keyboard overlaps content.
How can I make jetpack compose respect visible area changes (due to software keyboard)?
You can use the standard android procedure, but I don't know if a Compose specific way exists.
If you set the SoftInputMode to SOFT_INPUT_ADJUST_RESIZE, the Layout will resize on keyboard change.
class YourActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
setContent { /* Composable Content */ }
}
}
otherwise, you could use the flags in the manifest. See here for more information:
Move layouts up when soft keyboard is shown?
You can use Accompanist's inset library https://google.github.io/accompanist/insets
first use ProvideWindowInsets at the root of your composable hierarchy most of the time below your app theme compose and set windowInsetsAnimationsEnabled true
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
// content }
The use navigationBarsWithImePadding() modifier on TextField
OutlinedTextField(
// other params,
modifier = Modifier.navigationBarsWithImePadding() )
Finaly make sure to call WindowCompat.setDecorFitsSystemWindows(window, false) from your activity(inside onCreate). If you want Software keyboard on/off to animate set your activity's windowSoftInputMode adjustResize in AndroidManifests
<activity
android:name=".MyActivity"
android:windowSoftInputMode="adjustResize">
Besides Compose, no external libraries are needed for this now.
Set android:windowSoftInputMode=adjustResize on the activity in your manifest file
e.g.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".MyActivity"
android:windowSoftInputMode="adjustResize"/>
</application>
</manifest>
Then for the composable that you want the UI to react to, you can use Modifier.imePadding() which reacts to IME visibility
Box(Modifier.imePadding()) {
// content
}
I faced the same problem.
Use OnGlobalLayoutListener which will observe the actual IME rect size and will be triggered when the soft keyboard is fully visible.
Worked solution for me:
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val scope = rememberCoroutineScope()
val view = LocalView.current
DisposableEffect(view) {
val listener = ViewTreeObserver.OnGlobalLayoutListener {
scope.launch { bringIntoViewRequester.bringIntoView() }
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(listener) }
}
TextField(
modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester),
...
)
Origin here
I came up with idea of using accompanist insets library.
Someone could be interested because my approach does not modificate the contents of an application.
I link my post below:
(Compose UI) - Keyboard (IME) overlaps content of app
If you want your TextField scroll up when keyboard appears.
The following it work for me.
You need to add these
class YourActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
setContent { /* Composable Content */ }
}
And add this to your app/build.gradle
implementation "com.google.accompanist:accompanist-insets-ui:0.24.7-alpha"
In my case just adding android:windowSoftInputMode="adjustResize" to activity was enough to solve the problem.
It depends on how you build your UI. If yours screen's root is a vertically scrollable container or a Box, the keyboard resize might get managed automatically.
Use the modifier imePadding(). This will allow the specific compositions to adjust themselves when the keyboard pops up. This does not require you to set any flag on the activity