8

Building a simple keyboard is fairly simple and straightforward in Jetpack Compose.

EDIT: Working example: https://github.com/THEAccess/compose-keyboard-ime

I built a really simple KeyRow by using this:

Key.kt

@Composable
fun Key(modifier: Modifier = Modifier, label: String, onClick: () -> Unit) {
    val shape = RoundedCornerShape(4.dp)
    //TODO: make clickable outside but don't show ripple
    Box(modifier = modifier
            .padding(2.dp)
            .clip(shape)
            .clickable(onClick = onClick)
            .background(Color.White)
            .padding(vertical = 12.dp, horizontal = 4.dp), contentAlignment = Alignment.Center) {
        Text(text = label, fontSize = 20.sp)
    }
}

KeyRow.kt

@Composable
fun KeyRow(keys: List<String>) {
    Row(modifier = Modifier.fillMaxWidth().background(color = grey200)) {
        keys.forEach {
            Key(modifier = Modifier.weight(1f), label = it, onClick = {  })
        }
    }
}

That's what it looks like:

2

I want to achieve this animation:

3

However, I'm currently stuck with this

![4]

Hierachy

-Keyboard
--KeyRow
---KeyLayout
----Key
----KeyPressedOverlay (only visible when pressed)

My main problem is that I don't know how to show the KeyPressedOverlay Composale (which is larger than the Key Composable) without making the parent Layout larger. As a result, I need to overflow the parent layout in some way.

3
  • Could you post the source code for this? I can't find a single example of building a keyboard using compose. Commented Mar 7, 2023 at 15:12
  • @thouliha github.com/THEAccess/compose-keyboard-ime Commented Mar 8, 2023 at 15:36
  • There's a PR over to get it working on current versions of compose. Commented Mar 20, 2023 at 23:13

2 Answers 2

6
+50

Not sure if it's the best way (probably not), but I found a solution using ConstraintLayout...

val keys = listOf("A", "B", "C", "D")
ConstraintLayout(
    modifier = Modifier.graphicsLayer(clip = false)
) {
    val refs = keys.map { createRef() }
    refs.forEachIndexed { index, ref ->
        val modifier = when (index) {
            0 -> Modifier.constrainAs(ref) {
                start.linkTo(parent.start)
            }
            refs.lastIndex -> Modifier.constrainAs(ref) {
                start.linkTo(refs[index - 1].end)
                end.linkTo(parent.end)
            }
            else -> Modifier.constrainAs(ref) {
                start.linkTo(refs[index - 1].end)
                end.linkTo(refs[index + 1].start)
            }
        }
        val modifierPressed = Modifier.constrainAs(createRef()) {
            start.linkTo(ref.start)
            end.linkTo(ref.end)
            bottom.linkTo(ref.bottom)
        }
        KeyboardKey(
            keyboardKey = keys[index],
            modifier = modifier,
            modifierPressed = modifierPressed,
            pressed = { s -> /* Do something with the key */}
        )
    }
}

One important detail here is graphicLayer(clip = false) (which is similar to the clipChildren in View Toolkit). Then, I'm creating a modifier to each key and to the pressed key. Noticed that the modifierPressed is aligned to the center/bottom of the other modifier. Finally the KeyboardKey is described below.

@Composable
fun KeyboardKey(
    keyboardKey: String,
    modifier: Modifier,
    modifierPressed: Modifier,
    pressed: (String) -> Unit
) {
    var isKeyPressed by remember { mutableStateOf(false) }
    Text(keyboardKey, Modifier
        .then(modifier)
        .pointerInput(Unit) {
            detectTapGestures(onPress = {
                isKeyPressed = true
                val success = tryAwaitRelease()
                if (success) {
                    isKeyPressed = false
                    pressed(keyboardKey)
                } else {
                    isKeyPressed = false
                }
            })
        }
        .background(Color.White)
        .padding(
            start = 12.dp,
            end = 12.dp,
            top = 16.dp,
            bottom = 16.dp
        ),
        color = Color.Black
    )
    if (isKeyPressed) {
        Text(
            keyboardKey, Modifier
                .then(modifierPressed)
                .background(Color.White)
                .padding(
                    start = 16.dp,
                    end = 16.dp,
                    top = 16.dp,
                    bottom = 48.dp
                ),
            color = Color.Black
        )
    }
}

This is the result I got: Keyboard effect

Edit: Adding some more logic, I was able to get this... enter image description here

I hope it helps this time ;) Here's the gist just in case... https://gist.github.com/nglauber/4cb1573efba9024c008ea71f3320b4d8

Sign up to request clarification or add additional context in comments.

5 Comments

Thank you for your anwser! Great idea to use a flat layout hierachy. Little hacky but it works :)
Is there a way to achieve this with custom layouts? ConstraintLayout can get very slow in complex situations
I was able to achieve the "overlapping" behavior without needing to use complex constraints (In my case it was getting so slow so I couldn't use it) using a simple custom layout that has a fixed height gist.github.com/THEAccess/b71e17d58d830ed6deaed7066a929576
The only problem is that is expands below and not above
I have a complete app so you can see what I mean here github.com/THEAccess/compose-keyboard-ime/blob/master/app/src/… and a gif imgur.com/a/GGL2ziI maybe you know how to solve this little problem
3

I guess you're looking for the pressIndicatorGestureFilter modifier... I tried this and worked for me...

var pressed by remember { mutableStateOf(false) }
val padding = if (pressed) 32.dp else 16.dp
Text("A", Modifier
    .pressIndicatorGestureFilter(
        onStart = {
            pressed = true
        },
        onStop = {
            pressed = false
        },
        onCancel = {
            pressed = false
        }
    )
    .background(Color.White)
    .padding(start = 16.dp, end = 16.dp, top = padding, bottom = padding)
)

1 Comment

Thank you for your answer. I have archieved the clicking behavior with interactionState. I have updated my question to be more clear about my particular problem

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.