{"id":13990183,"url":"https://github.com/iielse/imageviewer","last_synced_at":"2025-07-22T12:31:04.239Z","repository":{"id":37755684,"uuid":"79318521","full_name":"iielse/imageviewer","owner":"iielse","description":"A simple and customizable Android full-screen image viewer 一个简单且可自定义的Android全屏图像浏览器","archived":false,"fork":false,"pushed_at":"2023-05-06T03:28:15.000Z","size":27739,"stargazers_count":2237,"open_issues_count":0,"forks_count":311,"subscribers_count":32,"default_branch":"master","last_synced_at":"2024-11-29T09:40:10.628Z","etag":null,"topics":["draggable","gallery","gesture","gif","image","imageviewer","photobrowser","transferee","video","viewer","viewpager","wechat","zoom"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/iielse.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2017-01-18T08:18:26.000Z","updated_at":"2024-11-21T01:38:55.000Z","dependencies_parsed_at":"2024-01-18T04:09:41.040Z","dependency_job_id":"1cf68ac0-6bf1-4e07-bf5d-c83c051d1616","html_url":"https://github.com/iielse/imageviewer","commit_stats":null,"previous_names":[],"tags_count":42,"template":false,"template_full_name":null,"purl":"pkg:github/iielse/imageviewer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iielse%2Fimageviewer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iielse%2Fimageviewer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iielse%2Fimageviewer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iielse%2Fimageviewer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/iielse","download_url":"https://codeload.github.com/iielse/imageviewer/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iielse%2Fimageviewer/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266495904,"owners_count":23938619,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-22T02:00:09.085Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["draggable","gallery","gesture","gif","image","imageviewer","photobrowser","transferee","video","viewer","viewpager","wechat","zoom"],"created_at":"2024-08-09T13:02:26.188Z","updated_at":"2025-07-22T12:31:03.499Z","avatar_url":"https://github.com/iielse.png","language":"Kotlin","readme":"# Imageviewer\n\n提供查看缩略视图到原视图的无缝过渡转变的视觉效果，优雅的浏览普通图、长图、动图.\n\n#### 主要特征\n\n- **过渡动画** 缩略图到大图或大图到缩略图时提供无缝衔接动画\n- **浏览手势** 浏览大图时可使用常势操用手.如缩放图片等.（[PhotoView](https://github.com/chrisbanes/PhotoView)）\n- **超大图** 图片区块加载 （[SubsamplingScaleImageView](https://github.com/davemorrissey/subsampling-scale-image-view)）\n- **Video** 支持Video加载 ([ExoPlayer](https://github.com/google/ExoPlayer))\n- **拖拽关闭** 对大图进行上/下滑操作退出浏览.\n- **数据分页加载** 在浏览大图的情况下异步加载数据.\n- **数据删除**\n- **自定义UI** 对预览页的UI元素自定义追加\n- **已适配RTL**\n\n![](https://github.com/iielse/res/blob/master/imageviewer/1.gif)\n\n### 引入 [![](https://jitpack.io/v/iielse/imageviewer.svg)](https://jitpack.io/#iielse/imageviewer)\n\n```\nimplementation 'com.github.iielse:imageviewer:x.y.z' \n```\n\n### 最简单的调用代码\n\n```\nfun show() { //\n    val dataList： List\u003cPhoto\u003e = // 将要展示的图片集合列表\n    val clickedData: Photo = // 被点击的其中的那个图片元素信息\n    val builder = ImageViewerBuilder(\n        context = view.context,\n        dataProvider = SimpleDataProvider(clickedData, dataList), // 一次性全量加载 // 实现DataProvider接口支持分页加载\n        imageLoader = SimpleImageLoader(), // 可使用demo固定写法 // 实现对数据源的加载.支持自定义加载数据类型，加载方案\n        transformer = SimpleTransformer(), // 可使用demo固定写法 // 以photoId为标示，设置过渡动画的'配对'.\n    )\n    builder.show()\n}\n```\n\n```\n// 基本是固定写法. Glide 可以换成别的. demo代码中有video的写法.\nclass SimpleImageLoader : ImageLoader {\n    /** 根据自身photo数据加载图片.可以使用其它图片加载框架. */\n    override fun load(view: ImageView, data: Photo, viewHolder: RecyclerView.ViewHolder) {\n        val it = (data as? MyData?)?.url ?: return\n        Glide.with(view).load(it)\n                .placeholder(view.drawable)\n                .into(view)\n    }\n\n    /**\n     * 根据自身photo数据加载超大图.subsamplingView数据源需要先将内容完整下载到本地.\n     */\n    override fun load(subsamplingView: SubsamplingScaleImageView, data: Photo, viewHolder: RecyclerView.ViewHolder) {\n        val it = (data as? MyData?)?.url ?: return\n        subsamplingDownloadRequest(it)\n                .subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .doOnSubscribe { findLoadingView(viewHolder)?.visibility = View.VISIBLE }\n                .doFinally { findLoadingView(viewHolder)?.visibility = View.GONE }\n                .doOnNext { subsamplingView.setImage(ImageSource.uri(Uri.fromFile(it))) }\n                .doOnError { toast(it.message) }\n                .subscribe().bindLifecycle(subsamplingView)\n    }\n\n    private fun subsamplingDownloadRequest(url: String): Observable\u003cFile\u003e {\n        return Observable.create {\n            try {\n                it.onNext(Glide.with(appContext).downloadOnly().load(url).submit().get())\n                it.onComplete()\n            } catch (e: java.lang.Exception) {\n                if (!it.isDisposed) it.onError(e)\n            }\n        }\n    }\n\n    private fun findLoadingView(viewHolder: RecyclerView.ViewHolder): View? {\n        return viewHolder.itemView.findViewById\u003cProgressBar\u003e(R.id.loadingView)\n    }\n\n    ......\n}\n```\n\n```\n// 基本是可以作为固定写法.\nclass SimpleTransformer : Transformer {\n    override fun getView(key: Long): ImageView? = provide(key)\n    \n    companion object {\n        private val transition = HashMap\u003cImageView, Long\u003e()\n        fun put(photoId: Long, imageView: ImageView) {\n            require(isMainThread())\n            if (!imageView.isAttachedToWindow) return\n            imageView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {\n                override fun onViewAttachedToWindow(p0: View?) = Unit\n                override fun onViewDetachedFromWindow(p0: View?) {\n                    transition.remove(imageView)\n                    imageView.removeOnAttachStateChangeListener(this)\n                }\n            })\n            transition[imageView] = photoId\n        }\n\n        private fun provide(photoId: Long): ImageView? {\n            transition.keys.forEach {\n                if (transition[it] == photoId)\n                    return it\n            }\n            return null\n        }\n    }\n} \n```\n\n到此简单的集成已经完毕.\n\n\n## 进阶使用.\n\n（实现以下3个方法.可以追加自定义的展示和功能）\n\n* 自定义'每一页'上的UI.比如可显示图片的更多信息.提供存储分享等更多功能等 `builder.setVHCustomizer(MyCustomViewHolderUI())`\n* 自定义'覆盖(最上)层'上的UI.比如添加指示器等 `builder.setOverlayCustomizer(MyCustomIndicatorUI())`\n* 监听viewer的各种状态变化.包括页面的切换(显示当前在第几页).；过渡动画的执行状态；维护video的播放状态等 `builder.setViewerCallback(MyViewerStateChangedListener())`\n\n```\n// 一般监听翻页onPageSelected可以控制 video播放的状态\n// viewer 各状态监听回调\ninterface ViewerCallback : ImageViewerAdapterListener {\n    // 当点击缩略图变化大图的瞬间\n    override fun onInit(viewHolder: RecyclerView.ViewHolder) {}\n    // 当图片被拖动时\n    override fun onDrag(viewHolder: RecyclerView.ViewHolder, view: View, fraction: Float) {}\n    // 当图片被拖动但不至于退出浏览\n    override fun onRestore(viewHolder: RecyclerView.ViewHolder, view: View, fraction: Float) {}\n    // 当图片被拖动执行退出浏览\n    override fun onRelease(viewHolder: RecyclerView.ViewHolder, view: View) {}\n    // 翻页中状态变化\n    fun onPageScrollStateChanged(state: Int) {}\n    // 翻页中\n    fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}\n    // 当某大图页面被选中\n    fun onPageSelected(position: Int, viewHolder: RecyclerView.ViewHolder) {}\n}\n```\n\n#### 参数配置. 一般不用调整\n\n属性  | 作用说明\n------------- | -------------\nOFFSCREEN_PAGE_LIMIT  | viewer预加载条数\nVIEWER_ORIENTATION  | viewer滑动方向\nVIEWER_BACKGROUND_COLOR  | 大图预览时背景色(默认纯黑)\nDURATION_TRANSITION  | 过渡动画时长\nDURATION_BG  | 过渡动画背景变化时长\nSWIPE_DISMISS  | 是否支持拖拽返回\nSWIPE_TOUCH_SLOP | 拖拽触摸感知阈值\nDISMISS_FRACTION  | 拖拽返回边界阈值\nTRANSITION_OFFSET_Y | 修正透明状态栏下过渡动画的起始位置\n\n### 数据源的定义\n\n```\ninterface Photo {\n    fun id(): Long // 每条图片数据的唯一标示. 主要用于分页数据加载. 定位过渡动画的对应关系\n    fun itemType(): @ItemType.Type Int // 是否启用SubsamplingScaleImageView实现图片区块加载或ExoVideoView实现Video加载\n}\n```\n\n### FAQ\n\n- 如何手动关闭退出整个页面？\n- 如何删除一条数据？\n\n通过 `ViewModelProvider(activity).get(ImageViewerActionViewModel::class.java)`获取`viewer` 对象引用.\n之后可使用 `setCurrentItem(pos: Int)`切换大图位置到指定位置; `dismiss()`退出浏览大图; `remove(item: List\u003cPhoto\u003e)`删除其中的元素\n\n- 如何实现Video的展示？\n  可参考demo实现 demo代码位置 `SimpleViewerCustomizer`\n- 为什么没有过渡动画？\n  需要正确的配置 `Transformer`。需要保证`getView` 返回不为null.\n- 为什么动画的执行和原图有高度偏差？\n  注意状态栏的影响。配置`Config.TRANSITION_OFFSET_Y`\n\n### 其它重要说明\n\ndemo可运行. demo可运行. demo可运行 .demo代码已重构.\n\n都看到这里了，不点下`Star`吗 [旺柴]\n\n### Thanks\n\n如果您觉得我的开源库帮你节省了大量的开发时间，可扫描下方的二维码随意打赏。你的鼓励是我维护项目最大的动力\n","funding_links":[],"categories":["Kotlin"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiielse%2Fimageviewer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fiielse%2Fimageviewer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiielse%2Fimageviewer/lists"}