I have a LazyGrid composable nested within a vertically scrolling layout. I want to be able to detect whenever the nested grid scrolls up and as a result scroll the outer layout to a particular point on the screen.
I tried doing something like this:
#Composable
fun MyComposable(viewModel: ViewModel) {
val coroutineScope = rememberCoroutineScope()
val scrollState = rememberScrollState()
var scrollToPosition by remember {
mutableStateOf(0F)
}
ConstraintLayout(
modifier = Modifier
.padding(8.dp)
.fillMaxSize()
.verticalScroll(scrollState) {
val (ref1, ref2, ref3, gridView) = createRefs()
if (viewModel.scrollUp.value){
coroutineScope.launch {
scrollState.animateScrollTo(scrollToPosition.roundToInt())
}
}
Row(){//some view}
//Target scrol point
Row(modifier = Modifier
.height(200.dp)
.padding(top = 8.dp)
.onGloballyPositioned { coordinates ->
scrollToPosition = coordinates.positionInParent().y
}){ ... }
//row housing nested grid
Row(){ GridView(viewModel)}
}
}
In the grid view I have:
#Composable
fun GridView (viewModel: ViewModel) {
//monitor scroll position
val lazyListState = rememberLazyListState()
val pOffset = remember {
lazyListState.firstVisibleItemScrollOffset
}
val direction = lazyListState.firstVisibleItemScrollOffset - pOffset
val scrollDown = direction >0
LazyVerticalGrid( state = lazyListState ...) {
if (scrollUp) {
viewModel.scrollDown.value = true
...
}
}
}
The scroll variable in the view model is just:
val scrollUp = mutableStateOf(false)
Now, the issue is, this setup is not very responsive and I need to scroll up and down in the grid view before the system detects it.
Is there a more efficient way to set this up to track even the minutest scrolls?
I thought of using this but since it was a composable, i couldn't use it with my view model as before and when I called it directly from the outer layout scope, it kept scrolling the outer layout to the target point before the nested grid was touched.
Related
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?
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
I have a lazyRow and I want to show list of indicators:
what I want: I want to show 6 items and when user scrolls other indicators get visible.
#Composable
private fun ImagesDotsIndicator(
modifier: Modifier,
totalDots: Int,
selectedIndex: Int
) {
LazyRow(
modifier = modifier,
horizontalArrangement = Arrangement.Center,
reverseLayout = true,
verticalAlignment = Alignment.CenterVertically
) {
if (totalDots == 1) return#LazyRow
items(totalDots) { index ->
if (index == selectedIndex) {
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(color = Color.White)
)
} else {
Box(
modifier = Modifier
.size(6.dp)
.clip(CircleShape)
.background(color = Color.LightGray)
)
}
Spacer(modifier = Modifier.padding(horizontal = 2.dp))
}
}
}
how can I make this indicator?
I would suggest you use Google's Accompanist HorizontalPager and HorizontalPagerIndicator if you want to swipe pages and show the dots. This is a layout that lays out items in a horizontal row, and allows the user to horizontally swipe between pages and also show the page indicator.
You need to add these 2 lines to your app build gradle file to add the dependencies.
// Horizontal Pager and Indicators - Accompanist
implementation "com.google.accompanist:accompanist-pager:0.24.7-alpha"
implementation "com.google.accompanist:accompanist-pager-indicators:0.24.7-alpha"
On your composable file, you can add a simple Sealed class to hold the data that you want to display e.g. text.
sealed class CustomDisplayItem(val text1:String, val text2: String){
object FirstItem: CustomDisplayItem("Hi", "World")
object SecondItem: CustomDisplayItem("Hello", "I'm John")
}
Thereafter make a template of the composable element or page that you want to show if the user swipes left or right.
#Composable
fun DisplayItemTemplate(item: CustomDisplayItem) {
Column() {
Text(text = item.text2 )
Spacer(modifier = Modifier.height(4.dp))
Text(text = item.text2)
}
}
Lastly use HorizontalPager and HorizontalPageIndicator composables to display the corresponding page when a user swipes back and forth.
#OptIn(ExperimentalPagerApi::class)
#Composable
fun ImagesDotsIndicator(
modifier: Modifier,
) {
//list of pages to display
val displayItems = listOf(CustomDisplayItem.FirstItem, CustomDisplayItem.SecondItem)
val state = rememberPagerState()
Column(modifier = modifier.fillMaxSize()) {
//A horizontally scrolling layout that allows users to
// flip between items to the left and right.
HorizontalPager(
count = 6,
state = state,
) {
/*whenever we scroll sideways the page variable changes
displaying the corresponding page */
item ->
//call template item and add the data
DisplayItemTemplate(item = displayItems[item])
}
//HorizontalPagerIndicator dots
HorizontalPagerIndicator(
pagerState = state,
activeColor = MaterialTheme.colors.primary,
inactiveColor = Color.Gray,
indicatorWidth = 16.dp,
indicatorShape = CircleShape,
spacing = 8.dp,
modifier = Modifier
.weight(.1f)
.align(CenterHorizontally)
)
}
}
Please see the above links to read more on how you can customize your composables to work in your case.
Actually it is preaty straight forward without any additional library:
val list = (0..100).toList()
val state = rememberLazyListState()
val visibleIndex by remember {
derivedStateOf {
state.firstVisibleItemIndex
}
}
Text(text = visibleIndex.toString())
LazyColumn(state = state) {
items(list) { item ->
Text(text = item.toString())
}
}
Create scroll state and use it on your list, and on created scroll state observe first visible item.
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
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.