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
}