I am creating a screen with a color picker. Here's the code for the whole screen, including the color picker code. The reason why I'm including the whole screen's code instead of just the color picker's is because I want to give more context.
package com.gitlab.djsushi123.kingenscoreboard.presentation.screen.newplayer
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.gitlab.djsushi123.kingenscoreboard.domain.model.Player
import com.google.accompanist.systemuicontroller.SystemUiController
import kotlinx.coroutines.launch
import org.koin.androidx.compose.getViewModel
#Composable
fun NewPlayerScreen(
viewModel: NewPlayerScreenViewModel = getViewModel(),
appNavController: NavHostController,
systemUiController: SystemUiController,
modifier: Modifier = Modifier
) {
val coroutineScope = rememberCoroutineScope()
val state = viewModel.state
val animatedSelectedColor by animateColorAsState(
targetValue = state.playerColor,
animationSpec = tween(durationMillis = 600)
)
val primaryVariantColor = MaterialTheme.colors.primaryVariant
val darkIconsDisposable = MaterialTheme.colors.isLight
val useDarkIcons = with(animatedSelectedColor) {
(red + green + blue) / 3f > 0.5f
}
DisposableEffect(key1 = animatedSelectedColor) {
systemUiController.setStatusBarColor(
color = animatedSelectedColor,
darkIcons = useDarkIcons
)
onDispose {
systemUiController.setStatusBarColor(
color = primaryVariantColor,
darkIcons = darkIconsDisposable
)
}
}
Scaffold(
topBar = {
TopBar(
color = animatedSelectedColor,
onColor = if (useDarkIcons) Color.Black else Color.White,
saveButtonEnabled = state.saveButtonEnabled,
onBackButtonClicked = { appNavController.popBackStack() },
onSave = {
val player = Player(
name = state.playerName,
abbreviation = state.playerAbbreviation,
color = state.playerColor
)
coroutineScope.launch {
viewModel.savePlayer(player)
appNavController.popBackStack()
}
}
)
},
modifier = modifier
) { paddingValues ->
Surface(color = MaterialTheme.colors.surface, modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()) // so that we can scroll with the keyboard open
.padding(paddingValues)
.padding(start = 8.dp, top = 8.dp, end = 8.dp)
) {
// Player name field with error
OutlinedTextField(
value = state.playerName,
onValueChange = {
viewModel.onEvent(
NewPlayerFormEvent.PlayerNameChanged(name = it)
)
},
isError = state.playerNameError != null,
label = { Text(text = "Name") },
placeholder = { Text(text = "John Cena") },
singleLine = true,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Words
),
modifier = Modifier.fillMaxWidth()
)
Text(
text = state.playerNameError ?: "",
color = MaterialTheme.colors.error,
textAlign = TextAlign.End,
modifier = Modifier.fillMaxWidth()
)
// Player abbreviation field
OutlinedTextField(
value = state.playerAbbreviation,
onValueChange = {
viewModel.onEvent(
NewPlayerFormEvent.PlayerAbbreviationChanged(abbreviation = it)
)
},
isError = state.playerAbbreviationError != null,
label = { Text(text = "Abbreviation") },
singleLine = true,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Characters,
autoCorrect = false
),
modifier = Modifier.fillMaxWidth()
)
Text(
text = state.playerAbbreviationError ?: "",
color = MaterialTheme.colors.error,
textAlign = TextAlign.End,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
ColorPicker(
selected = state.playerColor,
onSelect = { color ->
viewModel.onEvent(
NewPlayerFormEvent.PlayerColorChanged(color = color)
)
},
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
#Composable
private fun TopBar(
color: Color,
onColor: Color,
saveButtonEnabled: Boolean,
onBackButtonClicked: () -> Unit,
onSave: () -> Unit
) {
val saveButtonAlpha by animateFloatAsState(
if (saveButtonEnabled) ContentAlpha.high else ContentAlpha.disabled
)
TopAppBar(
title = { Text(text = "New player", color = onColor) },
navigationIcon = {
IconButton(onClick = onBackButtonClicked) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Back",
tint = onColor
)
}
},
actions = {
IconButton(onClick = onSave, enabled = saveButtonEnabled) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Save",
tint = onColor.copy(alpha = saveButtonAlpha)
)
}
},
backgroundColor = color
)
}
#Composable
private fun ColorPicker(
selected: Color,
onSelect: (color: Color) -> Unit,
modifier: Modifier = Modifier
) {
val colors: List<Color> = remember {
(0..340 step 8).map { hue ->
Color.hsl(hue = hue.toFloat(), saturation = 0.5f, lightness = 0.5f)
}.dropLast(3)
}
Column(modifier = modifier) {
for (row in 0..(colors.size - 1) / 4) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
for (tile in 0 until 4) {
val color = colors[row * 4 + tile]
ColorPickerTile(
color = color,
selected = color == selected,
onClick = { onSelect(color) },
modifier = Modifier
.width(70.dp)
.height(70.dp)
)
}
}
}
}
}
#Composable
private fun ColorPickerTile(
color: Color,
selected: Boolean,
onClick: () -> Unit,
shape: Shape = MaterialTheme.shapes.small,
modifier: Modifier = Modifier
) {
val scale = remember { Animatable(initialValue = 1f) }
val borderWidth by animateDpAsState(
targetValue = if (selected) 6.dp else 0.dp,
animationSpec = tween(durationMillis = 350)
)
LaunchedEffect(key1 = selected) {
if (selected) {
launch {
scale.animateTo(
targetValue = 0.90f,
animationSpec = tween(
durationMillis = 50
)
)
scale.animateTo(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow
)
)
}
} else {
launch { scale.animateTo(1f) }
}
}
Box(
modifier = modifier
.scale(scale.value)
.border(
width = borderWidth,
color = if (borderWidth.value == 0f) Color.Transparent else MaterialTheme.colors.onSurface,
shape = shape
)
.clip(shape)
.background(color = color)
.clickable(onClick = onClick)
)
}
My color picker is composed of ColorPickerTiles, which are stacked in a grid that is 4 tiles wide. This grid is a made-up grid from Rows inside a big Column. My problem is related to performance. When we check the "layout inspector" tab in Android Studio, we can see that when the user selects a different color tile in the app, every single ColorPickerTile gets recomposed MULTIPLE TIMES because of an animation I've added. I don't understand why this is necessary and why is this even possible, since the ColorPickerTile composable is restartable and also skippable and none of its parameters changed AFAIK. Here's a screenshot of what I mean:
The fact that these recompositions happen for every single ColorPickerTile many times during an animation means that the whole animation lags, especially on low-end devices. And the lag increases exponentially, with more ColorPickerTiles. My question is, what is causing these useless recompositions? And how would I go about solving the issue?
Related
My current android applications main screen contains a androidx.compose.material.BackdropScaffold. The backdrop looks great and functions exactly as it says on the tin.
however I have an issue with the frontLayerContent which contains a list of items.
I allow the user to interact with the frontLayerContent list while the backdrop is revealed, the issue is with the backdrop in the revealed state the user cannot scroll down to see the last item in the frontLayerContent list.
The solution to this issue is to use backdropState.offset in the modifier of the frontLayerContent which I obtain as follows:-
val backdropState = rememberBackdropScaffoldState(initialValue = BackdropValue.Concealed)
var offset by (backdropState.offset as MutableState)
and set as follows:-
onBackdropReveal = {
if (!backdropState.isAnimationRunning) {
scope.launch {
if (backdropState.isConcealed) {
offset = backdropState.offset.value
backdropState.reveal()
} else {
offset = backdropState.offset.value
backdropState.conceal()
}
}
}
}
and make the offset value available to other composables with:-
frontLayerContent = {
CompositionLocalProvider(LocalBackdropStateOffset provides offset) {
frontLayerContent()
}
}
Then in my frontLayerContent I retrieve the offset value as use as follows:-
val context = LocalContext.current
val offset = LocalBackdropStateOffset.current
Scaffold(
topBar = { Spacer(modifier = Modifier.height(0.dp)) },
modifier = Modifier.fillMaxSize(),
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.offset { IntOffset(x = 0, y = -offset.toInt()) },
verticalArrangement = Arrangement.Top
) {
LazyVerticalGrid(
modifier = Modifier.padding(10.dp),
columns = GridCells.Adaptive(125.dp),
contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
content = {
items(state.size) { index ->
this solution is close to what is required, however it looks as though I have made a mistake somewhere as my frontLayerContent list is always vertically offset even when the backdrop is concealed. In fact revealing or concealing the backdrop does not change the amount of offset of my frontLayerContent list.
how do I fix this issue?
how can I correctly set the frontLayerContent list vertical offset depending on whether the backdrop is concealed or revealed?
UPDATE
I only need to "fix" when the backdrop is revealed. therefore i need a conditional configuration of my frontLayerContent Modifier.offset(y = -offset)
how are you supposed to use the backdropState.offset value when correcting the frontLayerContent offset?
as the compose coordinate systems origin is the top left hand corner (x = 0, y = 0) and y dimension increases down the screen and x dimension increases Left to Right. when the backdrop is revealed the offset is a value (on my pixel 5) of approx 600.dp and concealed value of approx 300.dp. why is the concealed offset not 0.dp? is this taking into account the screens TopAppBar?
UPDATE (2)
Heres a GIF I made earlier
UPDATE (3)
This basic sample shows the problem i am having.
where have i made my mistake that stops me being able to scroll down
to see the complete last item when the backdrop is revealed?
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.BackdropScaffold
import androidx.compose.material.BackdropValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.rememberBackdropScaffoldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.elsevier.daisy.ui.theme.MyTheme
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
#OptIn(ExperimentalMaterial3Api::class)
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val scaffoldState = rememberBackdropScaffoldState(initialValue = BackdropValue.Concealed)
val scope = rememberCoroutineScope()
MyTheme {
BackdropScaffold(
scaffoldState = scaffoldState,
frontLayerScrimColor = Color.Unspecified,
frontLayerElevation = 5.dp,
gesturesEnabled = false,
appBar = {
TopAppBar(
title = { Text("Backdrop") },
navigationIcon = {
if (scaffoldState.isConcealed) {
IconButton(
onClick = {
scope.launch { scaffoldState.reveal() }
}
) {
Icon(
Icons.Default.Menu,
contentDescription = "Menu"
)
}
} else {
IconButton(
onClick = {
scope.launch { scaffoldState.conceal() }
}
) {
Icon(
Icons.Default.Close,
contentDescription = "Close"
)
}
}
}
)
},
backLayerContent = {
Column {
Text(
text = "Menu Item 1", modifier = Modifier.padding(8.dp), color = Color.White, style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Black
)
)
Text(
text = "Menu Item 1", modifier = Modifier.padding(8.dp), color = Color.White, style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Black
)
)
}
},
frontLayerContent = {
LazyColumn(modifier = Modifier.fillMaxSize()) {
// Add 5 items
items(55) { index ->
Text(
text = "Item: ${index + 1}",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(5.dp)
)
}
}
},
peekHeight = 60.dp,
) {
}
}
}
}
}
I would suggest adding a padding to the LazyColumn/LazyVerticalGrid itself rather than playing with the offset.
Given your example, I would simply add a conditional padding to the LazyColumn:
val padding = if (
scaffoldState.isRevealed &&
!scaffoldState.isAnimationRunning
) {
64.dp
} else {
0.dp
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(bottom = padding)
) {
// Your items here
}
I've used 64.dp here but use whatever you feel is necessary according to your design.
If instead of using that magic number you want to calculate the height of the backLayerContent you can by using the onGloballyPositioned modifier and save the height in a state.
Add this before your BackdropScaffold
var backLayerContentHeight by remember { mutableStateOf(0.dp) }
Get the backLayerContent height:
backLayerContent = {
val localDensity = LocalDensity.current
Column(
modifier = Modifier.onGloballyPositioned {
val heightInPx = it.size.height
val heightInDp = with(localDensity) { heightInPx.toDp() }
backLayerContentHeight = heightInDp
}
) {
...
}
},
Then you can use this value for the padding:
val padding = if (
scaffoldState.isRevealed &&
!scaffoldState.isAnimationRunning
) {
backLayerContentHeight
} else {
0.dp
}
Alternative to BackdropScaffold
You may want to create a custom implementation to get a similar behavior without workarounds.
This custom component should help:
#Composable
fun CustomBackdropScaffold(
modifier: Modifier = Modifier,
appBar: #Composable () -> Unit,
backLayerContent: #Composable () -> Unit,
frontLayerContent: #Composable () -> Unit,
isRevealed: Boolean,
) {
Surface(modifier = modifier) {
Column {
appBar()
AnimatedVisibility(
visible = isRevealed,
enter = expandVertically(
expandFrom = Alignment.Top,
),
exit = shrinkVertically(
shrinkTowards = Alignment.Top,
),
) {
backLayerContent()
}
frontLayerContent()
}
}
}
Here as an example of how to use it:
var isRevealed by remember { mutableStateOf(false) }
CustomBackdropScaffold(
appBar = {
TopAppBar(
title = { Text("Backdrop") },
navigationIcon = {
if (!isRevealed) {
IconButton(
onClick = {
isRevealed = true
}
) {
Icon(
Icons.Default.Menu,
contentDescription = "Menu"
)
}
} else {
IconButton(
onClick = {
isRevealed = false
}
) {
Icon(
Icons.Default.Close,
contentDescription = "Close"
)
}
}
}
)
},
backLayerContent = {
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primary)
) {
Text(
text = "Menu Item 1",
modifier = Modifier.padding(8.dp),
color = Color.White,
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Black
)
)
Text(
text = "Menu Item 1",
modifier = Modifier.padding(8.dp),
color = Color.White,
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Black
)
)
}
},
frontLayerContent = {
LazyColumn(
modifier = Modifier
.fillMaxSize()
) {
// Add 5 items
items(55, key = { it }) { index ->
Text(
text = "Item: ${index + 1}",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(5.dp)
)
}
}
},
isRevealed = isRevealed
)
I managed to work this out, and setup 3 cards one on top of the other as seperate boxs compose elements with onclick and on drag properties.
The issue is now, that I'd like the card that I'm pressing/dragging to set to the front, so, I played with the z-index modifier, but, it looks like I'm doing something wrong. Any idea?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Test1Theme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
for (i in 1 until 4) {
DraggableBox(title = "Box_${+1}", initX = 100f*i.toFloat(), initY = 100f, content =
{
Text(text = "Box_${i}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
}
)
}
}
}
}
}
}
#Composable
fun DraggableBox(title: String, initX: Float = 0f, initY: Float = 0f, content: #Composable() () -> Unit) {
val cardInitWidth = 135f
val cardInitHeight = 190f
val expandValue = 20f
Box(
modifier = Modifier
.fillMaxSize()
) {
val shape = RoundedCornerShape(12.dp)
val coroutineScope = rememberCoroutineScope()
val enable = remember { mutableStateOf(true) }
var offsetX = remember { Animatable(initialValue = initX) }
var offsetY = remember { Animatable(initialValue = initY) }
val interactionSource = remember { MutableInteractionSource() }
val clickable = Modifier.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current
) { }
val isPressed by interactionSource.collectIsPressedAsState()
val size = animateSizeAsState(
targetValue = if (enable.value && !isPressed) {
Size(width = cardInitWidth, height = cardInitHeight)
} else {
Size(width = cardInitWidth + expandValue, height = cardInitHeight + expandValue)
}
)
Box(
Modifier
.offset {
IntOffset(
x = offsetX.value.roundToInt(),
y = offsetY.value.roundToInt()
)
}
.zIndex(zIndex = if (enable.value && !isPressed) 5f else 0f)
.size(size.value.width.dp, size.value.height.dp)
.clip(shape)
//.background(Color(0xFF5FA777))
.background(color = MaterialTheme.colors.primary)
.border(BorderStroke(2.dp, Color.Black), shape = shape)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
enable.value = !enable.value
},
onDrag = { change, dragAmount ->
change.consumeAllChanges()
coroutineScope.launch {
offsetX.snapTo(targetValue = offsetX.value + dragAmount.x)
offsetY.snapTo(targetValue = offsetY.value + dragAmount.y)
}
spring(stiffness = Spring.StiffnessHigh, visibilityThreshold = 0.1.dp)
},
onDragEnd = {
enable.value = !enable.value
spring(stiffness = Spring.StiffnessLow, visibilityThreshold = 0.1.dp)
coroutineScope.launch {
launch {
offsetY.animateTo(
targetValue = initY,
animationSpec = tween(
durationMillis = 700,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
}
offsetX.animateTo(
targetValue = initX,
animationSpec = tween(
durationMillis = 700,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
}
}
)
}
.then(clickable)
) {
Row (modifier = Modifier
.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
)
{
Column (modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
)
{
Column (
horizontalAlignment = Alignment.CenterHorizontally
)
{
Text(text = "init-X: ${initX.toString()}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
Text(text = "init-Y: ${initY.toString()}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
}
Column (
horizontalAlignment = Alignment.CenterHorizontally
)
{
Text(text = "offset-X: ${offsetX.value.roundToInt().toString()}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
Text(text = "offset-Y: ${offsetY.value.roundToInt().toString()}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
}
Column (
horizontalAlignment = Alignment.CenterHorizontally
)
{
content()
}
}
}
}
}
}
The Modifier.zIndex works only for children within the same parent.
In your case you should move this modifier to the topmost Box. To do so you have to move enable and isPressed one level up too, and I would move all the other variables as well - but that's just a matter of taste, I guess.
val enable = remember { mutableStateOf(true) }
val isPressed by interactionSource.collectIsPressedAsState()
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(zIndex = if (enable.value && !isPressed) 5f else 0f)
) {
// ...
I was making a login for my app in the new android jetpack's compose.
I want to make a OTP layout like in the given photo.
check full example here
const val PIN_VIEW_TYPE_UNDERLINE = 0
const val PIN_VIEW_TYPE_BORDER = 1
#Composable
fun PinView(
pinText: String,
onPinTextChange: (String) -> Unit,
digitColor: Color = MaterialTheme.colors.onBackground,
digitSize: TextUnit = 16.sp,
containerSize: Dp = digitSize.value.dp * 2,
digitCount: Int = 4,
type: Int = PIN_VIEW_TYPE_UNDERLINE,
) {
BasicTextField(value = pinText,
onValueChange = onPinTextChange,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
decorationBox = {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
repeat(digitCount) { index ->
DigitView(index, pinText, digitColor, digitSize, containerSize, type = type)
Spacer(modifier = Modifier.width(5.dp))
}
}
})
}
#Composable
private fun DigitView(
index: Int,
pinText: String,
digitColor: Color,
digitSize: TextUnit,
containerSize: Dp,
type: Int = PIN_VIEW_TYPE_UNDERLINE,
) {
val modifier = if (type == PIN_VIEW_TYPE_BORDER) {
Modifier
.width(containerSize)
.border(
width = 1.dp,
color = digitColor,
shape = MaterialTheme.shapes.medium
)
.padding(bottom = 3.dp)
} else Modifier.width(containerSize)
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center) {
Text(
text = if (index >= pinText.length) "" else pinText[index].toString(),
color = digitColor,
modifier = modifier,
style = MaterialTheme.typography.body1,
fontSize = digitSize,
textAlign = TextAlign.Center)
if (type == PIN_VIEW_TYPE_UNDERLINE) {
Spacer(modifier = Modifier.height(2.dp))
Box(
modifier = Modifier
.background(digitColor)
.height(1.dp)
.width(containerSize)
)
}
}
}
You can use a very simple layout for each char in the otp.
Something like
#Composable
fun OtpChar(){
var text by remember { mutableStateOf("1") }
val maxChar = 1
Column(Modifier.background(DarkGray),
horizontalAlignment = Alignment.CenterHorizontally){
TextField(
value =text,
onValueChange = {if (it.length <= maxChar) text = it},
modifier = Modifier.width(50.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(
fontSize = 20.sp,
textAlign= TextAlign.Center),
colors= TextFieldDefaults.textFieldColors(
textColor = White,
backgroundColor = Transparent,
unfocusedIndicatorColor = Transparent,
focusedIndicatorColor = Transparent)
)
Divider(Modifier
.width(28.dp)
.padding(bottom = 2.dp)
.offset(y=-10.dp),
color = White,
thickness = 1.dp)
}
}
You can add some features like:
manage the focus in Next direction with the TAB key
manage the focus in Previous direction with the BACK SPACE key
how to move to the next textfield when a digit is entered
Something like:
fun OtpChar(
modifier: Modifier = Modifier
){
val pattern = remember { Regex("^[^\\t]*\$") } //to not accept the tab key as value
var (text,setText) = remember { mutableStateOf("") }
val maxChar = 1
val focusManager = LocalFocusManager.current
LaunchedEffect(
key1 = text,
) {
if (text.isNotEmpty()) {
focusManager.moveFocus(
focusDirection = FocusDirection.Next,
)
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally
){
TextField(
value =text,
onValueChange = {
if (it.length <= maxChar &&
((it.isEmpty() || it.matches(pattern))))
setText(it)
},
modifier = modifier
.width(50.dp)
.onKeyEvent {
if (it.key == Key.Tab) {
focusManager.moveFocus(FocusDirection.Next)
true
}
if (text.isEmpty() && it.key == Key.Backspace) {
focusManager.moveFocus(FocusDirection.Previous)
}
false
},
textStyle = LocalTextStyle.current.copy(
fontSize = 20.sp,
textAlign= TextAlign.Center),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next
),
colors= TextFieldDefaults.textFieldColors(
backgroundColor = Transparent,
unfocusedIndicatorColor = Transparent,
focusedIndicatorColor = Transparent),
)
Divider(
Modifier
.width(28.dp)
.padding(bottom = 2.dp)
.offset(y = -10.dp),
color = Teal200,
thickness = 1.dp)
}
}
Then just use something like a Row to display 4 OtpChars
val (item1, item2, item3, item4) = FocusRequester.createRefs()
Row(horizontalArrangement = Arrangement.SpaceBetween){
OtpChar(
modifier = Modifier
.focusRequester(item1)
.focusProperties {
next = item2
previous = item1
}
)
OtpChar(
modifier = Modifier
.focusRequester(item2)
.focusProperties {
next = item3
previous = item1
}
)
OtpChar(
modifier = Modifier
.focusRequester(item3)
.focusProperties {
next = item4
previous = item2
}
)
OtpChar(
modifier = Modifier
.focusRequester(item4)
.focusProperties {
previous = item3
next = item4
}
)
//....
}
If you faced keyboard issues try the code below:
#Composable
fun OtpCell(
modifier: Modifier,
value: String,
isCursorVisible: Boolean = false
) {
val scope = rememberCoroutineScope()
val (cursorSymbol, setCursorSymbol) = remember { mutableStateOf("") }
LaunchedEffect(key1 = cursorSymbol, isCursorVisible) {
if (isCursorVisible) {
scope.launch {
delay(350)
setCursorSymbol(if (cursorSymbol.isEmpty()) "|" else "")
}
}
}
Box(
modifier = modifier
) {
Text(
text = if (isCursorVisible) cursorSymbol else value,
style = MaterialTheme.typography.body1,
modifier = Modifier.align(Alignment.Center)
)
}
}
#ExperimentalComposeUiApi
#Composable
fun PinInput(
modifier: Modifier = Modifier,
length: Int = 5,
value: String = "",
onValueChanged: (String) -> Unit
) {
val focusRequester = remember { FocusRequester() }
val keyboard = LocalSoftwareKeyboardController.current
TextField(
value = value,
onValueChange = {
if (it.length <= length) {
if (it.all { c -> c in '0'..'9' }) {
onValueChanged(it)
}
if (it.length >= length) {
keyboard?.hide()
}
}
},
// Hide the text field
modifier = Modifier
.size(0.dp)
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
repeat(length) {
OtpCell(
modifier = modifier
.size(width = 45.dp, height = 60.dp)
.clip(MaterialTheme.shapes.large)
.background(MaterialTheme.colors.surface)
.clickable {
focusRequester.requestFocus()
keyboard?.show()
},
value = value.getOrNull(it)?.toString() ?: "",
isCursorVisible = value.length == it
)
if (it != length - 1) Spacer(modifier = Modifier.size(8.dp))
}
}
}
Result:
First make the common TextField for OTP Screen
#Composable
fun CommonOtpTextField(otp: MutableState<String>) {
val max = 1
OutlinedTextField(
value = otp.value,
singleLine = true,
onValueChange = { if (it.length <= max) otp.value = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.width(60.dp)
.height(60.dp),
maxLines = 1,
textStyle = LocalTextStyle.current.copy(
textAlign = TextAlign.Center
)
)
}
And Now use above CommonTextField to make four otp Field
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 15.dp, start = 15.dp, end = 15.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
CommonOtpTextField(otp = otpOne)
CommonOtpTextField(otp = otpTwo)
CommonOtpTextField(otp = otpThree)
CommonOtpTextField(otp = otpFour)
}
Simple solution that uses one TextField and different code length
#Composable
fun RegistrationCodeInput(codeLength: Int) {
val code = remember { mutableStateOf("") }
val focusRequester = FocusRequester()
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
BasicTextField(
value = code.value,
onValueChange = { if (it.length <= codeLength) code.value = it },
Modifier.focusRequester(focusRequester = focusRequester),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
decorationBox = {
CodeInputDecoration(code.value, codeLength)
})
}
}
#Composable
private fun CodeInputDecoration(code: String, length: Int) {
Box(
modifier = Modifier
.padding(16.dp)
.border(
border = BorderStroke(2.dp, color = borderColor),
shape = Shapes.small
)
) {
Row(
) {
for (i in 0 until length) {
val text = if (i < code.length) code[i].toString() else ""
CodeEntry(text)
}
}
}
}
#Composable
private fun CodeEntry(text: String) {
Box(
modifier = Modifier
.width(42.dp)
.height(42.dp),
contentAlignment = Alignment.Center
) {
Text(text = text)
}
}
#Preview
#Composable
fun PreviewInput() {
RegistrationCodeInput(4)
}
I am searching how create custom dialog in Jetpack Compose. In XML or Material Design we can create easily custom Dialog in which we can take user input, radio button etc. but i am not finding such thing in Jetpack Compose.
Starting from M3 1.1.0-alpha04 there is an AlertDialog composable function with a slot for content.
val openDialog = remember { mutableStateOf(true) }
if (openDialog.value) {
androidx.compose.material3.AlertDialog(
onDismissRequest = {
// Dismiss the dialog when the user clicks outside the dialog or on the back
// button. If you want to disable that functionality, simply use an empty
// onDismissRequest.
openDialog.value = false
}
) {
Surface(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight(),
shape = MaterialTheme.shapes.large
) {
Column(modifier = Modifier.padding(16.dp)) {
//... AlertDialog content
}
}
}
}
Before M3 1.1.0-alpha04 or with M2, you can use the standard AlertDialog.
The text,title and buttons parameters support #Composable functions and in this way you can customize the dialog as you prefer.
For example:
val openDialog = remember { mutableStateOf(true) }
var text by remember { mutableStateOf("") }
if (openDialog.value) {
AlertDialog(
onDismissRequest = {
openDialog.value = false
},
title = {
Text(text = "Title")
},
text = {
Column() {
TextField(
value = text,
onValueChange = { text = it }
)
Text("Custom Text")
Checkbox(checked = false, onCheckedChange = {})
}
},
buttons = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { openDialog.value = false }
) {
Text("Dismiss")
}
}
}
)
}
This example demonstrates how to make custom dialog in android jet compose.
Read more
https://www.boltuix.com/2022/01/ice-cream-app-ui-ux.html
import android.annotation.SuppressLint
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.compose.example.ui.theme.Pink80
import com.compose.example.ui.theme.Purple40
import com.compose.example.ui.theme.Purple80
import com.compose.example.ui.theme.PurpleGrey40
#Composable
fun CustomDialog(openDialogCustom: MutableState<Boolean>) {
Dialog(onDismissRequest = { openDialogCustom.value = false}) {
CustomDialogUI(openDialogCustom = openDialogCustom)
}
}
//Layout
#Composable
fun CustomDialogUI(modifier: Modifier = Modifier, openDialogCustom: MutableState<Boolean>){
Card(
//shape = MaterialTheme.shapes.medium,
shape = RoundedCornerShape(10.dp),
// modifier = modifier.size(280.dp, 240.dp)
modifier = Modifier.padding(10.dp,5.dp,10.dp,10.dp),
elevation = 8.dp
) {
Column(
modifier
.background(Color.White)) {
//.......................................................................
Image(
painter = painterResource(id = R.drawable.notification),
contentDescription = null, // decorative
contentScale = ContentScale.Fit,
colorFilter = ColorFilter.tint(
color = Purple40
),
modifier = Modifier
.padding(top = 35.dp)
.height(70.dp)
.fillMaxWidth(),
)
Column(modifier = Modifier.padding(16.dp)) {
androidx.compose.material3.Text(
text = "Get Updates",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 5.dp)
.fillMaxWidth(),
style = MaterialTheme.typography.labelLarge,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
androidx.compose.material3.Text(
text = "Allow Permission to send you notifications when new art styles added.",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 10.dp, start = 25.dp, end = 25.dp)
.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium
)
}
//.......................................................................
Row(
Modifier
.fillMaxWidth()
.padding(top = 10.dp)
.background(Purple80),
horizontalArrangement = Arrangement.SpaceAround) {
androidx.compose.material3.TextButton(onClick = {
openDialogCustom.value = false
}) {
androidx.compose.material3.Text(
"Not Now",
fontWeight = FontWeight.Bold,
color = PurpleGrey40,
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
)
}
androidx.compose.material3.TextButton(onClick = {
openDialogCustom.value = false
}) {
androidx.compose.material3.Text(
"Allow",
fontWeight = FontWeight.ExtraBold,
color = Color.Black,
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
)
}
}
}
}
}
#SuppressLint("UnrememberedMutableState")
#Preview (name="Custom Dialog")
#Composable
fun MyDialogUIPreview(){
CustomDialogUI(openDialogCustom = mutableStateOf(false))
}
Also adding a lambda to return value from dialog back to any other composable is possible with
#Composable
private fun CustomDialogWithResultExample(
onDismiss: () -> Unit,
onNegativeClick: () -> Unit,
onPositiveClick: (Color) -> Unit
) {
var red by remember { mutableStateOf(0f) }
var green by remember { mutableStateOf(0f) }
var blue by remember { mutableStateOf(0f) }
val color = Color(
red = red.toInt(),
green = green.toInt(),
blue = blue.toInt(),
alpha = 255
)
Dialog(onDismissRequest = onDismiss) {
Card(
elevation = 8.dp,
shape = RoundedCornerShape(12.dp)
) {
Column(modifier = Modifier.padding(8.dp)) {
Text(
text = "Select Color",
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
modifier = Modifier.padding(8.dp)
)
Spacer(modifier = Modifier.height(8.dp))
// Color Selection
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Column {
Text(text = "Red ${red.toInt()}")
Slider(
value = red,
onValueChange = { red = it },
valueRange = 0f..255f,
onValueChangeFinished = {}
)
Spacer(modifier = Modifier.height(8.dp))
Text(text = "Green ${green.toInt()}")
Slider(
value = green,
onValueChange = { green = it },
valueRange = 0f..255f,
onValueChangeFinished = {}
)
Spacer(modifier = Modifier.height(8.dp))
Text(text = "Blue ${blue.toInt()}")
Slider(
value = blue,
onValueChange = { blue = it },
valueRange = 0f..255f,
onValueChangeFinished = {}
)
Spacer(modifier = Modifier.height(8.dp))
Surface(
border = BorderStroke(1.dp, Color.DarkGray),
color = color,
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
) {}
}
}
// Buttons
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
) {
TextButton(onClick = onNegativeClick) {
Text(text = "CANCEL")
}
Spacer(modifier = Modifier.width(4.dp))
TextButton(onClick = {
onPositiveClick(color)
}) {
Text(text = "OK")
}
}
}
}
}
}
And show it with
var showCustomDialogWithResult by remember { mutableStateOf(false) }
if (showCustomDialogWithResult) {
CustomDialogWithResultExample(
onDismiss = {
showCustomDialogWithResult = !showCustomDialogWithResult
Toast.makeText(context, "Dialog dismissed!", Toast.LENGTH_SHORT)
.show()
},
onNegativeClick = {
showCustomDialogWithResult = !showCustomDialogWithResult
Toast.makeText(context, "Negative Button Clicked!", Toast.LENGTH_SHORT)
.show()
},
onPositiveClick = { color ->
showCustomDialogWithResult = !showCustomDialogWithResult
Toast.makeText(context, "Selected color: $color", Toast.LENGTH_SHORT)
.show()
}
)
}
And result is
I had to achieve something like this:
Putting image in "title" slot and text in "text" slot of compose AlertDialog, ended with this:
Because "title" and "text" are wrapped with AlertDialogBaselineLayout that is adding padding, and I did not have a clue how to change it.
However, "buttons" slot was not wrapped, and my solution was like in following code ("title" and "text" slot must be set to null and all of dialog content goes into "buttons" slot):
#Composable
fun AppDialog(
modifier: Modifier = Modifier,
dialogState: Boolean = false,
onDialogPositiveButtonClicked: (() -> Unit)? = null,
onDialogStateChange: ((Boolean) -> Unit)? = null,
onDismissRequest: (() -> Unit)? = null,
) {
val textPaddingAll = 24.dp
val buttonPaddingAll = 8.dp
val dialogShape = RoundedCornerShape(16.dp)
if (dialogState) {
AlertDialog(
onDismissRequest = {
onDialogStateChange?.invoke(false)
onDismissRequest?.invoke()
},
title = null,
text = null,
buttons = {
Column{
Image(
painter = painterResource(R.drawable.dialog_top_image),
contentDescription = "",
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
Row(Modifier.padding(all = textPaddingAll)){
TextWithHTMLSupport(
text = stringResource(R.string.gdprText)
)
}
Divider(color = MaterialTheme.colors.onSurface, thickness = 1.dp)
Row(
modifier = Modifier.padding(all = buttonPaddingAll),
horizontalArrangement = Arrangement.Center
) {
TextButton(
modifier = Modifier.fillMaxWidth(),
onClick = {
onDialogStateChange?.invoke(false)
onDialogPositiveButtonClicked?.invoke()
}
) {
Text(text = stringResource(R.string.dialog_ok), color = MaterialTheme.colors.onSurface)
}
}
}
},
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = false),
modifier = modifier,
shape = dialogShape
)
}
}
If the content of your custom alert dialog need to scroll (eg: landscape mode or the content get longer). You can do like
#Composable
fun CustomDialogScrollable(
onConfirmClicked: () -> Unit,
onDismiss: () -> Unit,
) {
Dialog(
onDismissRequest = onDismiss,
) {
Surface(
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colors.surface,
) {
Column(modifier = Modifier.padding(16.dp)) {
// TITLE
Text(text = "Title", style = MaterialTheme.typography.subtitle1)
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.weight(weight = 1f, fill = false)
.padding(vertical = 16.dp)
) {
Text(
text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s",
style = MaterialTheme.typography.body2
)
OutlinedTextField(value = "", onValueChange = {
}, Modifier.padding(top = 8.dp), label = { Text(text = "Email") })
// other content can go here
}
// BUTTONS
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
TextButton(onClick = onDismiss) {
Text(text = "Cancel")
}
TextButton(onClick = onConfirmClicked) {
Text(text = "OK")
}
}
}
}
}
}
Using
val openDialog = remember { mutableStateOf(true) }
if (openDialog.value) {
CustomDialog({
// confirm clicked
}, {
openDialog.value = false
})
}
Custom dialog with Image
To create a custom dialog like this.
First, create Dialog composable and set dismissRequest and dialog properties.
Inside the dialog create your own view.
finally, set up the dismiss dialog on the desired button.
#Composable
fun CustomAlertDialog(onDismiss: () -> Unit, onExit: () -> Unit) {
Dialog(onDismissRequest = { onDismiss() }, properties = DialogProperties(
dismissOnBackPress = false,dismissOnClickOutside = false
)) {
Card(
//shape = MaterialTheme.shapes.medium,
shape = RoundedCornerShape(10.dp),
// modifier = modifier.size(280.dp, 240.dp)
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
elevation = 8.dp
) {
Column(
Modifier
.fillMaxWidth()
.background(Color.White)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color.Red.copy(alpha = 0.8F)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Image(
painter = painterResource(id = R.drawable.background_image),
contentDescription = "Exit app",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.FillWidth
)
}
Text(
text = "Lorem Ipsum is simply dummy text",
modifier = Modifier.padding(8.dp), fontSize = 20.sp
)
Text(
text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard",
modifier = Modifier.padding(8.dp)
)
Row(Modifier.padding(top = 10.dp)) {
OutlinedButton(
onClick = { onDismiss() },
Modifier
.fillMaxWidth()
.padding(8.dp)
.weight(1F)
) {
Text(text = "Cancel")
}
Button(
onClick = { onExit() },
Modifier
.fillMaxWidth()
.padding(8.dp)
.weight(1F)
) {
Text(text = "Exit")
}
}
}
}
}
}
Now we have created the Custom dialog, to show the dialog on button click, you need to create a mutableStateOf() variable to maintain the dialog show and dismiss state.
Also, create conditions like if the variable is true call the dialog, otherwise don’t call the dialog function.
#Composable
fun Content() {
val context = LocalContext.current
var showCustomDialog by remember {
mutableStateOf(false)
}
Column(
Modifier.fillMaxSize(),
Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { showCustomDialog = !showCustomDialog }, Modifier.wrapContentSize()) {
Text(text = "Show Alert Dialog")
}
}
if (showCustomDialog) {
CustomAlertDialog({
showCustomDialog = !showCustomDialog
}, {
val activity = (context as? Activity)
activity?.finish()
})
}
}
when clicking on the dismiss button on the dialog, need to update a variable to false to hide the dialog.
The final output of the code is below,
Alert dialog with Input field
As mentioned above, we need to create the dialog with a Dialog() composable function. But for the input field, we need to create a mutableStateOf() variable to hold the values of the input field.
#Composable
fun InputDialogView(onDismiss:() -> Unit) {
val context = LocalContext.current
var searchedFood by remember {
mutableStateOf("")
}
Dialog(onDismissRequest = { onDismiss() }) {
Card(
//shape = MaterialTheme.shapes.medium,
shape = RoundedCornerShape(10.dp),
// modifier = modifier.size(280.dp, 240.dp)
modifier = Modifier.padding(8.dp),
elevation = 8.dp
) {
Column(
Modifier
.background(Color.White)
) {
Text(
text = "Search your favorite food",
modifier = Modifier.padding(8.dp),
fontSize = 20.sp
)
OutlinedTextField(
value = searchedFood,
onValueChange = { searchedFood = it }, modifier = Modifier.padding(8.dp),
label = { Text("Favorite Food") }
)
Row {
OutlinedButton(
onClick = { onDismiss() },
Modifier
.fillMaxWidth()
.padding(8.dp)
.weight(1F)
) {
Text(text = "Cancel")
}
Button(
onClick = {
Toast.makeText(context, searchedFood, Toast.LENGTH_SHORT).show()
onDismiss() },
Modifier
.fillMaxWidth()
.padding(8.dp)
.weight(1F)
) {
Text(text = "Search")
}
}
}
}
}
}
To display the dialog you need to follow the same way above. by creating the mutableStateOf() variable and making it true or false.
The output of the above code is,
Loading Dialog
For the loading dialog, we need to use CircularProgressIndicator() composable function for loading animation. Apart from that everything is the same as other custom dialogs.
#Composable
fun LoadingView(onDismiss:() -> Unit) {
Dialog(onDismissRequest = { onDismiss() }) {
Card(
shape = RoundedCornerShape(8.dp),
modifier = Modifier,
elevation = 8.dp
) {
Column(
Modifier
.background(Color.White)
.padding(12.dp)
) {
Text(
text = "Loading.. Please wait..",
Modifier
.padding(8.dp), textAlign = TextAlign.Center
)
CircularProgressIndicator(
strokeWidth = 4.dp,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(8.dp)
)
}
}
}
}
The output of the about loading dialog code,
It's easy to create a custom dialog in Jetpack Compose.
Here is a dialog box that asks to enalbe 2 Step verification. I have also added click events.
Output:
Code:
For more designs with source code, see Jetpack Compose Samples
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
/*
For more designs with source code,
visit: https://semicolonspace.com/jetpack-compose-samples/
*/
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BlogPostsTheme(darkTheme = false) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colors.background),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
var openDialog by remember {
mutableStateOf(false) // Initially dialog is closed
}
ButtonClick(buttonText = "Open Dialog") {
openDialog = true
}
if (openDialog) {
DialogBox2FA {
openDialog = false
}
}
}
}
}
}
}
}
#Composable
fun DialogBox2FA(onDismiss: () -> Unit) {
val contextForToast = LocalContext.current.applicationContext
Dialog(
onDismissRequest = {
onDismiss()
}
) {
Surface(
modifier = Modifier
.fillMaxWidth(),
elevation = 4.dp
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.background(color = Color(0xFF35898f)),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier
.padding(top = 16.dp, bottom = 16.dp),
painter = painterResource(id = R.drawable.image_security),
contentDescription = "2-Step Verification",
alignment = Alignment.Center
)
}
Text(
modifier = Modifier.padding(top = 16.dp, bottom = 16.dp),
text = "2-Step Verification",
textAlign = TextAlign.Center,
style = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_bold, FontWeight.Bold)),
fontSize = 20.sp
)
)
Text(
modifier = Modifier.padding(start = 12.dp, end = 12.dp),
text = "Setup 2-Step Verification to add additional layer of security to your account.",
textAlign = TextAlign.Center,
style = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_regular, FontWeight.Normal)),
fontSize = 14.sp
)
)
Button(
modifier = Modifier
.fillMaxWidth()
.padding(top = 36.dp, start = 36.dp, end = 36.dp, bottom = 8.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFF35898f)),
onClick = {
onDismiss()
Toast.makeText(
contextForToast,
"Click: Setup Now",
Toast.LENGTH_SHORT
).show()
}) {
Text(
text = "Setup Now",
color = Color.White,
style = TextStyle(
fontFamily = FontFamily(
Font(
R.font.roboto_medium,
FontWeight.Medium
)
),
fontSize = 16.sp
)
)
}
TextButton(
onClick = {
onDismiss()
Toast.makeText(
contextForToast,
"Click: I'll Do It Later",
Toast.LENGTH_SHORT
).show()
}) {
Text(
text = "I'll Do It Later",
color = Color(0xFF35898f),
style = TextStyle(
fontFamily = FontFamily(
Font(
R.font.roboto_regular,
FontWeight.Normal
)
),
fontSize = 14.sp
)
)
}
}
}
}
}
#Composable
fun ButtonClick(
buttonText: String,
onButtonClick: () -> Unit
) {
Button(
shape = RoundedCornerShape(5.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.primary),
onClick = {
onButtonClick()
}) {
Text(
text = buttonText,
fontSize = 16.sp,
color = Color.White
)
}
}
I wanted a layout similar to
`<Scrollview>
<Relativelayout>
<Recyclerview/>(Horizontal)
<Recyclerview/>(Vertical)
</Relativelayout>
</Scrollview>`
this is my compose code related to the issue
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.composeappdemo.R
import com.example.composeappdemo.model.Feature
import com.example.composeappdemo.ui.theme.*
#ExperimentalFoundationApi
#Composable
fun HomeScreen(list: List<String>) {
var features = listOf(
Feature(
title = "Summer Times",
R.drawable.ic__search,
BlueViolet1,
BlueViolet1,
BlueViolet1
),
Feature(
title = "Winter Vibes",
R.drawable.ic__search,
LightGreen1,
BlueViolet1,
BlueViolet1
),
Feature(
title = "Spring Times",
R.drawable.ic__search,
LightRed,
BlueViolet1,
BlueViolet1
)
)
Box(
modifier = Modifier
.fillMaxWidth()
.background(DeepBlue)
) {
LazyColumn() {
item {
GreetingScreen("Android")
}
item {
ChipSection(list)
}
item {
CurrentMeditation()
}
item {
Text(
text = "Features",
color = Color.White,
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(15.dp)
)
}
items(features.windowed(2, 2, true)) { list ->
Row(Modifier.fillMaxWidth()) {
list.forEach {
FeatureItem(it)
}
}
}
}
}
}
#Composable
fun GreetingScreen(name: String = "Android") {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(15.dp)
) {
Column(
verticalArrangement = Arrangement.Center
) {
Text(
text = "Good Morning , $name",
style = MaterialTheme.typography.h6,
color = Color.White
)
Text(
text = "We wish you a good day today",
style = MaterialTheme.typography.body1,
color = Color.White
)
}
Icon(
painter = painterResource(id = R.drawable.ic__search),
contentDescription = "Search",
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
}
#Composable
fun ChipSection(
chips: List<String>
) {
var selectedChipIndex by remember {
mutableStateOf(0)
}
LazyRow {
items(chips.size) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(start = 15.dp, top = 15.dp, bottom = 15.dp)
.clickable {
selectedChipIndex = it
}
.clip(RoundedCornerShape(10.dp))
.background(
if (selectedChipIndex == it) ButtonBlue
else DarkerButtonBlue
)
.padding(15.dp)
) {
Text(text = chips[it], color = Color.White)
}
}
}
}
#Composable
fun CurrentMeditation(
color: Color = LightRed
) {
Box(
modifier = Modifier
.padding(15.dp)
.clip(RoundedCornerShape(15.dp))
.background(color)
.fillMaxWidth()
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(15.dp)
) {
Column(verticalArrangement = Arrangement.Center) {
Text(
text = "Daily Thought",
color = TextWhite,
fontSize = 18.sp
)
Text(
text = "Meditation 3 - 10 mins",
color = TextWhite,
fontSize = 13.sp
)
}
Icon(
painter = painterResource(id = R.drawable.ic__play),
contentDescription = "Play",
tint = Color.White
)
}
}
}
#ExperimentalFoundationApi
#Composable
fun FeatureSection(features: List<Feature>) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item {
Text(
text = "Features",
color = Color.White,
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(15.dp)
)
}
items(features.windowed(2, 2, true)) { list ->
Row(Modifier.fillMaxWidth()) {
list.forEach {
FeatureItem(feature = it)
}
}
}
/*LazyVerticalGrid(
cells = GridCells.Fixed(2),
contentPadding = PaddingValues(start = 5.dp, top = 5.dp, bottom = 5.dp),
modifier = Modifier.fillMaxHeight()
) {
items(features.size) {
}
}*/
}
}
#Composable
fun FeatureItem(feature: Feature) {
Box(
modifier = Modifier
.padding(15.dp)
.fillMaxWidth(.3f)
.clip(RoundedCornerShape(15.dp))
.aspectRatio(1f)
.background(color = feature.lightColor)
) {
Text(
text = feature.title,
color = Color.White,
style = MaterialTheme.typography.h6,
lineHeight = 24.sp,
modifier = Modifier.align(Alignment.TopStart)
)
Icon(
painter = painterResource(id = feature.iconId),
contentDescription = feature.title,
tint = Color.White,
modifier = Modifier.align(Alignment.BottomStart)
)
Text(
text = "Start",
color = TextWhite,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.clickable {
}
.align(
Alignment.BottomEnd
)
.clip(RoundedCornerShape(10.dp))
.background(ButtonBlue)
.padding(vertical = 6.dp, horizontal = 15.dp)
)
}
}
When I try to achieve it in jetpack compose. The list scrolls under it's own layout but doesn't expand making the whole scrolling. Horizontal one is fine but the vertical one tends to have a small height not making it the sameheight as the list itself.
Needed a view something like this so that the view can expand as well as scroll with the layout.
#Composable
fun NewsList() {
LazyColumn {
items(rows) { item ->
Text(
modifier = Modifier
.height(80.dp),
text = item
)
}
}
something like this you can create fr lazycolum
for gride you can try this(Not sure for this) is it right or not
LazyVerticalGrid(
cells = GridCells.Fixed(cellState),
content = {
}
}
)
Don't forget to import this library explicitly
import androidx.compose.foundation.lazy.items
#Composable
fun Conversation(messages: List<Message>) {
LazyColumn {
items(messages) { message ->
MessageCard(message)
}
}
}