Scroll multiple lists at different speeds in Jetpack Compose - android

Currently I'm trying to develop a special kind of image carousel which basically has two rows (one at the top with the images, and another one (shorter in width) at the bottom that use names for reference)
The thing is that I need to scroll the top row faster than the bottom one to achieve this with Jetpack Compose (can be done in regular Android with just some scroll listeners)
I was able to achieve scroll the rows simultaneously but they are scrolling at the same speed. I need to scroll the first one twice as fast to achieve the effect I want.
Here's the code I tried.
val scrollState = rememberScrollState()
Row(modifier = Modifier.horizontalScroll(scrollState)) {
Images()
}
Row(modifier = Modifier.horizontalScroll(scrollState)) {
Legend()
}

If you only need one Row to be scrollable, you can create new ScrollState each time.
val scrollState = rememberScrollState()
Row(modifier = Modifier.horizontalScroll(scrollState)) {
repeat(100) {
Text(it.toString())
}
}
Row(
modifier = Modifier.horizontalScroll(
ScrollState(scrollState.value * 2),
enabled = false
)
) {
repeat(200) {
Text(it.toString())
}
}
Note that this solution may not be the best in terms of performance, as it creates a class on each pixel scrolled.
An other solution is to sync them with LaunchedEffect. It'll also allow you both way scrolling synchronization.
#Composable
fun TestScreen(
) {
val scrollState1 = rememberScrollState()
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.horizontalScroll(scrollState1)
) {
repeat(100) {
Text(it.toString())
}
}
val scrollState2 = rememberScrollState()
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.horizontalScroll(scrollState2)
) {
repeat(200) {
Text(it.toString())
}
}
SyncScrollTwoWay(scrollState1, scrollState2, 2f)
}
#Composable
fun SyncScrollTwoWay(scrollState1: ScrollState, scrollState2: ScrollState, multiplier: Float) {
SyncScroll(scrollState1, scrollState2, multiplier)
SyncScroll(scrollState2, scrollState1, 1 / multiplier)
}
#Composable
fun SyncScroll(scrollState1: ScrollState, scrollState2: ScrollState, multiplier: Float) {
if (scrollState1.isScrollInProgress) {
LaunchedEffect(scrollState1.value) {
scrollState2.scrollTo((scrollState1.value * multiplier).roundToInt())
}
}
}
But it also have one downside: there'll be little delay in scrolling, so the second scroll view will always be one frame behind the scrolling one.

Related

How to make a child composable match parent composable's height only if the child is smaller than the parent?

