How can I show a composable on top of the visible Keyboard? - android-jetpack-compose

Since we have differen screen sizes and resolutions, I would like to place a Composable on top of unfolded keybard:
The keyboard (see above picture) is visible and I want to show nother composable (red square) like a message for a few seconds..
Using Jetpack Compose what would be a easy way to position that red composable?

This question can be solved using WindowInsets.isImeVisible however for it to return correct values you should set
WindowCompat.setDecorFitsSystemWindows(window, false)
i set this in Activity before setContent{}
Using WindowInsets.isImeVisible to check whether keyboard is open or not.
And we need to show message while keyboard is opening up so
val offsetY = WindowInsets.ime.getBottom(density)
var previousOffset by remember { mutableStateOf(0) }
val isKeyboardGoingDown by remember(offsetY) {
derivedStateOf {
val isGoingDown = previousOffset - offsetY > 0
previousOffset = offsetY
isGoingDown
}
}
is used for tracking whether keyboard is going up or down with LaunchedEffect
LaunchedEffect(key1 = isImeVisible, key2 = isKeyboardGoingDown) {
if (isImeVisible && !isKeyboardGoingDown) {
showMessage = true
delay(1000)
showMessage = false
} else {
showMessage = false
}
}
Full implementation
#OptIn(ExperimentalLayoutApi::class)
#Composable
private fun TimedMessageLayout() {
val isImeVisible = WindowInsets.isImeVisible
var showMessage by remember { mutableStateOf(false) }
val density = LocalDensity.current
val offsetY = WindowInsets.ime.getBottom(density)
var previousOffset by remember { mutableStateOf(0) }
val isKeyboardGoingDown by remember(offsetY) {
derivedStateOf {
val isGoingDown = previousOffset - offsetY > 0
previousOffset = offsetY
isGoingDown
}
}
LaunchedEffect(key1 = isImeVisible, key2 = isKeyboardGoingDown) {
if (isImeVisible && !isKeyboardGoingDown) {
showMessage = true
delay(1000)
showMessage = false
} else {
showMessage = false
}
}
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomStart) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(WindowInsets.systemBars.asPaddingValues())
.border(2.dp, Color.Green)
) {
Image(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(4 / 3f),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = null
)
var text by remember { mutableStateOf("") }
Text(
"Ime visible: ${WindowInsets.isImeVisible}, isKeyboardGoingDown: $isKeyboardGoingDown\n" +
"ime bottom: ${WindowInsets.ime.getBottom(density)}\n"
)
Spacer(modifier = Modifier.weight(1f))
TextField(
value = text,
onValueChange = { text = it }
)
}
if (showMessage && !isKeyboardGoingDown && offsetY != 0) {
Box(modifier = Modifier
.offset {
IntOffset(0, -offsetY)
}
.fillMaxWidth()
.height(200.dp)
.border(3.dp, Color.Red))
}
}
}
Result

Try this solution from here, I slightly modified
#Composable
fun keyboardHeightAsState(): State<Int> {
val keyboardHeight = remember { mutableStateOf(0) }
val view = LocalView.current
DisposableEffect(view) {
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
keyboardHeight.value = if (keypadHeight > screenHeight * 0.15) {
keypadHeight
} else {
0
}
}
}
return keyboardHeight
}
But I still have a small difference. I guess its the top bar, which I have to subtract

Related

Jetpack Compose animation is jumping

