# ImageViewer: the Android gallery library built around one hard problem

> A Kotlin full-screen image and video viewer distributed through JitPack, where the interesting engineering sits in the thumbnail to full-screen transition and in tiled loading for images taller than the screen.

**iielse/imageviewer** — A simple and customizable Android full-screen image viewer.

- Repository: https://github.com/iielse/imageviewer
- Stars: 2,303 · Forks: 311
- Language: Kotlin
- License: MIT
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/iielse-imageviewer

## A library, not a gallery app

The project describes itself as an Android image and video preview library supporting a smooth transition from thumbnail to full image, and compatible with ordinary images, long images, GIFs, video and paged data sources. That framing matters, because it sets expectations correctly: there is no app here, only a dependency you add to your own module and configure.

The repository structure reflects that. There is an `imageviewer/` directory for the library itself and a separate `androidApp/` for the demo application, with the Gradle files at the root. The distribution route is JitPack, with a build badge and a single dependency line.

```gradle
implementation("com.github.iielse:imageviewer:x.y.z")
```

That version placeholder is a reminder that this is not published to Maven Central: JitPack builds from the repository, so pinning a specific tag matters and there is no stable channel to rely on. The environment requirements are modest and stated plainly: minSdk 23, compileSdk 36 and Java 17. For a library, that floor is reasonably low, so it will drop into most existing Android projects without raising the minimum level.

Around 2300 stars and just over 300 forks, with the repository showing no open issues, this is a well-used library with a small public footprint. The last push was in June 2026 and the most recent release, 2.2.0, is a toolchain bump: Gradle 9.5.1, AGP 9.2.1 and Java 17.

## The minimum integration is six lines

The quick start shows a builder constructed with four arguments and then shown, which is about as small an API as a transition-based viewer can have.

```kotlin
fun show(view: View) {
    val dataList: List<Photo> = TODO()
    val clickedData: Photo = TODO()

    val builder = ImageViewerBuilder(
        context = view.context,
        dataProvider = SimpleDataProvider(clickedData, dataList),
        imageLoader = SimpleImageLoader(),
        transformer = SimpleTransformer(),
    )

    builder.show()
}
```

The four collaborators each own one concern. The data provider supplies the clicked item plus the surrounding list, the image loader decides how pixels reach an `ImageView`, and the transformer links a thumbnail to its full-screen counterpart. Only the transformer is genuinely unusual, and it is the reason this library is worth reading about.

One thing to notice is the data model. The interface is deliberately tiny:

```kotlin
interface Photo {
    fun id(): Long
    fun itemType(): @ItemType.Type Int
}
```

Two methods, and the README explains why both matter. `id()` must be unique, because it is used both for paging position and for the transition mapping, so a list keyed on something unstable will produce transitions that jump to the wrong image. `itemType()` tells the library whether an entry is an ordinary image, a long image or a video, which is how one adapter can hold mixed content. Bringing your own model means implementing two methods rather than inheriting from a base class, and it keeps your entities out of the library's type hierarchy.

## Transformer is the load-bearing idea

A thumbnail to full-screen transition needs to know where a given image came from, and by the time the viewer opens, the grid is usually scrolled, recycled or gone. The library solves this with a `Transformer` whose job is to answer one question: given an id, which `ImageView` currently displays it?

```kotlin
class SimpleTransformer : Transformer {
    override fun getView(key: Long): ImageView? = provide(key)

    companion object {
        private val transition = HashMap<ImageView, Long>()

        fun put(photoId: Long, imageView: ImageView) {
            require(isMainThread())
            if (!imageView.isAttachedToWindow) return

            transition[imageView] = photoId
        }
    }
}
```

The reference implementation keeps a static map from view to id, records entries when a thumbnail binds, and removes them on detach. That detail is the whole ballgame. A stale entry pointing at a recycled view produces a transition that starts from the wrong place on screen, and the detach listener is what prevents it.

