Observing position of item in Lazy Column in Jetpack Compose - android-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

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)
)
}

How can I show a composable on top of the visible Keyboard?

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

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?

Call popup composable function inside onclick of Button in jetpack compose

Hi i want to show popup when something happens . I have this popup:
#Composable
fun popup(message:String,height:Dp,width:Dp,icon:String=""){
Column() {
val openDialog = remember { mutableStateOf(true) }
val dialogWidth = width/(1.3F)
val dialogHeight = height/2
if (openDialog.value) {
Dialog(onDismissRequest = { openDialog.value = false }) {
// Draw a rectangle shape with rounded corners inside the dialog
Box(
Modifier
.size(dialogWidth, dialogHeight)
.background(Color.White)){
Column(modifier = Modifier.fillMaxWidth().padding()) {
Text(text = message)
}
}
}
}
Button(onClick = {
openDialog.value=!openDialog.value
}) {
}
}
}
But i am trying to call him inside onclick Button event :
Button(modifier = Modifier
.padding(start = 6.dp, end = 6.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = Azul99),
onClick = {
if (vm.validateCredentials()=="ok"){
vm.createUser()
}else{
popup(vm.validateCredentials(),200.dp,200.dp,"fill")
}
},
shape = RoundedCornerShape(percent = 28)
) {
Text(text = "Registrarme",
modifier= Modifier.fillMaxWidth(),
style= TextStyle(fontWeight = FontWeight.Bold),
color= Color.White,
textAlign = TextAlign.Center)
}
and Android Studio says: "#Composable invocations can only happen from the context of a #Composable function" How can i call the popup ??
Store showPopUp boolean as state and show popUp by that state;
val showPopUp by remember { mutableStateOf(false)} // -> STATE
Button(
modifier = Modifier
.padding(start = 6.dp, end = 6.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = Azul99),
onClick = {
if (vm.validateCredentials()=="ok"){
vm.createUser()
}else{
showPopUp = !showPopUp // -> CHANGE IN HERE
}
},
shape = RoundedCornerShape(percent = 28)
) {
Text(
text = "Registrarme",
modifier= Modifier.fillMaxWidth(),
style= TextStyle(fontWeight = FontWeight.Bold),
color= Color.White,
textAlign = TextAlign.Center
)
}
if(showPopUp){
popup(vm.validateCredentials(),200.dp,200.dp,"fill") // -> SHOW HERE
}
Change

Nested scrolling Column and LazyList

I need the whole column as one scroll component. I can't use item directly since I need to use aclassen/ComposeReorderable library for drag and drop scrolling. Any ideas?
val scrollState = rememberScrollState()
Column(
modifier = Modifier.verticalScroll(scrollState)
) {
LazyColumn(){
repeat(20) {
item {
Text(text = "Item: " + it)
}
}
}
Text(text = "Header")
LazyColumn(){
repeat(20) {
item {
Text(text = "Item: " + it)
}
}
}
}
I tried this but the scrolling feels unnatural and I don't know how to add the nestedScroll on non list Composables like the header
val offset = remember { mutableStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
offset.value = (offset.value + available.y).coerceIn(-200f, 0f)
return Offset.Zero
}
}
}
Column(
modifier = Modifier.offset(y = offset.value.dp)
) {
LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollConnection)
){
repeat(20) {
item { Text(text = "Item: $it") }
}
}
Text(text = "Header")
LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollConnection)
){
repeat(20) {
item { Text(text = "Item: $it") }
}
}
}

Resources