How to make scrollbar view in android jetpack compose - android

i want something like this image. tried many solution but no result.

I use this code to show how much of the page has been scrolled
BoxWithConstraints() {
val scrollState = rememberScrollState()
val viewMaxHeight = maxHeight.value
val columnMaxScroll = scrollState.maxValue
val scrollStateValue = scrollState.value
val paddingSize = (scrollStateValue * viewMaxHeight) / columnMaxScroll
val animation = animateDpAsState(targetValue = paddingSize.dp)
val scrollBarHeight = viewMaxHeight / items
Column(
Modifier
.verticalScroll(state = scrollState)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
if (scrollStateValue < columnMaxScroll) {
Box(
modifier = Modifier
.paddingFromBaseline(animation.value)
.padding(all = 4.dp)
.height(scrollBarHeight.dp)
.width(4.dp)
.background(
color = MaterialTheme.colors.primary.copy(alpha = ContentAlpha.disabled),
shape = MaterialTheme.shapes.medium
)
.align(Alignment.TopEnd),
) {}
}
}
}
Result:

Related

I have made horizontal pager with get data from API in jetpack compose, and i dont know how to make my pager scroll automaticly like a slider

Would you like to help me, i have no idea what i am doing. I want to make a dashboard and there are banner inside it. so i decide to make banner and get data from api to fill it in banner. but i want to make it scroll automaticly
Dashboard screen
#OptIn(ExperimentalPagerApi::class, FlowPreview::class)
#Composable
fun DashboardScreen(
navController: NavHostController = rememberAnimatedNavController(),
datastore: DataStoreRepository,
dashboardViewModel: DashboardViewModel = hiltViewModel(),
) {
val state by dashboardViewModel.dashboardState.collectAsState()
val username = datastore.getUsername().collectAsState(initial = "")
val company = datastore.getUserCompany().collectAsState(initial = "")
val pagerState = rememberPagerState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp, vertical = 20.dp)
.padding(12.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
TopSectionDashboard(name = username.value, company = company.value)
Spacer(modifier = Modifier.height(20.dp))
Box {
state.dashboard?.let {
BannerSlider(
state = pagerState,
listBanner = it.banner
)
Indicators(
size = it.banner.size,
index = pagerState.currentPage,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
if (state.isLoading) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator()
}
}
}
}
BannerSlider
#OptIn(ExperimentalPagerApi::class)
#Composable
fun BannerSlider(
modifier: Modifier = Modifier,
state: PagerState,
listBanner: List<Banner>,
) {
HorizontalPager(
count = listBanner.size,
state = state
) {
BannerItem(
item = Banner(
count = listBanner[it].count,
text = listBanner[it].text,
icon = listBanner[it].icon
),
)
}
}
Banner Item
#Composable
fun BannerItem(item: Banner, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.height(200.dp)
) {
Image(
painter = painterResource(id = R.drawable.background_dashboard_slider1),
contentDescription = null,
modifier = Modifier.fillMaxSize(),
alignment = Alignment.Center,
contentScale = ContentScale.Crop,
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.align(Alignment.CenterStart)
.offset(y = (-20).dp)
) {
Column(horizontalAlignment = Alignment.Start) {
Text(
text = item.text,
fontFamily = Poppins,
fontSize = 20.sp,
color = Color.White,
modifier = Modifier.padding(horizontal = 24.dp)
)
Text(
text = "${item.count}",
fontFamily = Poppins,
fontSize = 25.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier
.padding(horizontal = 24.dp)
.offset(y = (-10).dp)
)
}
AsyncImage(
model = item.icon,
contentDescription = "$item.title",
modifier = Modifier.size(120.dp),
alignment = Alignment.CenterEnd
)
}
}
}
Please help me, i have no idea. i really appreciate it to someone who give a hand to me
in a simple way, You can make it possible by
LaunchedEffect(pagerState.currentPage) {
launch {
delay(2000L)
with(pagerState) {
val nextPage = if (currentPage < pageCount - 1) currentPage + 1 else 0
animateScrollToPage(nextPage)
}
}
}
Keep it in my mind to pass count to HorizontalPager as well

