Nested scrolling Column and LazyList - android-jetpack-compose

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

Related

How can I disable a full view in Jetpack Compose

I want to disable a whole view from any interaction (e.g. button presses) when a Boolean in my view model is true. How can I do this in Jetpack Compose without having to disable each of the elements within the view?
See example below as to what I'm trying to do.
#Composable
fun MyView(alertViewModel: AlertViewModel = viewModel()) {
var text by remember { mutableStateOf(TextFieldValue("")) }
Column(
/*
Disable all elements in the column so I don't need to disable each element individually for example:
modifier = Modifier
.disabled(
if (alertViewModel.showAlert == true) {
true
} else {
false
}
*/
) {
Text(text = "My View")
TextField(
value = text,
onValueChange = { newText ->
text = newText
}
)
Button(onClick = { /*TODO*/ }) {
}
}
}
Proceed like this :
#Composable
fun MyView(alertViewModel: AlertViewModel = viewModel()) {
var text by remember { mutableStateOf(TextFieldValue("")) }
if (alertViewModel.showAlert == true) {
Text(text = "Nothing to show")
} else {
Column(modifier = Modifier) {
Text(text = "My View")
TextField(
value = text,
onValueChange = { newText ->
text = newText
}
)
Button(onClick = { /*TODO*/ }) {
}
}
}
}
Example:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun MyView(alertViewModel: AlertViewModel = viewModel()) {
var text by remember { mutableStateOf(TextFieldValue("")) }
Box() {
Column() {
Text(text = "My View")
TextField(
value = text,
onValueChange = { newText ->
text = newText
}
)
Button(onClick = { /*TODO*/ }) {
}
}
Box(
modifier = Modifier
.then(
if(alertViewModel.showAlert == true){
Modifier.fillMaxSize().disabled(true)
}
)
)
}
}
Seems as though it isn't possible to disable a whole view in Jetpack Compose. All interact-able elements such as Button and TextField and have to be set to enabled = false individually.
Buttons: Jetpack Compose: How to disable FloatingAction Button?
TextFields: Jetpack Compose: Disable Interaction with TextField

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