Reputation: 157
I'm currently working on an keyboard app where I have implemented a popup that should display instantly when a key press. I am using jetpack compose own PopUp. However, I'm experiencing an unexpected delay in the popup's appearance. I'm seeking guidance on how to resolve this delay issue. When key is pressed it should instantly shows up and i have implemented a handler that hides it after 250 mili second. But i shows after delay of about some hundred mili-second. The popup should appear instantly when the trigger action is performed, without any noticeable delay.
This is my PopUp.kt.
package com.soloftech.keyboard.presentation.layout
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import com.soloftech.keyboard.R
@Composable
fun PopupScreen(
label: String,
isVisible: Boolean
) {
val popupWidth = dimensionResource(id = R.dimen.key_width)
val popupHeight = dimensionResource(id = R.dimen.key_height)
val cornerSize = dimensionResource(id = R.dimen.key_borderRadius)
if (isVisible) {
Popup(
properties = PopupProperties(
clippingEnabled = true,
dismissOnClickOutside = true
),
alignment = Alignment.BottomCenter
) {
Box(
Modifier
.size(width = popupWidth, height = popupHeight * 2)
.background(
colorResource(id = R.color.PopUpColor),
RoundedCornerShape(cornerSize)
),
contentAlignment = Alignment.TopCenter
) {
Text(
text = label,
color = colorResource(id = R.color.KeyTextColor),
fontSize = dimensionResource(id = R.dimen.key_popup_textSize).value.sp
)
}
}
}
}
@Preview
@Composable
fun PopUpPreview() {
PopupScreen(label = "F", isVisible = true)
}
Here i am calling this.
if (key.shouldShowPopUp) {
// Display a pop-up screen if Clicked
PopupScreen(
label = popUpLabel,
isVisible = isPopupVisible.value
)
}
IsPopUpVisible value and handler code is here.
.pointerInput(Unit) {
detectTapGestures(
onTap = {
// Handle tap gesture on the key
isPopupVisible.value = true
Handler(Looper.getMainLooper()).postDelayed({
isPopupVisible.value = false
}, 250)
if (context != null) {
playSound(context = context)
}
vibrate(vibrator)
when (key.labelMain) {
LABEL_ABC -> onLayoutSwitchClick()
LABEL_123 -> onSymbolsLayoutSwitchClick()
LABEL_EXTENDED_SYMBOLS -> onExtendedSymbolsSwitchClick()
LABEL_SYMBOLS -> onSymbolsLayoutSwitchClick()
LABEL_NUMBERS -> onNumbersSwitchClick()
LABEL_CAPS -> {
onCapsClick()
}
else -> onKeyPressed()
}
},
And here is declaration of isPopupVisible.
val isPopupVisible = remember { mutableStateOf(false) }
Here is whole CustomKey code.
package com.soloftech.keyboard.presentation.layout
import android.content.Context
import android.os.Vibrator
import android.view.inputmethod.EditorInfo
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import com.soloftech.keyboard.R
import com.soloftech.keyboard.domain.constants.LABEL_123
import com.soloftech.keyboard.domain.constants.LABEL_ABC
import com.soloftech.keyboard.domain.constants.LABEL_CAPS
import com.soloftech.keyboard.domain.constants.LABEL_EXTENDED_SYMBOLS
import com.soloftech.keyboard.domain.constants.LABEL_NUMBERS
import com.soloftech.keyboard.domain.constants.LABEL_SPACE
import com.soloftech.keyboard.domain.constants.LABEL_SYMBOLS
import com.soloftech.keyboard.domain.keyboard.Key
import com.soloftech.keyboard.domain.playSound
import com.soloftech.keyboard.domain.showPopUp
import com.soloftech.keyboard.domain.vibrate
@Composable
fun CustomKey(
key: Key,
isCapsEnabled: Boolean = false,
isCapsLockEnabled: Boolean = false,
onKeyPressed: () -> Unit,
onDragGesture: (Float) -> Unit,
onLongKeyPressed: () -> Unit,
onLongKeyPressedEnd: () -> Unit,
modifier: Modifier,
onLayoutSwitchClick: () -> Unit,
onExtendedSymbolsSwitchClick: () -> Unit,
onNumbersSwitchClick: () -> Unit,
onSymbolsLayoutSwitchClick: () -> Unit,
onCapsClick: () -> Unit,
onCapsClickToLock: () -> Unit,
context: Context?,
vibrator: Vibrator? = null,
iconResourceId: Int = key.icon,
textColor: Color = colorResource(id = R.color.KeyTextColor),
imeAction: Int? = null
) {
// State to track the visibility of the pop-up
val isPopupVisible = remember { mutableStateOf(false) }
// Determine the background color of the key based on whether it's a special character
val keyColor = if (key.isSpecialCharacter) {
colorResource(id = R.color.SpecialKeyBackground)
} else {
colorResource(id = R.color.KeyBackground)
}
// Define the shape of the key based on its label
val keyShape =
if (key.shouldBeRounded) {
RoundedCornerShape(56.dp)
} else {
RoundedCornerShape(6.dp)
}
// Calculate the main label of the key, considering Caps and Caps Lock state
val labelMain = remember(key.labelMain, isCapsEnabled, isCapsLockEnabled) {
if (isCapsEnabled || isCapsLockEnabled) {
key.labelMain.uppercase()
} else {
key.labelMain
}
}
// Determine the icon to display on the key
val icon = when (key.labelMain) {
LABEL_CAPS -> {
if (isCapsEnabled) {
R.drawable.caps_lock_on
} else if (isCapsLockEnabled) {
R.drawable.caps_lock_on_locked
} else {
R.drawable.caps_lock_off
}
}
"Done" -> {
when (imeAction) {
EditorInfo.IME_ACTION_DONE -> R.drawable.done_icon
EditorInfo.IME_ACTION_SEARCH -> R.drawable.search_icon
EditorInfo.IME_ACTION_NEXT -> R.drawable.next_icon
EditorInfo.IME_ACTION_SEND -> R.drawable.send_icon
EditorInfo.IME_ACTION_GO -> R.drawable.go_icon
EditorInfo.IME_ACTION_NONE -> R.drawable.keyboard_return
EditorInfo.IME_ACTION_PREVIOUS -> R.drawable.previous_icon
else -> key.icon
}
}
else -> iconResourceId
}
// State to track long press
var isLongPressed by remember { mutableStateOf(false) }
// Determine the label for the pop-up
val popUpLabel = if (isLongPressed) key.labelSecondary ?: "" else key.labelMain
// Interaction source for detecting gestures
val interactionSource = remember { MutableInteractionSource() }
ConstraintLayout(modifier = modifier
.clip(keyShape)
.background(color = keyColor)
.indication(
interactionSource = interactionSource, indication = rememberRipple(
color = colorResource(id = R.color.white), radius = 256.dp
)
)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
// Handle tap gesture on the key
onClick(
isPopupVisible = isPopupVisible,
context = context,
vibrator = vibrator
)
when (key.labelMain) {
LABEL_ABC -> onLayoutSwitchClick()
LABEL_123 -> onSymbolsLayoutSwitchClick()
LABEL_EXTENDED_SYMBOLS -> onExtendedSymbolsSwitchClick()
LABEL_SYMBOLS -> onSymbolsLayoutSwitchClick()
LABEL_NUMBERS -> onNumbersSwitchClick()
LABEL_CAPS -> {
onCapsClick()
}
else -> onKeyPressed()
}
},
onDoubleTap = {
// Handle double tap gesture on the key
if (key.labelMain == LABEL_CAPS) {
onCapsClickToLock()
} else {
onKeyPressed()
onKeyPressed()
}
onClick(
isPopupVisible = isPopupVisible,
context = context,
vibrator = vibrator
)
},
onLongPress = {
// Handle long press gesture on the key
isLongPressed = true
if (key.labelMain == LABEL_CAPS) {
onCapsClickToLock()
} else {
onLongKeyPressed()
}
isPopupVisible.value = true
if (context != null) {
playSound(context = context)
}
vibrate(vibrator)
},
onPress = { offset ->
// Handle press gesture on the key
val press = PressInteraction.Press(offset)
interactionSource.emit(press)
tryAwaitRelease()
if (isPopupVisible.value) {
isPopupVisible.value = false
}
onLongKeyPressedEnd()
isLongPressed = false
interactionSource.emit(PressInteraction.Release(press))
})
detectHorizontalDragGestures { _, dragAmount ->
// Handle horizontal drag gesture on the key
onDragGesture(dragAmount)
}
}
.defaultMinSize(minHeight = dimensionResource(id = R.dimen.key_height).value.dp)
) {
if (key.shouldShowIcon) {
// Display an icon on the key
Icon(painter = painterResource(id = icon),
contentDescription = key.contentDescription,
tint = textColor,
modifier = Modifier.constrainAs(createRef()) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
})
} else {
when (key.labelMain) {
LABEL_123, LABEL_ABC, LABEL_SPACE, LABEL_SYMBOLS ->
AutoResizedText(text = key.labelMain,
color = textColor,
modifier = Modifier.constrainAs(createRef()) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
})
else -> Text(
text = labelMain,
color = textColor,
fontSize = dimensionResource(id = R.dimen.key_textSize).value.sp,
modifier = Modifier.constrainAs(createRef()) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
})
}
}
key.labelSecondary?.let {
// Display secondary label if available
Text(text = it,
color = textColor,
fontSize = dimensionResource(id = R.dimen.key_textHintSize).value.sp,
modifier = Modifier.constrainAs(createRef()) {
top.linkTo(parent.top, margin = 2.dp)
end.linkTo(parent.end, margin = 2.dp)
})
}
if (key.shouldShowPopUp) {
// Display a pop-up screen if Clicked
PopupScreen(
label = popUpLabel,
isVisible = isPopupVisible.value
)
}
}
}
@Preview
@Composable
fun CustomKeyPreview() {
// Preview for CustomKey composable
CustomKey(
key = Key("f", 1F, labelSecondary = "@"),
onKeyPressed = {},
onDragGesture = {},
onLongKeyPressed = {},
onLongKeyPressedEnd = {},
modifier = Modifier,
onLayoutSwitchClick = {},
onExtendedSymbolsSwitchClick = {},
onNumbersSwitchClick = {},
onSymbolsLayoutSwitchClick = { },
onCapsClick = {},
onCapsClickToLock = {},
context = null
)
}
fun onClick(
isPopupVisible: MutableState<Boolean>, context: Context?, vibrator: Vibrator?
) {
// Handle the click event on the key
showPopUp(isPopupVisible)
if (context != null) {
playSound(context = context)
}
vibrate(vibrator)
}
Here is Keyboard layout.
package com.soloftech.keyboard.presentation.layout
import android.content.Context
import android.os.Vibrator
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.soloftech.keyboard.R
import com.soloftech.keyboard.data.layout.extendedSymbolsLayout
import com.soloftech.keyboard.data.layout.numbersLayout
import com.soloftech.keyboard.data.layout.qwertyLayout_small
import com.soloftech.keyboard.data.layout.symbolsLayout
import com.soloftech.keyboard.domain.KeyboardVisibilityProvider
import com.soloftech.keyboard.domain.keyboard.LayoutState
import com.soloftech.keyboard.presentation.MainActivity
import com.soloftech.keyboard.presentation.service.CustomImeService
@Composable
fun CustomKeyboard(
imeService: CustomImeService? = null,
vibrator: Vibrator? = null,
context: Context?,
showNumbersRow: Boolean = false,
imeActionType: Int? = null
) {
// Define the height of the keyboard based on whether the numbers row should be shown
val keyboardHeight = if (showNumbersRow) 248.dp else 208.dp
// Mutable state variables for Caps and Caps Lock
var isCapsEnabled by remember { mutableStateOf(true) }
var isCapsLockEnabled by remember { mutableStateOf(false) }
// Mutable state variable to track the current layout
var currentLayout by remember { mutableStateOf(LayoutState.Qwerty) }
// Detect changes in keyboard visibility
var isKeyboardVisible by remember { mutableStateOf(false) }
KeyboardVisibilityProvider { isVisible ->
isKeyboardVisible = isVisible
if (!isKeyboardVisible) {
// Keyboard is hidden, switch back to qwerty layout and disable Caps Lock
currentLayout = LayoutState.Qwerty
isCapsLockEnabled = false
}
}
// Determine which keys to display based on the current layout
var keys = when (currentLayout) {
LayoutState.Qwerty -> qwertyLayout_small
LayoutState.Symbols -> symbolsLayout
LayoutState.ExtendedSymbols -> extendedSymbolsLayout
LayoutState.Numbers -> numbersLayout
}
// Click handlers for layout switches
val onLayoutSwitchClick = {
currentLayout = LayoutState.Qwerty
}
val onNumbersSwitchClick = {
currentLayout = LayoutState.Numbers
}
val onSymbolsLayoutSwitchClick = {
currentLayout = LayoutState.Symbols
}
val onExtendedSymbolsSwitchClick = {
currentLayout = LayoutState.ExtendedSymbols
}
// Click handler for Caps
val onCapsClick = {
if (isCapsLockEnabled) {
isCapsLockEnabled = false
} else {
isCapsEnabled = !isCapsEnabled
}
}
// Click handler to toggle Caps Lock
val onCapsClickToLock = {
isCapsLockEnabled = !isCapsLockEnabled
isCapsEnabled = false
}
// Composable layout for the keyboard
key(keys) {
Column(
modifier = Modifier
.fillMaxWidth()
.height(keyboardHeight)
.background(colorResource(id = R.color.KeyboardBackground)),
) {
// Adjust the keys based on whether the numbers row is shown
keys = keys.drop(if (!showNumbersRow && currentLayout == LayoutState.Qwerty) 1 else 0)
.toTypedArray()
keys.forEachIndexed { rowIndex, row ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = if (showNumbersRow) {
if (rowIndex == 2 && currentLayout == LayoutState.Qwerty) 16.dp
else dimensionResource(id = R.dimen.key_marginH).value.dp
} else {
if (rowIndex == 1 && currentLayout == LayoutState.Qwerty) 16.dp
else dimensionResource(id = R.dimen.key_marginH).value.dp
},
vertical = dimensionResource(id = R.dimen.key_marginV).value.dp
),
horizontalArrangement = Arrangement.Center
) {
row.forEach { key ->
CustomKey(
key = key,
isCapsEnabled = isCapsEnabled,
isCapsLockEnabled = isCapsLockEnabled,
onKeyPressed = {
if (key.isCharacter) {
imeService?.commitText(
key = key,
isCapsEnabled = isCapsEnabled,
isCapsLockEnabled = isCapsLockEnabled
)
if (!isCapsLockEnabled && isCapsEnabled) {
isCapsEnabled = false
}
} else {
imeService?.doSomethingWith(key, false)
}
},
onDragGesture = { dragAmount ->
if (key.labelMain == "Delete") {
Log.d(MainActivity.TAG, "CustomKey: ${dragAmount.toString()}")
}
},
onLongKeyPressed = {
imeService?.doSomethingWith(key, true)
if (!isCapsLockEnabled && isCapsEnabled) {
isCapsEnabled = false
}
},
onLongKeyPressedEnd = {
imeService?.longPressedStops()
},
modifier = Modifier
.padding(horizontal = 2.dp)
.weight(key.weight),
onLayoutSwitchClick = {
onLayoutSwitchClick()
},
onExtendedSymbolsSwitchClick = {
onExtendedSymbolsSwitchClick()
},
onNumbersSwitchClick = {
onNumbersSwitchClick()
},
onSymbolsLayoutSwitchClick = {
onSymbolsLayoutSwitchClick()
},
onCapsClick = {
onCapsClick()
},
onCapsClickToLock = {
onCapsClickToLock()
},
context = context,
vibrator = vibrator,
imeAction = imeActionType
)
}
}
}
}
}
}
@Preview
@Composable
fun CustomKeyboardPreview() {
CustomKeyboard(
vibrator = null,
context = null,
)
}
I'm looking for suggestions or guidance on how to troubleshoot and resolve this delay issue with the popup. Has anyone encountered a similar problem, and what steps can I take to optimize the app and popup's performance?
Any insights or recommendations would be greatly appreciated.
Thank you in advance for your assistance.
Upvotes: 0
Views: 2455
Reputation: 10555
EDIT
It appears that there is a bug with the Popup Composable causing a memory leak. Several people reported it on the Google Issue Tracker in the last weeks. This might be related to the problem you are experiencing.
For now, you can try to downgrade your Compose dependencies to an older version and see if the issue also is present there.
implementation "androidx.compose.ui:ui:1.3.0" // use Compose Version 1.3.0
You might need to downgrade your other dependencies to match the same Compose version.
I am not sure how Hander.postDelayed()
behaves in the context of Jetpack Compose. You might want to try using a CoroutineScope
or a LaunchedEffect
instead, for example like this:
val coroutineScope = rememberCoroutineScope() // in top of Composable
// ...
onTap = {
// Handle tap gesture on the key
isPopupVisible.value = true
coroutineScope.launch{
delay(250)
isPopupVisible.value = false
}
// ...
}
Or you could also move the reponsability for handling the delay to the PopupScreen similar to this:
Composable
fun PopupScreen(
label: String,
isVisible: Boolean,
onHide: () -> Unit
) {
val popupWidth = dimensionResource(id = R.dimen.key_width)
val popupHeight = dimensionResource(id = R.dimen.key_height)
val cornerSize = dimensionResource(id = R.dimen.key_borderRadius)
LaunchedEffect(Unit){
delay(250)
onHide()
}
// ...
}
In the calling Composable, react to the onHide
callback accordingly:
if (key.shouldShowPopUp) {
// Display a pop-up screen if Clicked
PopupScreen(
label = popUpLabel,
isVisible = isPopupVisible.value,
onHide = { isPopupVisible.value = false }
)
}
Have a look at the documentation about LaunchedEffect
and rememberCoroutineScope
.
Upvotes: 1