Let's say I want to show a composable view with an image. Instead of an original image I would like to trim part of the image away, for example 20% from the right edge. How would I do this? Can I use a .clip-modifier with a custom shape?
You can create your own shape and clip using that shape as
private val cropShape = GenericShape { size: Size, layoutDirection: LayoutDirection ->
addRect(Rect(0f, 0f, size.width * .8f, size.height))
}
0.8 is arbitrary number, you can customize your Rectangle as you wish
Image(
modifier = Modifier.clip(cropShape),
painter = bitmap,
contentDescription = null
)
The one on top without clip modifier, the one at the bottom is clipped with Modifier.clip(cropShape)
You can crop it with a shape, as #Thracian suggests, but in that case the view size won't change. For example, if you put it in Row, you will have an unexpected padding to next item.
Instead, you can use Modifier.layout to actually reduce the size of the view, and clip it with RectangleShape, since by default Compose views are not clip to bounds.
Image(
painter = painterResource(id = R.drawable.my_image_1),
contentDescription = null,
modifier = Modifier
.clip(RectangleShape)
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(width = placeable.width * 8 / 10, height = placeable.height) {
placeable.place(0, 0)
}
}
)
Related
I want the image to fill the max width of container, and have a specific height. I tried using ContentScale.FillWidth, but it still leaves some background (green).
#Preview(showBackground = true)
#Composable
fun testImg() {
Card(modifier = Modifier.fillMaxWidth()) {
Image(
painter = painterResource(id = R.drawable.jwimage1),
null,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Green),
contentScale = ContentScale.FillWidth
)
}
}
prefview fro testImg
I tried using different ContentScale, adding height specification, but they don't change the result.
Edit: Solved! It's really silly that I used a png image with two transparent strips on its sides :(
I'm trying to set it so that a TextField refuses to shrink past its intrinsic height (defined by minLines), but also grow to fill max height:
// context: this is inside of a Column that has fillMaxHeight on it
OutlinedTextField(
// unrelated fields
minLines = 26,
modifier = Modifier
//.height(IntrinsicSize.Min)
.fillMaxWidth()
.fillMaxHeight()
.weight(1f)
.height(IntrinsicSize.Max)
)
However, everything I've tried (setting fillMaxHeight, weight(1f), using requiredHeight instead of height, etc) has issues. It seems like it's mutually exclusive: I can either set a minimum height based on intrinsic height or I can grow to fill the height (using weight seems to work for that, but not fillMaxHeight for some reason.
What is the proper way to have IntrinsicSize be the minimum, but grow to fill remaining space?
If i understand this question correctly you want your TextField height to be one line tall then increases as user enters text. You can selectively change modifier using Modifier.then()
Edit
As of jetpack compose 1.4.0-alpha02 minLines param is added to OutlineTextField and other text composables
#Composable
private fun Sample() {
var text by remember {
mutableStateOf("Hello World")
}
var focused by remember {
mutableStateOf(false)
}
Column(modifier = Modifier.fillMaxHeight()) {
OutlinedTextField(
value = text,
onValueChange = {
text = it
},
minLines = 26,
// unrelated fields
modifier = Modifier
.fillMaxWidth()
.then(
if (focused) {
Modifier.wrapContentHeight()
} else Modifier.fillMaxHeight()
)
.onFocusChanged {
focused = it.isFocused
}
)
}
}
I have to create a reusable component that tries to achieve this goal: I have a column that can have content that's larger than the screen height. On the bottom of the screen we have panel with gradient background that can contain button or something else (it's basically a slot in the component). This bottom panel have to be always visible on the screen, and in case of the column being bigger than screen - bottom panel have to be on the top of this column. Gradient background does a nice UX effect so user knows what is going on. It looks like that:
I have that solved, but here's the challenge. The column content have to be scrollable to be on top of the bottom panel when scrolled to the end. Current solution I have is to add a spacer on the bottom of this column. This spacer have the calculated height of the bottom parent. And here's the issue - right now we have calculation done in onSizeChanged which basically results in additional frame needed for the spacer to have correct size.
We did not observe any negative impact of that performance or UX wise. The spacer height calculation never does anything that user can see, but I still want to solve that properly.
AFAIK this can be done using custom Layout, but that seems a little bit excessive for what I want to achieve. Is there another way to do this properly?
Current solution:
#Composable
fun FloatingPanelColumn(
modifier: Modifier = Modifier,
contentModifier: Modifier = Modifier,
contentHorizontalAlignment: Alignment.Horizontal = Alignment.Start,
bottomPanelContent: #Composable ColumnScope.() -> Unit,
content: #Composable ColumnScope.() -> Unit
) {
val scrollState = rememberScrollState()
var contentSize by remember {
mutableStateOf(1)
}
Box(modifier) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = scrollState)
.then(contentModifier),
horizontalAlignment = contentHorizontalAlignment,
) {
content()
val contentSizeInDp = with(LocalDensity.current) { contentSize.toDp() }
Spacer(modifier = Modifier.height(contentSizeInDp))
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.onSizeChanged {
contentSize = it.height
}
.wrapContentHeight()
.align(Alignment.BottomStart)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0x00FAFCFF),
Color(0xFFF6F9FB),
)
)
),
content = bottomPanelContent
)
}
}
The best way to depend on an other view size during layout is using SubcomposeLayout:
SubcomposeLayout { constraints ->
// subcompose the view you need to measure first
val bottomPanel = subcompose("bottomPanel") {
Column(
// ...
)
}[0].measure(constraints)
// use calculated value in next views layout, like bottomPanel.height
val mainList = subcompose("mainList") {
LazyColumn(
contentPadding = PaddingValues(bottom = bottomPanel.height.toDp())
) {
// ...
}
}[0].measure(constraints)
layout(mainList.width, mainList.height) {
mainList.place(0, 0)
bottomPanel.place(
(mainList.width - bottomPanel.width) / 2,
mainList.height - bottomPanel.height
)
}
}
I've got a some dynamic content that can vary in size and below that is a circular image. The problem is that when the content becomes too big, the image overflows the content.
The ideal thing would be that if dynamic sized content is too big, the image should be hidden instead. Is it possible to hide the image (the box) if it will not fit?
Example code:
#Composable
fun ImageTest(modifier: Modifier = Modifier,
outlineColor: Color = Color.White,
outlineSize: Dp = 16.dp,
image: Int) {
Column(
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
.fillMaxSize()
) {
Text("Content that can be of different sizes", style = MaterialTheme.typography.h2, modifier = Modifier.height(550.dp).background(Color.Red))
// Circular image - HIDE THE BOX IF IT DOESN'T FIT
Box(
modifier = modifier
.background(color = outlineColor, shape = CircleShape)
.padding(outlineSize)
.aspectRatio(ratio = 1f)
) {
Image(
painter = painterResource(id = image),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize()
)
}
}
}
Replace image container Box with BoxWithConstraints. Then, inside BoxWithConstraintsScope you can check how much space is available for your view, and if you don't have enough - don't display it.
BoxWithConstraints(
modifier = modifier
.background(color = outlineColor, shape = CircleShape)
.padding(outlineSize)
.aspectRatio(ratio = 1f)
) {
if (maxHeight > 200.dp) {
Image(
painter = painterResource(id = image),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize()
)
}
}
If you wanna hide box background too, move it inside if.
If you want add an animation, replace if (maxHeight > 200.dp) { with AnimatedVisibility(visible = maxHeight > 200.dp) {
I'm refactoring a screen to use jetpack compose. It had an ImageView that sits over the top of the background containing a large 1920x795dp PNG image (mdpi and hdpi buckets) which gets scaled down. After changing to use an Image it now has noticably jaggier edges.
ImageView:
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:scaleType="fitXY"
android:layout_marginBottom="-90dp"
android:src="#drawable/ic_swoosh"/>
Image:
Image(
modifier = Modifier
.fillMaxWidth()
.offset(y = 90.dp)
.align(Alignment.BottomCenter),
painter = painterResource(R.drawable.ic_swoosh),
contentDescription = null,
contentScale = ContentScale.Fit,
)
Drilling into the source of Image, it uses AndroidPaint which supposedly has it enabled:
internal fun makeNativePaint() =
android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG)
If I wrap ImageView in AndroidView the problem is not present. I can also verify that it is positioned and sized exactly the same as the Image version, but it is anti-aliased correctly.
AndroidView(
modifier = Modifier
.fillMaxWidth()
.offset(y = 90.dp)
.align(Alignment.BottomCenter),
factory = {
ImageView(it).apply {
scaleType = android.widget.ImageView.ScaleType.FIT_XY
setImageResource(R.drawable.ic_swoosh)
}
}
)
If I remove scaling with contentScale = ContentScale.None the image is also smooth, so it does appear to be due to being scaled down. Removing contentScale entirely shows the same problem because the image is automatically scaled down to fit the view.
Edit: If I remove the hdpi variant it also becomes smooth!