I have a peculiar problem with Compose. I have a scrolling view, which may have either one or more children. I would need to make the content scroll if it's larger than the scroll view, but always fill the remaining height if the content is not as high as the scroll view.
The naive way I thought it would originally work was like this:
val scrollState = rememberScrollState()
Column(
modifier = modifier.fillMaxSize()
.verticalScroll(scrollState)
) {
Box(modifier = Modifier.fillMaxHeight()) {
// Content here should fill the screen and scroll if needed
}
}
However, this doesn't seem to work. While the Column fills the screen, the Box will only take as much space as its children need.
Currently I've achieved this functionality with a hack like this:
var contentHeight by remember { mutableStateOf(0) }
var containerHeight by remember { mutableStateOf(0) }
val scrollState = rememberScrollState()
Column(
modifier.fillMaxSize()
.onGloballyPositioned { containerHeight = it.size.height }
.verticalScroll(scrollState)
) {
val contentModifier = Modifier
.onGloballyPositioned {
if (contentHeight == 0) contentHeight = it.boundsInParent().height.toInt()
}
.let {
if (contentHeight > 0 && contentHeight < containerHeight) {
it.height(with(LocalDensity.current) {
containerHeight.toDp()
}
} else it
}
// The actual content
Box(modifier = contentModifier) {
// ...
}
}
This "works", however it makes the content flash for a single frame and causes multiple recompositions. Is there a cleaner way of achieving the same result?

Wear OS show image with horizontal scroll

I need to show long image like this on Galaxy 5 watch
Where can I find code examples for this job? Or which component I can use?
Use Coil to render the image.
AsyncImage(
model = "https://example.com/image.jpg",
contentDescription = null
)
You can use a horizontally scrollable Row in Compose.
Row(
modifier = Modifier.horizontalScroll(scrollState)
) {
// ...
}
https://developer.android.com/jetpack/compose/lists
You can make it scroll horizontally with RSB/Bezel with
public fun Modifier.scrollableRow(
focusRequester: FocusRequester,
scrollableState: ScrollableState
): Modifier = composed {
val coroutineScope = rememberCoroutineScope()
onPreRotaryScrollEvent {
coroutineScope.launch {
// events are vertical, but apply horizontally
scrollableState.scrollBy(it.verticalScrollPixels)
scrollableState.animateScrollBy(0f)
}
true
}
.focusRequester(focusRequester)
.focusable()
}
Here is full code example https://github.com/enginer/wear-os-horizontal-scroll-image

Android Jetpack Compose position children in LazyRow

I have a sequence of items I want to show as list of items in a Track. Only thing is, at the beginning, items have to be lined starting from middle of the track. Afterwards, the items can be scrolled normally.
e.g. at beginning :
MyTrack : [----blankSpace 50%-------[dynamic item1[[dynamic item2][dynamic item3]--]
after scrolling when all items are visible for example:
MyTrack[[item1][item2][item3][item4][item5]]
This Row has to be scrollable and each item could have varying width.
This is the item on the track:
data class MyItem(val widthFactor: Long, val color: Color)
Question : is there. a way to give start position of the items in the LazyRow ? Or is there a better Layout I should use in Jetpack Compose ?
A layout like LazyRow() won't work because there is no way to tell to start lining up items from middle of it.
I can use something like Canvas and drawRect of items in it but then I need to implement the swipe and scroll features like in LazyRow.
Thanks in advance.
You can use spacer item with .fillParentMaxWidth which is available for LazyList items:
LazyRow(
modifier = Modifier.fillMaxWidth(),
) {
item {
Spacer(modifier = Modifier.fillParentMaxWidth(0.5f))
}
// Your other items
}
This is better than the other two provided solutions - Configuration.screenWidthDp is only usable when your LazyRow fills whole screen width which is not always the case, BoxWithConstraints adds complexity that is not necessary here.
Use this to get screen width
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp.dp
Source - https://stackoverflow.com/a/68919901/9636037
Code
#Composable
fun ScrollableRowWithSpace() {
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp.dp
val list = Array(10) {
"Item ${it + 1}"
}
LazyRow(
modifier = Modifier
.background(LightGray)
.fillMaxWidth(),
) {
item {
Spacer(modifier = Modifier
.background(White)
.width(screenWidth / 2))
}
items(list) {
Text(
text = it,
modifier = Modifier
.padding(16.dp)
.background(White),
)
}
}
}
I would recommend following solution you can use it anywhere and get half of the width.
data class MyItem(val widthFactor: Long, val color: Color)
#Composable
fun LazyRowWithPadding() {
val myItemList = mutableListOf(
MyItem(12345, Color.Blue),
MyItem(12345, Color.Cyan),
MyItem(12345, Color.Green),
MyItem(12345, Color.Gray),
MyItem(12345, Color.Magenta),
)
BoxWithConstraints(Modifier.fillMaxSize()) {
LazyRow(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(start = maxWidth / 2)
) {
items(myItemList) {
Box(
Modifier
.padding(end = 8.dp)
.background(it.color)
.padding(24.dp)) {
Text(text = it.widthFactor.toString())
}
}
}
}
}
Example

How to prevent Jetpack Compose ExposedDropdownMenuBox from showing menu when scrolling

I'm trying to use Jetpack Compose's ExposedDropdownMenuBox but I can't prevent it from showing the menu when scrolling.
For example, here's the code to replicate this problem:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(color = MaterialTheme.colors.background) {
Column(
modifier = Modifier
.padding(horizontal = 8.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
repeat(20){
ExposedDropdownMenuSample()
}
}
}
}
}
}
}
ExposedDropdownMenuSample was taken from the official samples.
This is a GIF showing the problem.
How can I prevent this from happening?
This code is using compose version 1.1.0-rc01.
edit: now it doesn't swallow fling-motion as reported by m.reiter 😁
I was able to fix this with this ugly hack:
private fun Modifier.cancelOnExpandedChangeIfScrolling(cancelNext: () -> Unit) = pointerInput(Unit) {
forEachGesture {
coroutineScope {
awaitPointerEventScope {
var event: PointerEvent
var startPosition = Offset.Unspecified
var cancel = false
do {
event = awaitPointerEvent(PointerEventPass.Initial)
if (event.changes.any()) {
if (startPosition == Offset.Unspecified) {
startPosition = event.changes.first().position
}
val distance =
startPosition.minus(event.changes.last().position).getDistance()
cancel = distance > 10f || cancel
}
} while (!event.changes.all { it.changedToUp() })
if (cancel) {
cancelNext.invoke()
}
}
}
}
}
then add it to the ExposedDropdownMenuBox like:
var cancelNextExpandedChange by remember { mutableStateOf(false) } //this is to prevent fling motion from being swallowed
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
if (!cancelNextExpandedChange) expanded = !expanded
cancelNextExpandedChange = false
}, modifier = Modifier.cancelOnExpandedChangeIfScrolling() { cancelNextExpandedChange = true }
)
So it basically checks if there was a drag for more than 10 pixels? and if true, invokes the callback that sets cancelNextExpandedChange to true so it will skip the next onExpandedChange.
10 is just a magic number that worked well for my tests, but it seems to be too low for a high res screen device. I'm sure there's a better way to calculate this number... Maybe someone more experienced can help with this until we have a proper fix?
I found a slightly less hacky workaround:
You get the info whether a scrolling is in progress from the scroll-state of the Column.
val scrollState = rememberScrollState()
val isScrolling = scrollState.isScrollInProgress
Column(
modifier = Modifier
.padding(horizontal = 8.dp)
.verticalScroll(scrollState),
...
) ...
In the ExposedDropdownMenuBox you can then change the listener to
onExpandedChange = {
expanded = !expanded && !isScrolling
},
=> The dropdown is never opened while scrolling. It is also automatically closed as soon as you start scrolling in the main-column. However scrolling inside the dropdown is possible.
Of course you can also cahnge it to something like
expanded = if (isScrolling) expanded else !expanded
To just leave everything like it is while scrolling
This issue was reported in:
https://issuetracker.google.com/issues/212091796
and fixed in androidx.compose.material:material:1.4.0-alpha02

