A simple and customizable Android full-screen image viewer 一个简单且可自定义的Android全屏图像浏览器
Kotlin
2,302
143 commits
updated Jun 5, 2026
一个面向 Android 的图片/视频预览库,支持从缩略图到原图的平滑过渡动画,并兼容普通图、长图、GIF、视频和分页数据源。

implementation("com.github.iielse:imageviewer:x.y.z")
minSdk 23compileSdk 36Java 17最简单的调用方式:
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()
}
ImageLoader 示例普通图使用 Glide,长图先下载到本地文件再交给 SubsamplingScaleImageView:
class SimpleImageLoader : ImageLoader {
override fun load(view: ImageView, data: Photo, viewHolder: RecyclerView.ViewHolder) {
val url = (data as? MyData)?.url ?: return
Glide.with(view)
.load(url)
.placeholder(view.drawable)
.into(view)
}
override fun load(
subsamplingView: SubsamplingScaleImageView,
data: Photo,
viewHolder: RecyclerView.ViewHolder,
) {
val url = (data as? MyData)?.url ?: return
val cache = subsamplingCache.get(url)
if (cache != null) {
subsamplingView.setImage(cache)
return
}
subsamplingView.lifecycleOwner.lifecycleScope.launch {
try {
findLoadingView(viewHolder)?.visibility = View.VISIBLE
val file = subsamplingDownloadRequest(url)
subsamplingView.setImage(
ImageSource.uri(Uri.fromFile(file)).also { source ->
subsamplingCache.put(url, source)
}
)
} catch (e: Throwable) {
toast(e.message)
} finally {
findLoadingView(viewHolder)?.visibility = View.GONE
}
}
}
private suspend fun subsamplingDownloadRequest(url: String): File =
withContext(Dispatchers.IO) {
Glide.with(appContext).downloadOnly().load(url).submit().get()
}
}
说明:
subsamplingDownloadRequest() 现在是协程挂起函数,不再依赖 RxJava。Transformer 示例Transformer 用于把缩略图和预览页建立“配对关系”,决定过渡动画的起点和终点。
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
imageView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) = Unit
override fun onViewDetachedFromWindow(v: View) {
transition.remove(imageView)
imageView.removeOnAttachStateChangeListener(this)
}
})
transition[imageView] = photoId
}
private fun provide(photoId: Long): ImageView? {
transition.keys.forEach {
if (transition[it] == photoId) return it
}
return null
}
}
}
到这里,基础接入已经完成。
你可以在 builder 上追加 3 类自定义能力:
自定义每一页上的 UI
例如:更多信息、保存、分享、操作按钮
builder.setVHCustomizer(MyCustomViewHolderUI())
自定义覆盖层 UI
例如:指示器、顶部栏、底部工具栏
builder.setOverlayCustomizer(MyCustomIndicatorUI())
监听预览状态变化
例如:翻页、拖拽、过渡动画、视频播放状态
builder.setViewerCallback(MyViewerStateChangedListener())
ViewerCallbackinterface ViewerCallback : ImageViewerAdapterListener {
override fun onInit(viewHolder: RecyclerView.ViewHolder) {}
override fun onDrag(viewHolder: RecyclerView.ViewHolder, view: View, fraction: Float) {}
override fun onRestore(viewHolder: RecyclerView.ViewHolder, view: View, fraction: Float) {}
override fun onRelease(viewHolder: RecyclerView.ViewHolder, view: View) {}
fun onPageScrollStateChanged(state: Int) {}
fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
fun onPageSelected(position: Int, viewHolder: RecyclerView.ViewHolder) {}
}
一般情况下不需要调整,但以下参数可按业务需要覆盖:
| 属性 | 说明 |
|---|---|
OFFSCREEN_PAGE_LIMIT | Viewer 预加载页数 |
VIEWER_ORIENTATION | Viewer 滑动方向 |
VIEWER_BACKGROUND_COLOR | 预览背景色 |
DURATION_TRANSITION | 过渡动画时长 |
DURATION_BG | 背景动画时长 |
SWIPE_DISMISS | 是否支持拖拽关闭 |
SWIPE_TOUCH_SLOP | 拖拽触摸阈值 |
DISMISS_FRACTION | 拖拽关闭判定阈值 |
TRANSITION_OFFSET_Y | 透明状态栏场景下的过渡位置修正 |
interface Photo {
fun id(): Long
fun itemType(): @ItemType.Type Int
}
说明:
id() 必须唯一,用于分页定位和过渡动画映射。itemType() 决定当前数据是普通图、长图还是视频。通过:
ViewModelProvider(activity).get(ImageViewerActionViewModel::class.java)
获取 viewer 的动作控制器后,可以调用:
setCurrentItem(pos: Int) 切换到指定位置dismiss() 关闭预览remove(item: List<Photo>) 删除指定元素可直接参考 demo 中的实现:
SimpleViewerCustomizerSimpleImageLoaderExoVideoView通常是 Transformer 配置不正确。要保证:
getView() 能返回当前缩略图对应的 ImageViewphotoId注意状态栏/沉浸式布局的影响,必要时调整:
Config.TRANSITION_OFFSET_Y
仓库内自带 demo,包含以下能力示例:
Paging 3SharedFlowAndroidX Media3412 followers · starred Jun 2026
Kotlin
100.0%
A simple and customizable Android full-screen image viewer 一个简单且可自定义的Android全屏图像浏览器
Kotlin
2,302
143 commits
updated Jun 5, 2026
一个面向 Android 的图片/视频预览库,支持从缩略图到原图的平滑过渡动画,并兼容普通图、长图、GIF、视频和分页数据源。

