onKeyEvent Modifier doesn't work in Jetpack Compose - android

return ComposeView(requireContext()).apply {
setContent {
Box(
Modifier
.onKeyEvent {
if (it.isCtrlPressed && it.key == Key.A) {
println("Ctrl + A is pressed")
true
} else {
false
}
}
.focusable()
)
}
}
Why the key event cannot be called in fragment while using hardware keyboard of tablet?

As documentation of onKeyEvent says:
will allow it to intercept hardware key events when it (or one of its children) is focused.
Which means you need to make your box focused, not just focusable. To do this you need a FocusRequester, in my example I'm asking focus when view renders. Check out more in this article
For the future note, that if user taps on a text field, your box will loose focus, but onKeyEvent still gonna work if this txt field is inside the box
Looks like empty box cannot become focused, so you need to add some size with a modifier. It still will be invisible:
val requester = remember { FocusRequester() }
Box(
Modifier
.onKeyEvent {
if (it.isCtrlPressed && it.key == Key.A) {
println("Ctrl + A is pressed")
true
} else {
false
}
}
.focusRequester(requester)
.focusable()
.size(10.dp)
)
LaunchedEffect(Unit) {
requester.requestFocus()
}
Alternatively just add content to Box so it will stretch and .size modifier won't be needed anymore
This code works fine with my Bluetooth keyboard + android smartphone, emulator seems not recognizing CTRL

Related

Jetpack Compose: Focus Styling

