Adjust Texview on the basis of Imageview in Jetpack compose - kotlin

Align Imageview in left and 3 textview in right in such a way that height of the image is depending on the aspect ratio and the first text view should be align with the top of imageview and bottom of 3rd textview should be align with bottom of image. The space between these 2 textview should be given to 2nd textview.
Expected:
#Preview
#Composable
fun ShowUi() {
Row
modifier = Modifier
.padding(10.dp)
.wrapContentHeight()
.fillMaxWidth()
) {
Box(
modifier = Modifier
.weight(7f)
.aspectRatio(1.77f)
.background(Color.Yellow)
) {
}
Column(
modifier = Modifier
.weight(3f)
.background(Color.Green)
) {
Text(
text = "Title 1",
fontSize = 20.sp,
maxLines = 1,
modifier = Modifier.background(Color.Green)
)
Text(
text = "You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0.",
overflow = TextOverflow.Ellipsis,
modifier = Modifier.background(Color.Gray),
)
Text(
text = "PLAY NOW",
modifier = Modifier.background(Color.Green),
maxLines = 1
)
}
}
}
Output of the above snippet:
Note: Can't use maxLines in 2nd TextView as the number of lines which can be shown is dynamic i.e depends on the space available between 1st and 3rd textview.

You can consider using Compose ConstraintLayout to achive this.
https://developer.android.com/jetpack/compose/layouts/constraintlayout

You can prevent Row from growing with .height(IntrinsicSize.Min): it will not allow the children to grow beyond maximum inherent height.
Next step is to override min intrinsic height for your Column: but default it'll be calculated on sum of text sizes. To do so you can create the following modifier:
fun Modifier.zeroMinIntrinsicHeight() = then(
object : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
) = measurable
.measure(constraints)
.run {
layout(width, height) {
placeRelative(0, 0)
}
}
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
): Int {
return 0
}
}
)
In order to compress your middle text, you need to apply .weight(1f) modifier: this way, all other view sizes inside Column will be calculated before the long text.
There is also a bug that prevents TextOverflow.Ellipsis from working correctly with the limited size, be sure to star it to bring more attention to the problem. Therefore, I removed this argument.
Result code:
Row(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
.background(Color.Red)
.height(IntrinsicSize.Min)
) {
Box(
modifier = Modifier
.weight(7f)
.aspectRatio(1.77f)
.background(Color.Yellow)
) {
}
Column(
modifier = Modifier
.weight(3f)
.background(Color.Green)
.zeroMinIntrinsicHeight()
) {
Text(
text = "Title 1",
fontSize = 20.sp,
maxLines = 1,
modifier = Modifier.background(Color.Green)
)
Text(
text = "You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0.",
modifier = Modifier
.background(Color.Gray)
.weight(1f)
)
Text(
text = "PLAY NOW",
modifier = Modifier.background(Color.Green),
maxLines = 1
)
}
}
Output:

Related

change background color surface light/dark jetpack compose

