OnKeyEvent without focus in Jetpack Compose - android

I am creating an app that makes use of a physical button on the device.
This button will have a different functionality depending on the screen that is active.
With Activities what I would do would be to have an Activity for each screen and in each one I would override the onKeyDown function. How would I do this with a single activity that navigates between different Jetpack Compose screens?
According to the Android documentation the correct way would be something like this...
Box(modifier = Modifier
.onKeyEvent {
Log.e("Pressed", it.nativeKeyEvent.keyCode.toString())
true
}
.focusable()
.fillMaxSize()
.background(Color.Gray)
) {
// All screen components
}
But this only works when one of the elements on the screen is focused and what I require is that it always works or not, is there a way to achieve this?

One option is to keep the focus on the view so that the Modifier.onKeyEvent always works. Note that it may conflict with other focusable views, like TextField, so all these views should be children(at any level) of this always-focusable view.
val focusRequester = remember { FocusRequester() }
var hasFocus by remember { mutableStateOf(false) }
Box(
Modifier
.focusRequester(focusRequester)
.onFocusChanged {
hasFocus = it.hasFocus
}
.focusable()
.onKeyEvent {
TODO()
}
) {
}
if (!hasFocus) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
Another option is to create compositional local handlers and pass events from your activity:
class MainActivity : AppCompatActivity() {
private val keyEventHandlers = mutableListOf<KeyEventHandler>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CompositionLocalProvider(LocalKeyEventHandlers provides keyEventHandlers) {
// your app
}
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
return keyEventHandlers.reversed().any { it(keyCode, event) } || super.onKeyDown(keyCode, event)
}
}
val LocalKeyEventHandlers = compositionLocalOf<MutableList<KeyEventHandler>> {
error("LocalKeyEventHandlers is not provided")
}
typealias KeyEventHandler = (Int, KeyEvent) -> Boolean
#Composable
fun ListenKeyEvents(handler: KeyEventHandler) {
val handlerState = rememberUpdatedState(handler)
val eventHandlers = LocalKeyEventHandlers.current
DisposableEffect(handlerState) {
val localHandler: KeyEventHandler = {
handlerState.value(it)
}
eventHandlers.add(localHandler)
onDispose {
eventHandlers.remove(localHandler)
}
}
}
Usage:
ListenKeyEvents { code, event ->
TODO()
}

Related

How to handle back press on AndroidViewBinding & finish host compose?

The composable host an AndroidView that is a FragmentContainerView which has multiple child Fragments on back press of the FragmentContainerView we want to close the #Game composable.
#Composable
fun Game(data: Bundle? = null) {
val user = GamingHubAuthManager.getUser().observeAsState()
AndroidViewBinding(EntryPointBinding::inflate) {
// val myFragment = fragmentGameContainerView.getFragment<FeatureCardFragment>
}
}
You can control if your Game composable is part of the composition from its parent composable with some state and a simple if statement.
To change the state on back press you can use the BackHandler composable.
A working example:
import androidx.compose.runtime.*
#Composable
fun GameParent() {
var gameIsActive by remember { mutableStateOf(true) } // or false for the starting state
BackHandler(enabled = gameIsActive) {
gameIsActive = false
}
if (gameIsActive) {
Game()
} else {
Button(
onClick = { gameIsActive = true }
) {
Text("Start game")
}
}
}
#Composable
fun Game(data: Bundle? = null) {
val user = GamingHubAuthManager.getUser().observeAsState()
AndroidViewBinding(EntryPointBinding::inflate) {
// val myFragment = fragmentGameContainerView.getFragment<FeatureCardFragment>
}
}
If you will have to close the game from some other handler(s) from inside the Game composable then taking this approach might be better
import androidx.compose.runtime.*
#Composable
fun GameParent() {
var gameIsActive by remember { mutableStateOf(true) } // or false for the starting state
if (gameIsActive) {
Game(onClose = { gameIsActive = false })
} else {
Button(
onClick = { gameIsActive = true }
) {
Text("Start game")
}
}
}
#Composable
fun Game(data: Bundle? = null, onClose: () -> Unit) {
BackHandler(enabled = true) {
// this way you can even pass some result back if you parametrize
// this callback, for example won/lost/draw/quit.
onClose()
}
val user = GamingHubAuthManager.getUser().observeAsState()
AndroidViewBinding(EntryPointBinding::inflate) {
// val myFragment = fragmentGameContainerView.getFragment<FeatureCardFragment>
// call onClose() from some other handler
}
}

Passing State value, or State, as Composable function parameter