scroll lags when trying to listen to scroll position in android jetpack compose

I'm using Jetpack compose in my project. I have a scrollable column. I want to show a column as the top bar when the user scrolls the screen. For this purpose, I listen to the state of the scroll in this way:
val scrollState = rememberScrollState()
Box {
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(scrollState)
) {
...
...
...
}
TopBar(scrollOffset = (scrollState.value * 0.1))
}
and the TopBar is another composable:
#Composable
fun HiddenTopBar(scrollOffset: Double, onSearchListener: () -> Unit) {
val offset = if (-50 + scrollOffset < 0) (-50 + scrollOffset).dp else 0.dp
Box(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.offset(y = offset)
.background(MaterialTheme.colors.secondary)
.padding(vertical = MaterialTheme.space.small)
) {
...
...
...
}
}
The problem is that due to constant recomposition, the scroll lags, and it is not smooth. Is there any way I can implement it more efficiently?
Yes, it's because of constant recomposition in performance documentation.
If you were checking a state derived from scroll state such as if it's scrolled you could go for derivedState but you need it on each change, nestedScrollConnection might help i guess.
This sample might help you how to implement it
#Composable
private fun NestedScrollExample() {
val density = LocalDensity.current
val statusBarTop = WindowInsets.statusBars.getTop(density)
val toolbarHeight = 100.dp
val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() }
// our offset to collapse toolbar
val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newOffset = toolbarOffsetHeightPx.value + delta
toolbarOffsetHeightPx.value =
newOffset.coerceIn(-(2 * statusBarTop + toolbarHeightPx), 0f)
return Offset.Zero
}
}
}
Box(
Modifier
.fillMaxSize()
// attach as a parent to the nested scroll system
.nestedScroll(nestedScrollConnection)
) {
Column(
modifier = Modifier
.padding(
PaddingValues(
top = toolbarHeight + 8.dp,
start = 8.dp,
end = 8.dp,
bottom = 8.dp
)
)
.verticalScroll(rememberScrollState())
,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(modifier = Modifier
.fillMaxWidth()
.height(2000.dp))
}
TopAppBar(modifier = Modifier
.height(toolbarHeight)
.offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) },
elevation = 2.dp,
backgroundColor = Color.White,
title = { Text("toolbar offset is ${toolbarOffsetHeightPx.value}") })
}
}

Offset a wide image for horizontal parallax effect in Android Compose

