Swipe to popBackStack in Jetpack Compose - android-jetpack-compose

Swipe to close the picture browser similar to Twitter.
I implemented something simply and it is much worse.
Video demonstration effect
#Composable
fun ScrollableSample() {
var offsetY by remember { mutableStateOf(0f) }
Box(contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()) {
Box(
Modifier
.offset { IntOffset(0, offsetY.roundToInt()) }
.background(Color.Blue)
.fillMaxWidth()
.height(400.dp)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
},
onDragEnd = {
println(offsetY)
}) { change, dragAmount ->
change.consume()
offsetY += dragAmount.y
}
}
)
}
}

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.

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?

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