In a Composable function, I can pass as parameter the State, or the value of the State. Any reason for preferring to pass the value of the State, instead of the State?
In both cases, the composable is stateless, so why should I distinguish both cases?
It's possible to pass state's value. For example:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val isLoading = mutableStateOf(false)
val onClickAtButton = {
lifecycleScope.launch(Dispatchers.Main) {
isLoading.value = true
withContext(Dispatchers.IO) {
//Do some heavy operation live REST call
}
isLoading.value = false
}
}
setContent {
MyComposable(isLoading.value, onClickAtButton)
}
}
}
#Composable
fun MyComposable(
isLoading: Boolean = false,
onClickAtButton: () -> Unit = {}
){
Box(modifier = Modifier.fillMaxSize(){
Button(onClick = onClickAtButton)
if(isLoading){
CircularProgressIndicator()
}
}
}
Hope it helps somebody.
There is a slight difference between passing State or just the value of a State regarding recomposition.
Let's start with passing State:
#Composable
fun Example1(text: State<String>) {
SideEffect { Log.d("Example", "Example1 recomposition") }
Example2(text)
}
#Composable
fun Example2(text: State<String>) {
SideEffect { Log.d("Example", "Example2 recomposition") }
Text(text.value)
}
#Composable
fun Screen() {
val text = remember { mutableStateOf("hello") } }
Example1(text)
Button(
onClick = { text.value = "world" }
) {
Text("Click me")
}
}
On first start you will see the log output
Example1 recomposition
Example2 recomposition
However when you click the button, you will only see an additional
Example2 recomposition
Because you're passing down State and only Example2 is reading the state, Example1 does not need to be recomposed.
Let's change the parameters to a plain type:
#Composable
fun Example1(text: String) {
SideEffect { Log.d("Example", "Example1 recomposition") }
Example2(text)
}
#Composable
fun Example2(text: String) {
SideEffect { Log.d("Example", "Example2 recomposition") }
Text(text)
}
#Composable
fun Screen() {
val text = remember { mutableStateOf("hello") } }
Example1(text.value)
Button(
onClick = { text.value = "world" }
) {
Text("Click me")
}
}
When you click the button now, you will see two additional lines in the log output
Example1 recomposition
Example2 recomposition
Since text is now a plain type of the function signatures of both composables, both need to be recomposed when the value changes.
However always passing down State can become quite cumbersome. Compose is quite good at detecting what needs to be recomposed so this should be considered a micro optimization. I just wanted to point out that there is a slight difference which every developer using Compose should know about.

Can we still use the variable delegate for variable pass through parameter?

We have the below compose which works well.
#Composable
fun MyInnerControl() {
var timerStartStop by remember { mutableStateOf(true) }
Button(onClick = {
timerStartStop = !timerStartStop
}) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
But if we have the timerStartStop passed through the function parameter, I cannot do as below
#Composable
fun MyInnerControl(timerStartStop: Boolean) {
Button(onClick = {
timerStartStop = !timerStartStop
}) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
I can pass as below, but then I have to change my timerStartStop to timerStartStop.value
#Composable
fun MyInnerControl(timerStartStop: MutableState<Boolean>) {
Button(onClick = {
timerStartStop.value = !timerStartStop.value
}) {
Text(if (timerStartStop.value) "Stop" else "Start")
}
}
Is there any way to have the MutableState<Boolean> passed over the argument, yet within the function we can use the delegate approach, i.e. using timerStartStop instead of timerStartStop.value?
Jetpack Compose tells you to respect Single Source of Truth principle, which means that you can not pass state as argument if you want to change that state inside your function.
So generally you have to choose one of two approaches:
Pass both timerStartStop variable and onClick callback
#Composable
fun MyInnerControl(timerStartStop: Boolean, onClick: ()->Unit) {
Button(onClick = onClick) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
Pass none of these
#Composable
fun MyInnerControl() {
var timerStartStop by remember { mutableStateOf(true) }
Button(onClick = {
timerStartStop = !timerStartStop
}) {
Text(if (timerStartStop) "Stop" else "Start")
}
}
I think it's better to apply proper state hoisting methods and give MyInnerControl the ability to both read and edit the parameter in the following way.
#Composable
fun ParentComposable() {
val isTimerRunning by remember { mutableStateOf(false) } }
MyInnerControl(
isTimerRunning = isTimerRunning,
flipTimerState {
isTimerRunning = !isTimerRunning
}
}
}
#Composable
fun MyInnerControl(
isTimerRunning: Boolean,
flipTimerState: () -> Unit,
) {
Button(
onClick = { flipTimerState() }
) {
Text(if (isTimerRunning) "Stop" else "Start")
}
}

How to disable copy/paste/cut in a TextField Jetpack Compose?

I'm trying to find a simple solution on how to disable copy/paste/cut in a TextField. I did come across a couple of question but no answer.
Create an empty toolbar:
object EmptyTextToolbar: TextToolbar {
override val status: TextToolbarStatus = TextToolbarStatus.Hidden
override fun hide() { }
override fun showMenu(
rect: Rect,
onCopyRequested: (() -> Unit)?,
onPasteRequested: (() -> Unit)?,
onCutRequested: (() -> Unit)?,
onSelectAllRequested: (() -> Unit)?,
) {
}
}
Then you can provide it using LocalTextToolbar.
Most probably you also don't need text selection in this case, here's how you can disable it too:
var textValue by remember { mutableStateOf(TextFieldValue("")) }
CompositionLocalProvider(
LocalTextToolbar provides EmptyTextToolbar
) {
TextField(
value = textValue,
onValueChange = { newValue ->
textValue = if (newValue.selection.length > 0) {
newValue.copy(selection = textValue.selection)
} else {
newValue
}
}
)
}
For now, you could just add a dummy Transparent Composable on TOP of the TextField and hook up it's click listener with the TextField's focusRequestor.
val tffr = remember { FocusRequestor() } // TextField Focus-Requestor
Box{
TextField(
value = value,
onValueChange = { value = it }
modifier = Modifier.focusRequestor(tffr).focusable()
)
Surface(
modifier = Modifier.clickable { tffr.requestFocus() }.alpha(0)
)
}
This way, when users try to click on the TextField, the tap would be intercepted by our dummy Composable, which would transfer the focus, effectively, to the TextField.
This means that they will technically be allowed to tap on the TextField and type in it, but the long press won't be registered by the field for them.
You could even add a Long-Click-Listener to the Surface using the combinedClick modifier, and display a toast to they users stating that the copy/paste actions are disabled, if that is something you'd require, that is.
Try this answer it's disable cut, copy in textfield
etSearchData.customSelectionActionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
return false
}
override fun onDestroyActionMode(mode: ActionMode?) {
}
}
where etSearchData is your textField ID