Jetpack Compose - HorizontalPager item spacing/padding for items with max width

Using Jetpack Compose and the accompanist pager, I'm trying to create a HorizontalPager where:
Edges of items to the left and right of current item are shown
There is a max width to the Pager items
As an example, I wrote the following code (for simplicities sake, I made Text items, but in reality, I'm actually making more complex Card items):
#Composable
fun MyText(modifier: Modifier) {
Text(
text = LOREM_IPSUM_TEXT,
modifier = modifier
.wrapContentHeight()
.border(BorderStroke(1.dp, Color.Red))
)
}
#ExperimentalPagerApi
#Composable
fun MyPager(pagerItem: #Composable () -> Unit = {}) {
Scaffold {
Column(
modifier = Modifier
.fillMaxSize()
// In case items in the VP are taller than the screen -> scrollable
.verticalScroll(rememberScrollState())
) {
HorizontalPager(
contentPadding = PaddingValues(32.dp),
itemSpacing = 16.dp,
count = 3,
) {
pagerItem()
}
}
}
}
#ExperimentalPagerApi
#Preview
#Composable
fun MyPager_200dpWidth() {
MyPager { MyText(modifier = Modifier.widthIn(max = 200.dp)) }
}
#ExperimentalPagerApi
#Preview
#Composable
fun MyPager_500dpWidth() {
MyPager { MyText(modifier = Modifier.widthIn(max = 500.dp)) }
}
#ExperimentalPagerApi
#Preview
#Composable
fun MyPager_FillMaxWidth() {
MyPager { MyText(modifier = Modifier.fillMaxWidth()) }
}
The issue I'm having is that when I make the item have a max width that seems to be smaller than the screen width (see MyPager_200dpWidth), I no longer see the items on the side anymore. On the other hand, using items with larger max widths (See MyPager_500dpWidth) or fillMaxWidth (See MyPager_FillMaxWidth) allows me to see the items on the side.
It seems weird to me that items with the smaller max width are actually taking up more horizontal space than the items with the larger max width. This can be shown in the Preview section...
See how the images on the left (MyPager_500dpWidth) and middle (MyPager_FillMaxWidth) show the next item, while the image on the right (MyPager_200dpWidth`) takes up the whole screen? Here's another comparison when hovering my mouse over the items to see the skeleton boxes...
Just wondering if someone could help me figure out how I can solve this use case. Is it possible that there's a bug in Compose?
The page size is controlled by the HorizontalPager.contentPadding parameter.
Applying widthIn(max = 200.dp) to your text only reduces the size of your element inside the page, while the page size remains unchanged.
Applying more padding should solve your problem, for example, contentPadding = PaddingValues(100.dp) looks like this:
You can fix this by adding your contentPadding like this
val horizontalPadding = 16.dp
val itemWidth = 340.dp
val screenWidth = LocalConfiguration.current.screenWidthDp
val contentPadding = PaddingValues(start = horizontalPadding, end = (screenWidth - itemWidth + horizontalPadding).dp)

Categories

Resources