implementation("com.github.iielse:imageviewer:x.y.z")
minSdk 23compileSdk 36Java 17最简单的调用方式:
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()
}
ImageLoader 示例普通图使用 Glide,长图先下载到本地文件再交给 SubsamplingScaleImageView:
class SimpleImageLoader : ImageLoader {
override fun load(view: ImageView, data: Photo, viewHolder: RecyclerView.ViewHolder) {
val url = (data as? MyData)?.url ?: return
Glide.with(view)
.load(url)
.placeholder(view.drawable)
.into(view)
}
override fun load(
subsamplingView: SubsamplingScaleImageView,
data: Photo,
viewHolder: RecyclerView.ViewHolder,
) {
val url = (data as? MyData)?.url ?: return
val cache = subsamplingCache.get(url)
if (cache != null) {
subsamplingView.setImage(cache)
return
}
subsamplingView.lifecycleOwner.lifecycleScope.launch {
try {
findLoadingView(viewHolder)?.visibility = View.VISIBLE
val file = subsamplingDownloadRequest(url)
subsamplingView.setImage(
ImageSource.uri(Uri.fromFile(file)).also { source ->
subsamplingCache.put(url, source)
}
)
} catch (e: Throwable) {
toast(e.message)
} finally {
findLoadingView(viewHolder)?.visibility = View.GONE
}
}
}
private suspend fun subsamplingDownloadRequest(url: String): File =
withContext(Dispatchers.IO) {
Glide.with(appContext).downloadOnly().load(url).submit().get()
}
}
说明:
subsamplingDownloadRequest() 现在是协程挂起函数,不再依赖 RxJava。Transformer 示例Transformer 用于把缩略图和预览页建立“配对关系”,决定过渡动画的起点和终点。
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
imageView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) = Unit
override fun onViewDetachedFromWindow(v: View) {
transition.remove(imageView)
imageView.removeOnAttachStateChangeListener(this)
}
})
transition[imageView] = photoId
}
private fun provide(photoId: Long): ImageView? {
transition.keys.forEach {
if (transition[it] == photoId) return it
}
return null
}
}
}
到这里,基础接入已经完成。
你可以在 builder 上追加 3 类自定义能力:
自定义每一页上的 UI
例如:更多信息、保存、分享、操作按钮
builder.setVHCustomizer(MyCustomViewHolderUI())
自定义覆盖层 UI
例如:指示器、顶部栏、底部工具栏
builder.setOverlayCustomizer(MyCustomIndicatorUI())
监听预览状态变化
例如:翻页、拖拽、过渡动画、视频播放状态
builder.setViewerCallback(MyViewerStateChangedListener())
ViewerCallbackinterface ViewerCallback : ImageViewerAdapterListener {
override fun onInit(viewHolder: RecyclerView.ViewHolder) {}
override fun onDrag(viewHolder: RecyclerView.ViewHolder, view: View, fraction: Float) {}
override fun onRestore(viewHolder: RecyclerView.ViewHolder, view: View, fraction: Float) {}
override fun onRelease(viewHolder: RecyclerView.ViewHolder, view: View) {}
fun onPageScrollStateChanged(state: Int) {}
fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
fun onPageSelected(position: Int, viewHolder: RecyclerView.ViewHolder) {}
}
一般情况下不需要调整,但以下参数可按业务需要覆盖:
| 属性 | 说明 |
|---|---|
OFFSCREEN_PAGE_LIMIT | Viewer 预加载页数 |
VIEWER_ORIENTATION | Viewer 滑动方向 |
VIEWER_BACKGROUND_COLOR | 预览背景色 |
DURATION_TRANSITION | 过渡动画时长 |
DURATION_BG | 背景动画时长 |
SWIPE_DISMISS | 是否支持拖拽关闭 |
SWIPE_TOUCH_SLOP | 拖拽触摸阈值 |
DISMISS_FRACTION | 拖拽关闭判定阈值 |
TRANSITION_OFFSET_Y | 透明状态栏场景下的过渡位置修正 |
interface Photo {
fun id(): Long
fun itemType(): @ItemType.Type Int
}
说明:
id() 必须唯一,用于分页定位和过渡动画映射。itemType() 决定当前数据是普通图、长图还是视频。通过:
ViewModelProvider(activity).get(ImageViewerActionViewModel::class.java)
获取 viewer 的动作控制器后,可以调用:
setCurrentItem(pos: Int) 切换到指定位置dismiss() 关闭预览remove(item: List<Photo>) 删除指定元素可直接参考 demo 中的实现:
SimpleViewerCustomizerSimpleImageLoaderExoVideoView通常是 Transformer 配置不正确。要保证:
getView() 能返回当前缩略图对应的 ImageViewphotoId注意状态栏/沉浸式布局的影响,必要时调整:
Config.TRANSITION_OFFSET_Y
仓库内自带 demo,包含以下能力示例:
Paging 3SharedFlowAndroidX Media3412 followers · starred Jun 2026
Kotlin
100.0%