I'm trying to add a fade in/fade out animation to switch between 2 views but the animation is jumping when I run it on the simulator.
I've added x2 AnimatedVisibility to switch between each of the views in my SignInOrSignUpMasterView.
How can I fix this?
class SignInOrSignUp : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
Surface(modifier = Modifier.fillMaxSize()) {
SignInOrSignUpMasterView()
}
}
}
}
}
//--------------------------------------------------------------------------------------------------
//SignInOrSignUpMethod enum
enum class SignInOrSignUpMethod {
Google,
EmailOnly
}
//--------------------------------------------------------------------------------------------------
//SignInOrSignUpViewModel
class SignInOrSignUpViewModel : ViewModel() {
var signInOrSignUpMethod: SignInOrSignUpMethod by mutableStateOf(SignInOrSignUpMethod.EmailOnly)
}
//--------------------------------------------------------------------------------------------------
//SignInOrSignUpMasterView
#Composable
fun SignInOrSignUpMasterView(
signInOrSignUpViewModel: SignInOrSignUpViewModel = viewModel()
) {
Box {
//Background image
Image(
painter = painterResource(id = R.drawable.sign_in_sign_up_background),
contentDescription = null,
contentScale = ContentScale.Crop,
alpha = 0.50F,
modifier = Modifier
.background(Color.White)
.fillMaxSize()
)
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
//App Logo
Image(
painter = painterResource(id = R.drawable.app_logo),
contentDescription = null,
colorFilter = ColorFilter.tint(
(if (isSystemInDarkTheme()) {
Color.Black
} else {
Color.White
})
),
modifier = Modifier
.size(125.dp, 125.dp)
.offset(0.dp, 50.dp)
)
//Spacer to fill the rest of screen to position the buttons at the bottom of the screen
Spacer(modifier = Modifier.weight(1f))
//Sign In/Up Views
AnimatedVisibility(
visible = signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.Google,
enter = fadeIn(),
exit = fadeOut()
) {
SignInOrSignUpWithGoogleView()
}
AnimatedVisibility(
visible = signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.EmailOnly,
enter = fadeIn(),
exit = fadeOut()
) {
SignInOrSignUpWithEmailAddressView()
}
Spacer(Modifier.height(10.dp))
Text("Legal information")
Spacer(Modifier.height(40.dp))
}
}
}
//--------------------------------------------------------------------------------------------------
//SignInOrSignUpWithGoogleView
#Composable
fun SignInOrSignUpWithGoogleView(
signInOrSignUpViewModel: SignInOrSignUpViewModel = viewModel()
) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
Text("Sign in with Google view")
ClickableText(
text = AnnotatedString("Continue with email address"),
onClick = {
signInOrSignUpViewModel.signInOrSignUpMethod = SignInOrSignUpMethod.EmailOnly
},
style = TextStyle(
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = if (isSystemInDarkTheme()) {
Color.Black
} else {
Color.White
}
)
)
}
}
//--------------------------------------------------------------------------------------------------
//SignInOrSignUpWithEmailAddressView
#Composable
fun SignInOrSignUpWithEmailAddressView(
signInOrSignUpViewModel: SignInOrSignUpViewModel = viewModel()
) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
Text("Sign in with email address view")
ClickableText(
text = AnnotatedString("Continue with Google"),
onClick = {
signInOrSignUpViewModel.signInOrSignUpMethod = SignInOrSignUpMethod.Google
},
style = TextStyle(
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = if (isSystemInDarkTheme()) {
Color.Black
} else {
Color.White
}
)
)
}
}
You Should Wrap your animated visibility in a box because if they are in a column the column will allocate space for the showing view while the other one is fading away resulting in a janky animation.
Box {
// Wrap your Animated Visibility in a Box
// Sign In/Up Views
androidx.compose.animation.AnimatedVisibility(
visible = signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.Google,
enter = fadeIn(),
exit = fadeOut()
) {
SignInOrSignUpWithGoogleView()
}
androidx.compose.animation.AnimatedVisibility(
visible = signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.EmailOnly,
enter = fadeIn(),
exit = fadeOut()
) {
SignInOrSignUpWithEmailAddressView()
}
}
Update
Box {
val googleAlpha by animateFloatAsState(
if (signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.Google) 1f
else 0f
)
val emailAlpha by animateFloatAsState(
if (signInOrSignUpViewModel.signInOrSignUpMethod == SignInOrSignUpMethod.EmailOnly) 1f
else 0f
)
SignInOrSignUpWithGoogleView(
modifier = Modifier.alpha(googleAlpha)
)
SignInOrSignUpWithEmailAddressView(
modifier = Modifier.alpha(emailAlpha)
)
}

Jetpack Compose detect drag gesture and detect Interaction source