I'm trying to start with jetpack compose and it's getting complicated to be able to change the background automatically according to the theme chosen by the user (light dark).
I am editing the colors from theme.kt
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80,
surface = Color(0xFF0BB000),
background = Color(0xFF0BB000),
onBackground = Color(0xFF0BB000))
The problem is that when I run the app, the background color is still grey.
I think the problem is that my app doesn't take the colors from the theme, since I tried to set it directly, but it doesn't change the background color either.
Surface ( color = MaterialTheme.colorScheme.background)
If anyone has any ideas why it doesn't change color automatically and point me to it, I'd appreciate it.
I can set the palette again from my activity and change it, it depends on the mode chosen by the user, but it is not an optimal solution and it looks ugly.
I leave my activity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TruequeGualeguaychuTheme {
Surface ( color = MaterialTheme.colorScheme.background){
Text(
modifier = Modifier
.fillMaxSize()
.wrapContentHeight(),
textAlign = TextAlign.Center,
text = "Hello Android!",
)
}
}
}
}
}
It should have a green background but it does not take the color declared in ( background = Color(0xFF0BB000) )
val Colors.myBackgroundColor get() = if (isLight) Color.White else Color.Black
you can create color like this and use this color in Surface background color.
Surface(modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colors.myBackgroundColor)) {
// your code
}
The part it looks like you're missing is the hinted at in #vaspike's answer. All views that should utilize your custom theme should derive from an instance of the theme. For example:
#Composable
fun MyTheme(content: #Composable () -> Unit) {
MaterialTheme(
colors = LightColors, // A list of key/values I define.
content = content
)
}
#Composable
fun HomePage(){
MyTheme(){
// composables that use the theme.
}
}
This comes from the example found here, https://developer.android.com/codelabs/jetpack-compose-theming#3
The problem is that newer versions of android have dynamic colors that use a color palette based on the system.
By default the theme has this option activated which limits the setting that one can make of the color palette.
dynamicColor: Boolean = true
I have implemented similar functions before,but it's at Jb-compose desktop.Provide a reference:
MainApp.kt:
#OptIn(ExperimentalMaterialApi::class)
#Composable
#Preview
fun launchApp() {
MaterialTheme(
colors = darkColors(
primary = MyColor.primaryColor,
primaryVariant = MyColor.primaryColor,
secondary = MyColor.primaryColor,
secondaryVariant = MyColor.primaryColor,
background = MyColor.darkBackgroundColor,
surface = MyColor.darkBackgroundColor,
onPrimary = Color.White,
onSecondary = Color.White,
onBackground = Color.White,
onSurface = Color.White,
error = Color.Red,
onError = Color.White
),
){
//...
}
}
Main.kt:
fun main(args: Array<String>) = application {
Window(
onCloseRequest = ::exitApplication,
icon = painterResource("icon/xx.ico"),
title = "xx ${RuntimeContext.APP_VERSION}",
) {
MainApp.launchApp()
}
}

How can I get the font size of a text when I use Text() in Compose?

I create a new project by Empty Compose Activity wizard in Android Studio.
The Code A is generated code by Android Studio.
Text(text = "Hello $name!") displays a text with default font style, I hope to get the size and unit of the text, such as 16sp, where can I find these informations?
Code A
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Greeting("Android")
}
}
}
}
}
#Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
#Composable
fun MyApplicationTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: #Composable () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
You can check it here.
https://developer.android.com/jetpack/compose/text
Change text size and font style
Text("Hello World", fontSize = 30.sp,fontStyle = FontStyle.Italic)
Text has a fontFamily parameter to allow setting the font used in the composable. By default, serif, sans-serif, monospace and cursive font families are included.
You can use the TextLayoutResult to get the front style
The font style is in TextLayoutInput -> TextStyle
Text("Hello World",
onTextLayout = { result: TextLayoutResult ->
Log.d(TAG, "FrontSize: "+ result.layoutInput.style.fontSize)
Log.d(TAG, "FrontStyle: "+ result.layoutInput.style.toString())
})
If you are using default theme it's in
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
// HERE
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
However, if you apply any other theme or get Text from another library can get size of your text using onTextLayout callback which returns a lot of data about your text.
Without removing default padding on top or bottom of your font size won't be able to get correct height in sp. You get the height of Text in sp with the method below. I tried using LineHeightStyle Api also need to check lineHeight either for correct results.
Column {
var heightInPx by remember { mutableStateOf(0f) }
var info by remember { mutableStateOf("") }
val heightInSp = LocalDensity.current.run { heightInPx.toSp() }
Text("Hello World",
modifier = Modifier
.border(1.dp, Color.Green)
.fillMaxWidth(),
style = TextStyle(
fontSize = 40.sp,
platformStyle = PlatformTextStyle(
includeFontPadding = false
),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Top,
trim = LineHeightStyle.Trim.Both
)
),
onTextLayout = { result: TextLayoutResult ->
val cursorRect = result.getCursorRect(0)
info = "firstBaseline: ${result.firstBaseline}, " +
"lastBaseline: ${result.lastBaseline}\n" +
"cursorRect: $cursorRect\n" +
"getLineBottom: ${result.getLineBottom(0)}, " +
"getBoundingBox: ${result.getBoundingBox(0)}"
heightInPx =
result.size.height.toFloat()
}
)
Text("Height in px: $heightInPx, height in sp: $heightInSp")
Text(info)
}
As you can see in the Text composable source code, when you explicitly don't specify fontSize or style (which you don't), it takes font size from the default style, which is LocalTextStyle.current.
Now LocalTextStyle can contain different styles throughout your code, you can override it yourself by doing something like this:
CompositionLocalProvider(LocalTextStyle provides TextStyle(...)) {
Text() // will use specified TextStyle
}
A lot of material components do this, for example TopAppBar and Button, so you don't have to care about anything and correct style will always be used.
Finally, your style comes from the MaterialTheme composable, where is this piece of code:
ProvideTextStyle(value = typography.body1) {
content()
}
So your font size comes from the Typography.body1 text style.

