Generate media thumbnails

Media thumbnails provide users with a quick visual preview of images and videos, allowing for faster browsing while making the app interface more visually appealing and engaging. Because thumbnails are smaller than full-sized media, they help to save memory, storage space, and bandwidth while improving the media browsing performance.

Depending on the file type and the file access you have in your application and your media assets, you can create thumbnails in a variety of different ways.

Create a thumbnail using an image loading library

Image loading libraries do a lot of the heavy lifting for you; they can handle caching along with the logic to fetch the source media from the local or network resource based upon a Uri. The following code demonstrates the use of the Coil image loading library works for both images and videos, and works on a local or network resource.

// Use Coil to create and display a thumbnail of a video or image with a specific height
// ImageLoader has its own memory and storage cache, and this one is configured to also
// load frames from videos
val videoEnabledLoader = ImageLoader.Builder(context)
    .components {
        add(VideoFrameDecoder.Factory())
    }.build()
// Coil requests images that match the size of the AsyncImage composable, but this allows
// for precise control of the height
val request = ImageRequest.Builder(context)
    .data(mediaUri)
    .size(Int.MAX_VALUE, THUMBNAIL_HEIGHT)
    .build()
AsyncImage(
    model = request,
    imageLoader = videoEnabledLoader,
    modifier = Modifier
        .clip(RoundedCornerShape(20))    ,
    contentDescription = null
)

If at all possible, create thumbnails server-side. See Loading images for detail on how to load images using Compose, and Loading large bitmaps efficiently for guidance on how to work with large images.

Create a thumbnail from a local image file

Getting thumbnail images involves efficient downscaling while preserving visual quality, avoiding excessive memory usage, dealing with a variety of image formats, and making correct use of Exif data.

The createImageThumbnail method does all of these, providing you have access to the path of the image file.

val bitmap = ThumbnailUtils.createImageThumbnail(File(file_path), Size(640, 480), null)

If you only have the Uri, you can use the loadThumbnail method in ContentResolver starting with Android 10, API level 29.

val thumbnail: Bitmap =
        applicationContext.contentResolver.loadThumbnail(
        content-uri, Size(640, 480), null)

The ImageDecoder, available starting with Android 9, API level 28, has some solid options to resample the image as you decode it to prevent extra memory use.

class DecodeResampler(val size: Size, val signal: CancellationSignal?) : OnHeaderDecodedListener {
    private val size: Size

   override fun onHeaderDecoded(decoder: ImageDecoder, info: ImageInfo, source:
       // sample down if needed.
        val widthSample = info.size.width / size.width
        val heightSample = info.size.height / size.height
        val sample = min(widthSample, heightSample)
        if (sample > 1) {
            decoder.setTargetSampleSize(sample)
        }
    }
}

val resampler = DecoderResampler(size, null)
val source = ImageDecoder.createSource(context.contentResolver, imageUri)
val bitmap = ImageDecoder.decodeBitmap(source, resampler);

You can use BitmapFactory to create thumbnails for apps targeting earlier Android releases. BitmapFactory.Options has a setting to decode just the bounds of an image for the purpose of resampling.

First, decode just the bounds of the bitmap into the BitmapFactory.Options:

private fun decodeResizedBitmap(context: Context, uri: Uri, size: Size): Bitmap?{
    val boundsStream = context.contentResolver.openInputStream(uri)
    val options = BitmapFactory.Options()
    options.inJustDecodeBounds = true
    BitmapFactory.decodeStream(boundsStream, null, options)
    boundsStream?.close()

Use the width and height from BitmapFactory.Options to set the sample size:

if ( options.outHeight != 0 ) {
        // we've got bounds
        val widthSample = options.outWidth / size.width
        val heightSample = options.outHeight / size.height
        val sample = min(widthSample, heightSample)
        if (sample > 1) {
            options.inSampleSize = sample
        }
    }

Decode the stream. The size of the resulting image is sampled by powers of two based upon the inSampleSize.

    options.inJustDecodeBounds = false
    val decodeStream = context.contentResolver.openInputStream(uri)
    val bitmap =  BitmapFactory.decodeStream(decodeStream, null, options)
    decodeStream?.close()
    return bitmap
}

Create a thumbnail from a local video file

Getting video thumbnail images involves many of the same challenges as with getting image thumbnails, but the file sizes can be much larger and getting a representative video frame isn't always as straightforward as picking the first frame of the video.

The createVideoThumbnail method is a solid choice if you have access to the path of the video file.

val bitmap = ThumbnailUtils.createVideoThumbnail(File(file_path), Size(640, 480), null)

If you only have access to a content Uri, you can use MediaMetadataRetriever.

First, check to see if the video has an embedded thumbnail, and use that if possible:

private suspend fun getVideoThumbnailFromMediaMetadataRetriever(context: Context, uri: Uri, size: Size): Bitmap? {
    val mediaMetadataRetriever = MediaMetadataRetriever()
    mediaMetadataRetriever.setDataSource(context, uri)
    val thumbnailBytes = mediaMetadataRetriever.embeddedPicture
    val resizer = Resizer(size, null)
    ImageDecoder.createSource(context.contentResolver, uri)
    // use a built-in thumbnail if the media file has it
    thumbnailBytes?.let {
        return ImageDecoder.decodeBitmap(ImageDecoder.createSource(it));
    }

Fetch the width and height of the video from the MediaMetadataRetriever to calculate the scaling factor:

val width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
            ?.toFloat() ?: size.width.toFloat()
    val height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
            ?.toFloat() ?: size.height.toFloat()
    val widthRatio = size.width.toFloat() / width
    val heightRatio = size.height.toFloat() / height
    val ratio = max(widthRatio, heightRatio)

On Android 9+ (API level 28), the MediaMetadataRetriever can return a scaled frame:

if (ratio > 1) {
        val requestedWidth = width * ratio
        val requestedHeight = height * ratio
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            val frame = mediaMetadataRetriever.getScaledFrameAtTime(
                -1, OPTION_PREVIOUS_SYNC,
                requestedWidth.toInt(), requestedHeight.toInt())
            mediaMetadataRetriever.close()
            return frame
        }
    }

Otherwise, return the first frame unscaled:

    // consider scaling this after the fact
    val frame = mediaMetadataRetriever.frameAtTime
    mediaMetadataRetriever.close()
    return frame
}