I am trying to create a parallax effect with a wide image lets say:
https://placekitten.com/2000/400
On top of it i show a LazyRow with items. Whilst the user goes through those i would like to offset the image so that it 'moves along' slowly with the items.
The image should basically FillHeight and align to the Start so that it can move left to right.
The calculation part of the offset is done and works as it should. So does overlaying the lazy row. Now displaying the image properly is where i struggle.
I tried variations of this:
Image(
modifier = Modifier
.height(BG_IMAGE_HEIGHT)
.graphicsLayer {
translationX = -parallaxOffset
},
painter = painter,
contentDescription = "",
alignment = Alignment.CenterStart,
contentScale = ContentScale.FillHeight
)
Unfortunately though the rendered image is chopped off at the end of the initially visible portion so when the image moves there is just empty space coming up.
DEMO
As you can see while going through the list white space appears on the right instead of the remaining image.
How do i do this properly?
Image is too smart and doesn't draw anything beyond the bounds. translationX doesn't change the bound but only moves the view.
Here's how you can draw it manually:
val painter = painterResource(id = R.drawable.my_image_1)
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(BG_IMAGE_HEIGHT)
) {
translate(
left = -parallaxOffset,
) {
with(painter) {
draw(Size(width = painter.intrinsicSize.aspectRatio * size.height, height = size.height))
}
}
}
I don't see your code that calculates parallaxOffset, but just in case, I suggest you watch this video to get the best performance.
You can do it by drawing image to Canvas and setting srcOffset to set which section of the image should be drawn and dstOffset to where it should be drawn in canvas of drawImage function
#Composable
private fun MyComposable() {
Column {
var parallaxOffset by remember { mutableStateOf(0f) }
Spacer(modifier = Modifier.height(100.dp))
Slider(
value = parallaxOffset, onValueChange = {
parallaxOffset = it
},
valueRange = 0f..1500f
)
val imageBitmap = ImageBitmap.imageResource(id = R.drawable.kitty)
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.border(2.dp, Color.Red)
) {
val canvasWidth = size.width.toInt()
val canvasHeight = size.height.toInt()
val imageHeight = imageBitmap.height
val imageWidth = imageBitmap.width
drawImage(
image = imageBitmap,
srcOffset = IntOffset(
parallaxOffset.toInt().coerceAtMost(kotlin.math.abs(canvasWidth - imageWidth)),
0
),
dstOffset = IntOffset(0, kotlin.math.abs(imageHeight - canvasHeight) /2)
)
}
}
}
Result
I'm leaving my solution here...
#Composable
private fun ListBg(
firstVisibleIndex: Int,
totalVisibleItems: Int,
firstVisibleItemOffset: Int,
itemsCount: Int,
itemWidth: Dp,
maxWidth: Dp
) {
val density = LocalDensity.current
val firstItemOffsetDp = with(density) { firstVisibleItemOffset.toDp() }
val hasNoScroll = itemsCount <= totalVisibleItems
val totalWidth = if (hasNoScroll) maxWidth else maxWidth * 2
val scrollableBgWidth = if (hasNoScroll) maxWidth else totalWidth - maxWidth
val scrollStep = scrollableBgWidth / itemsCount
val firstVisibleScrollPercentage = firstItemOffsetDp.value / itemWidth.value
val xOffset =
if (hasNoScroll) 0.dp else -(scrollStep * firstVisibleIndex) - (scrollStep * firstVisibleScrollPercentage)
Box(
Modifier
.wrapContentWidth(unbounded = true, align = Alignment.Start)
.offset { IntOffset(x = xOffset.roundToPx(), y = 0) }
) {
Image(
painter = rememberAsyncImagePainter(
model = "https://placekitten.com/2000/400",
contentScale = ContentScale.FillWidth,
),
contentDescription = null,
alignment = Alignment.TopCenter,
modifier = Modifier
.height(232.dp)
.width(totalWidth)
)
}
}
#Composable
fun ListWithParallaxImageScreen() {
val lazyListState = rememberLazyListState()
val firstVisibleIndex by remember {
derivedStateOf {
lazyListState.firstVisibleItemIndex
}
}
val totalVisibleItems by remember {
derivedStateOf {
lazyListState.layoutInfo.visibleItemsInfo.size
}
}
val firstVisibleItemOffset by remember {
derivedStateOf {
lazyListState.firstVisibleItemScrollOffset
}
}
val itemsCount = 10
val itemWidth = 300.dp
val itemPadding = 16.dp
BoxWithConstraints(Modifier.fillMaxSize()) {
ListBg(
firstVisibleIndex,
totalVisibleItems,
firstVisibleItemOffset,
itemsCount,
itemWidth + (itemPadding * 2),
maxWidth
)
LazyRow(state = lazyListState, modifier = Modifier.fillMaxSize()) {
items(itemsCount) {
Card(
backgroundColor = Color.LightGray.copy(alpha = .5f),
modifier = Modifier
.padding(itemPadding)
.width(itemWidth)
.height(200.dp)
) {
Text(
text = "Item $it",
Modifier
.padding(horizontal = 16.dp, vertical = 6.dp)
)
}
}
}
}
}
Here is the result:

step progress bar with android compose