The FAQ in the repository answers the two questions people actually ask. When there is no transition animation at all, the answer is a misconfigured `Transformer`: `getView()` must return the `ImageView` for the current thumbnail, and the thumbnail and the preview page must use the same id. When the transition lands in the wrong place, the answer is the status bar: immersive or translucent layouts shift the coordinates, and `Config.TRANSITION_OFFSET_Y` exists to correct it. Both are documented as FAQ entries rather than left to be discovered, which is a good sign about the library's maturity.

The design does mean the mapping is your responsibility. That is a real cost, and it is the price of not forcing a particular list implementation on you.

## Long images are a separate code path with a real cost

Images taller than the screen cannot be decoded at full width and held in memory, so they need tiled decoding, which is what `SubsamplingScaleImageView` provides. The catch is that the library does not choose a downloader for you: the README's guidance is that long images should be downloaded to a local file first and then handed to the subsampling view, and the sample loader implements exactly that.

The shape of that loader is instructive. It checks a cache keyed by URL, returns immediately on a hit, and otherwise launches a coroutine scoped to the view's lifecycle owner, shows a loading view on the view holder, downloads with Glide, then sets the image and stores the resulting `ImageSource` in the cache. Errors are surfaced with a toast and the loading view is hidden in a `finally` block.

The README also carries a migration note on this exact code: the download request function is now a suspend function and no longer depends on RxJava, and the guidance is to bind the download to the lifecycle so the page does not keep calling back into a destroyed view. Those two notes together describe a real library evolution from an RxJava era to coroutines, and they are the kind of detail that saves an integrator an hour.

The trade-off is honest: you bring the downloader, the cache and the Glide wiring. In exchange you control eviction policy and can swap the download layer. For a library that many apps will use with their own image stack, that is the right way round.

## Three extension points and a config table

Beyond the four constructor arguments, the builder accepts three kinds of customisation, and the README keeps them separate because they solve different problems.

Per-page UI is for content inside an individual item: more information, a save button, a share button, an action row. It is set with a view holder customizer, which means you supply your own holder and keep full control of the item layout. Overlay UI is for chrome shared across pages: a page indicator, a top bar, a bottom toolbar, set with an overlay customizer. State listening is for behaviour: page changes, drag fractions, transition progress and video playback state, set through a callback.

The callback interface is worth reading because it shows how much of the interaction is observable.

```kotlin
interface ViewerCallback : ImageViewerAdapterListener {
       override fun onInit(viewHolder: RecyclerView.ViewHolder) {}
    override fun onDrag(viewHolder: RecyclerView.ViewHolder, view: View, fraction: Float) {}
    override fun onRelease(viewHolder: RecyclerView.ViewHolder, view: View) {}

    fun onPageSelected(position: Int, viewHolder: RecyclerView.ViewHolder) {}
}
```

The drag fraction is the interesting one, since it lets a host app drive a background blur or a toolbar fade in step with the dismiss gesture.

Then there is the tuning table, which lists the constants you are most likely to need: `OFFSCREEN_PAGE_LIMIT` for preload depth, `VIEWER_ORIENTATION` for the swipe direction, `VIEWER_BACKGROUND_COLOR`, the two animation durations `DURATION_TRANSITION` and `DURATION_BG`, and the gesture parameters `SWIPE_DISMISS`, `SWIPE_TOUCH_SLOP` and `DISMISS_FRACTION`. `TRANSITION_OFFSET_Y` completes the set for the status bar case. The README says plainly that most apps will not need to touch these, which is a reasonable expectation rather than a guarantee.

## Paging, deletion and the imperative escape hatch

The demo has migrated to Paging 3, and viewer action events now travel on a SharedFlow, so the library's own notes point at the modern Android stack: Paging for data, Flow for events, Media3 for video. The changelog entries behind that are visible in the releases, where 2.1.22 added landscape offset support and 2.1.23 updated the subsampling view.

The feature list also includes paged data loading and deletion of preview data, which is unusual for a viewer library and reflects its origin in messaging apps. Rather than exposing the viewer object itself, the FAQ documents obtaining an action controller through a ViewModel:

```kotlin
ViewModelProvider(activity).get(ImageViewerActionViewModel::class.java)
```

