├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── github │ │ └── xuqk │ │ └── kdimageviewer │ │ └── sample │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── github │ │ │ └── xuqk │ │ │ └── kdimageviewer │ │ │ └── sample │ │ │ ├── DemoGlideModule.kt │ │ │ ├── KDImageViewLoader.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainActivityJava.java │ │ │ └── MyCoverModule.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── example.png │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── app_widget_cover.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── github │ └── xuqk │ └── kdimageviewer │ └── sample │ └── ExampleUnitTest.kt ├── build.gradle ├── demo.gif ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kdimageviewer ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── github │ └── xuqk │ └── kdimageviewer │ ├── KDCoverModule.kt │ ├── KDImageViewer.kt │ ├── KDsl.kt │ ├── PhotoViewContainer.kt │ └── photoview │ ├── Compat.java │ ├── CustomGestureDetector.java │ ├── OnGestureListener.java │ ├── OnMatrixChangedListener.java │ ├── OnOutsidePhotoTapListener.java │ ├── OnPhotoTapListener.java │ ├── OnScaleChangedListener.java │ ├── OnSingleFlingListener.java │ ├── OnViewDragListener.java │ ├── OnViewTapListener.java │ ├── PhotoView.java │ ├── PhotoViewAttacher.java │ └── Util.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | 16 | 17 | 18 | # Created by https://www.gitignore.io/api/androidstudio 19 | 20 | ### AndroidStudio ### 21 | # Covers files to be ignored for android development using Android Studio. 22 | 23 | # Built application files 24 | *.apk 25 | *.ap_ 26 | 27 | # Files for the ART/Dalvik VM 28 | *.dex 29 | 30 | # Java class files 31 | *.class 32 | 33 | # Generated files 34 | bin/ 35 | gen/ 36 | out/ 37 | 38 | # Gradle files 39 | .gradle 40 | .gradle/ 41 | build/ 42 | 43 | # Signing files 44 | .signing/ 45 | 46 | # Local configuration file (sdk path, etc) 47 | local.properties 48 | 49 | # Proguard folder generated by Eclipse 50 | proguard/ 51 | 52 | # Log Files 53 | *.log 54 | 55 | # Android Studio 56 | /*/build/ 57 | /*/local.properties 58 | /*/out 59 | /*/*/build 60 | /*/*/production 61 | captures/ 62 | .navigation/ 63 | *.ipr 64 | *~ 65 | *.swp 66 | 67 | # Android Patch 68 | gen-external-apklibs 69 | 70 | # External native build folder generated in Android Studio 2.2 and later 71 | .externalNativeBuild 72 | 73 | # NDK 74 | obj/ 75 | 76 | # IntelliJ IDEA 77 | *.iml 78 | *.iws 79 | /out/ 80 | 81 | # User-specific configurations 82 | .idea/ 83 | 84 | # OS-specific files 85 | .DS_Store 86 | .DS_Store? 87 | ._* 88 | .Spotlight-V100 89 | .Trashes 90 | ehthumbs.db 91 | Thumbs.db 92 | 93 | # Legacy Eclipse project files 94 | .classpath 95 | .project 96 | .cproject 97 | .settings/ 98 | 99 | # Mobile Tools for Java (J2ME) 100 | .mtj.tmp/ 101 | 102 | # Package Files # 103 | *.war 104 | *.ear 105 | 106 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 107 | hs_err_pid* 108 | 109 | ## Plugin-specific files: 110 | 111 | # mpeltonen/sbt-idea plugin 112 | .idea_modules/ 113 | 114 | # JIRA plugin 115 | atlassian-ide-plugin.xml 116 | 117 | # Mongo Explorer plugin 118 | .idea/mongoSettings.xml 119 | 120 | # Crashlytics plugin (for Android Studio and IntelliJ) 121 | com_crashlytics_export_strings.xml 122 | crashlytics.properties 123 | crashlytics-build.properties 124 | fabric.properties 125 | 126 | ### AndroidStudio Patch ### 127 | 128 | !/gradle/wrapper/gradle-wrapper.jar 129 | 130 | 131 | # End of https://www.gitignore.io/api/androidstudio 132 | 133 | # bugly 134 | /*/bugly 135 | 136 | # gradle profile reports 137 | /reports -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 KongKong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://jitpack.io/v/XuQK/KDImageViewer.svg)](https://jitpack.io/#XuQK/KDImageViewer) 2 | 3 | ![](demo.gif) 4 | 5 | 仿微信的大图浏览器,可对加载中,加载失败,大图蒙层进行自定义。 6 | 7 | 代码基本上都是[XPopup](https://github.com/li-xiaojun/XPopup)中的图片浏览部分进行的抽离与修改,将之抽出来,进行了一点修改,对缩略图和原图的关系做了一些处理。 8 | 9 | 主要代码用Kotlin改写了一下,实现效果基本上与该库一致。 10 | 11 | 大图可以添加到任意ViewGroup,而不仅仅是全屏效果。 12 | 13 | 灵活的Cover模块,可按需自定义大图上覆盖的模块,实现各种按钮功能。 14 | 15 | 进行了DSL改造,可以用DSL方式调用。 16 | 17 | 18 | # 使用方式: 19 | 20 | ## 将JitPack存储库添加到构建文件(项目根目录下build.gradle文件) 21 | 22 | ```groovy 23 | allprojects { 24 | repositories { 25 | ... 26 | maven { url 'https://jitpack.io' } 27 | } 28 | } 29 | ``` 30 | 31 | ## 添加依赖项 32 | 33 | ```groovy 34 | // 版本号参看Release 35 | implementation 'com.github.XuQK:KDImageViewer:versionCode' 36 | 37 | // 项目依赖于以下库,如果没有需要在主工程中添加 38 | implementation 'androidx.appcompat:appcompat:versionCode' 39 | implementation 'androidx.transition:transition:versionCode' 40 | ``` 41 | 42 | ## 使用方式 43 | 44 | ```kotlin 45 | // 初始化 46 | val viewer = kdImageViewer { 47 | context = this@MainActivity 48 | imageLoader = KDImageViewLoader 49 | animDuration = 400 50 | coverModule = MyCoverModule(this@MainActivity) 51 | onShowAnimateStart = { 52 | Log.d("标签", "显示动画开始") 53 | } 54 | onShowAnimateEnd = { 55 | Log.d("标签", "显示动画结束") 56 | } 57 | onDismissAnimateStart = { 58 | Log.d("标签", "消失动画开始") 59 | } 60 | onDismissAnimateEnd = { 61 | Log.d("标签", "消失动画结束") 62 | } 63 | } 64 | 65 | // 想达到完美的大小图切换效果,需要设置 srcImageFetcher 66 | viewer.srcImageViewFetcher = { position -> 67 | // 这里返回 position 位置的缩略图ImageView 68 | } 69 | 70 | // 设置完毕后调用show 71 | viewer.show(viewGroupToCover, originImageUrlList, currentImageViewPosition) 72 | 73 | // 记得必要的时候重写返回键 74 | override fun onBackPressed() { 75 | if (viewer.handleBackPressed()) return 76 | super.onBackPressed() 77 | } 78 | ``` 79 | 80 | 81 | # Release Note 82 | 83 | ### 1.2.0 84 | 85 | DSL化改造,不想使用DSL方式的,请使用1.1.2版本 86 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | apply plugin: 'kotlin-kapt' 7 | 8 | android { 9 | compileSdkVersion 30 10 | 11 | defaultConfig { 12 | applicationId "github.xuqk.kdimageviewer.sample" 13 | minSdkVersion 21 14 | targetSdkVersion 30 15 | versionCode 1 16 | versionName "1.0" 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: 'libs', include: ['*.jar']) 29 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 30 | implementation 'androidx.appcompat:appcompat:1.2.0' 31 | implementation 'androidx.core:core-ktx:1.3.2' 32 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 33 | testImplementation 'junit:junit:4.12' 34 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 35 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 36 | 37 | implementation 'androidx.transition:transition:1.3.1' 38 | 39 | implementation 'com.github.bumptech.glide:glide:4.11.0' 40 | implementation 'com.github.bumptech.glide:okhttp3-integration:4.11.0' 41 | kapt 'com.github.bumptech.glide:compiler:4.11.0' 42 | 43 | implementation project(':kdimageviewer') 44 | } 45 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/github/xuqk/kdimageviewer/sample/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.sample 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("github.xuqk.demo", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/github/xuqk/kdimageviewer/sample/DemoGlideModule.kt: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.sample 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import com.bumptech.glide.GlideBuilder 6 | import com.bumptech.glide.annotation.GlideModule 7 | import com.bumptech.glide.module.AppGlideModule 8 | 9 | /** 10 | * Created By:XuQK 11 | * Created Date:2/2/20 11:33 AM 12 | * Creator Email:xuqiankun66@gmail.com 13 | * Description: 14 | */ 15 | 16 | @GlideModule 17 | class DemoGlideModule : AppGlideModule() { 18 | override fun applyOptions(context: Context, builder: GlideBuilder) { 19 | builder.setLogLevel(Log.WARN) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/github/xuqk/kdimageviewer/sample/KDImageViewLoader.kt: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.sample 2 | 3 | import android.graphics.drawable.Animatable 4 | import android.graphics.drawable.Drawable 5 | import android.widget.ImageView 6 | import com.bumptech.glide.Glide 7 | import com.bumptech.glide.request.target.CustomTarget 8 | import com.bumptech.glide.request.target.Target 9 | import com.bumptech.glide.request.transition.Transition 10 | import github.xuqk.kdimageviewer.ImageLoader 11 | 12 | /** 13 | * Created By:XuQK 14 | * Created Date:1/24/20 7:07 PM 15 | * Creator Email:xuqiankun66@gmail.com 16 | * Description: 17 | */ 18 | 19 | object KDImageViewLoader : ImageLoader { 20 | override fun load(imageView: ImageView, url: String?, loaderListener: ImageLoader.ImageLoaderListener?) { 21 | Glide.with(imageView).load(url) 22 | .override(Target.SIZE_ORIGINAL) 23 | .into(object : CustomTarget() { 24 | override fun onLoadCleared(placeholder: Drawable?) { 25 | 26 | } 27 | 28 | override fun onResourceReady(resource: Drawable, transition: Transition?) { 29 | imageView.post { 30 | imageView.setImageDrawable(resource) 31 | if (resource is Animatable && !resource.isRunning) { 32 | resource.start() 33 | } 34 | 35 | loaderListener?.onLoadSuccess(resource) 36 | } 37 | } 38 | 39 | override fun onLoadFailed(errorDrawable: Drawable?) { 40 | imageView.post { loaderListener?.onLoadFailed(errorDrawable) } 41 | } 42 | 43 | override fun onLoadStarted(placeholder: Drawable?) { 44 | 45 | } 46 | }) 47 | } 48 | 49 | override fun stopLoad(imageView: ImageView) { 50 | Glide.with(imageView).clear(imageView) 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/github/xuqk/kdimageviewer/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.sample 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.view.ViewGroup 7 | import android.widget.TextView 8 | import com.bumptech.glide.Glide 9 | import com.bumptech.glide.request.target.Target 10 | import github.xuqk.kdimageviewer.KDImageViewer 11 | import github.xuqk.kdimageviewer.kdImageViewer 12 | import kotlinx.android.synthetic.main.activity_main.* 13 | 14 | class MainActivity : AppCompatActivity() { 15 | 16 | private val ivHelper: KDImageViewer by lazy { 17 | kdImageViewer { 18 | context = this@MainActivity 19 | imageLoader = KDImageViewLoader 20 | animDuration = 400 21 | coverModule = MyCoverModule(this@MainActivity) 22 | onShowAnimateStart = { 23 | Log.d("标签", "显示动画开始") 24 | } 25 | onShowAnimateEnd = { 26 | Log.d("标签", "显示动画结束") 27 | } 28 | onDismissAnimateStart = { 29 | Log.d("标签", "消失动画开始") 30 | } 31 | onDismissAnimateEnd = { 32 | Log.d("标签", "消失动画结束") 33 | } 34 | } 35 | } 36 | 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | super.onCreate(savedInstanceState) 39 | setContentView(R.layout.activity_main) 40 | 41 | val list = mutableListOf() 42 | list.add("https://xgimg1test.hktanis.com/data/upload/mall/store/goods/63/2019/05/20/63_06116634387516739.jpeg?x-oss-process=image/resize,m_mfit,w_196,h_196/quality,Q_10/format,webp") 43 | list.add("https://xgimg1.hktanis.com/data/upload/image/im/101033103918464/2020/02/01/101156431746240.jpeg?x-oss-process=image/resize,m_mfit,w_393,h_525/format,webp") 44 | list.add("https://xgimg1test.hktanis.com/data/upload/mall/store/goods/64/2020/01/14/64_06323362325244775.jpeg?x-oss-process=image/resize,m_mfit,w_196,h_196/quality,Q_10/format,webp") 45 | list.add("https://xgimg1test.hktanis.com/data/upload/mall/store/goods/338/2020/01/04/338_06314621000006721.jpeg?x-oss-process=image/resize,m_mfit,w_196,h_196/quality,Q_10/format,webp") 46 | list.add("https://xgimg1test.hktanis.com/data/upload/mall/store/goods/338/2020/01/04/338_06314618961051248.jpeg?x-oss-process=image/resize,m_mfit,w_196,h_196/quality,Q_10/format,webp") 47 | list.add("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1551692956639&di=8ee41e070c6a42addfc07522fda3b6c8&imgtype=0&src=http%3A%2F%2Fimg.mp.itc.cn%2Fupload%2F20160413%2F75659e9b05b04eb8adf5b52669394897.jpg") 48 | list.add("https://user-gold-cdn.xitu.io/2019/1/25/168839e977414cc1?imageView2/2/w/800/q/100") 49 | 50 | val originList = mutableListOf() 51 | originList.add("https://xgimg1test.hktanis.com/data/upload/mall/store/goods/63/2019/05/20/63_06116634387516739.jpeg?x-oss-process=image/format,webp") 52 | originList.add("https://xgimg1.hktanis.com/data/upload/image/im/101033103918464/2020/02/01/101156314360704.jpeg?x-oss-process=image/format,webp") 53 | originList.add("https://xgimg1test.hktanis.com/data/upload/mall/store/goods/64/2020/01/14/64_06323362325244775.jpeg?x-oss-process=image/format,webp") 54 | originList.add("https://xgimg1test.hktanis.com/data/upload/mall/store/goods/338/2020/01/04/338_06314621000006721.jpeg?x-oss-process=image/format,webp") 55 | originList.add("https://xgimg1test.hktanis.com/data/upload/mall/store/goods/338/2020/01/04/338_06314618961051248.jpeg?x-oss-process=image/format,webp") 56 | originList.add("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1551692956639&di=8ee41e070c6a42addfc07522fda3b6c8&imgtype=0&src=http%3A%2F%2Fimg.mp.itc.cn%2Fupload%2F20160413%2F75659e9b05b04eb8adf5b52669394897.jpg") 57 | originList.add("https://user-gold-cdn.xitu.io/2019/1/25/168839e977414cc1?imageView2/2/w/800/q/100") 58 | 59 | val views = listOf(iv0, iv1, iv2, iv3, iv4, iv5, iv6) 60 | 61 | ivHelper.srcImageViewFetcher = { 62 | views[it] 63 | } 64 | 65 | ivHelper.onPageSelected = { 66 | ivHelper.coverModule?.getCoverView()?.findViewById(R.id.tv_index) 67 | ?.text = "${it + 1}/${originList.size}" 68 | } 69 | 70 | Glide.with(iv0) 71 | .load(list[0]) 72 | .override(Target.SIZE_ORIGINAL) 73 | .into(iv0) 74 | iv0.setOnClickListener { 75 | ivHelper.show(window.decorView as ViewGroup, originList, 0) 76 | } 77 | 78 | GlideApp.with(iv1) 79 | .load(list[1]) 80 | .override(Target.SIZE_ORIGINAL) 81 | .into(iv1) 82 | iv1.setOnClickListener { 83 | ivHelper.show(window.decorView as ViewGroup, originList, 1) 84 | } 85 | 86 | GlideApp.with(iv2) 87 | .load(list[2]) 88 | .override(Target.SIZE_ORIGINAL) 89 | .into(iv2) 90 | iv2.setOnClickListener { 91 | ivHelper.show(window.decorView as ViewGroup, originList, 2) 92 | } 93 | 94 | GlideApp.with(iv3) 95 | .load(list[3]) 96 | .override(Target.SIZE_ORIGINAL) 97 | .into(iv3) 98 | iv3.setOnClickListener { 99 | ivHelper.show(window.decorView as ViewGroup, originList, 3) 100 | } 101 | 102 | GlideApp.with(iv4) 103 | .load(list[4]) 104 | .override(Target.SIZE_ORIGINAL) 105 | .into(iv4) 106 | iv4.setOnClickListener { 107 | ivHelper.show(window.decorView as ViewGroup, originList, 4) 108 | } 109 | 110 | GlideApp.with(iv5) 111 | .load(list[5]) 112 | .override(Target.SIZE_ORIGINAL) 113 | .into(iv5) 114 | iv5.setOnClickListener { 115 | ivHelper.show(window.decorView as ViewGroup, originList, 5) 116 | } 117 | 118 | GlideApp.with(iv6) 119 | .load(list[6]) 120 | .override(Target.SIZE_ORIGINAL) 121 | .into(iv6) 122 | iv6.setOnClickListener { 123 | ivHelper.show(window.decorView as ViewGroup, originList, 6) 124 | } 125 | 126 | } 127 | 128 | override fun onBackPressed() { 129 | if (ivHelper.handleBackPressed()) return 130 | super.onBackPressed() 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /app/src/main/java/github/xuqk/kdimageviewer/sample/MainActivityJava.java: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.sample; 2 | 3 | import android.os.Bundle; 4 | import android.widget.ImageView; 5 | 6 | import androidx.annotation.Nullable; 7 | import androidx.appcompat.app.AppCompatActivity; 8 | 9 | import github.xuqk.kdimageviewer.KDImageViewer; 10 | import github.xuqk.kdimageviewer.KDslKt; 11 | import kotlin.Unit; 12 | import kotlin.jvm.functions.Function1; 13 | 14 | /** 15 | * Created By:XuQK 16 | * Created Date:2020/7/9 15:13 17 | * Creator Email:xu.qiankun@xiji.com 18 | * Description: 19 | */ 20 | 21 | class MainActivityJava extends AppCompatActivity { 22 | 23 | @Override 24 | protected void onCreate(@Nullable Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | 27 | KDImageViewer kd = KDslKt.kdImageViewer(new Function1() { 28 | @Override 29 | public Unit invoke(KDImageViewer kdImageViewer) { 30 | 31 | return null; 32 | } 33 | }); 34 | 35 | kd.setSrcImageViewFetcher(new Function1() { 36 | @Override 37 | public ImageView invoke(Integer integer) { 38 | return null; 39 | } 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/github/xuqk/kdimageviewer/sample/MyCoverModule.kt: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.sample 2 | 3 | import android.app.Activity 4 | import android.util.TypedValue 5 | import android.view.Gravity 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.widget.TextView 9 | import android.widget.Toast 10 | import github.xuqk.kdimageviewer.KDCoverModule 11 | 12 | /** 13 | * Created By:XuQK 14 | * Created Date:1/31/20 10:04 PM 15 | * Creator Email:xuqiankun66@gmail.com 16 | * Description: 17 | */ 18 | 19 | class MyCoverModule(private val activity: Activity): KDCoverModule(activity) { 20 | 21 | private var coverView: View? = null 22 | 23 | override fun getLoadFailedView(): View { 24 | return TextView(activity).apply { 25 | setTextSize(TypedValue.COMPLEX_UNIT_SP, 24f) 26 | setTextColor(0xffffffff.toInt()) 27 | text = "点击重新加载" 28 | gravity = Gravity.CENTER 29 | } 30 | } 31 | 32 | override fun getCoverView(): View? { 33 | if (coverView == null) { 34 | coverView = LayoutInflater.from(activity).inflate(R.layout.app_widget_cover, null) 35 | coverView!!.findViewById(R.id.tv_download).setOnClickListener { 36 | Toast.makeText(activity, "你就当我下载了吧", Toast.LENGTH_SHORT).show() 37 | } 38 | coverView!!.findViewById(R.id.tv_click).setOnClickListener { 39 | Toast.makeText(activity, "点击", Toast.LENGTH_SHORT).show() 40 | } 41 | } 42 | return coverView!! 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuQK/KDImageViewer/d8d232237f1815d89b3e1f38f25b09b1296cf0e3/app/src/main/res/drawable/example.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 22 | 23 | 29 | 30 | 36 | 37 | 43 | 44 | 50 | 51 | 57 | 58 | 64 | 65 | 66 | 76 | 77 | -------------------------------------------------------------------------------- /app/src/main/res/layout/app_widget_cover.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 15 | 16 | 25 | 26 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuQK/KDImageViewer/d8d232237f1815d89b3e1f38f25b09b1296cf0e3/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuQK/KDImageViewer/d8d232237f1815d89b3e1f38f25b09b1296cf0e3/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuQK/KDImageViewer/d8d232237f1815d89b3e1f38f25b09b1296cf0e3/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuQK/KDImageViewer/d8d232237f1815d89b3e1f38f25b09b1296cf0e3/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuQK/KDImageViewer/d8d232237f1815d89b3e1f38f25b09b1296cf0e3/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuQK/KDImageViewer/d8d232237f1815d89b3e1f38f25b09b1296cf0e3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuQK/KDImageViewer/d8d232237f1815d89b3e1f38f25b09b1296cf0e3/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuQK/KDImageViewer/d8d232237f1815d89b3e1f38f25b09b1296cf0e3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuQK/KDImageViewer/d8d232237f1815d89b3e1f38f25b09b1296cf0e3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuQK/KDImageViewer/d8d232237f1815d89b3e1f38f25b09b1296cf0e3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | KDImageViewer 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/github/xuqk/kdimageviewer/sample/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.sample 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.4.10' 5 | repositories { 6 | google() 7 | jcenter() 8 | 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:4.1.1' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | 16 | // classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | jcenter() 24 | 25 | maven { url 'https://jitpack.io' } 26 | } 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuQK/KDImageViewer/d8d232237f1815d89b3e1f38f25b09b1296cf0e3/demo.gif -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XuQK/KDImageViewer/d8d232237f1815d89b3e1f38f25b09b1296cf0e3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Nov 16 20:16:27 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /kdimageviewer/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | buildToolsVersion "29.0.2" 8 | 9 | 10 | defaultConfig { 11 | minSdkVersion 21 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles 'consumer-rules.pro' 18 | } 19 | } 20 | 21 | dependencies { 22 | compileOnly 'androidx.appcompat:appcompat:1.1.0' 23 | compileOnly 'androidx.transition:transition:1.3.1' 24 | } -------------------------------------------------------------------------------- /kdimageviewer/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/KDCoverModule.kt: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer 2 | 3 | import android.content.Context 4 | import android.content.res.ColorStateList 5 | import android.graphics.Color 6 | import android.util.TypedValue 7 | import android.view.Gravity 8 | import android.view.View 9 | import android.widget.FrameLayout 10 | import android.widget.ProgressBar 11 | 12 | /** 13 | * Created By:XuQK 14 | * Created Date:1/31/20 10:04 PM 15 | * Creator Email:xuqiankun66@gmail.com 16 | * Description: 17 | */ 18 | 19 | open class KDCoverModule(private val context: Context) { 20 | 21 | open fun getLoadingView(): View { 22 | val wrapView = FrameLayout(context) 23 | val progressBarLp = FrameLayout.LayoutParams( 24 | TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 36f, context.resources.displayMetrics).toInt(), 25 | TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 36f, context.resources.displayMetrics).toInt()) 26 | progressBarLp.gravity = Gravity.CENTER 27 | val progressBar = ProgressBar(context) 28 | progressBar.alpha = 0.7f 29 | progressBar.indeterminateTintList = ColorStateList.valueOf(Color.WHITE) 30 | wrapView.addView(progressBar, progressBarLp) 31 | return wrapView 32 | } 33 | 34 | open fun getLoadFailedView(): View { 35 | return View(context) 36 | } 37 | 38 | open fun getCoverView(): View? { 39 | return null 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/KDImageViewer.kt: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer 2 | 3 | import android.animation.ArgbEvaluator 4 | import android.animation.ValueAnimator 5 | import android.content.Context 6 | import android.graphics.Color 7 | import android.graphics.Matrix 8 | import android.graphics.RectF 9 | import android.graphics.drawable.ColorDrawable 10 | import android.graphics.drawable.Drawable 11 | import android.view.View 12 | import android.view.ViewGroup 13 | import android.view.ViewParent 14 | import android.view.animation.LinearInterpolator 15 | import android.widget.FrameLayout 16 | import android.widget.ImageView 17 | import androidx.annotation.ColorInt 18 | import androidx.interpolator.view.animation.FastOutSlowInInterpolator 19 | import androidx.transition.* 20 | import androidx.viewpager.widget.PagerAdapter 21 | import androidx.viewpager.widget.ViewPager 22 | import github.xuqk.kdimageviewer.photoview.PhotoView 23 | 24 | /** 25 | * Created By:XuQK 26 | * Created Date:1/24/20 4:52 PM 27 | * Creator Email:xuqiankun66@gmail.com 28 | * Description: 29 | */ 30 | class KDImageViewer : OnDragChangeListener { 31 | 32 | var context: Context? = null 33 | var imageLoader: ImageLoader? = null 34 | /**动画时长*/ 35 | var animDuration: Long = 300L 36 | /**大图模式的背景色*/ 37 | @ColorInt val defaultBgColor: Int = Color.BLACK 38 | /**蒙层模块*/ 39 | var coverModule: KDCoverModule? = null 40 | 41 | /**进入大图模式动画开始*/ 42 | var onShowAnimateStart: (() -> Unit)? = null 43 | /**进入大图模式动画结束*/ 44 | var onShowAnimateEnd: (() -> Unit)? = null 45 | /**退出大图模式动画开始*/ 46 | var onDismissAnimateStart: (() -> Unit)? = null 47 | /**退出大图模式动画结束*/ 48 | var onDismissAnimateEnd: (() -> Unit)? = null 49 | 50 | /** 51 | * 根据position获取当前大图对应的小图ImageView,如果要实现完美效果,必须实现它 52 | */ 53 | var srcImageViewFetcher: (position: Int) -> ImageView? = { null } 54 | 55 | var onPageScrolled: ((position: Int, positionOffset: Float, positionOffsetPixels: Int) -> Unit)? = null 56 | var onPageSelected: ((position: Int) -> Unit)? = null 57 | var onPageScrollStateChanged: ((state: Int) -> Unit)? = null 58 | 59 | private lateinit var containerView: ViewGroup 60 | private lateinit var photoViewContainer: PhotoViewContainer 61 | private lateinit var pager: ViewPager 62 | private lateinit var snapshotView: PhotoView 63 | 64 | private val originUrlList = mutableListOf() 65 | 66 | private var argbEvaluator: ArgbEvaluator = ArgbEvaluator() 67 | 68 | val currentPosition: Int 69 | get() = pager.currentItem 70 | 71 | /**当前展示状态*/ 72 | var showing: Boolean = false 73 | private set 74 | 75 | fun init() { 76 | require(context != null) { 77 | "context can't be null" 78 | } 79 | require(imageLoader != null) { 80 | "imageLoader can't be null" 81 | } 82 | 83 | containerView = FrameLayout(context!!) 84 | photoViewContainer = PhotoViewContainer(context!!) 85 | pager = ViewPager(context!!) 86 | pager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { 87 | override fun onPageScrollStateChanged(state: Int) { 88 | onPageScrollStateChanged?.invoke(state) 89 | } 90 | 91 | override fun onPageScrolled( 92 | position: Int, 93 | positionOffset: Float, 94 | positionOffsetPixels: Int 95 | ) { 96 | onPageScrolled?.invoke(position, positionOffset, positionOffsetPixels) 97 | } 98 | 99 | override fun onPageSelected(position: Int) { 100 | onPageSelected?.invoke(position) 101 | } 102 | }) 103 | 104 | snapshotView = PhotoView(context!!) 105 | 106 | photoViewContainer.run { 107 | addView(pager, generateDefaultLayoutParams()) 108 | addView(snapshotView, generateDefaultLayoutParams()) 109 | viewPager = pager 110 | dragChangeListener = this@KDImageViewer 111 | } 112 | 113 | // 这里加个边距,在华为rom上,如果不加这个边距,ViewPager里的内容在scale=1的时候有可能不显示,原理不明。。 114 | containerView.addView(photoViewContainer, generateDefaultLayoutParams().apply { setMargins(1, 1, 1, 1) }) 115 | containerView.translationZ = 100f 116 | coverModule?.getCoverView()?.let { 117 | containerView.addView(it, generateDefaultLayoutParams()) 118 | } 119 | } 120 | 121 | fun show(attachedView: ViewGroup, originUrlList: List, position: Int) { 122 | if (photoViewContainer.isAnimating) return 123 | showing = true 124 | 125 | this.originUrlList.clear() 126 | this.originUrlList.addAll(originUrlList) 127 | pager.adapter = ImageViewerAdapter() 128 | 129 | pager.setCurrentItem(position, false) 130 | 131 | photoViewContainer.isAnimating = true 132 | 133 | // 将snapshotView设置成列表中的srcView的样子 134 | val srcView = srcImageViewFetcher.invoke(position) 135 | updateSrcViewParams(attachedView, srcView) 136 | 137 | containerView.setBackgroundColor(Color.TRANSPARENT) 138 | 139 | (containerView.parent as? ViewGroup)?.removeView(containerView) 140 | attachedView.addView(containerView, generateDefaultLayoutParams()) 141 | pager.visibility = View.INVISIBLE 142 | 143 | snapshotView.run { 144 | visibility = View.VISIBLE 145 | translationX = rect.left 146 | translationY = rect.top 147 | scaleX = 1f 148 | scaleY = 1f 149 | scaleType = if (srcView != null) srcView.scaleType else ImageView.ScaleType.CENTER_CROP 150 | 151 | layoutParams = layoutParams.apply { 152 | width = rect.width().toInt() 153 | height = rect.height().toInt() 154 | } 155 | 156 | setImageDrawable(srcView?.drawable?.constantState?.newDrawable()) 157 | } 158 | 159 | snapshotView.post { 160 | onShowAnimateStart?.invoke() 161 | TransitionManager.beginDelayedTransition( 162 | snapshotView.parent as ViewGroup, 163 | TransitionSet() 164 | .setDuration(animDuration) 165 | .addTransition(ChangeBounds()) 166 | .addTransition(ChangeTransform()) 167 | .addTransition(ChangeImageTransform()) 168 | .setInterpolator(FastOutSlowInInterpolator()) 169 | .addListener(object : TransitionListenerAdapter() { 170 | override fun onTransitionEnd(transition: Transition) { 171 | photoViewContainer.isAnimating = false 172 | 173 | pager.visibility = View.VISIBLE 174 | snapshotView.visibility = View.INVISIBLE 175 | pager.scaleX = 1f 176 | pager.scaleY = 1f 177 | 178 | onShowAnimateEnd?.invoke() 179 | } 180 | }) 181 | ) 182 | 183 | snapshotView.run { 184 | translationY = 0f 185 | translationX = 0f 186 | scaleX = 1f 187 | scaleY = 1f 188 | scaleType = ImageView.ScaleType.FIT_CENTER 189 | 190 | layoutParams = layoutParams.apply { 191 | width = containerView.width 192 | height = containerView.height 193 | } 194 | } 195 | 196 | animateShadowBg(defaultBgColor) 197 | coverModule?.getCoverView()?.animate()?.alpha(1f)?.setDuration(animDuration)?.start() 198 | } 199 | } 200 | 201 | private fun dismiss() { 202 | if (photoViewContainer.isAnimating) return 203 | photoViewContainer.isAnimating = true 204 | 205 | val srcView = srcImageViewFetcher.invoke(currentPosition) 206 | updateSrcViewParams((containerView.parent as ViewGroup), srcView) 207 | 208 | // 将snapshotView设置成当前pager中photoView的样子(matrix) 209 | (pager.adapter as ImageViewerAdapter).currentPhotoView?.let { 210 | snapshotView.run { 211 | setImageDrawable(it.drawable) 212 | 213 | val matrix = Matrix() 214 | it.getSuppMatrix(matrix) 215 | setSuppMatrix(matrix) 216 | } 217 | } 218 | 219 | // 替换成snapshotView来进行动画 220 | pager.visibility = View.INVISIBLE 221 | snapshotView.visibility = View.VISIBLE 222 | 223 | onDismissAnimateStart?.invoke() 224 | TransitionManager.beginDelayedTransition( 225 | snapshotView.parent as ViewGroup, 226 | TransitionSet() 227 | .setDuration(animDuration) 228 | .addTransition(ChangeBounds()) 229 | .addTransition(ChangeTransform()) 230 | .addTransition(ChangeImageTransform()) 231 | .setInterpolator(FastOutSlowInInterpolator()) 232 | .addListener(object : TransitionListenerAdapter() { 233 | override fun onTransitionEnd(transition: Transition) { 234 | photoViewContainer.isAnimating = false 235 | (containerView.parent as? ViewGroup)?.removeView(containerView) 236 | pager.visibility = View.INVISIBLE 237 | (pager.adapter as? ImageViewerAdapter)?.currentPhotoView?.let { 238 | imageLoader!!.stopLoad(it) 239 | } 240 | snapshotView.visibility = View.VISIBLE 241 | pager.scaleX = 1f 242 | pager.scaleY = 1f 243 | snapshotView.scaleX = 1f 244 | snapshotView.scaleY = 1f 245 | 246 | onDismissAnimateEnd?.invoke() 247 | reset() 248 | showing = false 249 | } 250 | }) 251 | ) 252 | 253 | snapshotView.run { 254 | translationY = rect.top 255 | translationX = rect.left 256 | scaleX = 1f 257 | scaleY = 1f 258 | scaleType = if (srcView != null) srcView.scaleType else ImageView.ScaleType.CENTER_CROP 259 | layoutParams = layoutParams.apply { 260 | width = rect.width().toInt() 261 | height = rect.height().toInt() 262 | } 263 | } 264 | 265 | animateShadowBg(Color.TRANSPARENT) 266 | coverModule?.getCoverView()?.animate()?.alpha(0f)?.setDuration(animDuration)?.start() 267 | } 268 | 269 | fun handleBackPressed(): Boolean { 270 | if (photoViewContainer.isAnimating) return true 271 | 272 | if (showing) { 273 | dismiss() 274 | return true 275 | } 276 | return false 277 | } 278 | 279 | private fun animateShadowBg(endColor: Int) { 280 | val startColor = (containerView.background as ColorDrawable).color 281 | ValueAnimator.ofFloat(0f, 1f).apply { 282 | addUpdateListener { 283 | containerView.setBackgroundColor( 284 | argbEvaluator.evaluate(it.animatedFraction, startColor, endColor) as Int 285 | ) 286 | } 287 | duration = animDuration 288 | interpolator = LinearInterpolator() 289 | }.start() 290 | } 291 | 292 | private val rect = RectF() 293 | private val currentOriginViewLocation = intArrayOf(0, 0) 294 | /** 295 | * 更新srcView参数 296 | */ 297 | private fun updateSrcViewParams(attachedView: ViewGroup, srcView: ImageView?) { 298 | if (srcView == null) { 299 | val x = (containerView.width / 2 + containerView.left).toFloat() 300 | val y = (containerView.height / 2 + containerView.top).toFloat() 301 | rect.set(x, y, x, y) 302 | } else { 303 | attachedView.getLocationOnScreen(currentOriginViewLocation) 304 | val containerX = currentOriginViewLocation[0] 305 | val containerY = currentOriginViewLocation[1] 306 | 307 | srcView.getLocationOnScreen(currentOriginViewLocation) 308 | val x = currentOriginViewLocation[0] - containerX.toFloat() 309 | val y = currentOriginViewLocation[1] - containerY.toFloat() 310 | rect.set( 311 | x, 312 | y, 313 | x + srcView.width, 314 | y + srcView.height 315 | ) 316 | } 317 | } 318 | 319 | override fun onRelease() { 320 | dismiss() 321 | } 322 | 323 | override fun onDragChange(dy: Int, scale: Float, fraction: Float) { 324 | coverModule?.getCoverView()?.let { 325 | it.alpha = 1 - fraction 326 | } 327 | 328 | containerView.setBackgroundColor( 329 | argbEvaluator.evaluate( 330 | fraction * 0.8f, 331 | Color.BLACK, 332 | Color.TRANSPARENT 333 | ) as Int 334 | ) 335 | } 336 | 337 | private fun reset() { 338 | snapshotView.setImageDrawable(null) 339 | } 340 | 341 | private fun generateDefaultLayoutParams() = ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) 342 | 343 | inner class ImageViewerAdapter : PagerAdapter() { 344 | private val onClickListener = View.OnClickListener { dismiss() } 345 | var currentPhotoView: PhotoView? = null 346 | private set 347 | 348 | override fun setPrimaryItem(container: ViewGroup, position: Int, `object`: Any) { 349 | super.setPrimaryItem(container, position, `object`) 350 | currentPhotoView = (`object` as ViewGroup).getChildAt(0) as PhotoView 351 | } 352 | 353 | override fun instantiateItem(container: ViewGroup, position: Int): Any { 354 | val view = FrameLayout(container.context) 355 | container.addView(view, generateDefaultLayoutParams()) 356 | 357 | val photoView = PhotoView(container.context) 358 | val loadingView = coverModule?.getLoadingView() 359 | val loadFailedView = coverModule?.getLoadFailedView() 360 | 361 | view.addView(photoView, generateDefaultLayoutParams()) 362 | view.addView(loadingView, generateDefaultLayoutParams()) 363 | view.addView(loadFailedView, generateDefaultLayoutParams()) 364 | 365 | photoView.setOnClickListener(onClickListener) 366 | 367 | loadFailedView?.setOnClickListener { 368 | loadOriginImage(photoView, loadFailedView, loadingView, position) 369 | } 370 | 371 | // 再加载原图 372 | loadOriginImage(photoView, loadFailedView, loadingView, position) 373 | 374 | return view 375 | } 376 | 377 | override fun isViewFromObject(view: View, `object`: Any): Boolean { 378 | return view == `object` 379 | } 380 | 381 | override fun getCount(): Int { 382 | return originUrlList.size 383 | } 384 | 385 | override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { 386 | container.removeView(`object` as View) 387 | } 388 | 389 | private fun loadOriginImage(photoView: PhotoView, loadFailedView: View?, loadingView: View?, position: Int) { 390 | if (!showing) return 391 | 392 | loadFailedView?.visibility = View.GONE 393 | loadingView?.visibility = View.VISIBLE 394 | 395 | imageLoader!!.load(photoView, originUrlList[position], object : 396 | ImageLoader.ImageLoaderListener { 397 | override fun onLoadFailed(errorDrawable: Drawable?) { 398 | loadingView?.visibility = View.INVISIBLE 399 | loadFailedView?.visibility = View.VISIBLE 400 | } 401 | 402 | override fun onLoadSuccess(drawable: Drawable?) { 403 | loadingView?.visibility = View.INVISIBLE 404 | loadFailedView?.visibility = View.INVISIBLE 405 | } 406 | }) 407 | } 408 | } 409 | } 410 | 411 | interface OnDragChangeListener { 412 | fun onRelease() 413 | fun onDragChange(dy: Int, scale: Float, fraction: Float) 414 | } 415 | 416 | interface ImageLoader { 417 | fun load(imageView: ImageView, url: String?, loaderListener: ImageLoaderListener?) 418 | 419 | fun stopLoad(imageView: ImageView) 420 | 421 | interface ImageLoaderListener { 422 | fun onLoadFailed(errorDrawable: Drawable?) 423 | 424 | fun onLoadSuccess(drawable: Drawable?) 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/KDsl.kt: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer 2 | 3 | /** 4 | * Created By:XuQK 5 | * Created Date:2020/7/9 14:40 6 | * Creator Email:xu.qiankun@xiji.com 7 | * Description: 8 | */ 9 | 10 | fun kdImageViewer(block: KDImageViewer.() -> Unit) : KDImageViewer { 11 | val v = KDImageViewer() 12 | v.block() 13 | v.init() 14 | return v 15 | } -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/PhotoViewContainer.kt: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.util.AttributeSet 7 | import android.util.TypedValue 8 | import android.view.MotionEvent 9 | import android.view.View 10 | import android.widget.FrameLayout 11 | import androidx.core.view.ViewCompat 12 | import androidx.customview.widget.ViewDragHelper 13 | import androidx.viewpager.widget.ViewPager 14 | import github.xuqk.kdimageviewer.photoview.PhotoView 15 | import kotlin.math.abs 16 | import kotlin.math.min 17 | 18 | /** 19 | * wrap ViewPager, process drag event. 20 | */ 21 | class PhotoViewContainer @JvmOverloads constructor( 22 | context: Context, 23 | attrs: AttributeSet? = null, 24 | defStyleAttr: Int = 0 25 | ) : FrameLayout(context, attrs, defStyleAttr) { 26 | 27 | private val cb: ViewDragHelper.Callback = object : ViewDragHelper.Callback() { 28 | override fun tryCaptureView(view: View, i: Int): Boolean { 29 | return !isAnimating 30 | } 31 | 32 | override fun getViewVerticalDragRange(child: View): Int { 33 | return 1 34 | } 35 | 36 | override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int { 37 | val t = viewPager!!.top + dy / 2 38 | return if (t >= 0) { 39 | min(t, maxOffset) 40 | } else { 41 | -min(-t, maxOffset) 42 | } 43 | } 44 | 45 | override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) { 46 | super.onViewPositionChanged(changedView, left, top, dx, dy) 47 | if (changedView != viewPager) { 48 | viewPager!!.offsetTopAndBottom(dy) 49 | } 50 | val fraction = abs(top) * 1f / maxOffset 51 | val pageScale = 1 - fraction * .2f 52 | viewPager!!.scaleX = pageScale 53 | viewPager!!.scaleY = pageScale 54 | changedView.scaleX = pageScale 55 | changedView.scaleY = pageScale 56 | dragChangeListener?.onDragChange(dy, pageScale, fraction) 57 | } 58 | 59 | override fun onViewReleased( 60 | releasedChild: View, 61 | xvel: Float, 62 | yvel: Float 63 | ) { 64 | super.onViewReleased(releasedChild, xvel, yvel) 65 | if (abs(releasedChild.top) > hideTopThreshold) { 66 | dragChangeListener?.onRelease() 67 | } else { 68 | dragHelper.smoothSlideViewTo(viewPager!!, 0, 0) 69 | dragHelper.smoothSlideViewTo(releasedChild, 0, 0) 70 | ViewCompat.postInvalidateOnAnimation(this@PhotoViewContainer) 71 | } 72 | } 73 | } 74 | 75 | private val dragHelper: ViewDragHelper = ViewDragHelper.create(this, cb) 76 | var viewPager: ViewPager? = null 77 | private val hideTopThreshold = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 80f, resources.displayMetrics) 78 | private var maxOffset = 0 79 | var dragChangeListener: OnDragChangeListener? = null 80 | /**该参数表示是否正在被拖拽或是否正处于拖拽返回的动画之中*/ 81 | var isAnimating = false 82 | private var isVertical = false 83 | private var touchX = 0f 84 | private var touchY = 0f 85 | 86 | init { 87 | setBackgroundColor(Color.TRANSPARENT) 88 | } 89 | 90 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 91 | super.onSizeChanged(w, h, oldw, oldh) 92 | maxOffset = height / 3 93 | } 94 | 95 | override fun dispatchTouchEvent(ev: MotionEvent): Boolean { 96 | when (ev.action) { 97 | MotionEvent.ACTION_DOWN -> { 98 | touchX = ev.x 99 | touchY = ev.y 100 | } 101 | MotionEvent.ACTION_MOVE -> { 102 | val dx = ev.x - touchX 103 | val dy = ev.y - touchY 104 | viewPager!!.dispatchTouchEvent(ev) 105 | isVertical = abs(dy) > abs(dx) 106 | touchX = ev.x 107 | touchY = ev.y 108 | } 109 | MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { 110 | touchX = 0f 111 | touchY = 0f 112 | isVertical = false 113 | } 114 | } 115 | return super.dispatchTouchEvent(ev) 116 | } 117 | 118 | private fun isTopOrBottomEnd(): Boolean { 119 | val photoView: PhotoView? = getCurrentPhotoView() 120 | return photoView != null && (photoView.attacher.isTopEnd || photoView.attacher.isBottomEnd) 121 | } 122 | 123 | private fun getCurrentPhotoView(): PhotoView? { 124 | return (viewPager?.adapter as? KDImageViewer.ImageViewerAdapter)?.currentPhotoView 125 | } 126 | 127 | override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { 128 | val result = dragHelper.shouldInterceptTouchEvent(ev) 129 | if (ev.pointerCount > 1 && ev.action == MotionEvent.ACTION_MOVE) return false 130 | return if (isTopOrBottomEnd() && isVertical) true else result && isVertical 131 | } 132 | 133 | @SuppressLint("ClickableViewAccessibility") 134 | override fun onTouchEvent(ev: MotionEvent): Boolean { 135 | if (ev.pointerCount > 1) return false 136 | try { 137 | dragHelper.processTouchEvent(ev) 138 | return true 139 | } catch (e: Exception) { 140 | e.printStackTrace() 141 | } 142 | return true 143 | } 144 | 145 | override fun computeScroll() { 146 | super.computeScroll() 147 | if (dragHelper.continueSettling(false)) { 148 | ViewCompat.postInvalidateOnAnimation(this@PhotoViewContainer) 149 | } 150 | } 151 | 152 | override fun onDetachedFromWindow() { 153 | super.onDetachedFromWindow() 154 | isAnimating = false 155 | } 156 | } -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/Compat.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package github.xuqk.kdimageviewer.photoview; 17 | 18 | import android.annotation.TargetApi; 19 | import android.os.Build.VERSION; 20 | import android.os.Build.VERSION_CODES; 21 | import android.view.View; 22 | 23 | class Compat { 24 | 25 | private static final int SIXTY_FPS_INTERVAL = 1000 / 60; 26 | 27 | public static void postOnAnimation(View view, Runnable runnable) { 28 | if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { 29 | postOnAnimationJellyBean(view, runnable); 30 | } else { 31 | view.postDelayed(runnable, SIXTY_FPS_INTERVAL); 32 | } 33 | } 34 | 35 | @TargetApi(16) 36 | private static void postOnAnimationJellyBean(View view, Runnable runnable) { 37 | view.postOnAnimation(runnable); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/CustomGestureDetector.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 |