I want to develop a float draggable Button using jetpack compose,
also I need to know when user drag interaction starts and when it ends.
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
content = {
val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
Log.i("dragInteraction", "-> $interaction")
}
}
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Surface(
modifier = Modifier
.offset {
IntOffset(
x = offsetX.roundToInt(),
y = offsetY.roundToInt()
)
}
.size(60.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
offsetX += dragAmount.x
offsetY += dragAmount.y
change.consume()
}
},
interactionSource = interactionSource,
onClick = {
},
content = {
},
color = Purple500
)
}
)
in this code my Surface moves currectly but I can't get DragInteraction.Start and
DragInteraction.Stop when I'm dragging it!
all I get is
androidx.compose.foundation.interaction.PressInteraction$Press#c38442d androidx.compose.foundation.interaction.PressInteraction$Cancel#e6d1ef3
any suggestion how can I detect drag state ?
detectDragGestures doesn't emit DragInteraction by default. You should either emit DragInteraction.Start, DragInteraction.Stop, DragInteraction.Cancel
as
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun DragInteractionSample() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
content = {
val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }
var text by remember { mutableStateOf("") }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is DragInteraction.Start -> {
text = "Drag Start"
}
is DragInteraction.Stop -> {
text = "Drag Stop"
}
is DragInteraction.Cancel -> {
text = "Drag Cancel"
}
}
}
}
val coroutineScope = rememberCoroutineScope()
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
val modifier = Modifier
.offset {
IntOffset(
x = offsetX.roundToInt(),
y = offsetY.roundToInt()
)
}
.size(60.dp)
.pointerInput(Unit) {
var interaction: DragInteraction.Start? = null
detectDragGestures(
onDragStart = {
coroutineScope.launch {
interaction = DragInteraction.Start()
interaction?.run {
interactionSource.emit(this)
}
}
},
onDrag = { change: PointerInputChange, dragAmount: Offset ->
offsetX += dragAmount.x
offsetY += dragAmount.y
},
onDragCancel = {
coroutineScope.launch {
interaction?.run {
interactionSource.emit(DragInteraction.Cancel(this))
}
}
},
onDragEnd = {
coroutineScope.launch {
interaction?.run {
interactionSource.emit(DragInteraction.Stop(this))
}
}
}
)
}
Surface(
modifier = modifier,
interactionSource = interactionSource,
onClick = {},
content = {},
color = MaterialTheme.colorScheme.primary
)
Text(text = text)
}
)
}
Result
or simply create an enum class and set it to a MutableState on each gesture function
enum class DragState {
Idle, DragStart, Drag, DragEnd, DragCancel
}
var dragState by remember{mutableStateOf(Idle}
And change this state on every drag callback.

Observing position of item in Lazy Column in Jetpack Compose

Is there a way where I can observer the Cyan color field position progress in percentage/fraction, to get 100% when it is fully visible and 0% when it is closed(list elements are scrolled), the idea is that I want to remove "First Last" text on left with animation.
Here is the code:
val scaffoldState = rememberScaffoldState()
val lazyListState = rememberLazyListState()
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = {
Text(fontSize = 30.sp, text = "Account", color = Color.White)
},
backgroundColor = Color.Black
)
},
content = {
LazyColumn(state = lazyListState) {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.background(color = Color.Cyan)
) {
Text(
text = "First Last",
fontSize = 20.sp,
color = Color.White,
modifier = Modifier
.padding(20.dp)
)
}
}
items(list) {
Row(
Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 20.dp, top = 10.dp, bottom = 10.dp)
) {
Text(it)
}
}
}
}
)
You can achieve this by getting height of your RowItem and using a derivedStateOf that calculates using this height or getting it using lazyListState.layoutInfo.visibleItemsInfo
val percent = remember {
derivedStateOf {
val isFirstItemVisible = lazyListState.firstVisibleItemIndex == 0
// If your first item is not visible then our progress is 0%
if (isFirstItemVisible) {
// ALTERNATIVE 1
// // This is for not dividing when it's 0 initially
// if (rowHeight == 0f) {
// 100f
// } else {
// (100f * (1 - lazyListState.firstVisibleItemScrollOffset / rowHeight)).roundToInt()
// }
// ALTERNATIVE 2
val items = lazyListState.layoutInfo.visibleItemsInfo
if (items.isNotEmpty()) {
val firstItem = lazyListState.layoutInfo.visibleItemsInfo.first()
(100f * (1f + firstItem.offset / firstItem.size.toFloat())).roundToInt()
} else {
100f
}
} else {
0f
}
}
}
Full example
You don't have to set Modifier.onSizeChanged{} with alternative 2
#Composable
private fun MyList() {
val lazyListState = rememberLazyListState()
var rowHeight by remember { mutableStateOf(0f) }
val list = mutableListOf<String>()
val percent = remember {
derivedStateOf {
val isFirstItemVisible = lazyListState.firstVisibleItemIndex == 0
// If your first item is not visible then our progress is 0%
if (isFirstItemVisible) {
// ALTERNATIVE 1
// // This is for not dividing when it's 0 initially
// if (rowHeight == 0f) {
// 100f
// } else {
// (100f * (1 - lazyListState.firstVisibleItemScrollOffset / rowHeight)).roundToInt()
// }
// ALTERNATIVE 2
val items = lazyListState.layoutInfo.visibleItemsInfo
if (items.isNotEmpty()) {
val firstItem = lazyListState.layoutInfo.visibleItemsInfo.first()
(100f * (1f + firstItem.offset / firstItem.size.toFloat())).roundToInt()
} else {
100f
}
} else {
0f
}
}
}
repeat(20) {
list.add("Row $it")
}
Box(contentAlignment = Alignment.TopEnd) {
LazyColumn(state = lazyListState) {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.onSizeChanged {
if (rowHeight == 0f) {
rowHeight = it.height.toFloat()
}
}
.background(color = Color.Cyan)
) {
Text(
text = "First Last",
fontSize = 20.sp,
color = Color.White,
modifier = Modifier
.padding(20.dp)
)
}
}
items(items = list) {
Row(
Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 20.dp, top = 10.dp, bottom = 10.dp)
) {
Text(it)
}
}
}
Text(
"Percent: ${percent.value}"
)
}
}
Result

