I have this logic with which I can already change the background alpha as the user scrolls, but it's not exactly what I want. I want the alpha to start decreasing until it disappears after the user has scrolled 1.5 of the screen. How can I do this?
val list = mutableListOf<String>().apply {
for (i in 0..1000) {
add("Item $i")
}
}
setContent {
val lazyListState = rememberLazyListState()
val visibility by remember {
derivedStateOf {
when {
lazyListState.layoutInfo.visibleItemsInfo.isNotEmpty() && lazyListState.firstVisibleItemIndex == 0 -> {
val imageSize = lazyListState.layoutInfo.visibleItemsInfo[0].size
val scrollOffset = lazyListState.firstVisibleItemScrollOffset
scrollOffset / imageSize.toFloat()
}
else -> 1f
}
}
}
ScrollableBackgroundTheme {
// A surface container using the 'background' color from the theme
Box {
Image(
painter = painterResource(id = R.drawable.main_background),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.graphicsLayer {
alpha = 1f - visibility
}
)
LazyColumn(
state = lazyListState
) {
items(list.size) { index ->
Text(text = list[index], modifier = Modifier.padding(20.dp))
}
}
}
}
}
Related
I am learning jetpack compose.I am trying to implement a viewpager in jetpack compose where 5 image will be auto scrolled after 3 sec just like a carousel banner.Everything is alright before last index item image.After auto scroll to last index ,page should be scrolled to 0 index and will repeat.That's where the problem begain.The pager not working perfectly here .It's reapeting 3-4 index and sometimes stuck between to image/page after first auto scroll.
This is the img
My Code
#OptIn(ExperimentalPagerApi::class)
#Composable
fun HorizontalPagerScreen() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 5.dp)
) {
val items = createItems()
val pagerState = rememberPagerState()
HorizontalPager(
modifier = Modifier
.fillMaxWidth()
.height(250.dp),
count = items.size,
state = pagerState,
verticalAlignment = Alignment.Top,
) { currentPage ->
Image(
painter = rememberAsyncImagePainter(items[currentPage].Image),
contentDescription = items[currentPage].title,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth(),
)
//Here's the code for auto scrolling
LaunchedEffect(key1= Unit, key2= pagerState.currentPage) {
while (true) {
yield()
delay(3000)
var newPage = pagerState.currentPage + 1
if (newPage > items.lastIndex) newPage = 0
pagerState.animateScrollToPage(newPage)
}
}
}
}
}
**How to make it auto scroll for infinite times **
You can create a loopingCount variable that you increment every few seconds using a LaunchedEffect and then mod it with the max amount of pages, you also need to take into account if the user is dragging on the pager or not.
The full code sample can be found here, but added below too:
#Composable
fun HorizontalPagerLoopingIndicatorSample() {
Scaffold(
modifier = Modifier.fillMaxSize()
) { padding ->
Column(
Modifier
.fillMaxSize()
.padding(padding)
) {
// Display 10 items
val pageCount = 10
// We start the pager in the middle of the raw number of pages
val loopingCount = Int.MAX_VALUE
val startIndex = loopingCount / 2
val pagerState = rememberPagerState(initialPage = startIndex)
fun pageMapper(index: Int): Int {
return (index - startIndex).floorMod(pageCount)
}
HorizontalPager(
// Set the raw page count to a really large number
pageCount = loopingCount,
state = pagerState,
// Add 32.dp horizontal padding to 'center' the pages
contentPadding = PaddingValues(horizontal = 32.dp),
// Add some horizontal spacing between items
pageSpacing = 4.dp,
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) { index ->
// We calculate the page from the given index
val page = pageMapper(index)
PagerSampleItem(
page = page,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
)
}
HorizontalPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp),
pageCount = pageCount,
pageIndexMapping = ::pageMapper
)
val loopState = remember {
mutableStateOf(true)
}
LoopControl(loopState, Modifier.align(Alignment.CenterHorizontally))
ActionsRow(
pagerState = pagerState,
modifier = Modifier.align(Alignment.CenterHorizontally),
infiniteLoop = true
)
var underDragging by remember {
mutableStateOf(false)
}
LaunchedEffect(key1 = Unit) {
pagerState.interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> underDragging = true
is PressInteraction.Release -> underDragging = false
is PressInteraction.Cancel -> underDragging = false
is DragInteraction.Start -> underDragging = true
is DragInteraction.Stop -> underDragging = false
is DragInteraction.Cancel -> underDragging = false
}
}
}
val looping = loopState.value
if (underDragging.not() && looping) {
LaunchedEffect(key1 = underDragging) {
try {
while (true) {
delay(1000L)
val current = pagerState.currentPage
val currentPos = pageMapper(current)
val nextPage = current + 1
if (underDragging.not()) {
val toPage = nextPage.takeIf { nextPage < pageCount } ?: (currentPos + startIndex + 1)
if (toPage > current) {
pagerState.animateScrollToPage(toPage)
} else {
pagerState.scrollToPage(toPage)
}
}
}
} catch (e: CancellationException) {
Log.i("page", "Launched paging cancelled")
}
}
}
}
}
}
#Composable
fun LoopControl(
loopState: MutableState<Boolean>,
modifier: Modifier = Modifier,
) {
IconButton(
onClick = { loopState.value = loopState.value.not() },
modifier = modifier
) {
val icon = if (loopState.value) {
Icons.Default.PauseCircle
} else {
Icons.Default.PlayCircle
}
Icon(imageVector = icon, contentDescription = null)
}
}
private fun Int.floorMod(other: Int): Int = when (other) {
0 -> this
else -> this - floorDiv(other) * other
}
How to clip or cut Composable content to have Image, Button or Composables to have custom shapes? This question is not about using Modifier.clip(), more like accomplishing task with alternative methods that allow outcomes that are not possible or when it's difficult to create a shape like cloud or Squircle.
This is share your knowledge, Q&A-style question which inspired by M3 BottomAppBar or BottomNavigation not having cutout shape, couldn't find question, and drawing a Squircle shape being difficult as in this question.
More and better ways of clipping or customizing shapes and Composables are more than welcome.
One of the ways for achieving cutting or clipping a Composable without the need of creating a custom Composable is using
Modifier.drawWithContent{} with a layer and a BlendMode or PorterDuff modes.
With Jetpack Compose for these modes to work you either need to set alpha less than 1f or use a Layer as in answer here.
I go with layer solution because i don't want to change content alpha
fun ContentDrawScope.drawWithLayer(block: ContentDrawScope.() -> Unit) {
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
block()
restoreToCount(checkPoint)
}
}
block lambda is the draw scope for Modifier.drawWithContent{} to do clipping
and another extension for simplifying further
fun Modifier.drawWithLayer(block: ContentDrawScope.() -> Unit) = this.then(
Modifier.drawWithContent {
drawWithLayer {
block()
}
}
)
Clip button at the left side
First let's draw the button that is cleared a circle at left side
#Composable
private fun WhoAteMyButton() {
val circleSize = LocalDensity.current.run { 100.dp.toPx() }
Box(
modifier = Modifier
.fillMaxWidth()
.drawWithLayer {
// Destination
drawContent()
// Source
drawCircle(
center = Offset(0f, 10f),
radius = circleSize,
blendMode = BlendMode.SrcOut,
color = Color.Transparent
)
}
) {
Button(
modifier = Modifier
.padding(horizontal = 10.dp)
.fillMaxWidth(),
onClick = { /*TODO*/ }) {
Text("Hello World")
}
}
}
We simply draw a circle but because of BlendMode.SrcOut intersection of destination is removed.
Clip button and Image with custom image
For squircle button i found an image from web
And clipped button and image using this image with
#Composable
private fun ClipComposables() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
val imageBitmap = ImageBitmap.imageResource(id = R.drawable.squircle)
Box(modifier = Modifier
.size(150.dp)
.drawWithLayer {
// Destination
drawContent()
// Source
drawImage(
image = imageBitmap,
dstSize = IntSize(width = size.width.toInt(), height = size.height.toInt()),
blendMode = BlendMode.DstIn
)
}
) {
Box(
modifier = Modifier
.size(150.dp)
.clickable { }
.background(MaterialTheme.colorScheme.inversePrimary),
contentAlignment = Alignment.Center
) {
Text(text = "Squircle", fontSize = 20.sp)
}
}
Box(modifier = Modifier
.size(150.dp)
.drawWithLayer {
// Destination
drawContent()
// Source
drawImage(
image = imageBitmap,
dstSize = IntSize(width = size.width.toInt(), height = size.height.toInt()),
blendMode = BlendMode.DstIn
)
}
) {
Image(
painterResource(id = R.drawable.squirtle),
modifier = Modifier
.size(150.dp),
contentScale = ContentScale.Crop,
contentDescription = ""
)
}
}
}
There are 2 things to note here
1- Blend mode is BlendMode.DstIn because we want texture of Destination with shape of Source
2- Drawing image inside ContentDrawScope with dstSize to match Composable size. By default it's drawn with png size posted above.
Creating a BottomNavigation with cutout shape
#Composable
private fun BottomBarWithCutOutShape() {
val density = LocalDensity.current
val shapeSize = density.run { 70.dp.toPx() }
val cutCornerShape = CutCornerShape(50)
val outline = cutCornerShape.createOutline(
Size(shapeSize, shapeSize),
LocalLayoutDirection.current,
density
)
val icons =
listOf(Icons.Filled.Home, Icons.Filled.Map, Icons.Filled.Settings, Icons.Filled.LocationOn)
Box(
modifier = Modifier.fillMaxWidth()
) {
BottomNavigation(
modifier = Modifier
.drawWithLayer {
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
val width = size.width
val outlineWidth = outline.bounds.width
val outlineHeight = outline.bounds.height
// Destination
drawContent()
// Source
withTransform(
{
translate(
left = (width - outlineWidth) / 2,
top = -outlineHeight / 2
)
}
) {
drawOutline(
outline = outline,
color = Color.Transparent,
blendMode = BlendMode.Clear
)
}
restoreToCount(checkPoint)
}
},
backgroundColor = Color.White
) {
var selectedIndex by remember { mutableStateOf(0) }
icons.forEachIndexed { index, imageVector: ImageVector ->
if (index == 2) {
Spacer(modifier = Modifier.weight(1f))
BottomNavigationItem(
icon = { Icon(imageVector, contentDescription = null) },
label = null,
selected = selectedIndex == index,
onClick = {
selectedIndex = index
}
)
} else {
BottomNavigationItem(
icon = { Icon(imageVector, contentDescription = null) },
label = null,
selected = selectedIndex == index,
onClick = {
selectedIndex = index
}
)
}
}
}
// This is size fo BottomNavigationItem
val bottomNavigationHeight = LocalDensity.current.run { 56.dp.roundToPx() }
FloatingActionButton(
modifier = Modifier
.align(Alignment.TopCenter)
.offset {
IntOffset(0, -bottomNavigationHeight / 2)
},
shape = cutCornerShape,
onClick = {}
) {
Icon(imageVector = Icons.Default.Add, contentDescription = null)
}
}
}
This code is a bit long but we basically create a shape like we always and create an outline to clip
val cutCornerShape = CutCornerShape(50)
val outline = cutCornerShape.createOutline(
Size(shapeSize, shapeSize),
LocalLayoutDirection.current,
density
)
And before clipping we move this shape section up as half of the height to cut only with half of the outline
withTransform(
{
translate(
left = (width - outlineWidth) / 2,
top = -outlineHeight / 2
)
}
) {
drawOutline(
outline = outline,
color = Color.Transparent,
blendMode = BlendMode.Clear
)
}
Also to have a BottomNavigation such as BottomAppBar that places children on both side
i used a Spacer
icons.forEachIndexed { index, imageVector: ImageVector ->
if (index == 2) {
Spacer(modifier = Modifier.weight(1f))
BottomNavigationItem(
icon = { Icon(imageVector, contentDescription = null) },
label = null,
selected = selectedIndex == index,
onClick = {
selectedIndex = index
}
)
} else {
BottomNavigationItem(
icon = { Icon(imageVector, contentDescription = null) },
label = null,
selected = selectedIndex == index,
onClick = {
selectedIndex = index
}
)
}
}
Then we simply add a FloatingActionButton, i used offset but you can create a bigger parent and put our custom BottomNavigation and button inside it.
I have my composable which I am displaying the content according to some states. First time I open the screen everything works fine but when I move my app to background and then come back it stops being recomposed. When I try to debug it I can see that the state is changing but for some reason my view is not being updated. Here is my composable:
val coroutineScope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val isKeyboardOpen by keyboardAsState()
var buttonHeight by remember { mutableStateOf(0.dp) }
Column(
modifier = Modifier
.fillMaxSize()
.imePadding()
.verticalScroll(scrollState)
.background(color = White),
) {
Text(
modifier = Modifier.padding(
start = 18.dp,
end = 18.dp,
top = 22.dp,
bottom = 12.dp
),
text = "text",
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface
)
BasicTextField(
modifier = Modifier
.padding(horizontal = 18.dp)
.onGloballyPositioned { coordinates ->
if (isKeyboardOpen == Keyboard.Opened) {
coroutineScope.launch {
scrollState.animateScrollTo(coordinates.positionInParent().y.roundToInt())
}
}
},
onValueChange = { onEvent.invoke(Event.ReasonFieldValueChanged(it)) },
onFocused = { onEvent.invoke(Event.ReasonFieldFocused) }
)
Card(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 24.dp),
backgroundColor = MaterialTheme.colors.background,
elevation = 0.dp
) {
Text(
modifier = Modifier.padding(16.dp),
text = "text",
style = MaterialTheme.typography.body1.secondary
)
}
if (isKeyboardOpen == Keyboard.Closed) {
Spacer(modifier = Modifier.weight(1f))
CloseAccountButton(state, onEvent, isKeyboardOpen) {
buttonHeight = it
}
} else {
Spacer(modifier = Modifier.size(buttonHeight))
}
}
if (isKeyboardOpen == Keyboard.Opened) {
CloseAccountButton(state, onEvent, isKeyboardOpen) {
buttonHeight = it
}
}
And here is how I get keyboard state:
enum class Keyboard {
Opened, Closed
}
#Composable
fun keyboardAsState(): State<Keyboard> {
val keyboardState = remember { mutableStateOf(Keyboard.Closed) }
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
keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
Keyboard.Opened
} else {
Keyboard.Closed
}
}
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
}
}
return keyboardState
}
As I said on first opening of the screen everything is fine but when coming back from background neither scrolling to position nor raacting to keyboard visibility is working. I don't have much experience with compose, what is wrong here?
I am working on recreating facebook reactions with jetpack compose. Have now an issue with how I can detect when hovering over an item in a row and scale it? To be more clear I am creating THIS. I reused popup from jetpack compose that part is fine. But dragging/hovering over the row not sure how to scale images. Any advice or help?
I did this initial implementation for you. I hope you can continue your implementation from here:
#ExperimentalComposeUiApi
#Composable
fun ReactionsComponent() {
val density = LocalDensity.current
var selectedIndex by remember {
mutableStateOf(-1)
}
val iconSize = 48.dp
val boxPadding = 8.dp
val iconSizePx = with(density) { iconSize.toPx() }
val boxPaddingPx = with(density) { boxPadding.toPx() }
val increaseSize = iconSize.times(2f)
val icons = listOf(
Icons.Default.Favorite,
Icons.Default.Star,
Icons.Default.Call,
Icons.Default.AccountBox,
Icons.Default.ThumbUp
)
Box(
Modifier
.height(increaseSize)
.width(IntrinsicSize.Min)
.pointerInteropFilter {
val selection = ((it.x - boxPaddingPx) / iconSizePx).toInt()
if (selection >= icons.size || selection < 0 || it.x < boxPaddingPx) {
selectedIndex = -1
} else if (it.action == MotionEvent.ACTION_UP) {
selectedIndex = -1 // finger released
} else {
selectedIndex = selection
}
true
}
) {
Box(
Modifier
.align(Alignment.BottomStart)
.fillMaxWidth()
.height(iconSize + boxPadding.times(2))
.background(Color.LightGray, CircleShape)
)
Row(
Modifier
.align(Alignment.BottomStart)
.width(IntrinsicSize.Min)
.padding(boxPadding),
verticalAlignment = Alignment.Bottom
) {
icons.forEachIndexed { index, icon ->
val size = if (selectedIndex == index) increaseSize else iconSize
Box(
Modifier
.border(1.dp, Color.LightGray, CircleShape)
.background(Color.White, CircleShape)
.height(animateDpAsState(size).value)
.width(animateDpAsState(size).value)
) {
Icon(
icon,
contentDescription = null,
tint = Color.Magenta,
modifier = Modifier.fillMaxSize().padding(8.dp)
)
}
}
}
}
}
Here's the result:
You can find the full code here (which includes item selection).
I want to build this awesome button animation pressed from the AirBnB App with Jetpack Compose
Unfortunately, the Animation/Transition API was changed recently and there's almost no documentation for it. Can someone help me get the right approach to implement this button press animation?
Edit
Based on #Amirhosein answer I have developed a button that looks almost exactly like the Airbnb example
Code:
#Composable
fun AnimatedButton() {
val boxHeight = animatedFloat(initVal = 50f)
val relBoxWidth = animatedFloat(initVal = 1.0f)
val fontSize = animatedFloat(initVal = 16f)
fun animateDimensions() {
boxHeight.animateTo(45f)
relBoxWidth.animateTo(0.95f)
// fontSize.animateTo(14f)
}
fun reverseAnimation() {
boxHeight.animateTo(50f)
relBoxWidth.animateTo(1.0f)
//fontSize.animateTo(16f)
}
Box(
modifier = Modifier
.height(boxHeight.value.dp)
.fillMaxWidth(fraction = relBoxWidth.value)
.clip(RoundedCornerShape(8.dp))
.background(Color.Black)
.clickable { }
.pressIndicatorGestureFilter(
onStart = {
animateDimensions()
},
onStop = {
reverseAnimation()
},
onCancel = {
reverseAnimation()
}
),
contentAlignment = Alignment.Center
) {
Text(text = "Explore Airbnb", fontSize = fontSize.value.sp, color = Color.White)
}
}
Video:
Unfortunately, I cannot figure out how to animate the text correctly as It looks very bad currently
Are you looking for something like this?
#Composable
fun AnimatedButton() {
val selected = remember { mutableStateOf(false) }
val scale = animateFloatAsState(if (selected.value) 2f else 1f)
Column(
Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = { },
modifier = Modifier
.scale(scale.value)
.height(40.dp)
.width(200.dp)
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
selected.value = true }
MotionEvent.ACTION_UP -> {
selected.value = false }
}
true
}
) {
Text(text = "Explore Airbnb", fontSize = 15.sp, color = Color.White)
}
}
}
Here's the implementation I used in my project. Seems most concise to me.
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val sizeScale by animateFloatAsState(if (isPressed) 0.5f else 1f)
Button(
onClick = { },
modifier = Modifier
.wrapContentSize()
.graphicsLayer(
scaleX = sizeScale,
scaleY = sizeScale
),
interactionSource = interactionSource
) { Text(text = "Open the reward") }
Use pressIndicatorGestureFilter to achieve this behavior.
Here is my workaround:
#Preview
#Composable
fun MyFancyButton() {
val boxHeight = animatedFloat(initVal = 60f)
val boxWidth = animatedFloat(initVal = 200f)
Box(modifier = Modifier
.height(boxHeight.value.dp)
.width(boxWidth.value.dp)
.clip(RoundedCornerShape(4.dp))
.background(Color.Black)
.clickable { }
.pressIndicatorGestureFilter(
onStart = {
boxHeight.animateTo(55f)
boxWidth.animateTo(180f)
},
onStop = {
boxHeight.animateTo(60f)
boxWidth.animateTo(200f)
},
onCancel = {
boxHeight.animateTo(60f)
boxWidth.animateTo(200f)
}
), contentAlignment = Alignment.Center) {
Text(text = "Utforska Airbnb", color = Color.White)
}
}
The default jetpack compose Button consumes tap gestures in its onClick event and pressIndicatorGestureFilter doesn't receive taps. That's why I created this custom button
You can use the Modifier.pointerInput to detect the tapGesture.
Define an enum:
enum class ComponentState { Pressed, Released }
Then:
var toState by remember { mutableStateOf(ComponentState.Released) }
val modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = {
toState = ComponentState.Pressed
tryAwaitRelease()
toState = ComponentState.Released
}
)
}
// Defines a transition of `ComponentState`, and updates the transition when the provided [targetState] changes
val transition: Transition<ComponentState> = updateTransition(targetState = toState, label = "")
// Defines a float animation to scale x,y
val scalex: Float by transition.animateFloat(
transitionSpec = { spring(stiffness = 50f) }, label = ""
) { state ->
if (state == ComponentState.Pressed) 1.25f else 1f
}
val scaley: Float by transition.animateFloat(
transitionSpec = { spring(stiffness = 50f) }, label = ""
) { state ->
if (state == ComponentState.Pressed) 1.05f else 1f
}
Apply the modifier and use the Modifier.graphicsLayer to change also the text dimension.
Box(
modifier
.padding(16.dp)
.width((100 * scalex).dp)
.height((50 * scaley).dp)
.background(Color.Black, shape = RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center) {
Text("BUTTON", color = Color.White,
modifier = Modifier.graphicsLayer{
scaleX = scalex;
scaleY = scaley
})
}
Here is the ScalingButton, the onClick callback is fired when users click the button and state is reset when users move their finger out of the button area after pressing the button and not releasing it. I'm using Modifier.pointerInput function to detect user inputs:
#Composable
fun ScalingButton(onClick: () -> Unit, content: #Composable RowScope.() -> Unit) {
var selected by remember { mutableStateOf(false) }
val scale by animateFloatAsState(if (selected) 0.7f else 1f)
Button(
onClick = onClick,
modifier = Modifier
.scale(scale)
.pointerInput(Unit) {
while (true) {
awaitPointerEventScope {
awaitFirstDown(false)
selected = true
waitForUpOrCancellation()
selected = false
}
}
}
) {
content()
}
}
OR
Another approach without using an infinite loop:
#Composable
fun ScalingButton(onClick: () -> Unit, content: #Composable RowScope.() -> Unit) {
var selected by remember { mutableStateOf(false) }
val scale by animateFloatAsState(if (selected) 0.75f else 1f)
Button(
onClick = onClick,
modifier = Modifier
.scale(scale)
.pointerInput(selected) {
awaitPointerEventScope {
selected = if (selected) {
waitForUpOrCancellation()
false
} else {
awaitFirstDown(false)
true
}
}
}
) {
content()
}
}
If you want to animated button with different types of animation like scaling, rotating and many different kind of animation then you can use this library in jetpack compose. Check Here