Images are powerful visual elements that can evoke emotions, convey information, and enhance the overall aesthetic of an app. Whether you've scrolled through meme-filled social media feeds, glanced at news articles with eye-catching visuals, or even just admired the neatly organized thumbnails in your phone's gallery, you've witnessed the power of images. And let's be honest, sometimes the best part is staring at perfectly cropped cat pictures. In this topic, we'll equip you with the knowledge necessary to work with images and icons in Jetpack Compose.
Image fundamentals
Before we dive into displaying images, it's essential to grasp the two fundamental types of digital images: raster and vector images:
Raster Images (Pixel-based):
Raster images are composed of a grid of small colored squares called pixels. Think of them like a super intricate paint-by-numbers where each square contributes to the bigger picture. Popular raster formats include:
JPEG (JPG): A lossy image compression format that trades off image quality for storage size.
PNG: A lossless image format that supports transparency (note that JPEG does not). It is often used for web graphics and UI elements.
WebP: An image format developed by Google that offers both lossy and lossless compression, aiming to provide smaller file sizes for images compared to formats like JPEG and PNG while maintaining similar quality.
Raster images are excellent for complex, detailed images like photographs taken by cameras. The downside of raster images is that they lose quality when enlarged (becomes pixelated) and file sizes can get large for high-resolution images.
Lossy compression reduces file size by discarding some image data, which may decrease image quality. In contrast, lossless compression reduces file size without losing any original image data, preserving quality.
Vector Images:
Vector images are made up of mathematical definitions of shapes, lines, curves, etc. Think of them as a set of instructions for drawing the image, rather than storing the color of every single pixel. A common vector format is SVG.
Vector images are scalable without losing quality (great for logos and icons). Generally, they have smaller file sizes compared to high-resolution raster images. Note that not all images can be represented as vector images, such as images taken by camera as they are less ideal for highly detailed images with complex color blends.
Image Composable
In Jetpack Compose, the primary way to display an image is through the Image composable. To start, you'll need an image! Android Studio's "Resource Manager" window makes it easy to import different types of image files as drawables for your project:
The "Resource Manager" window is usually located in the left-hand pane of Android Studio. If you cannot find it, you can open it by going to the menu bar at the top and selecting View -> Tool Windows -> Resource Manager.
In Android, drawables are graphic resources used for images and other on-screen visuals. They include raster images (PNG, JPG, WebP, etc.), vector images (SVG, etc.), and others that are advanced for this topic.
After clicking "Import Drawables", select one or more images using the file picker. When you are done, you can optionally change the file name(s) and then import them. The images will be placed in the "res/drawable" directory:
It's a common convention to use "snake_case" to name your resources.
Alternatively, you can also just drag & drop an image into the "res/drawable" directory. In this topic, we will be using an image of a flower in pastel style, you are free to use any image you'd like:
Now let's use the Image composable to display our image - you just need to provide two essential arguments:
Image source: This is where you specify the source of the image you want to display. We can use the
painterResourcemethod with an image reference to load an image.Content description: This text describes the image's content for accessibility purposes. Screen readers can then convey this information to visually impaired users. If the image is purely used for decorative purposes, you can provide
nullas an argument.
Here's a basic example:
Image(
painter = painterResource(R.drawable.flower), // Load image from resources
contentDescription = "Flower in pastel style" // Describe the image
)Essentially, to display an image we need a Painter that is used to draw the image. The method painterResource takes care of creating a painter from the resource provided. As shown in the snippet above, we can access our drawable resources as R.drawable followed by the name of the resource without its extension.
R.drawable.* is used to retrieve a resource id which is an integer assigned to our resource.
The snippet above produces the following output:
Let's take a look at the parameters that the composable provides:
@Composable
fun Image(
bitmap: ImageBitmap | imageVector: ImageVector | painter: Painter,
contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
// `filterQuality` is only available in the overload that uses ImageBitmap
filterQuality: FilterQuality = DefaultFilterQuality
)The Image composable has multiple overloads, each providing a different way of providing the image source. In this topic, we'll focus mainly on the overload that takes in a painter (painter: Painter). We'll only cover some of the parameters. Note that the filterQuality parameter applies only when you are working directly with a bitmap image (ImageBitmap). This won't be covered in the topic.
Loading images
Before we dive into manipulating images, let's take a look at how we load images. The Image composable gives you flexibility in loading images. For images stored directly in your app's resources, the painter overload is usually the easiest way (as we saw in the previous example).
However, if you need to modify an image before displaying it, you can first load it as an ImageVector or ImageBitmap. Use ImageVector.vectorResource(id = R.drawable.your_image) for vector images or ImageBitmap.imageResource(id = R.drawable.your_image) for raster images.
var imageVector = ImageVector.vectorResource(R.drawable.ic_launcher_foreground)
/* Do something to `imageVector` */
Image(imageVector = imageVector, ...)
// OR
var imageBitmap = ImageBitmap.imageResource(R.drawable.flower)
/* Do something to `imageBitmap` */
Image(bitmap = imageBitmap, ...)ImageVector provides convenient methods you can use to manipulate the vector image. For ImageBitmap, you can convert it to a Bitmap using the extension function ImageBitmap.asAndroidBitmap() which has a bunch of methods you can use to manipulate the bitmap image but that is out of the scope of this topic.
Internally, both ImageVector and ImageBitmap are converted to VectorPainter and BitmapPainter respectively which are then used with the painter overload of the Image composable.
To load an image from the internet, you can use third-party libraries that handle everything from the networking logic to download the image to caching to avoid downloading the same image multiple times. Some popular libraries include Coil and Glide.
For example, to use the coil library, we first need to add its dependency:
implementation "io.coil-kt:coil-compose:$coilVersion"implementation("io.coil-kt:coil-compose:$coilVersion")We can then use the AsyncImage composable to load an image:
AsyncImage(
model = "https://example.com/image.jpg",
contentDescription = null
)Manipulating images
The Image composable isn't just for display - it opens up a world of image customization. Jetpack Compose makes it easy to reshape, filter, and style your image to achieve the look you are going for. Let's explore these techniques:
Resizing and content scaling:
We can set the
Imagecomposable's size with thesizemodifier as we would with any other composable.The
sizemodifier controls the space allocated for the image, not the image's actual image content. To determine how the image content fits within that space, you'll use content scaling.Content scaling dictates how an image is resized to fit the space provided by its
Imagecomposable. By default,ContentScale.Fitis used, which scales the image evenly, maintaining the aspect ratio so it isn't distorted while ensuring it fits inside the available space. You can customize this behavior with thecontentScaleparameter. For instance,ContentScale.Cropwill scale the image to fill the space potentially cutting off some parts completely.Let's illustrate this with an example:
Image( ..., modifier = Modifier.size(200.dp), // Image composable size 200dp x 200dp contentScale = ContentScale.Crop )This will produce the output:
You can look at the Android documentation for the full list of content scale options, alongside helpful illustrations of each.
Aspect ratio modifier:
The
aspectRatiomodifier allows you to specify the aspect ratio of an image. This is useful for ensuring that images are displayed correctly on different screen sizes and orientations.The
aspectRatiomodifier takes two arguments, the required one being the desired aspect ratio. The aspect ratio is expressed as a floating-point number, where the numerator is the width of the image and the denominator is the height of the image. For example, an aspect ratio of 16:9 would be expressed as16f / 9f.The second parameter is
matchHeightConstraintsFirst, a boolean value that determines whether the height constraints should be matched first when calculating the size of the image. This isfalseby default. We won't cover it in this topic.Here is an example of how to use the
aspectRatiomodifier:Image( ..., modifier = Modifier .fillMaxWidth() .aspectRatio(16f / 9f) .background(Color.Black) )This code will display the image with an aspect ratio of 16:9. The image will be scaled to fill the screen's width, and the height will be adjusted accordingly:
To illustrate that the
Imagecomposable respects the specified aspect ratio, we've added a black background color. To re-iterate, by default, the actual image is scaled uniformly to fit within the composable's bounds without cropping or distorting the image. However, changing thecontentScale, for example, setting it toContentScale.Cropwill scale the image to fill the entire space, even if it means cropping parts of the image.Clipping and shapes:
The clip modifier allows you to define the shape of your image. This is useful for creating rounded corners, circular images, or applying other custom shapes:
Image( ..., modifier = Modifier .size(200.dp) .clip(CircleShape) // Clip to a circle shape )This will produce the output:
Jetpack Compose provides several basic shapes out of the box, including
CircleShape,RoundedCornerShape(for rounded-like corners), andCutCornerShape(for sharp cut corners). These shapes are perfect for common use cases like creating circular profile images. We'll cover more about shapes and custom shapes in another topic.Alpha
The
Imagecomposable exposes analphaparameter (taking in a float value) that controls the image's transparency. An alpha of 1.0 means the image is fully opaque, while an alpha of 0.0 means the image is completely transparent.Here's an example of setting the alpha:
Image( ..., alpha = 0.5f // 50% opacity )This will produce the output:
Color filter:
The
colorfilterparameter takes in aColorFilterobject that can be used to transform the individual pixel colors of the image. You can take a look at the Android documentation to learn more.
Icon Composable
The Icon composable serves as the primary way to display icons within your Android apps. It is designed with Material guidelines in mind, providing a seamless and optimized experience.
Icon(s) just like Image(s) require an image source, as a Painter, ImageBitmap, or ImageVector and a content description:
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = "Favorite"
)In the snippet above, we used the Favorite icon provided by Jetpack Compose. Alongside the Favorite icon, there are many other icons under the Default baseline theme. There are other themes too, such as the Rounded, or Outlined. You can also use your own app's resources to provide the icon source. In this case, a vector-based icon is always preferred to a raster one.
Additionally, the Icon composable exposes a modifier, and tint parameters. You can use the modifier to change the icon's size as expected. By default, the Icon composable sets the icon's size to 24.dp as per the Material design guidelines. You can pass a Color object to the tint parameter to tint the icon with the supplied color:
Icon(
...,
tint = Color.Red
)The tint parameter of the Icon composable is a convenience method for applying a color filter to an icon. Under the hood, it simply uses a ColorFilter.tint color filter. You could do the same with an Image composable!
So, why use Icon over Image in certain scenarios?
Material Design Adherence: The Icon composable ensures your icons perfectly match the Material Design system (such as being
24.dpin size), creating a visually coherent user interface.
Conclusion
In this topic, we covered a brief introduction to the types of images, raster, and vector, how you can add images to your app's resources, display them using the Image or Icon composable, and finally change their appearance. In another topic, you will learn about color filters, shapes in general, custom shapes, and custom painters to further customize your images!