Continuous recomposition in Jetpack Compose

I'm trying to create a sky view in my Android app using Jetpack Compose. I want to display it inside a Card with a fixed height. During nigth time, the card background turns dark blue and I'd like to have some blinking stars spread over the sky.
To create the stars blinking animation, I'm using an InfiniteTransition object and a scale property with animateFloat that I apply to several Icons. Those Icons are created inside a BoxWithConstraints, to spread then randomly using a for loop. The full code I'm using is shown below:
#Composable
fun NightSkyCard() {
Card(
modifier = Modifier
.height(200.dp)
.fillMaxWidth(),
elevation = 2.dp,
shape = RoundedCornerShape(20.dp),
backgroundColor = DarkBlue
) {
val infiniteTransition = rememberInfiniteTransition()
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
)
BoxWithConstraints(
modifier = Modifier.fillMaxSize()
) {
for (n in 0..20) {
val size = Random.nextInt(3, 5)
val start = Random.nextInt(0, maxWidth.value.toInt())
val top = Random.nextInt(10, maxHeight.value.toInt())
Icon(
imageVector = Icons.Filled.Circle,
contentDescription = null,
modifier = Modifier
.padding(start = start.dp, top = top.dp)
.size(size.dp)
.scale(scale),
tint = Color.White
)
}
}
}
}
The problem with this code is that the BoxWithConstraints's scope is recomposing continously, so I get a lot of dots appearing and dissapearing from the screen very fast. I'd like the scope to just run once, so that the dots created at first time would blink using the scale property animation. How could I achieve that?
Instead of continuous recomposition you should look for least amount of recompositions to achieve your goal.
Compose has 3 phases. Composition, Layout and Draw, explained in official document. When you use a lambda you defer state read from composition to layout or draw phase.
If you use Modifier.scale() or Modifier.offset() both of three phases above are called. If you use Modifier.graphicsLayer{scale} or Modifier.offset{} you defer state read to layout phase. And the best part, if you use Canvas, which is a Spacer with Modifier.drawBehind{} under the hood, you defer state read to draw phase as in example below and you achieve your goal only with 1 composition instead of recomposing on every frame.
For instance from official document
// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(Modifier.fillMaxSize().background(color))
Here the box's background color is switching rapidly between two
colors. This state is thus changing very frequently. The composable
then reads this state in the background modifier. As a result, the box
has to recompose on every frame, since the color is changing on every
frame.
To improve this, we can use a lambda-based modifier–in this case,
drawBehind. That means the color state is only read during the draw
phase. As a result, Compose can skip the composition and layout phases
entirely–when the color changes, Compose goes straight to the draw
phase.
val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
Modifier
.fillMaxSize()
.drawBehind {
drawRect(color)
}
)
And how you can achieve your result
#Composable
fun NightSkyCard2() {
Card(
modifier = Modifier
.height(200.dp)
.fillMaxWidth(),
elevation = 2.dp,
shape = RoundedCornerShape(20.dp),
backgroundColor = Color.Blue
) {
val infiniteTransition = rememberInfiniteTransition()
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
)
val stars = remember { mutableStateListOf<Star>() }
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.background(Color.Blue)
) {
SideEffect {
println("🔥 Recomposing")
}
LaunchedEffect(key1 = Unit) {
repeat(20) {
stars.add(
Star(
Random.nextInt(2, 5).toFloat(),
Random.nextInt(0, constraints.maxWidth).toFloat(),
Random.nextInt(10, constraints.maxHeight).toFloat()
)
)
}
}
Canvas(modifier = Modifier.fillMaxSize()) {
if(stars.size == 20){
stars.forEach { star ->
drawCircle(
Color.White,
center = Offset(star.xPos, star.yPos),
radius = star.radius *(scale)
)
}
}
}
}
}
}
#Immutable
data class Star(val radius: Float, val xPos: Float, val yPos: Float)
One solution is to wrap your code in a LaunchedEffect so that the animation runs once:
#Composable
fun NightSkyCard() {
Card(
modifier = Modifier
.height(200.dp)
.fillMaxWidth(),
elevation = 2.dp,
shape = RoundedCornerShape(20.dp),
backgroundColor = DarkBlue
) {
val infiniteTransition = rememberInfiniteTransition()
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
)
BoxWithConstraints(
modifier = Modifier.fillMaxSize()
) {
for (n in 0..20) {
var size by remember { mutableStateOf(0) }
var start by remember { mutableStateOf(0) }
var top by remember { mutableStateOf(0) }
LaunchedEffect(key1 = Unit) {
size = Random.nextInt(3, 5)
start = Random.nextInt(0, maxWidth.value.toInt())
top = Random.nextInt(10, maxHeight.value.toInt())
}
Icon(
imageVector = Icons.Filled.Circle,
contentDescription = null,
modifier = Modifier
.padding(start = start.dp, top = top.dp)
.size(size.dp)
.scale(scale),
tint = Color.White
)
}
}
}
}
You then get 21 blinking stars.