That controller exposes `setCurrentItem(pos: Int)` to jump to a position, `dismiss()` to close the viewer, and `remove(item: List<Photo>)` to delete entries. Going through a ViewModel rather than a direct reference means the host survives configuration changes and the viewer does not hold an activity it should not hold.

Video is handled the same way: rather than shipping a video component, the README points at three classes in the demo, `SimpleViewerCustomizer`, `SimpleImageLoader` and `ExoVideoView`, and the notes confirm video moved to AndroidX Media3. So video support is real but delegated to the demo, which makes it the part of the library you will need to read most carefully before relying on it. The demo also covers immersive DialogFragment hosting, custom overlays and per-page UI, which is where the remaining implementation detail lives.

## Conclusion

ImageViewer earns its place because it isolates the part of a photo browser that is genuinely hard. Zooming, panning and paging are solved by RecyclerView and ViewPager2, but animating a thumbnail from a grid cell into a full-screen view and then back again, while the underlying image is a different object at a different resolution, is a problem libraries usually leave to the developer. The design here answers it with a `Transformer` that maps an id back to the source `ImageView`, which is a small interface doing a lot of work. Two caveats for adoption: it is a library, not an app, so the demo is your only reference for video and paging, and the long image path requires you to supply your own downloader and cache. Check `Config.TRANSITION_OFFSET_Y` early if your app uses a translucent status bar, since a misaligned transition is the most common complaint the FAQ itself anticipates.

## FAQ

### How do I add ImageViewer to an Android project?

It is distributed through JitPack rather than Maven Central, so add the dependency line `implementation("com.github.iielse:imageviewer:x.y.z")` with a pinned version, because JitPack builds from the repository and there is no stable channel. The environment requirements are minSdk 23, compileSdk 36 and Java 17. The library lives in the `imageviewer/` module and the demo app in `androidApp/`.

### Why does the thumbnail transition animation not play?

The README attributes a missing transition to a misconfigured `Transformer`. Two conditions have to hold: `getView()` must return the `ImageView` currently displaying the thumbnail, and the thumbnail and the preview page must use the same photo id. The sample implementation keeps a view to id map and removes entries on detach, which is what keeps a recycled view from being matched.

### How do I fix a transition that lands in the wrong position?

The cause is usually the status bar or an immersive layout shifting the coordinates the transition is computed against. The README points to `Config.TRANSITION_OFFSET_Y` as the correction for exactly that case, described as a position fix for the transparent status bar scenario. The related drag parameters, `SWIPE_TOUCH_SLOP` and `DISMISS_FRACTION`, control when a dismiss gesture is recognised.

### How are very tall images handled?

Long images go through `SubsamplingScaleImageView`, which decodes in tiles rather than loading the whole bitmap. The library does not pick a downloader for you: the sample implementation uses Glide to download the image to a local file first, then hands a URI based `ImageSource` to the view and caches it. The README also notes that the download function is now a suspend function and no longer depends on RxJava.

### How do I close the viewer programmatically or delete an item?

Get the action controller through a ViewModel with `ViewModelProvider(activity).get(ImageViewerActionViewModel::class.java)`. It exposes `setCurrentItem(pos: Int)` to jump to a position, `dismiss()` to close the viewer, and `remove(item: List<Photo>)` to delete entries. Going through a ViewModel keeps the viewer decoupled from the activity.

### Does the library handle video?

Yes, but the implementation is in the demo rather than the library, and the README points at `SimpleViewerCustomizer`, `SimpleImageLoader` and `ExoVideoView` for a reference. Video playback has moved to AndroidX Media3. The feature list includes video preview and playback controls, and `onPageScrollStateChanged` in the viewer callback covers video playback state changes.

## Sources

- [iielse/imageviewer on GitHub](https://github.com/iielse/imageviewer)
- [Issues](https://github.com/iielse/imageviewer/issues)
- [License: MIT](https://github.com/iielse/imageviewer/blob/master/LICENSE)
- [README](https://github.com/iielse/imageviewer/blob/master/README.md)
- [Releases](https://github.com/iielse/imageviewer/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/iielse-imageviewer