4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 |

8 | http://www.apache.org/licenses/LICENSE-2.0 9 |

10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package github.xuqk.kdimageviewer.photoview; 17 | 18 | import android.content.Context; 19 | import android.view.MotionEvent; 20 | import android.view.ScaleGestureDetector; 21 | import android.view.VelocityTracker; 22 | import android.view.ViewConfiguration; 23 | 24 | /** 25 | * Does a whole lot of gesture detecting. 26 | */ 27 | class CustomGestureDetector { 28 | 29 | private static final int INVALID_POINTER_ID = -1; 30 | 31 | private int mActivePointerId = INVALID_POINTER_ID; 32 | private int mActivePointerIndex = 0; 33 | private final ScaleGestureDetector mDetector; 34 | 35 | private VelocityTracker mVelocityTracker; 36 | private boolean mIsDragging; 37 | private float mLastTouchX; 38 | private float mLastTouchY; 39 | private final float mTouchSlop; 40 | private final float mMinimumVelocity; 41 | private OnGestureListener mListener; 42 | 43 | CustomGestureDetector(Context context, OnGestureListener listener) { 44 | final ViewConfiguration configuration = ViewConfiguration 45 | .get(context); 46 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 47 | mTouchSlop = configuration.getScaledTouchSlop(); 48 | 49 | mListener = listener; 50 | ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() { 51 | 52 | @Override 53 | public boolean onScale(ScaleGestureDetector detector) { 54 | float scaleFactor = detector.getScaleFactor(); 55 | if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) 56 | return false; 57 | if (scaleFactor >= 0) { 58 | mListener.onScale(scaleFactor, 59 | detector.getFocusX(), detector.getFocusY()); 60 | } 61 | return true; 62 | } 63 | 64 | @Override 65 | public boolean onScaleBegin(ScaleGestureDetector detector) { 66 | return true; 67 | } 68 | 69 | @Override 70 | public void onScaleEnd(ScaleGestureDetector detector) { 71 | // NO-OP 72 | } 73 | }; 74 | mDetector = new ScaleGestureDetector(context, mScaleListener); 75 | } 76 | 77 | private float getActiveX(MotionEvent ev) { 78 | try { 79 | return ev.getX(mActivePointerIndex); 80 | } catch (Exception e) { 81 | return ev.getX(); 82 | } 83 | } 84 | 85 | private float getActiveY(MotionEvent ev) { 86 | try { 87 | return ev.getY(mActivePointerIndex); 88 | } catch (Exception e) { 89 | return ev.getY(); 90 | } 91 | } 92 | 93 | public boolean isScaling() { 94 | return mDetector.isInProgress(); 95 | } 96 | 97 | public boolean isDragging() { 98 | return mIsDragging; 99 | } 100 | 101 | public boolean onTouchEvent(MotionEvent ev) { 102 | try { 103 | if(ev.getPointerCount()>1) 104 | mDetector.onTouchEvent(ev); 105 | return processTouchEvent(ev); 106 | } catch (IllegalArgumentException e) { 107 | // Fix for support lib bug, happening when onDestroy is called 108 | return true; 109 | } 110 | } 111 | 112 | private boolean processTouchEvent(MotionEvent ev) { 113 | final int action = ev.getAction(); 114 | switch (action & MotionEvent.ACTION_MASK) { 115 | case MotionEvent.ACTION_DOWN: 116 | mActivePointerId = ev.getPointerId(0); 117 | 118 | mVelocityTracker = VelocityTracker.obtain(); 119 | if (null != mVelocityTracker) { 120 | mVelocityTracker.addMovement(ev); 121 | } 122 | 123 | mLastTouchX = getActiveX(ev); 124 | mLastTouchY = getActiveY(ev); 125 | mIsDragging = false; 126 | break; 127 | case MotionEvent.ACTION_MOVE: 128 | final float x = getActiveX(ev); 129 | final float y = getActiveY(ev); 130 | final float dx = x - mLastTouchX, dy = y - mLastTouchY; 131 | 132 | if (!mIsDragging) { 133 | // Use Pythagoras to see if drag length is larger than 134 | // touch slop 135 | mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop; 136 | } 137 | 138 | if (mIsDragging) { 139 | mListener.onDrag(dx, dy); 140 | mLastTouchX = x; 141 | mLastTouchY = y; 142 | 143 | if (null != mVelocityTracker) { 144 | mVelocityTracker.addMovement(ev); 145 | } 146 | } 147 | break; 148 | case MotionEvent.ACTION_CANCEL: 149 | mActivePointerId = INVALID_POINTER_ID; 150 | // Recycle Velocity Tracker 151 | if (null != mVelocityTracker) { 152 | mVelocityTracker.recycle(); 153 | mVelocityTracker = null; 154 | } 155 | break; 156 | case MotionEvent.ACTION_UP: 157 | mActivePointerId = INVALID_POINTER_ID; 158 | if (mIsDragging) { 159 | if (null != mVelocityTracker) { 160 | mLastTouchX = getActiveX(ev); 161 | mLastTouchY = getActiveY(ev); 162 | 163 | // Compute velocity within the last 1000ms 164 | mVelocityTracker.addMovement(ev); 165 | mVelocityTracker.computeCurrentVelocity(1000); 166 | 167 | final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker 168 | .getYVelocity(); 169 | 170 | // If the velocity is greater than minVelocity, call 171 | // listener 172 | if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { 173 | mListener.onFling(mLastTouchX, mLastTouchY, -vX, 174 | -vY); 175 | } 176 | } 177 | } 178 | 179 | // Recycle Velocity Tracker 180 | if (null != mVelocityTracker) { 181 | mVelocityTracker.recycle(); 182 | mVelocityTracker = null; 183 | } 184 | break; 185 | case MotionEvent.ACTION_POINTER_UP: 186 | final int pointerIndex = Util.getPointerIndex(ev.getAction()); 187 | final int pointerId = ev.getPointerId(pointerIndex); 188 | if (pointerId == mActivePointerId) { 189 | // This was our active pointer going up. Choose a new 190 | // active pointer and adjust accordingly. 191 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 192 | mActivePointerId = ev.getPointerId(newPointerIndex); 193 | mLastTouchX = ev.getX(newPointerIndex); 194 | mLastTouchY = ev.getY(newPointerIndex); 195 | } 196 | break; 197 | } 198 | 199 | mActivePointerIndex = ev 200 | .findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId 201 | : 0); 202 | return true; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/OnGestureListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package github.xuqk.kdimageviewer.photoview; 17 | 18 | interface OnGestureListener { 19 | 20 | void onDrag(float dx, float dy); 21 | 22 | void onFling(float startX, float startY, float velocityX, 23 | float velocityY); 24 | 25 | void onScale(float scaleFactor, float focusX, float focusY); 26 | 27 | } -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/OnMatrixChangedListener.java: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.photoview; 2 | 3 | import android.graphics.RectF; 4 | 5 | /** 6 | * Interface definition for a callback to be invoked when the internal Matrix has changed for 7 | * this View. 8 | */ 9 | public interface OnMatrixChangedListener { 10 | 11 | /** 12 | * Callback for when the Matrix displaying the Drawable has changed. This could be because 13 | * the View's bounds have changed, or the user has zoomed. 14 | * 15 | * @param rect - Rectangle displaying the Drawable's new bounds. 16 | */ 17 | void onMatrixChanged(RectF rect); 18 | } 19 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/OnOutsidePhotoTapListener.java: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.photoview; 2 | 3 | import android.widget.ImageView; 4 | 5 | /** 6 | * Callback when the user tapped outside of the photo 7 | */ 8 | public interface OnOutsidePhotoTapListener { 9 | 10 | /** 11 | * The outside of the photo has been tapped 12 | */ 13 | void onOutsidePhotoTap(ImageView imageView); 14 | } 15 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/OnPhotoTapListener.java: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.photoview; 2 | 3 | import android.widget.ImageView; 4 | 5 | /** 6 | * A callback to be invoked when the Photo is tapped with a single 7 | * tap. 8 | */ 9 | public interface OnPhotoTapListener { 10 | 11 | /** 12 | * A callback to receive where the user taps on a photo. You will only receive a callback if 13 | * the user taps on the actual photo, tapping on 'whitespace' will be ignored. 14 | * 15 | * @param view ImageView the user tapped. 16 | * @param x where the user tapped from the of the Drawable, as percentage of the 17 | * Drawable width. 18 | * @param y where the user tapped from the top of the Drawable, as percentage of the 19 | * Drawable height. 20 | */ 21 | void onPhotoTap(ImageView view, float x, float y); 22 | } 23 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/OnScaleChangedListener.java: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.photoview; 2 | 3 | 4 | /** 5 | * Interface definition for callback to be invoked when attached ImageView scale changes 6 | */ 7 | public interface OnScaleChangedListener { 8 | 9 | /** 10 | * Callback for when the scale changes 11 | * 12 | * @param scaleFactor the scale factor (less than 1 for zoom out, greater than 1 for zoom in) 13 | * @param focusX focal point X position 14 | * @param focusY focal point Y position 15 | */ 16 | void onScaleChange(float scaleFactor, float focusX, float focusY); 17 | } 18 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/OnSingleFlingListener.java: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.photoview; 2 | 3 | import android.view.MotionEvent; 4 | 5 | /** 6 | * A callback to be invoked when the ImageView is flung with a single 7 | * touch 8 | */ 9 | public interface OnSingleFlingListener { 10 | 11 | /** 12 | * A callback to receive where the user flings on a ImageView. You will receive a callback if 13 | * the user flings anywhere on the view. 14 | * 15 | * @param e1 MotionEvent the user first touch. 16 | * @param e2 MotionEvent the user last touch. 17 | * @param velocityX distance of user's horizontal fling. 18 | * @param velocityY distance of user's vertical fling. 19 | */ 20 | boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); 21 | } 22 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/OnViewDragListener.java: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.photoview; 2 | 3 | /** 4 | * Interface definition for a callback to be invoked when the photo is experiencing a drag event 5 | */ 6 | public interface OnViewDragListener { 7 | 8 | /** 9 | * Callback for when the photo is experiencing a drag event. This cannot be invoked when the 10 | * user is scaling. 11 | * 12 | * @param dx The change of the coordinates in the x-direction 13 | * @param dy The change of the coordinates in the y-direction 14 | */ 15 | void onDrag(float dx, float dy); 16 | } 17 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/OnViewTapListener.java: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.photoview; 2 | 3 | import android.view.View; 4 | 5 | public interface OnViewTapListener { 6 | 7 | /** 8 | * A callback to receive where the user taps on a ImageView. You will receive a callback if 9 | * the user taps anywhere on the view, tapping on 'whitespace' will not be ignored. 10 | * 11 | * @param view - View the user tapped. 12 | * @param x - where the user tapped from the left of the View. 13 | * @param y - where the user tapped from the top of the View. 14 | */ 15 | void onViewTap(View view, float x, float y); 16 | } 17 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/PhotoView.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 |

4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 |

8 | http://www.apache.org/licenses/LICENSE-2.0 9 |

10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package github.xuqk.kdimageviewer.photoview; 17 | 18 | import android.content.Context; 19 | import android.graphics.Matrix; 20 | import android.graphics.RectF; 21 | import android.graphics.drawable.Drawable; 22 | import android.net.Uri; 23 | import android.util.AttributeSet; 24 | import android.view.GestureDetector; 25 | 26 | import androidx.appcompat.widget.AppCompatImageView; 27 | 28 | ; 29 | 30 | /** 31 | * A modified version for https://github.com/chrisbanes/PhotoView. 32 | */ 33 | @SuppressWarnings("unused") 34 | public class PhotoView extends AppCompatImageView { 35 | 36 | public PhotoViewAttacher attacher; 37 | private ScaleType pendingScaleType; 38 | 39 | public PhotoView(Context context) { 40 | this(context, null); 41 | } 42 | 43 | public PhotoView(Context context, AttributeSet attr) { 44 | this(context, attr, 0); 45 | } 46 | 47 | public PhotoView(Context context, AttributeSet attr, int defStyle) { 48 | super(context, attr, defStyle); 49 | init(); 50 | } 51 | 52 | private void init() { 53 | attacher = new PhotoViewAttacher(this); 54 | //We always pose as a Matrix scale type, though we can change to another scale type 55 | //via the attacher 56 | super.setScaleType(ScaleType.MATRIX); 57 | //apply the previously applied scale type 58 | if (pendingScaleType != null) { 59 | setScaleType(pendingScaleType); 60 | pendingScaleType = null; 61 | } 62 | } 63 | 64 | /** 65 | * Get the current {@link PhotoViewAttacher} for this view. Be wary of holding on to references 66 | * to this attacher, as it has a reference to this view, which, if a reference is held in the 67 | * wrong place, can cause memory leaks. 68 | * 69 | * @return the attacher. 70 | */ 71 | public PhotoViewAttacher getAttacher() { 72 | return attacher; 73 | } 74 | 75 | @Override 76 | public ScaleType getScaleType() { 77 | return attacher.getScaleType(); 78 | } 79 | 80 | @Override 81 | public Matrix getImageMatrix() { 82 | return attacher.getImageMatrix(); 83 | } 84 | 85 | @Override 86 | public void setOnLongClickListener(OnLongClickListener l) { 87 | attacher.setOnLongClickListener(l); 88 | } 89 | 90 | @Override 91 | public void setOnClickListener(OnClickListener l) { 92 | attacher.setOnClickListener(l); 93 | } 94 | 95 | @Override 96 | public void setScaleType(ScaleType scaleType) { 97 | if (attacher == null) { 98 | pendingScaleType = scaleType; 99 | } else { 100 | attacher.setScaleType(scaleType); 101 | } 102 | } 103 | 104 | @Override 105 | public void setImageDrawable(Drawable drawable) { 106 | super.setImageDrawable(drawable); 107 | // setImageBitmap calls through to this method 108 | if (attacher != null) { 109 | attacher.update(); 110 | } 111 | } 112 | 113 | @Override 114 | public void setImageResource(int resId) { 115 | super.setImageResource(resId); 116 | if (attacher != null) { 117 | attacher.update(); 118 | } 119 | } 120 | 121 | @Override 122 | public void setImageURI(Uri uri) { 123 | super.setImageURI(uri); 124 | if (attacher != null) { 125 | attacher.update(); 126 | } 127 | } 128 | 129 | @Override 130 | protected boolean setFrame(int l, int t, int r, int b) { 131 | boolean changed = super.setFrame(l, t, r, b); 132 | if (changed) { 133 | attacher.update(); 134 | } 135 | return changed; 136 | } 137 | 138 | public void setRotationTo(float rotationDegree) { 139 | attacher.setRotationTo(rotationDegree); 140 | } 141 | 142 | public void setRotationBy(float rotationDegree) { 143 | attacher.setRotationBy(rotationDegree); 144 | } 145 | 146 | public boolean isZoomable() { 147 | return attacher.isZoomable(); 148 | } 149 | 150 | public void setZoomable(boolean zoomable) { 151 | attacher.setZoomable(zoomable); 152 | } 153 | 154 | public RectF getDisplayRect() { 155 | return attacher.getDisplayRect(); 156 | } 157 | 158 | public void getDisplayMatrix(Matrix matrix) { 159 | attacher.getDisplayMatrix(matrix); 160 | } 161 | 162 | @SuppressWarnings("UnusedReturnValue") public boolean setDisplayMatrix(Matrix finalRectangle) { 163 | return attacher.setDisplayMatrix(finalRectangle); 164 | } 165 | 166 | public void getSuppMatrix(Matrix matrix) { 167 | attacher.getSuppMatrix(matrix); 168 | } 169 | 170 | public boolean setSuppMatrix(Matrix matrix) { 171 | return attacher.setDisplayMatrix(matrix); 172 | } 173 | 174 | public float getMinimumScale() { 175 | return attacher.getMinimumScale(); 176 | } 177 | 178 | public float getMediumScale() { 179 | return attacher.getMediumScale(); 180 | } 181 | 182 | public float getMaximumScale() { 183 | return attacher.getMaximumScale(); 184 | } 185 | 186 | public float getScale() { 187 | return attacher.getScale(); 188 | } 189 | 190 | public void setAllowParentInterceptOnEdge(boolean allow) { 191 | attacher.setAllowParentInterceptOnEdge(allow); 192 | } 193 | 194 | public void setMinimumScale(float minimumScale) { 195 | attacher.setMinimumScale(minimumScale); 196 | } 197 | 198 | public void setMediumScale(float mediumScale) { 199 | attacher.setMediumScale(mediumScale); 200 | } 201 | 202 | public void setMaximumScale(float maximumScale) { 203 | attacher.setMaximumScale(maximumScale); 204 | } 205 | 206 | public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { 207 | attacher.setScaleLevels(minimumScale, mediumScale, maximumScale); 208 | } 209 | 210 | public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { 211 | attacher.setOnMatrixChangeListener(listener); 212 | } 213 | 214 | public void setOnPhotoTapListener(OnPhotoTapListener listener) { 215 | attacher.setOnPhotoTapListener(listener); 216 | } 217 | 218 | public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener listener) { 219 | attacher.setOnOutsidePhotoTapListener(listener); 220 | } 221 | 222 | public void setOnViewTapListener(OnViewTapListener listener) { 223 | attacher.setOnViewTapListener(listener); 224 | } 225 | 226 | public void setOnViewDragListener(OnViewDragListener listener) { 227 | attacher.setOnViewDragListener(listener); 228 | } 229 | 230 | public void setScale(float scale) { 231 | attacher.setScale(scale); 232 | } 233 | 234 | public void setScale(float scale, boolean animate) { 235 | attacher.setScale(scale, animate); 236 | } 237 | 238 | public void setScale(float scale, float focalX, float focalY, boolean animate) { 239 | attacher.setScale(scale, focalX, focalY, animate); 240 | } 241 | 242 | public void setZoomTransitionDuration(int milliseconds) { 243 | attacher.setZoomTransitionDuration(milliseconds); 244 | } 245 | 246 | public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener onDoubleTapListener) { 247 | attacher.setOnDoubleTapListener(onDoubleTapListener); 248 | } 249 | 250 | public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangedListener) { 251 | attacher.setOnScaleChangeListener(onScaleChangedListener); 252 | } 253 | 254 | public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { 255 | attacher.setOnSingleFlingListener(onSingleFlingListener); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/PhotoViewAttacher.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 |

4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 |

8 | http://www.apache.org/licenses/LICENSE-2.0 9 |

10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package github.xuqk.kdimageviewer.photoview; 17 | 18 | import android.content.Context; 19 | import android.graphics.Matrix; 20 | import android.graphics.Matrix.ScaleToFit; 21 | import android.graphics.RectF; 22 | import android.graphics.drawable.Drawable; 23 | import android.view.GestureDetector; 24 | import android.view.MotionEvent; 25 | import android.view.View; 26 | import android.view.View.OnLongClickListener; 27 | import android.view.ViewParent; 28 | import android.view.animation.AccelerateDecelerateInterpolator; 29 | import android.view.animation.Interpolator; 30 | import android.widget.ImageView; 31 | import android.widget.ImageView.ScaleType; 32 | import android.widget.OverScroller; 33 | 34 | 35 | /** 36 | * The component of which does the work allowing for zooming, scaling, panning, etc. 37 | * It is made public in case you need to subclass something other than AppCompatImageView and still 38 | * gain the functionality that {@link PhotoView} offers 39 | */ 40 | public class PhotoViewAttacher implements View.OnTouchListener, 41 | View.OnLayoutChangeListener { 42 | 43 | private static float DEFAULT_MAX_SCALE = 4.0f; 44 | private static float DEFAULT_MID_SCALE = 2.5f; 45 | private static float DEFAULT_MIN_SCALE = 1.0f; 46 | private static int DEFAULT_ZOOM_DURATION = 200; 47 | 48 | private static final int HORIZONTAL_EDGE_NONE = -1; 49 | private static final int HORIZONTAL_EDGE_LEFT = 0; 50 | private static final int HORIZONTAL_EDGE_RIGHT = 1; 51 | private static final int HORIZONTAL_EDGE_BOTH = 2; 52 | private static final int VERTICAL_EDGE_NONE = -1; 53 | private static final int VERTICAL_EDGE_TOP = 0; 54 | private static final int VERTICAL_EDGE_BOTTOM = 1; 55 | private static final int VERTICAL_EDGE_BOTH = 2; 56 | private static int SINGLE_TOUCH = 1; 57 | 58 | private Interpolator mInterpolator = new AccelerateDecelerateInterpolator(); 59 | private int mZoomDuration = DEFAULT_ZOOM_DURATION; 60 | private float mMinScale = DEFAULT_MIN_SCALE; 61 | private float mMidScale = DEFAULT_MID_SCALE; 62 | private float mMaxScale = DEFAULT_MAX_SCALE; 63 | 64 | private boolean mAllowParentInterceptOnEdge = true; 65 | private boolean mBlockParentIntercept = false; 66 | 67 | private ImageView mImageView; 68 | 69 | // Gesture Detectors 70 | private GestureDetector mGestureDetector; 71 | private CustomGestureDetector mScaleDragDetector; 72 | 73 | // These are set so we don't keep allocating them on the heap 74 | private final Matrix mBaseMatrix = new Matrix(); 75 | private final Matrix mDrawMatrix = new Matrix(); 76 | private final Matrix mSuppMatrix = new Matrix(); 77 | private final RectF mDisplayRect = new RectF(); 78 | private final float[] mMatrixValues = new float[9]; 79 | 80 | // Listeners 81 | private OnMatrixChangedListener mMatrixChangeListener; 82 | private OnPhotoTapListener mPhotoTapListener; 83 | private OnOutsidePhotoTapListener mOutsidePhotoTapListener; 84 | private OnViewTapListener mViewTapListener; 85 | private View.OnClickListener mOnClickListener; 86 | private OnLongClickListener mLongClickListener; 87 | private OnScaleChangedListener mScaleChangeListener; 88 | private OnSingleFlingListener mSingleFlingListener; 89 | private OnViewDragListener mOnViewDragListener; 90 | 91 | private FlingRunnable mCurrentFlingRunnable; 92 | private int mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; 93 | private int mVerticalScrollEdge = VERTICAL_EDGE_BOTH; 94 | private float mBaseRotation; 95 | public boolean isTopEnd, isBottomEnd, isLeftEnd, isRightEnd = false; 96 | public boolean isVertical, isHorizontal; 97 | private boolean mZoomEnabled = true; 98 | private boolean isLongImage = false;//是否是长图 99 | private ScaleType mScaleType = ScaleType.FIT_CENTER; 100 | private OnGestureListener onGestureListener = new OnGestureListener() { 101 | @Override 102 | public void onDrag(float dx, float dy) { 103 | if (mScaleDragDetector.isScaling()) { 104 | return; // Do not drag if we are already scaling 105 | } 106 | if (mOnViewDragListener != null) { 107 | mOnViewDragListener.onDrag(dx, dy); 108 | } 109 | mSuppMatrix.postTranslate(dx, dy); 110 | checkAndDisplayMatrix(); 111 | isTopEnd = (mVerticalScrollEdge == VERTICAL_EDGE_TOP) && getScale() != 1f; 112 | isBottomEnd = (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM) && getScale() != 1f; 113 | isLeftEnd = (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT) && getScale() != 1f; 114 | isRightEnd = (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT) && getScale() != 1f; 115 | 116 | ViewParent parent = mImageView.getParent(); 117 | if (parent == null) return; 118 | if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) { 119 | if ((mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH && !isLongImage) 120 | || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 0f && isHorizontal) 121 | || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -0f && isHorizontal) 122 | // || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f) 123 | // || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f) 124 | ) { 125 | parent.requestDisallowInterceptTouchEvent(false); 126 | } else if ((mVerticalScrollEdge == VERTICAL_EDGE_BOTH && isVertical) 127 | || (isTopEnd && dy > 0 && isVertical) 128 | || (isBottomEnd && dy < 0 && isVertical)) { 129 | parent.requestDisallowInterceptTouchEvent(false); 130 | } else if (isLongImage) { 131 | //长图特殊上下滑动 132 | if ((mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy > 0 && isVertical) 133 | || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy < 0 && isVertical)) { 134 | parent.requestDisallowInterceptTouchEvent(false); 135 | } 136 | } 137 | 138 | } else { 139 | if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH && isLongImage && isHorizontal) { 140 | //长图左右滑动 141 | parent.requestDisallowInterceptTouchEvent(false); 142 | }else{ 143 | parent.requestDisallowInterceptTouchEvent(true); 144 | } 145 | } 146 | } 147 | 148 | @Override 149 | public void onFling(float startX, float startY, float velocityX, float velocityY) { 150 | mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext()); 151 | mCurrentFlingRunnable.fling(getImageViewWidth(mImageView), 152 | getImageViewHeight(mImageView), (int) velocityX, (int) velocityY); 153 | mImageView.post(mCurrentFlingRunnable); 154 | } 155 | 156 | @Override 157 | public void onScale(float scaleFactor, float focusX, float focusY) { 158 | if (getScale() < mMaxScale || scaleFactor < 1f) { 159 | if (mScaleChangeListener != null) { 160 | mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); 161 | } 162 | mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); 163 | checkAndDisplayMatrix(); 164 | } 165 | } 166 | }; 167 | 168 | public PhotoViewAttacher(ImageView imageView) { 169 | mImageView = imageView; 170 | imageView.setOnTouchListener(this); 171 | imageView.addOnLayoutChangeListener(this); 172 | if (imageView.isInEditMode()) { 173 | return; 174 | } 175 | mBaseRotation = 0.0f; 176 | // Create Gesture Detectors... 177 | mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener); 178 | mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() { 179 | // forward long click listener 180 | @Override 181 | public void onLongPress(MotionEvent e) { 182 | if (mLongClickListener != null) { 183 | mLongClickListener.onLongClick(mImageView); 184 | } 185 | } 186 | 187 | @Override 188 | public boolean onFling(MotionEvent e1, MotionEvent e2, 189 | float velocityX, float velocityY) { 190 | if (mSingleFlingListener != null) { 191 | if (getScale() > DEFAULT_MIN_SCALE) { 192 | return false; 193 | } 194 | if (e1.getPointerCount() > SINGLE_TOUCH 195 | || e2.getPointerCount() > SINGLE_TOUCH) { 196 | return false; 197 | } 198 | return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY); 199 | } 200 | return false; 201 | } 202 | }); 203 | mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { 204 | @Override 205 | public boolean onSingleTapConfirmed(MotionEvent e) { 206 | if (mOnClickListener != null) { 207 | mOnClickListener.onClick(mImageView); 208 | } 209 | final RectF displayRect = getDisplayRect(); 210 | final float x = e.getX(), y = e.getY(); 211 | if (mViewTapListener != null) { 212 | mViewTapListener.onViewTap(mImageView, x, y); 213 | } 214 | if (displayRect != null) { 215 | // Check to see if the user tapped on the photo 216 | if (displayRect.contains(x, y)) { 217 | float xResult = (x - displayRect.left) 218 | / displayRect.width(); 219 | float yResult = (y - displayRect.top) 220 | / displayRect.height(); 221 | if (mPhotoTapListener != null) { 222 | mPhotoTapListener.onPhotoTap(mImageView, xResult, yResult); 223 | } 224 | return true; 225 | } else { 226 | if (mOutsidePhotoTapListener != null) { 227 | mOutsidePhotoTapListener.onOutsidePhotoTap(mImageView); 228 | } 229 | } 230 | } 231 | return false; 232 | } 233 | 234 | @Override 235 | public boolean onDoubleTap(MotionEvent ev) { 236 | try { 237 | float scale = getScale(); 238 | float x = ev.getX(); 239 | float y = ev.getY(); 240 | if (scale < getMediumScale()) { 241 | setScale(getMediumScale(), x, y, true); 242 | } else if (scale >= getMediumScale() && scale < getMaximumScale()) { 243 | setScale(getMaximumScale(), x, y, true); 244 | } else { 245 | setScale(getMinimumScale(), x, y, true); 246 | } 247 | } catch (ArrayIndexOutOfBoundsException e) { 248 | // Can sometimes happen when getX() and getY() is called 249 | } 250 | return true; 251 | } 252 | 253 | @Override 254 | public boolean onDoubleTapEvent(MotionEvent e) { 255 | // Wait for the confirmed onDoubleTap() instead 256 | return true; 257 | } 258 | }); 259 | } 260 | 261 | public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) { 262 | this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener); 263 | } 264 | 265 | public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangeListener) { 266 | this.mScaleChangeListener = onScaleChangeListener; 267 | } 268 | 269 | public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { 270 | this.mSingleFlingListener = onSingleFlingListener; 271 | } 272 | 273 | @Deprecated 274 | public boolean isZoomEnabled() { 275 | return mZoomEnabled; 276 | } 277 | 278 | public RectF getDisplayRect() { 279 | checkMatrixBounds(); 280 | return getDisplayRect(getDrawMatrix()); 281 | } 282 | 283 | public boolean setDisplayMatrix(Matrix finalMatrix) { 284 | if (finalMatrix == null) { 285 | throw new IllegalArgumentException("Matrix cannot be null"); 286 | } 287 | if (mImageView.getDrawable() == null) { 288 | return false; 289 | } 290 | mSuppMatrix.set(finalMatrix); 291 | checkAndDisplayMatrix(); 292 | return true; 293 | } 294 | 295 | public void setBaseRotation(final float degrees) { 296 | mBaseRotation = degrees % 360; 297 | update(); 298 | setRotationBy(mBaseRotation); 299 | checkAndDisplayMatrix(); 300 | } 301 | 302 | public void setRotationTo(float degrees) { 303 | mSuppMatrix.setRotate(degrees % 360); 304 | checkAndDisplayMatrix(); 305 | } 306 | 307 | public void setRotationBy(float degrees) { 308 | mSuppMatrix.postRotate(degrees % 360); 309 | checkAndDisplayMatrix(); 310 | } 311 | 312 | public float getMinimumScale() { 313 | return mMinScale; 314 | } 315 | 316 | public float getMediumScale() { 317 | return mMidScale; 318 | } 319 | 320 | public float getMaximumScale() { 321 | return mMaxScale; 322 | } 323 | 324 | public float getScale() { 325 | return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow 326 | (getValue(mSuppMatrix, Matrix.MSKEW_Y), 2)); 327 | } 328 | 329 | public ScaleType getScaleType() { 330 | return mScaleType; 331 | } 332 | 333 | @Override 334 | public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int 335 | oldRight, int oldBottom) { 336 | // Update our base matrix, as the bounds have changed 337 | if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { 338 | updateBaseMatrix(mImageView.getDrawable()); 339 | } 340 | } 341 | 342 | float x, y; 343 | 344 | @Override 345 | public boolean onTouch(View v, MotionEvent ev) { 346 | boolean handled = false; 347 | if (mZoomEnabled && Util.hasDrawable((ImageView) v)) { 348 | switch (ev.getAction()) { 349 | case MotionEvent.ACTION_DOWN: 350 | x = ev.getX(); 351 | y = ev.getY(); 352 | ViewParent parent = v.getParent(); 353 | // First, disable the Parent from intercepting the touch 354 | // event 355 | // If we're flinging, and the user presses down, cancel 356 | // fling 357 | cancelFling(); 358 | if (parent != null) { 359 | parent.requestDisallowInterceptTouchEvent(true); 360 | } 361 | 362 | break; 363 | case MotionEvent.ACTION_CANCEL: 364 | case MotionEvent.ACTION_UP: 365 | isTopEnd = false; 366 | // If the user has zoomed less than min scale, zoom back 367 | // to min scale 368 | if (getScale() < mMinScale) { 369 | RectF rect = getDisplayRect(); 370 | if (rect != null) { 371 | v.post(new AnimatedZoomRunnable(getScale(), mMinScale, 372 | rect.centerX(), rect.centerY())); 373 | handled = true; 374 | } 375 | } else if (getScale() > mMaxScale) { 376 | RectF rect = getDisplayRect(); 377 | if (rect != null) { 378 | v.post(new AnimatedZoomRunnable(getScale(), mMaxScale, 379 | rect.centerX(), rect.centerY())); 380 | handled = true; 381 | } 382 | } 383 | break; 384 | case MotionEvent.ACTION_MOVE: 385 | float dx = Math.abs(ev.getX() - x); 386 | float dy = Math.abs(ev.getY() - y); 387 | if(isLongImage){ 388 | isVertical = dy > dx; 389 | isHorizontal = dx > dy * 2; 390 | }else { 391 | isVertical = (getScale() != 1.0 && dy > dx); 392 | isHorizontal = (getScale() != 1.0 && dx > dy * 2); 393 | } 394 | break; 395 | } 396 | // Try the Scale/Drag detector 397 | if (mScaleDragDetector != null) { 398 | boolean wasScaling = mScaleDragDetector.isScaling(); 399 | boolean wasDragging = mScaleDragDetector.isDragging(); 400 | handled = mScaleDragDetector.onTouchEvent(ev); 401 | boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling(); 402 | boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging(); 403 | mBlockParentIntercept = didntScale && didntDrag; 404 | } 405 | // Check to see if the user double tapped 406 | if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) { 407 | handled = true; 408 | } 409 | 410 | } 411 | return handled; 412 | } 413 | 414 | public void setAllowParentInterceptOnEdge(boolean allow) { 415 | mAllowParentInterceptOnEdge = allow; 416 | } 417 | 418 | public void setMinimumScale(float minimumScale) { 419 | Util.checkZoomLevels(minimumScale, mMidScale, mMaxScale); 420 | mMinScale = minimumScale; 421 | } 422 | 423 | public void setMediumScale(float mediumScale) { 424 | Util.checkZoomLevels(mMinScale, mediumScale, mMaxScale); 425 | mMidScale = mediumScale; 426 | } 427 | 428 | public void setMaximumScale(float maximumScale) { 429 | Util.checkZoomLevels(mMinScale, mMidScale, maximumScale); 430 | mMaxScale = maximumScale; 431 | } 432 | 433 | public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { 434 | Util.checkZoomLevels(minimumScale, mediumScale, maximumScale); 435 | mMinScale = minimumScale; 436 | mMidScale = mediumScale; 437 | mMaxScale = maximumScale; 438 | } 439 | 440 | public void setOnLongClickListener(OnLongClickListener listener) { 441 | mLongClickListener = listener; 442 | } 443 | 444 | public void setOnClickListener(View.OnClickListener listener) { 445 | mOnClickListener = listener; 446 | } 447 | 448 | public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { 449 | mMatrixChangeListener = listener; 450 | } 451 | 452 | public void setOnPhotoTapListener(OnPhotoTapListener listener) { 453 | mPhotoTapListener = listener; 454 | } 455 | 456 | public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener mOutsidePhotoTapListener) { 457 | this.mOutsidePhotoTapListener = mOutsidePhotoTapListener; 458 | } 459 | 460 | public void setOnViewTapListener(OnViewTapListener listener) { 461 | mViewTapListener = listener; 462 | } 463 | 464 | public void setOnViewDragListener(OnViewDragListener listener) { 465 | mOnViewDragListener = listener; 466 | } 467 | 468 | public void setScale(float scale) { 469 | setScale(scale, false); 470 | } 471 | 472 | public void setScale(float scale, boolean animate) { 473 | setScale(scale, (mImageView.getRight()) / 2, (mImageView.getBottom()) / 2, animate); 474 | } 475 | 476 | public void setScale(float scale, float focalX, float focalY, 477 | boolean animate) { 478 | // Check to see if the scale is within bounds 479 | // if (scale < mMinScale || scale > mMaxScale) { 480 | // throw new IllegalArgumentException("Scale must be within the range of minScale and maxScale"); 481 | // } 482 | if (animate) { 483 | mImageView.post(new AnimatedZoomRunnable(getScale(), scale, 484 | focalX, focalY)); 485 | } else { 486 | mSuppMatrix.setScale(scale, scale, focalX, focalY); 487 | checkAndDisplayMatrix(); 488 | } 489 | } 490 | 491 | /** 492 | * Set the zoom interpolator 493 | * 494 | * @param interpolator the zoom interpolator 495 | */ 496 | public void setZoomInterpolator(Interpolator interpolator) { 497 | mInterpolator = interpolator; 498 | } 499 | 500 | public void setScaleType(ScaleType scaleType) { 501 | if (Util.isSupportedScaleType(scaleType) && scaleType != mScaleType) { 502 | mScaleType = scaleType; 503 | update(); 504 | } 505 | } 506 | 507 | public boolean isZoomable() { 508 | return mZoomEnabled; 509 | } 510 | 511 | public void setZoomable(boolean zoomable) { 512 | mZoomEnabled = zoomable; 513 | update(); 514 | } 515 | 516 | public void update() { 517 | if (mZoomEnabled) { 518 | // Update the base matrix using the current drawable 519 | updateBaseMatrix(mImageView.getDrawable()); 520 | } else { 521 | // Reset the Matrix... 522 | resetMatrix(); 523 | } 524 | } 525 | 526 | /** 527 | * Get the display matrix 528 | * 529 | * @param matrix target matrix to copy to 530 | */ 531 | public void getDisplayMatrix(Matrix matrix) { 532 | matrix.set(getDrawMatrix()); 533 | } 534 | 535 | /** 536 | * Get the current support matrix 537 | */ 538 | public void getSuppMatrix(Matrix matrix) { 539 | matrix.set(mSuppMatrix); 540 | } 541 | 542 | private Matrix getDrawMatrix() { 543 | mDrawMatrix.set(mBaseMatrix); 544 | mDrawMatrix.postConcat(mSuppMatrix); 545 | return mDrawMatrix; 546 | } 547 | 548 | public Matrix getImageMatrix() { 549 | return mDrawMatrix; 550 | } 551 | 552 | public void setZoomTransitionDuration(int milliseconds) { 553 | this.mZoomDuration = milliseconds; 554 | } 555 | 556 | /** 557 | * Helper method that 'unpacks' a Matrix and returns the required value 558 | * 559 | * @param matrix Matrix to unpack 560 | * @param whichValue Which value from Matrix.M* to return 561 | * @return returned value 562 | */ 563 | public float getValue(Matrix matrix, int whichValue) { 564 | matrix.getValues(mMatrixValues); 565 | return mMatrixValues[whichValue]; 566 | } 567 | 568 | /** 569 | * Resets the Matrix back to FIT_CENTER, and then displays its contents 570 | */ 571 | private void resetMatrix() { 572 | mSuppMatrix.reset(); 573 | setRotationBy(mBaseRotation); 574 | setImageViewMatrix(getDrawMatrix()); 575 | checkMatrixBounds(); 576 | } 577 | 578 | private void setImageViewMatrix(Matrix matrix) { 579 | mImageView.setImageMatrix(matrix); 580 | // Call MatrixChangedListener if needed 581 | if (mMatrixChangeListener != null) { 582 | RectF displayRect = getDisplayRect(matrix); 583 | if (displayRect != null) { 584 | mMatrixChangeListener.onMatrixChanged(displayRect); 585 | } 586 | } 587 | } 588 | 589 | /** 590 | * Helper method that simply checks the Matrix, and then displays the result 591 | */ 592 | private void checkAndDisplayMatrix() { 593 | if (checkMatrixBounds()) { 594 | setImageViewMatrix(getDrawMatrix()); 595 | } 596 | } 597 | 598 | /** 599 | * Helper method that maps the supplied Matrix to the current Drawable 600 | * 601 | * @param matrix - Matrix to map Drawable against 602 | * @return RectF - Displayed Rectangle 603 | */ 604 | private RectF getDisplayRect(Matrix matrix) { 605 | Drawable d = mImageView.getDrawable(); 606 | if (d != null) { 607 | mDisplayRect.set(0, 0, d.getIntrinsicWidth(), 608 | d.getIntrinsicHeight()); 609 | matrix.mapRect(mDisplayRect); 610 | return mDisplayRect; 611 | } 612 | return null; 613 | } 614 | 615 | /** 616 | * Calculate Matrix for FIT_CENTER 617 | * 618 | * @param drawable - Drawable being displayed 619 | */ 620 | private void updateBaseMatrix(Drawable drawable) { 621 | if (drawable == null) { 622 | return; 623 | } 624 | final float viewWidth = getImageViewWidth(mImageView); 625 | final float viewHeight = getImageViewHeight(mImageView); 626 | final int drawableWidth = drawable.getIntrinsicWidth(); 627 | final int drawableHeight = drawable.getIntrinsicHeight(); 628 | mBaseMatrix.reset(); 629 | final float widthScale = viewWidth / drawableWidth; 630 | final float heightScale = viewHeight / drawableHeight; 631 | if (mScaleType == ScaleType.CENTER) { 632 | mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, 633 | (viewHeight - drawableHeight) / 2F); 634 | 635 | } else if (mScaleType == ScaleType.CENTER_CROP) { 636 | float scale = Math.max(widthScale, heightScale); 637 | mBaseMatrix.postScale(scale, scale); 638 | mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, 639 | (viewHeight - drawableHeight * scale) / 2F); 640 | 641 | } else if (mScaleType == ScaleType.CENTER_INSIDE) { 642 | float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); 643 | mBaseMatrix.postScale(scale, scale); 644 | mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, 645 | (viewHeight - drawableHeight * scale) / 2F); 646 | 647 | } else { 648 | RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight); 649 | RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); 650 | if ((int) mBaseRotation % 180 != 0) { 651 | mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth); 652 | } 653 | switch (mScaleType) { 654 | case FIT_CENTER: 655 | // for long image, 图片高>view高,比例也大于view的高/宽,则认为是长图 656 | if (drawableHeight > viewHeight && drawableHeight * 1f / drawableWidth > viewHeight * 1f / viewWidth) { 657 | // mBaseMatrix.postScale(widthScale, widthScale); 658 | // setScale(widthScale); 659 | //长图特殊处理,宽度撑满屏幕,并且顶部对齐 660 | isLongImage = true; 661 | mBaseMatrix.setRectToRect(mTempSrc, new RectF(0, 0, viewWidth, drawableHeight * widthScale), ScaleToFit.START); 662 | } else { 663 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); 664 | } 665 | break; 666 | case FIT_START: 667 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); 668 | break; 669 | case FIT_END: 670 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); 671 | break; 672 | case FIT_XY: 673 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); 674 | break; 675 | default: 676 | break; 677 | } 678 | } 679 | resetMatrix(); 680 | } 681 | 682 | private boolean checkMatrixBounds() { 683 | final RectF rect = getDisplayRect(getDrawMatrix()); 684 | if (rect == null) { 685 | return false; 686 | } 687 | final float height = rect.height(), width = rect.width(); 688 | float deltaX = 0, deltaY = 0; 689 | final int viewHeight = getImageViewHeight(mImageView); 690 | if (height <= viewHeight && rect.top >= 0) { 691 | switch (mScaleType) { 692 | case FIT_START: 693 | deltaY = -rect.top; 694 | break; 695 | case FIT_END: 696 | deltaY = viewHeight - height - rect.top; 697 | break; 698 | default: 699 | deltaY = (viewHeight - height) / 2 - rect.top; 700 | break; 701 | } 702 | mVerticalScrollEdge = VERTICAL_EDGE_BOTH; 703 | } else if (rect.top >= 0) { 704 | mVerticalScrollEdge = VERTICAL_EDGE_TOP; 705 | deltaY = -rect.top; 706 | } else if (rect.bottom <= viewHeight) { 707 | mVerticalScrollEdge = VERTICAL_EDGE_BOTTOM; 708 | deltaY = viewHeight - rect.bottom; 709 | } else { 710 | mVerticalScrollEdge = VERTICAL_EDGE_NONE; 711 | } 712 | final int viewWidth = getImageViewWidth(mImageView); 713 | // Log.e("tag", "rect: " + rect.toShortString() + " viewWidth: " + viewWidth + " viewHeight: " + viewHeight 714 | // + " recLeft: " + rect.left + " recRight: " + rect.right + " mHorizontalScrollEdge: " + mHorizontalScrollEdge); 715 | if (width <= viewWidth && rect.left >= 0) { 716 | switch (mScaleType) { 717 | case FIT_START: 718 | deltaX = -rect.left; 719 | break; 720 | case FIT_END: 721 | deltaX = viewWidth - width - rect.left; 722 | break; 723 | default: 724 | deltaX = (viewWidth - width) / 2 - rect.left; 725 | break; 726 | } 727 | mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; 728 | } else if (rect.left >= 0) { 729 | mHorizontalScrollEdge = HORIZONTAL_EDGE_LEFT; 730 | deltaX = -rect.left; 731 | } else if (rect.right <= viewWidth) { 732 | deltaX = viewWidth - rect.right; 733 | mHorizontalScrollEdge = HORIZONTAL_EDGE_RIGHT; 734 | } else { 735 | mHorizontalScrollEdge = HORIZONTAL_EDGE_NONE; 736 | } 737 | // Finally actually translate the matrix 738 | mSuppMatrix.postTranslate(deltaX, deltaY); 739 | return true; 740 | } 741 | 742 | private int getImageViewWidth(ImageView imageView) { 743 | return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight(); 744 | } 745 | 746 | private int getImageViewHeight(ImageView imageView) { 747 | return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom(); 748 | } 749 | 750 | private void cancelFling() { 751 | if (mCurrentFlingRunnable != null) { 752 | mCurrentFlingRunnable.cancelFling(); 753 | mCurrentFlingRunnable = null; 754 | } 755 | } 756 | 757 | private class AnimatedZoomRunnable implements Runnable { 758 | 759 | private final float mFocalX, mFocalY; 760 | private final long mStartTime; 761 | private final float mZoomStart, mZoomEnd; 762 | 763 | public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, 764 | final float focalX, final float focalY) { 765 | mFocalX = focalX; 766 | mFocalY = focalY; 767 | mStartTime = System.currentTimeMillis(); 768 | mZoomStart = currentZoom; 769 | mZoomEnd = targetZoom; 770 | } 771 | 772 | @Override 773 | public void run() { 774 | float t = interpolate(); 775 | float scale = mZoomStart + t * (mZoomEnd - mZoomStart); 776 | float deltaScale = scale / getScale(); 777 | onGestureListener.onScale(deltaScale, mFocalX, mFocalY); 778 | // We haven't hit our target scale yet, so post ourselves again 779 | if (t < 1f) { 780 | Compat.postOnAnimation(mImageView, this); 781 | } 782 | } 783 | 784 | private float interpolate() { 785 | float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration; 786 | t = Math.min(1f, t); 787 | t = mInterpolator.getInterpolation(t); 788 | return t; 789 | } 790 | } 791 | 792 | private class FlingRunnable implements Runnable { 793 | 794 | private final OverScroller mScroller; 795 | private int mCurrentX, mCurrentY; 796 | 797 | public FlingRunnable(Context context) { 798 | mScroller = new OverScroller(context); 799 | } 800 | 801 | public void cancelFling() { 802 | mScroller.forceFinished(true); 803 | } 804 | 805 | public void fling(int viewWidth, int viewHeight, int velocityX, 806 | int velocityY) { 807 | final RectF rect = getDisplayRect(); 808 | if (rect == null) { 809 | return; 810 | } 811 | final int startX = Math.round(-rect.left); 812 | final int minX, maxX, minY, maxY; 813 | if (viewWidth < rect.width()) { 814 | minX = 0; 815 | maxX = Math.round(rect.width() - viewWidth); 816 | } else { 817 | minX = maxX = startX; 818 | } 819 | final int startY = Math.round(-rect.top); 820 | if (viewHeight < rect.height()) { 821 | minY = 0; 822 | maxY = Math.round(rect.height() - viewHeight); 823 | } else { 824 | minY = maxY = startY; 825 | } 826 | mCurrentX = startX; 827 | mCurrentY = startY; 828 | // If we actually can move, fling the scroller 829 | if (startX != maxX || startY != maxY) { 830 | mScroller.fling(startX, startY, velocityX, velocityY, minX, 831 | maxX, minY, maxY, 0, 0); 832 | } 833 | } 834 | 835 | @Override 836 | public void run() { 837 | if (mScroller.isFinished()) { 838 | return; // remaining post that should not be handled 839 | } 840 | if (mScroller.computeScrollOffset()) { 841 | final int newX = mScroller.getCurrX(); 842 | final int newY = mScroller.getCurrY(); 843 | mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); 844 | checkAndDisplayMatrix(); 845 | mCurrentX = newX; 846 | mCurrentY = newY; 847 | // Post On animation 848 | Compat.postOnAnimation(mImageView, this); 849 | } 850 | } 851 | } 852 | } 853 | -------------------------------------------------------------------------------- /kdimageviewer/src/main/java/github/xuqk/kdimageviewer/photoview/Util.java: -------------------------------------------------------------------------------- 1 | package github.xuqk.kdimageviewer.photoview; 2 | 3 | import android.view.MotionEvent; 4 | import android.widget.ImageView; 5 | 6 | class Util { 7 | 8 | static void checkZoomLevels(float minZoom, float midZoom, 9 | float maxZoom) { 10 | if (minZoom >= midZoom) { 11 | throw new IllegalArgumentException( 12 | "Minimum zoom has to be less than Medium zoom. Call setMinimumZoom() with a more appropriate value"); 13 | } else if (midZoom >= maxZoom) { 14 | throw new IllegalArgumentException( 15 | "Medium zoom has to be less than Maximum zoom. Call setMaximumZoom() with a more appropriate value"); 16 | } 17 | } 18 | 19 | static boolean hasDrawable(ImageView imageView) { 20 | return imageView.getDrawable() != null; 21 | } 22 | 23 | static boolean isSupportedScaleType(final ImageView.ScaleType scaleType) { 24 | if (scaleType == null) { 25 | return false; 26 | } 27 | switch (scaleType) { 28 | case MATRIX: 29 | // throw new IllegalStateException("Matrix scale type is not supported"); 30 | return false; 31 | } 32 | return true; 33 | } 34 | 35 | static int getPointerIndex(int action) { 36 | return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':kdimageviewer' 2 | rootProject.name='Demo' 3 | --------------------------------------------------------------------------------