Is there a Jetpack Compose equivalent for android:keepScreenOn to keep screen alive?

I have a Composable that uses a Handler to slowly update the alpha of an image inside a composable.
However, I'm seeing that the screen turns off before the animation could complete.
In XML layouts, we could keep it alive using
android:keepScreenOn
or
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
Is there a way to do this using compose without using the wake lock permission?
You can use LocalContext to get activity, and it has a window on which you can apply needed flags.
In such cases, when you need to run some code on both view appearance and disappearance, DisposableEffect can be used:
#Composable
fun KeepScreenOn() {
val context = LocalContext.current
DisposableEffect(Unit) {
val window = context.findActivity()?.window
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
onDispose {
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
}
fun Context.findActivity(): Activity? {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
return null
}
Usage: when screen appears flag is set to on, and when disappears - it's cleared.
#Composable
fun Screen() {
KeepScreenOn()
}
As #Louis CAD correctly pointed out, you can have problems if you use this "view" in many views: if one view appears that uses it, and then disappears previous views that also used it, it will reset the flag.
I haven't found a way of tracking flags state to update the view, I think #Louis CAD solution is OK until Compose have some system support.
This one should be safe from any interference if you have multiple usages in the same composition:
#Composable
fun KeepScreenOn() = AndroidView({ View(it).apply { keepScreenOn = true } })
Usage is then as simple as that:
if (screenShallBeKeptOn) {
KeepScreenOn()
}
In a more Compose way:
#Composable
fun KeepScreenOn() {
val currentView = LocalView.current
DisposableEffect(Unit) {
currentView.keepScreenOn = true
onDispose {
currentView.keepScreenOn = false
}
}
}
This will be disposed of as soon as views disappear from the composition.
Usage is as simple as:
#Composable
fun Screen() {
KeepScreenOn()
}
This is how I implemented mine
In my Composable function I have a button to activate the FLAG_KEEP_SCREEN_ON or clear FLAG_KEEP_SCREEN_ON
#Composable
fun MyButton() {
var state by rememberSaveable {
mutableStateOf(false)
}
val context = LocalContext.current
Button(
...
modifier = Modifier
.clickable {
state = !state
keepScreen(state, context)
}
...
)
}
fun keepScreen(state: Boolean, context : Context) {
val activity = context as Activity
if(state) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}else {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
The code below works best for me.
#Composable
fun ScreenOnKeeper() {
val activity = LocalContext.current as Activity
DisposableEffect(Unit) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
onDispose {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
}
The code below didn't work when I toggle its presence (conditionally add, remove, add the component).
#Composable
fun ScreenOnKeeper() {
val view = LocalView.current
DisposableEffect(Unit) {
view.keepScreenOn = true
onDispose {
view.keepScreenOn = false
}
}
}

Categories

Resources