How on earth do you reason about order of Jetpack Compose modifiers?

I get that Jetpack Compose operates from the outside in for modifiers, but what about this one?
What I asked for:
8 dp padding
then 2 dp border
then background colour for component
then 16 dp padding
#Composable
fun App() {
MaterialTheme(colors = darkColors()) {
Surface(
modifier = Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize()
.padding(8.dp)
) {
Column {
Card(
modifier = Modifier
.requiredSize(DpSize(300.dp, 200.dp))
.border(BorderStroke(2.dp, Color.White), RoundedCornerShape(8.dp))
.background(MaterialTheme.colors.surface)
.padding(16.dp)
) {
// Content still to come
}
}
}
}
}
What I got:
8 dp padding
then 2 dp border
--then 16 dp padding--
--then background colour for component--
How do I reason about the background colour not extending to the border here? I defined it immediately after.
Addendum:
By the way, swap border and background - same result:

Extending Material Theme with Kotlin extension property doesn't work

The official documentations gave this code example:
// Use with MaterialTheme.typography.textFieldInput
val Typography.textFieldInput: TextStyle
get() = TextStyle(/* ... */)
So I defined my own h1 style as follows
val Typography.h1: TextStyle
get() = TextStyle(
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
and use it
#Preview
#Composable
fun GenericEmptyState(
#PreviewParameter(EmptyStatePreviewParameters::class) data: EmptyStateData
) {
MaterialTheme {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize(1f)
.background(Color.White)
.padding(24.dp)
) {
Image(
painterResource(data.image),
contentDescription = "illustration",
modifier = Modifier
.size(150.dp)
.padding(6.dp)
)
Text(data.title, Modifier.padding(6.dp), style = MaterialTheme.typography.h1)
//...
But the result text style I got is definitely not size 20 or bold font weight
It looks like the default h1 style in Material Theme
Can't see what's wrong myself
Can anyone?
Well, the Typography class itself has a member named h1 which will be used instead of your extension. You should actually get this warning in Android studio which explains it pretty well:
Extension is shadowed by a member: public final val h1: TextStyle
The documentation you mention is called "Custom design systems in Compose" - this is meant for someone who doesn't want to use the default Typography properties - either needs more text styles than provided or simply wants to name them differently. You should probably look at this section first - it shows how to create your own theme and override the default Typography styles. It should look something like this:
#Composable
fun MyTheme(
content: #Composable () -> Unit,
) {
MaterialTheme(
typography = Typography(
h1 = TextStyle(...),
),
content = content,
)
}
#Composable
fun MyContent() {
MyTheme {
// MaterialTheme.typography.h1 will be your defined TextStyle here
}
}