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