I am making custom components and with my custom components I am required to cover the focus state.
At the top of my component hierarchy I am tracking focus with this.
val focused = remember { mutableStateOf(false) }
val focusModifier = modifier.onFocusEvent {
focused.value = it.hasFocus || it.isFocused
}
Component(modifier = focusModifier, focused = focused.value)
The component is basically this:
#Composable
fun Component(
modifier: Modifier = Modifier,
focused: Boolean = false
) {
...
val colorStuff = if(focused) focusColors else otherColors
var focusModifier = modifier
if(focused) {
focusModifier = modifier.border(BorderStroke(2.dp, Color.Red)).padding(16.dp)
}
NextComponent(
focusModifier,
colorStuff,
etc
)
}
If I leave the code with colorStuff and focused without the focusModifier code the focus state is done correctly and the colors for the component change appropriately. But when I add the focusModifier code and do a border and padding the focus state will trigger, but then instantly be lost. I'm assuming this is because the addition of modifier code changes the build order of the component and makes it discard focus. But that doesn't make full sense either.
I essentially need to add borders/shadows around components when they are focused so this will be something I have to do multiple times. Right now I can't get it done once. Any idea what I need to do to overcome this?
This is not an answer to your question, but I'll share my use-case, maybe it could help. Consider the codes below
data class PersonItem(
val id : Int,
val isActive: Boolean = false
)
List Item
#Composable
fun PersonInfoItem() {
Column {
// First Name
TextField(
modifier = Modifier.onFocusEvent {
// report to view model this personItem received focus events
},
value = "",
onValueChange = {
}
)
// Last Name
TextField(
modifier = Modifier.onFocusEvent {
// report to view model this personItem received focus events
},
value = "",
onValueChange = {
}
)
}
VieModel
class ViewModel {
val currentFocusedItem : PersonItem? = null
fun onPersonItemFocused(personItem: PersonItem) {
// this is where I validate that if the personItem argument
// is not the same with the currentFocusedPerson, if they are not then the last one is not on focus anymore (set !isActive)
// otherwise if they are the same, we are still on the same PersonItem
}
Regardless of which TextField received or lost its focus, as long as I'm keeping track of the Id of the PersonItem being reported to the ViewModel I'm guaranteed that if its the same PersonItem, it will retain what ever state I put into it during its Focus events
(say, the item will change background color when either of the TextField received focus, and revert back to its original background when none of the TextField is focused).
Another thing to consider, these kinds components (i.e TextFields) report multiple inactive focus events during its initial composition, and it's kind of annoying.

How to start new activity in jetpack compose

I want to start new activity in jetpack compose. So I want to know what is the idiomatic way of doing in jetpack compose. Is any side effect api need to be use or not when opening.
ClickableItemContainer.kt
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ClickableItemContainer(
rippleColor: Color = TealLight,
content: #Composable (MutableInteractionSource) -> Unit,
clickAction: () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
CompositionLocalProvider(
LocalRippleTheme provides RippleTheme(rippleColor),
content = {
Surface(
onClick = { clickAction() },
interactionSource = interactionSource,
indication = rememberRipple(true),
color = White
) {
content(interactionSource)
}
}
)
}
MaterialButton.kt
#Composable
fun MaterialButton(
text: String,
spacerHeight: Dp,
onActionClick: () -> Unit
) {
Spacer(modifier = Modifier.height(spacerHeight))
ClickableItemContainer(rippleColor = AquaDarker, content = {
Box(
modifier = Modifier
.background(Aqua)
.fillMaxWidth(),
) {
Text(
text = text,
modifier = Modifier
.align(Alignment.Center),
style = WhiteTypography.h5
)
}
}) {
onActionClick()
}
}
OpenPermissionSetting.kt
#Composable
fun OpenPermissionSetting(router: Router = get()) {
val activity = LocalContext.current as Activity
MaterialButton(
text = "Open Setting",
spacerHeight = 10.dp
) {
activity.startActivity(Intent(this#CurrentClassName,RequiredClassName::class.java)
}
}
So my question is this intent should be use in any Side-effect i.e. LaunchEffect?
LaunchedEffect(key1 = true){
activity.startActivity(Intent(this#CurrentClassName,RequiredClassName::class.java)
}
Thanks
#Composable
fun OpenPermissionSetting(router: Router = get()) {
val activity = LocalContext.current as Activity
MaterialButton(
text = "Open Setting",
spacerHeight = 10.dp
) {
activity.startActivity(Intent(this#CurrentClassName,RequiredClassName::class.java)
}
}
And this one opens new Activity when Button is clicked
var startNewActivity by remember {mutabelStateOf(false)}
#Composable
fun OpenPermissionSetting(router: Router = get()) {
MaterialButton(
text = "Open Setting",
spacerHeight = 10.dp
) {
startActivity = true
}
}
LaunchedEffect(key1 = startActivity){
if(startActivity) {
activity.startActivity(Intent(this#CurrentClassName,RequiredClassName::class.java)
}
}
This one opens activity as soon as your Composable enters composition .Setting a true, false, Unit key or any key doesn't change the fact that code inside LaunchedEffect will be invoked in when it enters composition. You can however change when that code will be run again using key or keys and a conditional statement inside LaunchedEffect.
LaunchedEffect(key1 = true){
activity.startActivity(Intent(this#CurrentClassName,RequiredClassName::class.java)
}
You should understand use cases SideEffect api how they work and ask yourself if this applies to my situation.
SideEffect is good for situations when you only want an action to happen if composition happens successfully. If, even if small chance your state changes fast and current composition is ignored then you shouldn't invoke that action for instance logging composition count is a very good use case for SideEffect function.
Recomposition starts whenever Compose thinks that the parameters of a
composable might have changed. Recomposition is optimistic, which
means Compose expects to finish recomposition before the parameters
change again. If a parameter does change before recomposition
finishes, Compose might cancel the recomposition and restart it with
the new parameter.
When recomposition is canceled, Compose discards the UI tree from the
recomposition. If you have any side-effects that depend on the UI
being displayed, the side-effect will be applied even if composition
is canceled. This can lead to inconsistent app state.
Ensure that all composable functions and lambdas are idempotent and
side-effect free to handle optimistic recomposition.
LaunchedEffect is good for when you wish to have a coroutineScope for animations, scrolling or calling other suspending functions. Another use case for LaunchedEffect is triggering one time actions when the key or keys you set changes.
As in sample above if you set key for LaunchedEffect and check if it's true in a code block you can trigger action only condition is true. LaunchedEffect is also useful when actions that don't require user interactions but a state change happens and only needs to be triggered once.
Executing a callback only when reaching a certain state without user interactions
DisposableEffect is required when you wish to check when your Composable enters and exits composition. onDispose function is also useful for clearing resources or callbacks, sensor register, or anything that needs to be cleared when your Composable exits recomposition.
I would use just LaunchedEffect with the flag let's say val showActivity: Boolean, and the LaunchedEffect function would look like:
#Composable
fun OpenPermissionSetting(viewModel: ViewModel) {
val uiState = viewModel.uiState
MaterialButton(
text = "Open Setting",
spacerHeight = 10.dp
) {
viewModel.onShowActivity()
}
}
LaunchedEffect(showActivity){
if (uiState.showActivity) {
activity.startActivity(...)
viewModel.onShowActivityDone()
}
}
Remember to avoid leaving the flag on true, because its may cause some problems if you have more recompositions :)
Composables are designed to only propagate states down the hierarchy, and propagate actions up the hierarchy. So, no, you shouldn't be launching activities from within the composable. You need to trigger a callback, like your onActionClick: () -> Unit, to the original ComponentActivity where your composables reside (and if this has to go through several nested composables, you'll need to propagste that action all the way up). Then, in your activity, you can direct it to process the actions that were selected. Something like this:
in ComponentActivity:
ClickableItemContainer(
rippleColor = ...,
content = ...,
clickAction = {
startActivity(...)
}
)

Cannot focus a Button in Compose

I have a simple view like this. In the emulator, when I press Tab on the keyboard, nothing happens. There's no indication that the button in focused, and the onClick is not called when I press Enter.
NOTE: This is for accessibility, i.e., those who cannot use their fingers to tap and need an indicator that the button has focus, or those who use the app in a tablet + keyboard.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(color = MaterialTheme.colors.background) {
Button(onClick = { println("clicked") }) {
Text(text = "A Button")
}
}
}
}
}
Please help. The Compose version is 1.0.5.
By the way,
I've read almost all pages of Compose documentation in Android Developers, including Accessibility in Compose, but I cannot find any useful information that solves my simple problem.
Compose TextField could receive focus either by pressing Tab or arrow keys.
I've also tried to place a View Button and a Compose Button side by side, only ViewButton could receive focus.
Update
val iSource = remember { MutableInteractionSource() }
val focused by iSource.collectIsFocusedAsState()
val pressed by iSource.collectIsPressedAsState()
Button(
onClick = { println("clicked") },
Modifier.focusable(interactionSource = iSource),
) {
Text(text = "A Button (focused=${focused}, pressed=${pressed})")
}
This way, "focused" become true when I navigate by using arrow keys (Up/Down), but:
There's no focused indication.
Pressing Tab still doesn't work.
When it's focused, pressing Space or Enter doesn't trigger the onClick listener.

Jetpack Compose TextField blur event

I'm implementing form controls, with validations etc. I want an error message to be shown only when a user "blurs" the TextField, in other words, when the field loses its focus. In Angular, we have touched state that we can proceed from. How to listen for losing focus state in Jetpack Compose?
#Composable
fun Screen() {
TextField(
onBlur = {
// P.S. This parameter does not exist
}
)
}
You can use onFocusChanged.
Sample Code:
var color by remember { mutableStateOf(Black) }
Box(
Modifier
.border(2.dp, color)
// The onFocusChanged should be added BEFORE the focusable that is being observed.
.onFocusChanged { color = if (it.isFocused) Green else Black }
.focusable()
)
Update Answer:
TextField API update - merged onFocus and onBlur callbacks into a single onFocusChange(Boolean) callback with parameter
Source: Version 0.1.0-dev15 - July 22, 2020
If anyone is still having this issue, I solved this using #snorlax answer, but improving on it. My problem was a TextField data validation. The TextField is by nature focusable, different from a Box, so adding .focusable() after defining the event was not doing what I planned: the validation happened once the component appeared on screen, and obviously we didn't want that.
On the .onFocusChanged modifier, we can check the event types and use it to change the blurred state, triggering then the validation.
I had a validate() function and a validState boolean state that I used between my presenter and my view, but I'll make it simple here and you may ignore the implementation of the state class:
We have the focus events, and we can emulate a blur event with a simple flag:
class FieldValidState {
var value: Boolean by mutableStateOf(true)
}
#Composable
fun TextFieldValidation(validState: FieldValidState = remember { FieldValidState() })
Column {
var isBlurred = false
TextField(
Modifier
.onFocusChanged {
if (!it.isFocused && isBlurred) validState.value = validate()
if (it.isFocused && !isBlurred) isBlurred = true }
)
}
}
So what happens here is: I want the validation function to be triggered after lost focus on the TextField. On startup, the field is inactive (i.e. it.isFocused == false) but I don't want to validate then. So once the focus is on the field and it was not blurred before, isBlurred becomes true and when it loses focus, it will be validated.
Let me know if this helps! Thanks!

Jetpack Compose: Make full-screen (absolutely positioned) component

How can I go about making a composable deep down within the render tree full screen, similar to how the Dialog composable works?
Say, for example, when a use clicks an image it shows a full-screen preview of the image without changing the current route.
I could do this in CSS with position: absolute or position: fixed but how would I go about doing this in Jetpack Compose? Is it even possible?
One solution would be to have a composable at the top of the tree that can be passed another composable as an argument from somewhere else in the tree, but this sounds kind of messy. Surely there is a better way.
From what I can tell you want to be able to draw from a nested hierarchy without being limited by the parent constraints.
We faced similar issues and looked at the implementation how Composables such as Popup, DropDown and Dialog function.
What they do is add an entirely new ComposeView to the Window.
Because of this they are basically starting from a blank canvas.
By making it transparent it looks like the Dialog/Popup/DropDown appears on top.
Unfortunately we could not find a Composable that provides us the functionality to just add a new ComposeView to the Window so we copied the relevant parts and made following.
#Composable
fun FullScreen(content: #Composable () -> Unit) {
val view = LocalView.current
val parentComposition = rememberCompositionContext()
val currentContent by rememberUpdatedState(content)
val id = rememberSaveable { UUID.randomUUID() }
val fullScreenLayout = remember {
FullScreenLayout(
view,
id
).apply {
setContent(parentComposition) {
currentContent()
}
}
}
DisposableEffect(fullScreenLayout) {
fullScreenLayout.show()
onDispose { fullScreenLayout.dismiss() }
}
}
#SuppressLint("ViewConstructor")
private class FullScreenLayout(
private val composeView: View,
uniqueId: UUID
) : AbstractComposeView(composeView.context) {
private val windowManager =
composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private val params = createLayoutParams()
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
init {
id = android.R.id.content
ViewTreeLifecycleOwner.set(this, ViewTreeLifecycleOwner.get(composeView))
ViewTreeViewModelStoreOwner.set(this, ViewTreeViewModelStoreOwner.get(composeView))
ViewTreeSavedStateRegistryOwner.set(this, ViewTreeSavedStateRegistryOwner.get(composeView))
setTag(R.id.compose_view_saveable_id_tag, "CustomLayout:$uniqueId")
}
private var content: #Composable () -> Unit by mutableStateOf({})
#Composable
override fun Content() {
content()
}
fun setContent(parent: CompositionContext, content: #Composable () -> Unit) {
setParentCompositionContext(parent)
this.content = content
shouldCreateCompositionOnAttachedToWindow = true
}
private fun createLayoutParams(): WindowManager.LayoutParams =
WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
token = composeView.applicationWindowToken
width = WindowManager.LayoutParams.MATCH_PARENT
height = WindowManager.LayoutParams.MATCH_PARENT
format = PixelFormat.TRANSLUCENT
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
}
fun show() {
windowManager.addView(this, params)
}
fun dismiss() {
disposeComposition()
ViewTreeLifecycleOwner.set(this, null)
windowManager.removeViewImmediate(this)
}
}
Here is an example how you can use it
#Composable
internal fun Screen() {
Column(
Modifier
.fillMaxSize()
.background(Color.Red)
) {
Text("Hello World")
Box(Modifier.size(100.dp).background(Color.Yellow)) {
DeeplyNestedComposable()
}
}
}
#Composable
fun DeeplyNestedComposable() {
var showFullScreenSomething by remember { mutableStateOf(false) }
TextButton(onClick = { showFullScreenSomething = true }) {
Text("Show full screen content")
}
if (showFullScreenSomething) {
FullScreen {
Box(
Modifier
.fillMaxSize()
.background(Color.Green)
) {
Text("Full screen text", Modifier.align(Alignment.Center))
TextButton(onClick = { showFullScreenSomething = false }) {
Text("Close")
}
}
}
}
}
The yellow box has set some constraints, which would prevent the Composables from inside to draw outside its bounds.
Using the Dialog composable, I have been able to get a proper fullscreen Composable in any nested one. It's quicker and easier than some of other answers.
Dialog(
onDismissRequest = { /* Do something when back button pressed */ },
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = false, usePlatformDefaultWidth = false)
){
/* Your full screen content */
}
If I understand correctly you just don't want to navigate anywhere. Id something like this.
when (val viewType = viewModel.viewTypeGallery.get()) {
is GalleryViewModel.GalleryViewType.Gallery -> {
Gallery(viewModel, scope, installId, filePathModifier, fragment, setImageUploadType)
}
is GalleryViewModel.GalleryViewType.ImageViewer -> {
Row(Modifier.fillMaxWidth()) {
Image(
modifier = Modifier
.fillMaxSize(),
painter = rememberCoilPainter(viewType.imgUrl),
contentScale = ContentScale.Crop,
contentDescription = null
)
}
}
}
I just keep track of what type the view is meant to be. In my case I'm not displaying a dialog I'm removing my entire gallery and showing an image instead.
Alternatively you could just have an if(viewImage) condition below your call your and layer the 'dialog' on top of it.
After notice that, at least for now, we don't have any Composable to do "easy" fullscreen, I decided to implement mine one, mostly based on ideas from #foxtrotuniform6969 and #ntoskrnl. Also, I tried to do it most possible without to use platform dependent functions then I think this is very suiteable to Desktop/Android.
You can check the basic implementation in this GitHub repository.
By the way, the implementation idea was just:
Create a composable to wrap the target composables tree that can call an FullScreen composable;
Retrieve the full screen dimensions/size from a auxiliary Box matched to the root screen size using the .onGloballyPositioned() modifier;
Store the full screen size and all FullScreen composables created in the tree onto appropriated compositionLocalOf instances (see documentation).
I tried to use this in a Desktop project and seems to be working, however I didn't tested in Android yet. The repository also contains a example.
Feel free to navigate in the repository and sent a pull request if you can. :)

Categories

Resources