Currently I'm trying to find an easy way to implement a step progress bar with compose like this:
Does anyone have experience with that? Is there maybe a good library?
I don't think you need a library from this one, a simple and quick 'do it yourself' solution could be something like this:
#Composable
fun StepsProgressBar(modifier: Modifier = Modifier, numberOfSteps: Int, currentStep: Int) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
for (step in 0..numberOfSteps) {
Step(
modifier = Modifier.weight(1F),
isCompete = step < currentStep,
isCurrent = step == currentStep
)
}
}
}
#Composable
fun Step(modifier: Modifier = Modifier, isCompete: Boolean, isCurrent: Boolean) {
val color = if (isCompete || isCurrent) Color.Red else Color.LightGray
val innerCircleColor = if (isCompete) Color.Red else Color.LightGray
Box(modifier = modifier) {
//Line
Divider(
modifier = Modifier.align(Alignment.CenterStart),
color = color,
thickness = 2.dp
)
//Circle
Canvas(modifier = Modifier
.size(15.dp)
.align(Alignment.CenterEnd)
.border(
shape = CircleShape,
width = 2.dp,
color = color
),
onDraw = {
drawCircle(color = innerCircleColor)
}
)
}
}
#Preview
#Composable
fun StepsProgressBarPreview() {
val currentStep = remember { mutableStateOf(1) }
StepsProgressBar(modifier = Modifier.fillMaxWidth(), numberOfSteps = 5, currentStep = currentStep.value)
}
This will be the result:
I have created a similar function that takes customisable params.
#Composable
fun Track(
items: Int,
brush: (from: Int) -> Brush,
modifier: Modifier = Modifier,
lineWidth: Dp = 1.dp,
pathEffect: ((from: Int) -> PathEffect?)? = null,
icon: #Composable (index: Int) -> Unit,
) {
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.zIndex(-1f)
) {
val width = drawContext.size.width
val height = drawContext.size.height
val yOffset = height / 2
val itemWidth = width / items
var startOffset = itemWidth / 2
var endOffset = startOffset
val barWidth = lineWidth.toPx()
repeat(items - 1) {
endOffset += itemWidth
drawLine(
brush = brush.invoke(it),
start = Offset(startOffset, yOffset),
end = Offset(endOffset, yOffset),
strokeWidth = barWidth,
pathEffect = pathEffect?.invoke(it)
)
startOffset = endOffset
}
}
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
) {
repeat(items) { index ->
Box(
contentAlignment = Alignment.Center,
) {
icon.invoke(index)
}
}
}
}
}
Add jitpack in your build.gradle:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Add compose-stepper library to your app/build.gradle
dependencies {
implementation 'com.github.maryamrzdh:compose-stepper:1.0.0-beta01'
}
Add a Stepper in a composable function
#Composable
fun StepperView() {
val numberOfSteps = 4
var currentStep by rememberSaveable { mutableStateOf(1) }
val titleList= arrayListOf("Step 1","Step 2","Step 3","Step 4")
Stepper(
modifier = Modifier.fillMaxWidth(),
numberOfSteps = numberOfSteps,
currentStep = currentStep,
stepDescriptionList = titleList,
unSelectedColor= Color.LightGray,
selectedColor = Color.Blue,
isRainbow = false
)
For more info go to Stepper using JetPack Compose

ScrollableTabRow indicator width to match text inside Tab

My problem is that i need a tab indicator to match exactly to the text that is above it (from designs):
However, all i managed to do is get something looking like this:
My code:
ScrollableTabRow(
selectedTabIndex = selectedSeason,
backgroundColor = Color.White,
edgePadding = 0.dp,
modifier = Modifier
.padding(vertical = 24.dp)
.height(40.dp),
indicator = { tabPositions ->
TabDefaults.Indicator(
color = Color.Red,
height = 4.dp,
modifier = Modifier
.tabIndicatorOffset(tabPositions[selectedSeason])
)
}
) {
item.seasonList().forEachIndexed { index, contentItem ->
Tab(
modifier = Modifier.padding(bottom = 10.dp),
selected = index == selectedSeason,
onClick = { selectedSeason = index }
)
{
Text(
"Season " + contentItem.seasonNumber(),
color = Color.Black,
style = styles.seasonBarTextStyle(index == selectedSeason)
)
}
}
}
}
Also a little bonus question, my code for this screen is inside lazy column, now i need to have this tab row to behave somewhat like a sticky header(when it gets to the top, screen stops scrolling, but i can still scroll the items inside it)
Thanks for your help
I had the same requirement, and came up with a simpler solution. By putting the same horizontal padding on the Tab and on the indicator, the indicator aligns with the tab's content:
ScrollableTabRow(selectedTabIndex = tabIndex,
indicator = { tabPositions ->
Box(
Modifier
.tabIndicatorOffset(tabPositions[tabIndex])
.height(TabRowDefaults.IndicatorHeight)
.padding(end = 20.dp)
.background(color = Color.White)
)
}) {
Tab(modifier = Modifier.padding(end = 20.dp, bottom = 8.dp),
selected = tabIndex == 0, onClick = { tabIndex = 0}) {
Text(text = "Tab 1!")
}
Tab(modifier = Modifier.padding(end = 20.dp, bottom = 8.dp),
selected = tabIndex == 1, onClick = { tabIndex = 1}) {
Text(text = "Tab 2!")
}
}
Have a look at the provided modifier, it internally computes a width value. If you change the Modifier yourself to the code below you can provide a width value.
fun Modifier.ownTabIndicatorOffset(
currentTabPosition: TabPosition,
currentTabWidth: Dp = currentTabPosition.width
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "tabIndicatorOffset"
value = currentTabPosition
}
) {
val indicatorOffset by animateAsState(
targetValue = currentTabPosition.left,
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
)
fillMaxWidth()
.wrapContentSize(Alignment.BottomStart)
.offset(x = indicatorOffset + ((currentTabPosition.width - currentTabWidth) / 2))
.preferredWidth(currentTabWidth)
}
Now to the point of how to get the width of your Text:
Warning: I think it's not the way to do it but I can't figure out a better one atm.
At first, I create a Composable to provide me the width of its contents.
#Composable
fun MeasureWidthOf(setWidth: (Int) -> Unit, content: #Composable () -> Unit) {
Layout(
content = content
) { list: List<Measurable>, constraints: Constraints ->
check(list.size == 1)
val placeable = list.last().measure(constraints)
layout(
width = placeable.width.also(setWidth),
height = placeable.height
) {
placeable.placeRelative(x = 0, y = 0)
}
}
}
Now I can use it in your example (simplified):
// Needed for Android
fun Float.asPxtoDP(density: Float): Dp {
return (this / (density)).dp
}
fun main(args: Array<String>) {
Window(size = IntSize(600, 800)) {
val (selectedSeason, setSelectedSeason) = remember { mutableStateOf(0) }
val seasonsList = mutableListOf(2020, 2021, 2022)
val textWidth = remember { mutableStateListOf(0, 0, 0) }
// Android
val density = AmbientDensity.current.density
ScrollableTabRow(
selectedTabIndex = selectedSeason,
backgroundColor = Color.White,
edgePadding = 0.dp,
modifier = Modifier
.padding(vertical = 24.dp)
.height(40.dp),
indicator = { tabPositions ->
TabDefaults.Indicator(
color = Color.Red,
height = 4.dp,
modifier = Modifier
.ownTabIndicatorOffset(
currentTabPosition = tabPositions[selectedSeason],
// Android:
currentTabWidth = textWidth[selectedSeason].asPxtoDP(density)
// Desktop:
currentTabWidth = textWidth[selectedSeason].dp
)
)
}
) {
seasonsList.forEachIndexed { index, contentItem ->
Tab(
modifier = Modifier.padding(bottom = 10.dp),
selected = index == selectedSeason,
onClick = { setSelectedSeason(index) }
)
{
val text = #Composable {
Text(
text = "Season $contentItem",
color = Color.Black,
textAlign = TextAlign.Center
)
}
if (index == selectedSeason) {
MeasureWidthOf(setWidth = { textWidth[index] = it }) {
text()
}
} else {
text()
}
}
}
}
}
}
Edit (05.01.2021): Simplified Modifier code
Edit (09.01.2021): Fixed density problem on android and tested on Desktop and Android

Categories

Resources