Is there a better way to wrap bottom sheet height for ModalBottomSheetLayout/BottomSheetScaffold

My solution works, but sometimes when switching between boxes using openSheet() height of content cannot be calculated in time and it's jumps when opens.
Working solution using ModalBottomSheetLayout:
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
val density = LocalDensity.current
var topAppBarHeight = Dp.Unspecified
var sumHeight by remember { mutableStateOf(Dp.Unspecified) }
var currentSheet by remember { mutableStateOf(0) }
val openSheet: (sheet: Int) -> Unit = {
currentSheet = it
sumHeight = topAppBarHeight + (currentSheet * 50).dp
scope.launch { sheetState.show() }
}
val closeSheet: () -> Unit = {
scope.launch { sheetState.hide() }
sumHeight = Dp.Unspecified
}
ModalBottomSheetLayout(
modifier = Modifier.fillMaxSize(),
sheetState = sheetState,
sheetContent = {
Scaffold(
modifier = Modifier.height(sumHeight),
topBar = {
TopAppBar(
modifier = Modifier
.clickable(onClick = closeSheet)
.onGloballyPositioned { lc ->
topAppBarHeight = with(density) { lc.size.height.toDp() }
},
title = { Text("TopAppBarSheetContent") }
)
},
content = {
Box(
modifier = Modifier
.height((currentSheet * 50).dp)
.fillMaxWidth()
.background(Color.Blue)
)
},
)
},
content = {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(title = { Text("TopAppBarContent") })
},
content = { innerPadding ->
LazyColumn(
modifier = Modifier.padding(innerPadding),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(50) { item ->
Box(
modifier = Modifier
.clip(CircleShape)
.fillMaxWidth()
.height(75.dp)
.background(Color.Red)
.clickable { openSheet(item) }
)
}
}
}
)
}
)
Maybe there is a better way to solve this problem?

Keyboard does not hide, when focused TextField leaves the LazyColumn

Perhaps this is the normal behaviour, but i wish it was different. I had tried to google the solution, but did not find anything suitable (or merely missed it).
Sample code (for simplicity i hold mutable states right here, not using ViewModel):
#Composable
fun Greeting() {
Scaffold(topBar = {
TopAppBar(title = { Text(text = "Some title") })
}) {
val focusManager = LocalFocusManager.current
LazyColumn(
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(count = 20) { index ->
val (value, onValueChange) = rememberSaveable { mutableStateOf("Some value $index") }
TextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth(),
label = { Text(text = "Some label $index") },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = {
if (!focusManager.moveFocus(FocusDirection.Down))
focusManager.clearFocus()
}),
singleLine = true
)
}
}
}
}
Compose version 1.0.5
You could try just hiding the keyboard whenever scrolling occurs. This is okay as long as you don't have a large set of items. But since you're using TextFields, it isn't likely that you'll have such a large number. This sample illustrates hiding the keyboard when scrolling occurs:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
Greeting(this)
}
}
}
#Composable
fun Greeting(activity: Activity) {
Scaffold(topBar = {
TopAppBar(title = { Text(text = "Some title") })
}) {
val lazyListState = rememberLazyListState()
val ctx = LocalContext.current
LaunchedEffect(lazyListState.firstVisibleItemIndex) {
val inputMethodManager = ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(activity.window?.decorView?.windowToken, 0)
}
LazyColumn(
state = lazyListState,
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(
count = 20,
key = { index ->
// Return a stable + unique key for the item
index
}
) { index ->
val (value, onValueChange) = rememberSaveable { mutableStateOf("Some value $index") }
TextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth(),
label = { Text(text = "Some label $index") },
singleLine = true
)
}
}
}
}

Resources