├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── photoviewex ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── wuzy │ │ └── photoviewex │ │ ├── Compat.java │ │ ├── CustomGestureDetector.java │ │ ├── DragCloseHelper.java │ │ ├── OnDragCloseListener.java │ │ ├── OnGestureListener.java │ │ ├── OnMatrixChangedListener.java │ │ ├── OnOutsidePhotoTapListener.java │ │ ├── OnPhotoTapListener.java │ │ ├── OnRotateListener.java │ │ ├── OnScaleChangedListener.java │ │ ├── OnSingleFlingListener.java │ │ ├── OnViewDragListener.java │ │ ├── OnViewTapListener.java │ │ ├── PhotoView.java │ │ ├── PhotoViewAttacher.java │ │ ├── RotateGestureDetector.java │ │ └── Util.java │ └── res │ ├── anim │ ├── dchlib_anim_alpha_out_long_time.xml │ └── dchlib_anim_empty.xml │ └── values │ └── strings.xml ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── wuzy │ │ └── photoviewex │ │ └── sample │ │ ├── ActivityTransitionActivity.java │ │ ├── ActivityTransitionToActivity.java │ │ ├── HackyDrawerLayout.java │ │ ├── HackyViewPager.java │ │ ├── ImageAdapter.java │ │ ├── ImageViewHolder.java │ │ ├── ImmersiveActivity.java │ │ ├── LauncherActivity.java │ │ ├── PicassoSampleActivity.java │ │ ├── RotationSampleActivity.java │ │ ├── SimpleSampleActivity.java │ │ └── ViewPagerActivity.java │ └── res │ ├── drawable-nodpi │ └── wallpaper.jpg │ ├── drawable │ └── ic_arrow_back_white_24dp.xml │ ├── layout │ ├── activity_immersive.xml │ ├── activity_launcher.xml │ ├── activity_rotation_sample.xml │ ├── activity_simple.xml │ ├── activity_simple_sample.xml │ ├── activity_transition.xml │ ├── activity_transition_to.xml │ ├── activity_view_pager.xml │ ├── item_image.xml │ └── item_sample.xml │ ├── menu │ ├── main_menu.xml │ └── rotation.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ ├── colors.xml │ ├── strings.xml │ ├── styles.xml │ └── transitions.xml ├── screenshoots └── 1.gif └── 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 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhotoViewEx 2 | 3 | 基于 [PhotoView](https://github.com/chrisbanes/PhotoView),增加手势旋转、拖动退出预览功能。 4 | 5 | [![](https://jitpack.io/v/zywudev/PhotoViewEx.svg)](https://jitpack.io/#zywudev/PhotoViewEx) 6 | 7 | 8 | ## 效果 9 | 10 | ![](https://github.com/zywudev/PhotoViewEx/blob/master/screenshoots/1.gif) 11 | 12 | ## 依赖 13 | 14 | 在项目 `build.gradle` 中添加依赖 15 | 16 | ```groovy 17 | allprojects { 18 | repositories { 19 | maven { url "https://jitpack.io" } 20 | } 21 | } 22 | ``` 23 | 24 | module 的 `build.gradle` 中添加依赖 25 | 26 | ```groovy 27 | dependencies { 28 | implementation 'com.github.zywudev:PhotoViewEx:1.0.0' 29 | } 30 | ``` 31 | 32 | ## 使用方法 33 | 34 | 使用下面的方式即可实现缩放、双指旋转功能。 35 | 36 | ```xml 37 | 41 | ``` 42 | 43 | ```java 44 | PhotoView photoView = (PhotoView) findViewById(R.id.iv_photo); 45 | photoView.setImageResource(R.drawable.image); 46 | ``` 47 | 48 | **拖动关闭使用方法**: 49 | 50 | 1、Activity 主题设为透明 51 | 52 | ```xml 53 | true 54 | ``` 55 | 56 | 2、初始化 57 | 58 | ```java 59 | DragCloseHelper mDragCloseHelper = new DragCloseHelper(this); 60 | ``` 61 | 62 | 3、设置需要拖拽的 View 以及背景 ViewGroup 63 | 64 | ```java 65 | mDragCloseHelper.setDragCloseViews(mConstraintLayout,mPhotoView); 66 | ``` 67 | 68 | 4、设置监听 69 | 70 | ```java 71 | mDragCloseHelper.setOnDragCloseListener(new OnDragCloseListener() { 72 | @Override 73 | public void onDragBegin() { 74 | 75 | } 76 | 77 | @Override 78 | public void onDragging(float percent) { 79 | 80 | } 81 | 82 | @Override 83 | public void onDragEnd(boolean isShareElementMode) { 84 | if (isShareElementMode) { 85 | onBackPressed(); 86 | } 87 | } 88 | 89 | @Override 90 | public void onDragCancel() { 91 | 92 | } 93 | 94 | @Override 95 | public boolean intercept() { 96 | // 默认false 97 | return false; 98 | } 99 | }); 100 | ``` 101 | 102 | 5、处理 Touch 事件 103 | 104 | ```java 105 | @Override 106 | public boolean dispatchTouchEvent(MotionEvent ev) { 107 | if (mDragCloseHelper.handleEvent(ev)) { 108 | return true; 109 | } else { 110 | return super.dispatchTouchEvent(ev); 111 | } 112 | } 113 | ``` 114 | 115 | 更多使用方法参见 [sample](https://github.com/zywudev/PhotoViewEx/tree/master/sample)。 116 | 117 | ## 参考 118 | 119 | - [PhotoView](https://github.com/chrisbanes/PhotoView) 120 | 121 | - [RotatePhotoView](https://github.com/ChenSiLiang/RotatePhotoView) 122 | - [DragCloseHelper](https://github.com/bauer-bao/DragCloseHelper) -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.3.2' 11 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' 12 | 13 | 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | jcenter() 23 | maven { url 'https://jitpack.io' } 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | 31 | ext { 32 | minSdkVersion = 15 33 | targetSdkVersion = 28 34 | compileSdkVersion = 28 35 | buildToolsVersion = '28.0.3' 36 | } 37 | -------------------------------------------------------------------------------- /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 | 15 | 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zywudev/PhotoViewEx/c26a1108c22bf0e7ddf40e56bdfffaca647ab94b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Apr 29 14:58:11 CST 2019 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-4.10.1-all.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 | -------------------------------------------------------------------------------- /photoviewex/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /photoviewex/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | // JitPack Maven 3 | apply plugin: 'com.github.dcendents.android-maven' 4 | // Your Group 5 | group='com.github.zywudev' 6 | 7 | android { 8 | compileSdkVersion rootProject.ext.compileSdkVersion 9 | 10 | defaultConfig { 11 | minSdkVersion rootProject.ext.minSdkVersion 12 | targetSdkVersion rootProject.ext.targetSdkVersion 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | } 26 | 27 | dependencies { 28 | implementation "androidx.appcompat:appcompat:1.0.0" 29 | } 30 | -------------------------------------------------------------------------------- /photoviewex/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 | -------------------------------------------------------------------------------- /photoviewex/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/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 com.wuzy.photoviewex; 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 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/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 com.wuzy.photoviewex; 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 mScaleGestureDetector; 34 | private final RotateGestureDetector mRotateGestureDetector; 35 | 36 | private VelocityTracker mVelocityTracker; 37 | private boolean mIsDragging; 38 | private float mLastTouchX; 39 | private float mLastTouchY; 40 | private final float mTouchSlop; 41 | private final float mMinimumVelocity; 42 | private OnGestureListener mListener; 43 | 44 | CustomGestureDetector(Context context, OnGestureListener listener) { 45 | final ViewConfiguration configuration = ViewConfiguration 46 | .get(context); 47 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 48 | mTouchSlop = configuration.getScaledTouchSlop(); 49 | 50 | mListener = listener; 51 | ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() { 52 | 53 | @Override 54 | public boolean onScale(ScaleGestureDetector detector) { 55 | float scaleFactor = detector.getScaleFactor(); 56 | 57 | if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) 58 | return false; 59 | 60 | if (scaleFactor >= 0) { 61 | mListener.onScale(scaleFactor, 62 | detector.getFocusX(), detector.getFocusY()); 63 | } 64 | return true; 65 | } 66 | 67 | @Override 68 | public boolean onScaleBegin(ScaleGestureDetector detector) { 69 | return true; 70 | } 71 | 72 | @Override 73 | public void onScaleEnd(ScaleGestureDetector detector) { 74 | // NO-OP 75 | } 76 | }; 77 | mScaleGestureDetector = new ScaleGestureDetector(context, mScaleListener); 78 | 79 | RotateGestureDetector.OnRotateGestureListener onRotateGestureListener = new RotateGestureDetector.OnRotateGestureListener() { 80 | @Override 81 | public void onRotate(int degrees, int pivotX, int pivotY) { 82 | mListener.onRotate(degrees, pivotX, pivotY); 83 | } 84 | 85 | @Override 86 | public void onToRightAngle(int pivotX, int pivotY) { 87 | mListener.onToRightAngle(pivotX,pivotY); 88 | } 89 | }; 90 | mRotateGestureDetector = new RotateGestureDetector(context, onRotateGestureListener); 91 | } 92 | 93 | private float getActiveX(MotionEvent ev) { 94 | try { 95 | return ev.getX(mActivePointerIndex); 96 | } catch (Exception e) { 97 | return ev.getX(); 98 | } 99 | } 100 | 101 | private float getActiveY(MotionEvent ev) { 102 | try { 103 | return ev.getY(mActivePointerIndex); 104 | } catch (Exception e) { 105 | return ev.getY(); 106 | } 107 | } 108 | 109 | public boolean isScaling() { 110 | return mScaleGestureDetector.isInProgress(); 111 | } 112 | 113 | public boolean isDragging() { 114 | return mIsDragging; 115 | } 116 | 117 | public boolean isRotating() { 118 | return mRotateGestureDetector.isRotating(); 119 | } 120 | 121 | public boolean onTouchEvent(MotionEvent ev) { 122 | try { 123 | mRotateGestureDetector.onTouchEvent(ev); 124 | mScaleGestureDetector.onTouchEvent(ev); 125 | return processTouchEvent(ev); 126 | } catch (IllegalArgumentException e) { 127 | // Fix for support lib bug, happening when onDestroy is called 128 | return true; 129 | } 130 | } 131 | 132 | private boolean processTouchEvent(MotionEvent ev) { 133 | final int action = ev.getAction(); 134 | switch (action & MotionEvent.ACTION_MASK) { 135 | case MotionEvent.ACTION_DOWN: 136 | mActivePointerId = ev.getPointerId(0); 137 | 138 | mVelocityTracker = VelocityTracker.obtain(); 139 | if (null != mVelocityTracker) { 140 | mVelocityTracker.addMovement(ev); 141 | } 142 | 143 | mLastTouchX = getActiveX(ev); 144 | mLastTouchY = getActiveY(ev); 145 | mIsDragging = false; 146 | break; 147 | case MotionEvent.ACTION_MOVE: 148 | final float x = getActiveX(ev); 149 | final float y = getActiveY(ev); 150 | final float dx = x - mLastTouchX, dy = y - mLastTouchY; 151 | 152 | if (!mIsDragging) { 153 | // Use Pythagoras to see if drag length is larger than 154 | // touch slop 155 | mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop; 156 | } 157 | 158 | if (mIsDragging) { 159 | mListener.onDrag(dx, dy); 160 | mLastTouchX = x; 161 | mLastTouchY = y; 162 | 163 | if (null != mVelocityTracker) { 164 | mVelocityTracker.addMovement(ev); 165 | } 166 | } 167 | break; 168 | case MotionEvent.ACTION_CANCEL: 169 | mActivePointerId = INVALID_POINTER_ID; 170 | // Recycle Velocity Tracker 171 | if (null != mVelocityTracker) { 172 | mVelocityTracker.recycle(); 173 | mVelocityTracker = null; 174 | } 175 | break; 176 | case MotionEvent.ACTION_UP: 177 | mActivePointerId = INVALID_POINTER_ID; 178 | if (mIsDragging) { 179 | if (null != mVelocityTracker) { 180 | mLastTouchX = getActiveX(ev); 181 | mLastTouchY = getActiveY(ev); 182 | 183 | // Compute velocity within the last 1000ms 184 | mVelocityTracker.addMovement(ev); 185 | mVelocityTracker.computeCurrentVelocity(1000); 186 | 187 | final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker 188 | .getYVelocity(); 189 | 190 | // If the velocity is greater than minVelocity, call 191 | // listener 192 | if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { 193 | mListener.onFling(mLastTouchX, mLastTouchY, -vX, 194 | -vY); 195 | } 196 | } 197 | } 198 | 199 | // Recycle Velocity Tracker 200 | if (null != mVelocityTracker) { 201 | mVelocityTracker.recycle(); 202 | mVelocityTracker = null; 203 | } 204 | break; 205 | case MotionEvent.ACTION_POINTER_UP: 206 | final int pointerIndex = Util.getPointerIndex(ev.getAction()); 207 | final int pointerId = ev.getPointerId(pointerIndex); 208 | if (pointerId == mActivePointerId) { 209 | // This was our active pointer going up. Choose a new 210 | // active pointer and adjust accordingly. 211 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 212 | mActivePointerId = ev.getPointerId(newPointerIndex); 213 | mLastTouchX = ev.getX(newPointerIndex); 214 | mLastTouchY = ev.getY(newPointerIndex); 215 | } 216 | break; 217 | } 218 | 219 | mActivePointerIndex = ev 220 | .findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId 221 | : 0); 222 | return true; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/DragCloseHelper.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ValueAnimator; 5 | import android.app.Activity; 6 | import android.content.Context; 7 | import android.view.MotionEvent; 8 | import android.view.View; 9 | import android.view.ViewConfiguration; 10 | import android.view.animation.LinearInterpolator; 11 | 12 | import androidx.annotation.FloatRange; 13 | 14 | /** 15 | * @author wuzy 16 | * @date 2019/4/30 17 | * @description 拖拽图片关闭类 18 | */ 19 | public class DragCloseHelper { 20 | 21 | private final static long DURATION = 100; 22 | private static final float MIN_SCALE = 0.4F; 23 | private final static int MAX_EXIT_Y = 500; 24 | 25 | private float mMinScale = MIN_SCALE; 26 | private int mMaxExitY = MAX_EXIT_Y; 27 | private ViewConfiguration mViewConfiguration; 28 | private boolean mIsSwipeToClose; 29 | private float mLastX, mLastRawY, mLastY, mLastRawX; 30 | private int mLastPointerId; 31 | private float mCurrentTranslationY, mCurrentTranslationX; 32 | private float mLastTranslationY, mLastTranslationX; 33 | private boolean mIsResetingAnimate = false; 34 | private boolean mIsShareElementMode = false; 35 | 36 | private View mParentView, mChildView; 37 | 38 | private OnDragCloseListener mOnDragCloseListener; 39 | private Context mContext; 40 | 41 | public DragCloseHelper(Context context) { 42 | this.mContext = context; 43 | mViewConfiguration = ViewConfiguration.get(context); 44 | } 45 | 46 | public void setOnDragCloseListener(OnDragCloseListener onDragCloseListener) { 47 | mOnDragCloseListener = onDragCloseListener; 48 | } 49 | 50 | /** 51 | * 设置拖拽关闭的view 52 | * 53 | * @param parentV 54 | * @param childV 55 | */ 56 | public void setDragCloseViews(View parentV, View childV) { 57 | this.mParentView = parentV; 58 | this.mChildView = childV; 59 | } 60 | 61 | /** 62 | * 设置最大退出距离 63 | * 64 | * @param maxExitY 65 | */ 66 | public void setMaxExitY(int maxExitY) { 67 | this.mMaxExitY = maxExitY; 68 | } 69 | 70 | /** 71 | * 设置最小缩放尺寸 72 | * 73 | * @param minScale 74 | */ 75 | public void setMinScale(@FloatRange(from = 0.1f, to = 1.0f) float minScale) { 76 | this.mMinScale = minScale; 77 | } 78 | 79 | public void setShareElementMode(boolean shareElementMode) { 80 | mIsShareElementMode = shareElementMode; 81 | } 82 | 83 | public boolean handleEvent(MotionEvent event) { 84 | if (mOnDragCloseListener != null && mOnDragCloseListener.intercept()) { 85 | //拦截 86 | mIsSwipeToClose = false; 87 | return false; 88 | } else { 89 | //不拦截 90 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 91 | //初始化数据 92 | mLastPointerId = event.getPointerId(0); 93 | reset(event); 94 | } else if (event.getAction() == MotionEvent.ACTION_MOVE) { 95 | if (event.getPointerCount() > 1) { 96 | //如果有多个手指 97 | if (mIsSwipeToClose) { 98 | //已经开始滑动关闭,恢复原状,否则需要派发事件 99 | mIsSwipeToClose = false; 100 | resetCallBackAnimation(); 101 | return true; 102 | } 103 | reset(event); 104 | return false; 105 | } 106 | if (mLastPointerId != event.getPointerId(0)) { 107 | //手指不一致,恢复原状 108 | if (mIsSwipeToClose) { 109 | resetCallBackAnimation(); 110 | } 111 | reset(event); 112 | return true; 113 | } 114 | float currentY = event.getY(); 115 | float currentX = event.getX(); 116 | if (mIsSwipeToClose || Math.abs(currentY - mLastY) > 2 * mViewConfiguration.getScaledTouchSlop()) { 117 | //已经触发或者开始触发,更新view 118 | mLastY = currentY; 119 | mLastX = currentX; 120 | float currentRawY = event.getRawY(); 121 | float currentRawX = event.getRawX(); 122 | if (!mIsSwipeToClose) { 123 | //准备开始 124 | mIsSwipeToClose = true; 125 | if (mOnDragCloseListener != null) { 126 | mOnDragCloseListener.onDragBegin(); 127 | } 128 | } 129 | //已经开始,更新view 130 | mCurrentTranslationY = currentRawY - mLastRawY + mLastTranslationY; 131 | mCurrentTranslationX = currentRawX - mLastRawX + mLastTranslationX; 132 | float percent = 1 - Math.abs(mCurrentTranslationY / (mMaxExitY + mChildView.getHeight())); 133 | if (percent > 1) { 134 | percent = 1; 135 | } else if (percent < 0) { 136 | percent = 0; 137 | } 138 | mParentView.getBackground().mutate().setAlpha((int) (percent * 255)); 139 | if (mOnDragCloseListener != null) { 140 | mOnDragCloseListener.onDragging(percent); 141 | } 142 | mChildView.setTranslationY(mCurrentTranslationY); 143 | mChildView.setTranslationX(mCurrentTranslationX); 144 | if (percent < mMinScale) { 145 | percent = mMinScale; 146 | } 147 | mChildView.setScaleX(percent); 148 | mChildView.setScaleY(percent); 149 | return true; 150 | } 151 | } else if (event.getAction() == MotionEvent.ACTION_UP) { 152 | //手指抬起事件 153 | if (mIsSwipeToClose) { 154 | if (mCurrentTranslationY > mMaxExitY) { 155 | if (mIsShareElementMode) { 156 | //会执行共享元素的离开动画 157 | if (mOnDragCloseListener != null) { 158 | mOnDragCloseListener.onDragEnd(true); 159 | } 160 | } else { 161 | //会执行定制的离开动画 162 | exitWithTranslation(mCurrentTranslationY); 163 | } 164 | } else { 165 | resetCallBackAnimation(); 166 | } 167 | mIsSwipeToClose = false; 168 | return true; 169 | } 170 | } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { 171 | //取消事件 172 | if (mIsSwipeToClose) { 173 | resetCallBackAnimation(); 174 | mIsSwipeToClose = false; 175 | return true; 176 | } 177 | } 178 | } 179 | return false; 180 | } 181 | 182 | 183 | private void reset(MotionEvent event) { 184 | mIsSwipeToClose = false; 185 | mLastY = event.getY(); 186 | mLastX = event.getX(); 187 | mLastRawY = event.getRawY(); 188 | mLastRawX = event.getRawX(); 189 | mLastTranslationY = 0; 190 | mLastTranslationX = 0; 191 | } 192 | 193 | /** 194 | * 更新缩放的view 195 | */ 196 | private void updateChildView(float transX, float transY) { 197 | mChildView.setTranslationY(transY); 198 | mChildView.setTranslationX(transX); 199 | float percent = Math.abs(transY / (mMaxExitY + mChildView.getHeight())); 200 | float scale = 1 - percent; 201 | if (scale < mMinScale) { 202 | scale = mMinScale; 203 | } 204 | mChildView.setScaleX(scale); 205 | mChildView.setScaleY(scale); 206 | } 207 | 208 | /** 209 | * 恢复到原位动画 210 | */ 211 | private void resetCallBackAnimation() { 212 | if (mIsResetingAnimate || mCurrentTranslationY == 0) { 213 | return; 214 | } 215 | final float ratio = mCurrentTranslationX / mCurrentTranslationY; 216 | ValueAnimator animatorY = ValueAnimator.ofFloat(mCurrentTranslationY, 0); 217 | animatorY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 218 | @Override 219 | public void onAnimationUpdate(ValueAnimator animation) { 220 | if (mIsResetingAnimate) { 221 | mCurrentTranslationY = (float) animation.getAnimatedValue(); 222 | mCurrentTranslationX = ratio * mCurrentTranslationY; 223 | mLastTranslationY = mCurrentTranslationY; 224 | mLastTranslationX = mCurrentTranslationX; 225 | updateChildView(mLastTranslationX, mCurrentTranslationY); 226 | } 227 | } 228 | }); 229 | animatorY.addListener(new Animator.AnimatorListener() { 230 | @Override 231 | public void onAnimationStart(Animator animation) { 232 | mIsResetingAnimate = true; 233 | } 234 | 235 | @Override 236 | public void onAnimationEnd(Animator animation) { 237 | if (mIsResetingAnimate) { 238 | mParentView.getBackground().mutate().setAlpha(255); 239 | mCurrentTranslationY = 0; 240 | mCurrentTranslationX = 0; 241 | mIsResetingAnimate = false; 242 | if (mOnDragCloseListener != null) { 243 | mOnDragCloseListener.onDragCancel(); 244 | } 245 | } 246 | } 247 | 248 | @Override 249 | public void onAnimationCancel(Animator animation) { 250 | 251 | } 252 | 253 | @Override 254 | public void onAnimationRepeat(Animator animation) { 255 | 256 | } 257 | }); 258 | animatorY.setDuration(DURATION).start(); 259 | } 260 | 261 | public void exitWithTranslation(float currentY) { 262 | int targetValue = currentY > 0 ? mChildView.getHeight() : -mChildView.getHeight(); 263 | ValueAnimator anim = ValueAnimator.ofFloat(mCurrentTranslationY, targetValue); 264 | anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 265 | @Override 266 | public void onAnimationUpdate(ValueAnimator animation) { 267 | updateChildView(mCurrentTranslationX, (float) animation.getAnimatedValue()); 268 | } 269 | }); 270 | anim.addListener(new Animator.AnimatorListener() { 271 | @Override 272 | public void onAnimationStart(Animator animation) { 273 | 274 | } 275 | 276 | @Override 277 | public void onAnimationEnd(Animator animation) { 278 | if (mOnDragCloseListener != null) { 279 | mOnDragCloseListener.onDragEnd(false); 280 | } 281 | ((Activity) mContext).finish(); 282 | ((Activity) mContext).overridePendingTransition(R.anim.dchlib_anim_empty, R.anim.dchlib_anim_alpha_out_long_time); 283 | } 284 | 285 | @Override 286 | public void onAnimationCancel(Animator animation) { 287 | 288 | } 289 | 290 | @Override 291 | public void onAnimationRepeat(Animator animation) { 292 | 293 | } 294 | }); 295 | anim.setDuration(DURATION); 296 | anim.setInterpolator(new LinearInterpolator()); 297 | anim.start(); 298 | } 299 | 300 | } 301 | 302 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/OnDragCloseListener.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex; 2 | 3 | /** 4 | * @author wuzy 5 | * @date 2019/4/30 6 | * @description 7 | */ 8 | public interface OnDragCloseListener { 9 | 10 | void onDragBegin(); 11 | 12 | void onDragging(float percent); 13 | 14 | void onDragEnd(boolean isShareElementMode); 15 | 16 | void onDragCancel(); 17 | 18 | boolean intercept(); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/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 com.wuzy.photoviewex; 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 | void onRotate(int degrees, int pivotX, int pivotY); 28 | 29 | void onToRightAngle(int pivotX, int pivotY); 30 | 31 | } -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/OnMatrixChangedListener.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex; 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 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/OnOutsidePhotoTapListener.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex; 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 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/OnPhotoTapListener.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex; 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 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/OnRotateListener.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex; 2 | 3 | /** 4 | * @author wuzy 5 | * @date 2019/4/29 6 | * @description A callback to be invoked when the ImageView is rotated. 7 | **/ 8 | public interface OnRotateListener { 9 | 10 | void onRotate(int degrees); 11 | } 12 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/OnScaleChangedListener.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex; 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 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/OnSingleFlingListener.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex; 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 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/OnViewDragListener.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex; 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 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/OnViewTapListener.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex; 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 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/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 com.wuzy.photoviewex; 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 | import android.view.View; 26 | import android.widget.ImageView; 27 | 28 | import androidx.appcompat.widget.AppCompatImageView; 29 | 30 | /** 31 | * A zoomable ImageView. See {@link PhotoViewAttacher} for most of the details on how the zooming 32 | * is accomplished 33 | */ 34 | @SuppressWarnings("unused") 35 | public class PhotoView extends AppCompatImageView { 36 | 37 | private PhotoViewAttacher attacher; 38 | private ImageView.ScaleType pendingScaleType; 39 | 40 | public PhotoView(Context context) { 41 | this(context, null); 42 | } 43 | 44 | public PhotoView(Context context, AttributeSet attr) { 45 | this(context, attr, 0); 46 | } 47 | 48 | public PhotoView(Context context, AttributeSet attr, int defStyle) { 49 | super(context, attr, defStyle); 50 | init(); 51 | } 52 | 53 | private void init() { 54 | attacher = new PhotoViewAttacher(this); 55 | //We always pose as a Matrix scale type, though we can change to another scale type 56 | //via the attacher 57 | super.setScaleType(ImageView.ScaleType.MATRIX); 58 | //apply the previously applied scale type 59 | if (pendingScaleType != null) { 60 | setScaleType(pendingScaleType); 61 | pendingScaleType = null; 62 | } 63 | } 64 | 65 | /** 66 | * Get the current {@link PhotoViewAttacher} for this view. Be wary of holding on to references 67 | * to this attacher, as it has a reference to this view, which, if a reference is held in the 68 | * wrong place, can cause memory leaks. 69 | * 70 | * @return the attacher. 71 | */ 72 | public PhotoViewAttacher getAttacher() { 73 | return attacher; 74 | } 75 | 76 | @Override 77 | public ImageView.ScaleType getScaleType() { 78 | return attacher.getScaleType(); 79 | } 80 | 81 | @Override 82 | public Matrix getImageMatrix() { 83 | return attacher.getImageMatrix(); 84 | } 85 | 86 | @Override 87 | public void setOnLongClickListener(View.OnLongClickListener l) { 88 | attacher.setOnLongClickListener(l); 89 | } 90 | 91 | @Override 92 | public void setOnClickListener(View.OnClickListener l) { 93 | attacher.setOnClickListener(l); 94 | } 95 | 96 | @Override 97 | public void setScaleType(ImageView.ScaleType scaleType) { 98 | if (attacher == null) { 99 | pendingScaleType = scaleType; 100 | } else { 101 | attacher.setScaleType(scaleType); 102 | } 103 | } 104 | 105 | @Override 106 | public void setImageDrawable(Drawable drawable) { 107 | super.setImageDrawable(drawable); 108 | // setImageBitmap calls through to this method 109 | if (attacher != null) { 110 | attacher.update(); 111 | } 112 | } 113 | 114 | @Override 115 | public void setImageResource(int resId) { 116 | super.setImageResource(resId); 117 | if (attacher != null) { 118 | attacher.update(); 119 | } 120 | } 121 | 122 | @Override 123 | public void setImageURI(Uri uri) { 124 | super.setImageURI(uri); 125 | if (attacher != null) { 126 | attacher.update(); 127 | } 128 | } 129 | 130 | @Override 131 | protected boolean setFrame(int l, int t, int r, int b) { 132 | boolean changed = super.setFrame(l, t, r, b); 133 | if (changed) { 134 | attacher.update(); 135 | } 136 | return changed; 137 | } 138 | 139 | public void setRotationTo(float rotationDegree) { 140 | attacher.setRotationTo(rotationDegree); 141 | } 142 | 143 | public void setRotationBy(float rotationDegree) { 144 | attacher.setRotationBy(rotationDegree); 145 | } 146 | 147 | public boolean isZoomable() { 148 | return attacher.isZoomable(); 149 | } 150 | 151 | public void setZoomable(boolean zoomable) { 152 | attacher.setZoomable(zoomable); 153 | } 154 | 155 | public void setRotatable(boolean rotatable) { 156 | attacher.setRotatable(rotatable); 157 | } 158 | 159 | public boolean isRotatable() { 160 | return attacher.isRotatable(); 161 | } 162 | 163 | public RectF getDisplayRect() { 164 | return attacher.getDisplayRect(); 165 | } 166 | 167 | public void getDisplayMatrix(Matrix matrix) { 168 | attacher.getDisplayMatrix(matrix); 169 | } 170 | 171 | @SuppressWarnings("UnusedReturnValue") public boolean setDisplayMatrix(Matrix finalRectangle) { 172 | return attacher.setDisplayMatrix(finalRectangle); 173 | } 174 | 175 | public void getSuppMatrix(Matrix matrix) { 176 | attacher.getSuppMatrix(matrix); 177 | } 178 | 179 | public boolean setSuppMatrix(Matrix matrix) { 180 | return attacher.setDisplayMatrix(matrix); 181 | } 182 | 183 | public float getMinimumScale() { 184 | return attacher.getMinimumScale(); 185 | } 186 | 187 | public float getMediumScale() { 188 | return attacher.getMediumScale(); 189 | } 190 | 191 | public float getMaximumScale() { 192 | return attacher.getMaximumScale(); 193 | } 194 | 195 | public float getScale() { 196 | return attacher.getScale(); 197 | } 198 | 199 | public void setAllowParentInterceptOnEdge(boolean allow) { 200 | attacher.setAllowParentInterceptOnEdge(allow); 201 | } 202 | 203 | public void setMinimumScale(float minimumScale) { 204 | attacher.setMinimumScale(minimumScale); 205 | } 206 | 207 | public void setMediumScale(float mediumScale) { 208 | attacher.setMediumScale(mediumScale); 209 | } 210 | 211 | public void setMaximumScale(float maximumScale) { 212 | attacher.setMaximumScale(maximumScale); 213 | } 214 | 215 | public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { 216 | attacher.setScaleLevels(minimumScale, mediumScale, maximumScale); 217 | } 218 | 219 | public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { 220 | attacher.setOnMatrixChangeListener(listener); 221 | } 222 | 223 | public void setOnPhotoTapListener(OnPhotoTapListener listener) { 224 | attacher.setOnPhotoTapListener(listener); 225 | } 226 | 227 | public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener listener) { 228 | attacher.setOnOutsidePhotoTapListener(listener); 229 | } 230 | 231 | public void setOnViewTapListener(OnViewTapListener listener) { 232 | attacher.setOnViewTapListener(listener); 233 | } 234 | 235 | public void setOnViewDragListener(OnViewDragListener listener) { 236 | attacher.setOnViewDragListener(listener); 237 | } 238 | 239 | public void setScale(float scale) { 240 | attacher.setScale(scale); 241 | } 242 | 243 | public void setScale(float scale, boolean animate) { 244 | attacher.setScale(scale, animate); 245 | } 246 | 247 | public void setScale(float scale, float focalX, float focalY, boolean animate) { 248 | attacher.setScale(scale, focalX, focalY, animate); 249 | } 250 | 251 | public void setZoomTransitionDuration(int milliseconds) { 252 | attacher.setZoomTransitionDuration(milliseconds); 253 | } 254 | 255 | public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener onDoubleTapListener) { 256 | attacher.setOnDoubleTapListener(onDoubleTapListener); 257 | } 258 | 259 | public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangedListener) { 260 | attacher.setOnScaleChangeListener(onScaleChangedListener); 261 | } 262 | 263 | public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { 264 | attacher.setOnSingleFlingListener(onSingleFlingListener); 265 | } 266 | 267 | public void setOnRotateListener(OnRotateListener onRotateListener) { 268 | attacher.setOnRotateListener(onRotateListener); 269 | } 270 | 271 | 272 | } 273 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/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 com.wuzy.photoviewex; 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.util.Log; 24 | import android.view.GestureDetector; 25 | import android.view.MotionEvent; 26 | import android.view.View; 27 | import android.view.View.OnLongClickListener; 28 | import android.view.ViewParent; 29 | import android.view.animation.AccelerateDecelerateInterpolator; 30 | import android.view.animation.Interpolator; 31 | import android.widget.ImageView; 32 | import android.widget.ImageView.ScaleType; 33 | import android.widget.OverScroller; 34 | 35 | /** 36 | * The component of {@link PhotoView} 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 = 3.0f; 44 | private static float DEFAULT_MID_SCALE = 1.75f; 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 mCustomGestureDetector; 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 | private OnRotateListener mOnRotateListener; 91 | 92 | private FlingRunnable mCurrentFlingRunnable; 93 | private RightAngleRunnable mRightAngleRunnable; 94 | private int mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; 95 | private int mVerticalScrollEdge = VERTICAL_EDGE_BOTH; 96 | private float mBaseRotation; 97 | 98 | private boolean mZoomEnabled = true; 99 | private boolean mRotateEnabled = true; 100 | private boolean mIsToRightAngle = true; 101 | private boolean mIsToRighting = true; 102 | private ScaleType mScaleType = ScaleType.FIT_CENTER; 103 | 104 | private OnGestureListener onGestureListener = new OnGestureListener() { 105 | @Override 106 | public void onDrag(float dx, float dy) { 107 | if (mCustomGestureDetector.isScaling()) { 108 | return; // Do not drag if we are already scaling 109 | } 110 | if (mOnViewDragListener != null) { 111 | mOnViewDragListener.onDrag(dx, dy); 112 | } 113 | mSuppMatrix.postTranslate(dx, dy); 114 | checkAndDisplayMatrix(); 115 | 116 | /* 117 | * Here we decide whether to let the ImageView's parent to start taking 118 | * over the touch event. 119 | * 120 | * First we check whether this function is enabled. We never want the 121 | * parent to take over if we're scaling. We then check the edge we're 122 | * on, and the direction of the scroll (i.e. if we're pulling against 123 | * the edge, aka 'overscrolling', let the parent take over). 124 | */ 125 | ViewParent parent = mImageView.getParent(); 126 | if (mAllowParentInterceptOnEdge && !mCustomGestureDetector.isScaling() && !mBlockParentIntercept) { 127 | if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH 128 | || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f) 129 | || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f) 130 | || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f) 131 | || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f)) { 132 | if (parent != null) { 133 | parent.requestDisallowInterceptTouchEvent(false); 134 | } 135 | } 136 | } else { 137 | if (parent != null) { 138 | parent.requestDisallowInterceptTouchEvent(true); 139 | } 140 | } 141 | } 142 | 143 | @Override 144 | public void onFling(float startX, float startY, float velocityX, float velocityY) { 145 | mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext()); 146 | mCurrentFlingRunnable.fling(getImageViewWidth(mImageView), 147 | getImageViewHeight(mImageView), (int) velocityX, (int) velocityY); 148 | mImageView.post(mCurrentFlingRunnable); 149 | } 150 | 151 | @Override 152 | public void onScale(float scaleFactor, float focusX, float focusY) { 153 | if (getScale() < mMaxScale || scaleFactor < 1f) { 154 | if (mScaleChangeListener != null) { 155 | mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); 156 | } 157 | mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); 158 | checkAndDisplayMatrix(); 159 | } 160 | } 161 | 162 | @Override 163 | public void onRotate(int degrees, int pivotX, int pivotY) { 164 | if (degrees < 1 && degrees > -1) { 165 | return; 166 | } 167 | if (mRightAngleRunnable != null && mIsToRighting) { 168 | mImageView.removeCallbacks(mRightAngleRunnable); 169 | } 170 | mSuppMatrix.postRotate(degrees, pivotX, pivotY); 171 | if (mOnRotateListener != null) { 172 | mOnRotateListener.onRotate(degrees); 173 | } 174 | checkAndDisplayMatrix(); 175 | } 176 | 177 | @Override 178 | public void onToRightAngle(int pivotX, int pivotY) { 179 | if (mIsToRightAngle) { 180 | float[] v = new float[9]; 181 | mSuppMatrix.getValues(v); 182 | // calculate the degree of rotation 183 | int angle = (int) (Math.round(Math.atan2(v[Matrix.MSKEW_X], v[Matrix.MSCALE_X]) * (180 / Math.PI))); 184 | if (angle <= 0) { 185 | angle = -angle; 186 | } else { 187 | angle = 360 - angle; 188 | } 189 | mRightAngleRunnable = new RightAngleRunnable(angle, pivotX, pivotY); 190 | mImageView.post(mRightAngleRunnable); 191 | } 192 | } 193 | 194 | }; 195 | 196 | public PhotoViewAttacher(ImageView imageView) { 197 | mImageView = imageView; 198 | imageView.setOnTouchListener(this); 199 | imageView.addOnLayoutChangeListener(this); 200 | if (imageView.isInEditMode()) { 201 | return; 202 | } 203 | mBaseRotation = 0.0f; 204 | // Create Gesture Detectors... 205 | mCustomGestureDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener); 206 | mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() { 207 | 208 | // forward long click listener 209 | @Override 210 | public void onLongPress(MotionEvent e) { 211 | if (mLongClickListener != null) { 212 | mLongClickListener.onLongClick(mImageView); 213 | } 214 | } 215 | 216 | @Override 217 | public boolean onFling(MotionEvent e1, MotionEvent e2, 218 | float velocityX, float velocityY) { 219 | if (mSingleFlingListener != null) { 220 | if (getScale() > DEFAULT_MIN_SCALE) { 221 | return false; 222 | } 223 | if (e1.getPointerCount() > SINGLE_TOUCH 224 | || e2.getPointerCount() > SINGLE_TOUCH) { 225 | return false; 226 | } 227 | return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY); 228 | } 229 | return false; 230 | } 231 | }); 232 | mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { 233 | @Override 234 | public boolean onSingleTapConfirmed(MotionEvent e) { 235 | if (mOnClickListener != null) { 236 | mOnClickListener.onClick(mImageView); 237 | } 238 | final RectF displayRect = getDisplayRect(); 239 | final float x = e.getX(), y = e.getY(); 240 | if (mViewTapListener != null) { 241 | mViewTapListener.onViewTap(mImageView, x, y); 242 | } 243 | if (displayRect != null) { 244 | // Check to see if the user tapped on the photo 245 | if (displayRect.contains(x, y)) { 246 | float xResult = (x - displayRect.left) 247 | / displayRect.width(); 248 | float yResult = (y - displayRect.top) 249 | / displayRect.height(); 250 | if (mPhotoTapListener != null) { 251 | mPhotoTapListener.onPhotoTap(mImageView, xResult, yResult); 252 | } 253 | return true; 254 | } else { 255 | if (mOutsidePhotoTapListener != null) { 256 | mOutsidePhotoTapListener.onOutsidePhotoTap(mImageView); 257 | } 258 | } 259 | } 260 | return false; 261 | } 262 | 263 | @Override 264 | public boolean onDoubleTap(MotionEvent ev) { 265 | try { 266 | float scale = getScale(); 267 | float x = ev.getX(); 268 | float y = ev.getY(); 269 | if (scale < getMediumScale()) { 270 | setScale(getMediumScale(), x, y, true); 271 | } else if (scale >= getMediumScale() && scale < getMaximumScale()) { 272 | setScale(getMaximumScale(), x, y, true); 273 | } else { 274 | setScale(getMinimumScale(), x, y, true); 275 | } 276 | } catch (ArrayIndexOutOfBoundsException e) { 277 | // Can sometimes happen when getX() and getY() is called 278 | } 279 | return true; 280 | } 281 | 282 | @Override 283 | public boolean onDoubleTapEvent(MotionEvent e) { 284 | // Wait for the confirmed onDoubleTap() instead 285 | return false; 286 | } 287 | }); 288 | } 289 | 290 | public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) { 291 | this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener); 292 | } 293 | 294 | public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangeListener) { 295 | this.mScaleChangeListener = onScaleChangeListener; 296 | } 297 | 298 | public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { 299 | this.mSingleFlingListener = onSingleFlingListener; 300 | } 301 | 302 | public void setOnRotateListener(OnRotateListener onRotateListener) { 303 | this.mOnRotateListener = onRotateListener; 304 | } 305 | 306 | @Deprecated 307 | public boolean isZoomEnabled() { 308 | return mZoomEnabled; 309 | } 310 | 311 | public RectF getDisplayRect() { 312 | checkMatrixBounds(); 313 | return getDisplayRect(getDrawMatrix()); 314 | } 315 | 316 | public boolean setDisplayMatrix(Matrix finalMatrix) { 317 | if (finalMatrix == null) { 318 | throw new IllegalArgumentException("Matrix cannot be null"); 319 | } 320 | if (mImageView.getDrawable() == null) { 321 | return false; 322 | } 323 | mSuppMatrix.set(finalMatrix); 324 | checkAndDisplayMatrix(); 325 | return true; 326 | } 327 | 328 | public void setBaseRotation(final float degrees) { 329 | mBaseRotation = degrees % 360; 330 | update(); 331 | setRotationBy(mBaseRotation); 332 | checkAndDisplayMatrix(); 333 | } 334 | 335 | public void setRotationTo(float degrees) { 336 | mSuppMatrix.setRotate(degrees % 360); 337 | checkAndDisplayMatrix(); 338 | } 339 | 340 | public void setRotationBy(float degrees) { 341 | mSuppMatrix.postRotate(degrees % 360); 342 | checkAndDisplayMatrix(); 343 | } 344 | 345 | public float getMinimumScale() { 346 | return mMinScale; 347 | } 348 | 349 | public float getMediumScale() { 350 | return mMidScale; 351 | } 352 | 353 | public float getMaximumScale() { 354 | return mMaxScale; 355 | } 356 | 357 | public float getScale() { 358 | return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow 359 | (getValue(mSuppMatrix, Matrix.MSKEW_Y), 2)); 360 | } 361 | 362 | public ScaleType getScaleType() { 363 | return mScaleType; 364 | } 365 | 366 | @Override 367 | public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int 368 | oldRight, int oldBottom) { 369 | // Update our base matrix, as the bounds have changed 370 | if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { 371 | updateBaseMatrix(mImageView.getDrawable()); 372 | } 373 | } 374 | 375 | @Override 376 | public boolean onTouch(View v, MotionEvent ev) { 377 | boolean handled = false; 378 | if (mZoomEnabled && Util.hasDrawable((ImageView) v)) { 379 | switch (ev.getAction()) { 380 | case MotionEvent.ACTION_DOWN: 381 | ViewParent parent = v.getParent(); 382 | // First, disable the Parent from intercepting the touch 383 | // event 384 | if (parent != null) { 385 | parent.requestDisallowInterceptTouchEvent(true); 386 | } 387 | // If we're flinging, and the user presses down, cancel 388 | // fling 389 | cancelFling(); 390 | break; 391 | case MotionEvent.ACTION_CANCEL: 392 | case MotionEvent.ACTION_UP: 393 | // If the user has zoomed less than min scale, zoom back 394 | // to min scale 395 | if (getScale() < mMinScale) { 396 | RectF rect = getDisplayRect(); 397 | if (rect != null) { 398 | v.post(new AnimatedZoomRunnable(getScale(), mMinScale, 399 | rect.centerX(), rect.centerY())); 400 | handled = true; 401 | } 402 | } else if (getScale() > mMaxScale) { 403 | RectF rect = getDisplayRect(); 404 | if (rect != null) { 405 | v.post(new AnimatedZoomRunnable(getScale(), mMaxScale, 406 | rect.centerX(), rect.centerY())); 407 | handled = true; 408 | } 409 | } 410 | break; 411 | } 412 | 413 | // Try the Scale/Drag detector 414 | if (mCustomGestureDetector != null) { 415 | boolean wasScaling = mCustomGestureDetector.isScaling(); 416 | boolean wasDragging = mCustomGestureDetector.isDragging(); 417 | boolean wasRotating = mCustomGestureDetector.isRotating(); 418 | handled = mCustomGestureDetector.onTouchEvent(ev); 419 | boolean didntScale = !wasScaling && !mCustomGestureDetector.isScaling(); 420 | boolean didntDrag = !wasDragging && !mCustomGestureDetector.isDragging(); 421 | boolean didntRotate = !wasRotating && !mCustomGestureDetector.isRotating(); 422 | mBlockParentIntercept = didntScale && didntDrag && didntRotate; 423 | } 424 | // Check to see if the user double tapped 425 | if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) { 426 | handled = true; 427 | } 428 | 429 | } 430 | return handled; 431 | } 432 | 433 | public void setAllowParentInterceptOnEdge(boolean allow) { 434 | mAllowParentInterceptOnEdge = allow; 435 | } 436 | 437 | public void setMinimumScale(float minimumScale) { 438 | Util.checkZoomLevels(minimumScale, mMidScale, mMaxScale); 439 | mMinScale = minimumScale; 440 | } 441 | 442 | public void setMediumScale(float mediumScale) { 443 | Util.checkZoomLevels(mMinScale, mediumScale, mMaxScale); 444 | mMidScale = mediumScale; 445 | } 446 | 447 | public void setMaximumScale(float maximumScale) { 448 | Util.checkZoomLevels(mMinScale, mMidScale, maximumScale); 449 | mMaxScale = maximumScale; 450 | } 451 | 452 | public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { 453 | Util.checkZoomLevels(minimumScale, mediumScale, maximumScale); 454 | mMinScale = minimumScale; 455 | mMidScale = mediumScale; 456 | mMaxScale = maximumScale; 457 | } 458 | 459 | public void setOnLongClickListener(OnLongClickListener listener) { 460 | mLongClickListener = listener; 461 | } 462 | 463 | public void setOnClickListener(View.OnClickListener listener) { 464 | mOnClickListener = listener; 465 | } 466 | 467 | public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { 468 | mMatrixChangeListener = listener; 469 | } 470 | 471 | public void setOnPhotoTapListener(OnPhotoTapListener listener) { 472 | mPhotoTapListener = listener; 473 | } 474 | 475 | public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener mOutsidePhotoTapListener) { 476 | this.mOutsidePhotoTapListener = mOutsidePhotoTapListener; 477 | } 478 | 479 | public void setOnViewTapListener(OnViewTapListener listener) { 480 | mViewTapListener = listener; 481 | } 482 | 483 | public void setOnViewDragListener(OnViewDragListener listener) { 484 | mOnViewDragListener = listener; 485 | } 486 | 487 | public void setScale(float scale) { 488 | setScale(scale, false); 489 | } 490 | 491 | public void setScale(float scale, boolean animate) { 492 | setScale(scale, 493 | (mImageView.getRight()) / 2, 494 | (mImageView.getBottom()) / 2, 495 | animate); 496 | } 497 | 498 | public void setScale(float scale, float focalX, float focalY, 499 | boolean animate) { 500 | // Check to see if the scale is within bounds 501 | if (scale < mMinScale || scale > mMaxScale) { 502 | throw new IllegalArgumentException("Scale must be within the range of minScale and maxScale"); 503 | } 504 | if (animate) { 505 | mImageView.post(new AnimatedZoomRunnable(getScale(), scale, 506 | focalX, focalY)); 507 | } else { 508 | mSuppMatrix.setScale(scale, scale, focalX, focalY); 509 | checkAndDisplayMatrix(); 510 | } 511 | } 512 | 513 | /** 514 | * Set the zoom interpolator 515 | * 516 | * @param interpolator the zoom interpolator 517 | */ 518 | public void setZoomInterpolator(Interpolator interpolator) { 519 | mInterpolator = interpolator; 520 | } 521 | 522 | public void setScaleType(ScaleType scaleType) { 523 | if (Util.isSupportedScaleType(scaleType) && scaleType != mScaleType) { 524 | mScaleType = scaleType; 525 | update(); 526 | } 527 | } 528 | 529 | public boolean isZoomable() { 530 | return mZoomEnabled; 531 | } 532 | 533 | public void setZoomable(boolean zoomable) { 534 | mZoomEnabled = zoomable; 535 | update(); 536 | } 537 | 538 | public boolean isRotatable() { 539 | return mRotateEnabled; 540 | } 541 | 542 | public void setRotatable(boolean rotatable) { 543 | mRotateEnabled = rotatable; 544 | } 545 | 546 | public void update() { 547 | if (mZoomEnabled) { 548 | // Update the base matrix using the current drawable 549 | updateBaseMatrix(mImageView.getDrawable()); 550 | } else { 551 | // Reset the Matrix... 552 | resetMatrix(); 553 | } 554 | } 555 | 556 | /** 557 | * Get the display matrix 558 | * 559 | * @param matrix target matrix to copy to 560 | */ 561 | public void getDisplayMatrix(Matrix matrix) { 562 | matrix.set(getDrawMatrix()); 563 | } 564 | 565 | /** 566 | * Get the current support matrix 567 | */ 568 | public void getSuppMatrix(Matrix matrix) { 569 | matrix.set(mSuppMatrix); 570 | } 571 | 572 | private Matrix getDrawMatrix() { 573 | mDrawMatrix.set(mBaseMatrix); 574 | mDrawMatrix.postConcat(mSuppMatrix); 575 | return mDrawMatrix; 576 | } 577 | 578 | public Matrix getImageMatrix() { 579 | return mDrawMatrix; 580 | } 581 | 582 | public void setZoomTransitionDuration(int milliseconds) { 583 | this.mZoomDuration = milliseconds; 584 | } 585 | 586 | /** 587 | * Helper method that 'unpacks' a Matrix and returns the required value 588 | * 589 | * @param matrix Matrix to unpack 590 | * @param whichValue Which value from Matrix.M* to return 591 | * @return returned value 592 | */ 593 | private float getValue(Matrix matrix, int whichValue) { 594 | matrix.getValues(mMatrixValues); 595 | return mMatrixValues[whichValue]; 596 | } 597 | 598 | /** 599 | * Resets the Matrix back to FIT_CENTER, and then displays its contents 600 | */ 601 | private void resetMatrix() { 602 | mSuppMatrix.reset(); 603 | setRotationBy(mBaseRotation); 604 | setImageViewMatrix(getDrawMatrix()); 605 | checkMatrixBounds(); 606 | } 607 | 608 | private void setImageViewMatrix(Matrix matrix) { 609 | mImageView.setImageMatrix(matrix); 610 | // Call MatrixChangedListener if needed 611 | if (mMatrixChangeListener != null) { 612 | RectF displayRect = getDisplayRect(matrix); 613 | if (displayRect != null) { 614 | mMatrixChangeListener.onMatrixChanged(displayRect); 615 | } 616 | } 617 | } 618 | 619 | /** 620 | * Helper method that simply checks the Matrix, and then displays the result 621 | */ 622 | private void checkAndDisplayMatrix() { 623 | if (checkMatrixBounds()) { 624 | setImageViewMatrix(getDrawMatrix()); 625 | } 626 | } 627 | 628 | /** 629 | * Helper method that maps the supplied Matrix to the current Drawable 630 | * 631 | * @param matrix - Matrix to map Drawable against 632 | * @return RectF - Displayed Rectangle 633 | */ 634 | private RectF getDisplayRect(Matrix matrix) { 635 | Drawable d = mImageView.getDrawable(); 636 | if (d != null) { 637 | mDisplayRect.set(0, 0, d.getIntrinsicWidth(), 638 | d.getIntrinsicHeight()); 639 | matrix.mapRect(mDisplayRect); 640 | return mDisplayRect; 641 | } 642 | return null; 643 | } 644 | 645 | /** 646 | * Calculate Matrix for FIT_CENTER 647 | * 648 | * @param drawable - Drawable being displayed 649 | */ 650 | private void updateBaseMatrix(Drawable drawable) { 651 | if (drawable == null) { 652 | return; 653 | } 654 | final float viewWidth = getImageViewWidth(mImageView); 655 | final float viewHeight = getImageViewHeight(mImageView); 656 | final int drawableWidth = drawable.getIntrinsicWidth(); 657 | final int drawableHeight = drawable.getIntrinsicHeight(); 658 | mBaseMatrix.reset(); 659 | final float widthScale = viewWidth / drawableWidth; 660 | final float heightScale = viewHeight / drawableHeight; 661 | if (mScaleType == ScaleType.CENTER) { 662 | mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, 663 | (viewHeight - drawableHeight) / 2F); 664 | 665 | } else if (mScaleType == ScaleType.CENTER_CROP) { 666 | float scale = Math.max(widthScale, heightScale); 667 | mBaseMatrix.postScale(scale, scale); 668 | mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, 669 | (viewHeight - drawableHeight * scale) / 2F); 670 | 671 | } else if (mScaleType == ScaleType.CENTER_INSIDE) { 672 | float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); 673 | mBaseMatrix.postScale(scale, scale); 674 | mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, 675 | (viewHeight - drawableHeight * scale) / 2F); 676 | 677 | } else { 678 | RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight); 679 | RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); 680 | if ((int) mBaseRotation % 180 != 0) { 681 | mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth); 682 | } 683 | switch (mScaleType) { 684 | case FIT_CENTER: 685 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); 686 | break; 687 | case FIT_START: 688 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); 689 | break; 690 | case FIT_END: 691 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); 692 | break; 693 | case FIT_XY: 694 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); 695 | break; 696 | default: 697 | break; 698 | } 699 | } 700 | resetMatrix(); 701 | } 702 | 703 | private boolean checkMatrixBounds() { 704 | final RectF rect = getDisplayRect(getDrawMatrix()); 705 | if (rect == null) { 706 | return false; 707 | } 708 | final float height = rect.height(), width = rect.width(); 709 | float deltaX = 0, deltaY = 0; 710 | final int viewHeight = getImageViewHeight(mImageView); 711 | if (height <= viewHeight) { 712 | switch (mScaleType) { 713 | case FIT_START: 714 | deltaY = -rect.top; 715 | break; 716 | case FIT_END: 717 | deltaY = viewHeight - height - rect.top; 718 | break; 719 | default: 720 | deltaY = (viewHeight - height) / 2 - rect.top; 721 | break; 722 | } 723 | mVerticalScrollEdge = VERTICAL_EDGE_BOTH; 724 | } else if (rect.top > 0) { 725 | mVerticalScrollEdge = VERTICAL_EDGE_TOP; 726 | deltaY = -rect.top; 727 | } else if (rect.bottom < viewHeight) { 728 | mVerticalScrollEdge = VERTICAL_EDGE_BOTTOM; 729 | deltaY = viewHeight - rect.bottom; 730 | } else { 731 | mVerticalScrollEdge = VERTICAL_EDGE_NONE; 732 | } 733 | final int viewWidth = getImageViewWidth(mImageView); 734 | if (width <= viewWidth) { 735 | switch (mScaleType) { 736 | case FIT_START: 737 | deltaX = -rect.left; 738 | break; 739 | case FIT_END: 740 | deltaX = viewWidth - width - rect.left; 741 | break; 742 | default: 743 | deltaX = (viewWidth - width) / 2 - rect.left; 744 | break; 745 | } 746 | mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; 747 | } else if (rect.left > 0) { 748 | mHorizontalScrollEdge = HORIZONTAL_EDGE_LEFT; 749 | deltaX = -rect.left; 750 | } else if (rect.right < viewWidth) { 751 | deltaX = viewWidth - rect.right; 752 | mHorizontalScrollEdge = HORIZONTAL_EDGE_RIGHT; 753 | } else { 754 | mHorizontalScrollEdge = HORIZONTAL_EDGE_NONE; 755 | } 756 | // Finally actually translate the matrix 757 | mSuppMatrix.postTranslate(deltaX, deltaY); 758 | return true; 759 | } 760 | 761 | private int getImageViewWidth(ImageView imageView) { 762 | return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight(); 763 | } 764 | 765 | private int getImageViewHeight(ImageView imageView) { 766 | return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom(); 767 | } 768 | 769 | private void cancelFling() { 770 | if (mCurrentFlingRunnable != null) { 771 | mCurrentFlingRunnable.cancelFling(); 772 | mCurrentFlingRunnable = null; 773 | } 774 | } 775 | 776 | 777 | private class AnimatedZoomRunnable implements Runnable { 778 | 779 | private final float mFocalX, mFocalY; 780 | private final long mStartTime; 781 | private final float mZoomStart, mZoomEnd; 782 | 783 | public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, 784 | final float focalX, final float focalY) { 785 | mFocalX = focalX; 786 | mFocalY = focalY; 787 | mStartTime = System.currentTimeMillis(); 788 | mZoomStart = currentZoom; 789 | mZoomEnd = targetZoom; 790 | } 791 | 792 | @Override 793 | public void run() { 794 | float t = interpolate(); 795 | float scale = mZoomStart + t * (mZoomEnd - mZoomStart); 796 | float deltaScale = scale / getScale(); 797 | onGestureListener.onScale(deltaScale, mFocalX, mFocalY); 798 | // We haven't hit our target scale yet, so post ourselves again 799 | if (t < 1f) { 800 | Compat.postOnAnimation(mImageView, this); 801 | } 802 | } 803 | 804 | private float interpolate() { 805 | float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration; 806 | t = Math.min(1f, t); 807 | t = mInterpolator.getInterpolation(t); 808 | return t; 809 | } 810 | } 811 | 812 | private class FlingRunnable implements Runnable { 813 | 814 | private final OverScroller mScroller; 815 | private int mCurrentX, mCurrentY; 816 | 817 | public FlingRunnable(Context context) { 818 | mScroller = new OverScroller(context); 819 | } 820 | 821 | public void cancelFling() { 822 | mScroller.forceFinished(true); 823 | } 824 | 825 | public void fling(int viewWidth, int viewHeight, int velocityX, 826 | int velocityY) { 827 | final RectF rect = getDisplayRect(); 828 | if (rect == null) { 829 | return; 830 | } 831 | final int startX = Math.round(-rect.left); 832 | final int minX, maxX, minY, maxY; 833 | if (viewWidth < rect.width()) { 834 | minX = 0; 835 | maxX = Math.round(rect.width() - viewWidth); 836 | } else { 837 | minX = maxX = startX; 838 | } 839 | final int startY = Math.round(-rect.top); 840 | if (viewHeight < rect.height()) { 841 | minY = 0; 842 | maxY = Math.round(rect.height() - viewHeight); 843 | } else { 844 | minY = maxY = startY; 845 | } 846 | mCurrentX = startX; 847 | mCurrentY = startY; 848 | // If we actually can move, fling the scroller 849 | if (startX != maxX || startY != maxY) { 850 | mScroller.fling(startX, startY, velocityX, velocityY, minX, 851 | maxX, minY, maxY, 0, 0); 852 | } 853 | } 854 | 855 | @Override 856 | public void run() { 857 | if (mScroller.isFinished()) { 858 | return; // remaining post that should not be handled 859 | } 860 | if (mScroller.computeScrollOffset()) { 861 | final int newX = mScroller.getCurrX(); 862 | final int newY = mScroller.getCurrY(); 863 | mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); 864 | checkAndDisplayMatrix(); 865 | mCurrentX = newX; 866 | mCurrentY = newY; 867 | // Post On animation 868 | Compat.postOnAnimation(mImageView, this); 869 | } 870 | } 871 | } 872 | 873 | /** 874 | * a RightAngleRunnable that finger lift rotate to 0,90,180,270 degree 875 | */ 876 | private class RightAngleRunnable implements Runnable { 877 | private static final int RECOVER_SPEED = 4; 878 | private int mOldDegree; 879 | private int mNeedToRotate; 880 | private int mRoPivotX; 881 | private int mRoPivotY; 882 | 883 | RightAngleRunnable(int degree, int pivotX, int pivotY) { 884 | this.mOldDegree = degree; 885 | this.mNeedToRotate = calDegree(degree) - mOldDegree; 886 | this.mRoPivotX = pivotX; 887 | this.mRoPivotY = pivotY; 888 | } 889 | 890 | /** 891 | * get right degree,when one finger lifts 892 | * 893 | * @param oldDegree current degree 894 | * @return 0, 90, 180, 270 according to oldDegree 895 | */ 896 | private int calDegree(int oldDegree) { 897 | int result; 898 | float n = (float) oldDegree / 45; 899 | if (n >= 0 && n < 1) { 900 | result = 0; 901 | } else if (n >= 1 && n <= 2.5) { 902 | result = 90; 903 | } else if (n > 2.5 && n < 5.5) { 904 | result = 180; 905 | } else if (n >= 5.5 && n <= 7) { 906 | result = 270; 907 | } else { 908 | result = 360; 909 | } 910 | return result; 911 | } 912 | 913 | @Override 914 | public void run() { 915 | if (mNeedToRotate == 0) { 916 | mIsToRighting = false; 917 | return; 918 | } 919 | if (mImageView == null) { 920 | mIsToRighting = false; 921 | return; 922 | } 923 | mIsToRighting = true; 924 | if (mNeedToRotate > 0) { 925 | // Clockwise rotation 926 | if (mNeedToRotate >= RECOVER_SPEED) { 927 | mSuppMatrix.postRotate(RECOVER_SPEED, mRoPivotX, mRoPivotY); 928 | mNeedToRotate -= RECOVER_SPEED; 929 | } else { 930 | mSuppMatrix.postRotate(mNeedToRotate, mRoPivotX, mRoPivotY); 931 | mNeedToRotate = 0; 932 | } 933 | } else if (mNeedToRotate < 0) { 934 | // Counterclockwise rotation 935 | if (mNeedToRotate <= -RECOVER_SPEED) { 936 | mSuppMatrix.postRotate(-RECOVER_SPEED, mRoPivotX, mRoPivotY); 937 | mNeedToRotate += RECOVER_SPEED; 938 | } else { 939 | mSuppMatrix.postRotate(mNeedToRotate, mRoPivotX, mRoPivotY); 940 | mNeedToRotate = 0; 941 | } 942 | } 943 | checkAndDisplayMatrix(); 944 | Compat.postOnAnimation(mImageView, this); 945 | } 946 | } 947 | } 948 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/RotateGestureDetector.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex; 2 | 3 | import android.content.Context; 4 | import android.view.MotionEvent; 5 | 6 | /** 7 | * @author wuzy 8 | * @date 2019/4/29 9 | * @description 10 | */ 11 | public class RotateGestureDetector { 12 | 13 | public interface OnRotateGestureListener { 14 | 15 | void onRotate(int degrees, int pivotX, int pivotY); 16 | 17 | void onToRightAngle(int pivotX, int pivotY); 18 | 19 | } 20 | 21 | private final Context mContext; 22 | private final OnRotateGestureListener mListener; 23 | 24 | private int mLastAngle = 0; 25 | private boolean mIsRotate; 26 | 27 | public RotateGestureDetector(Context context, OnRotateGestureListener listener) { 28 | this.mContext = context; 29 | this.mListener = listener; 30 | } 31 | 32 | public boolean onTouchEvent(MotionEvent event) { 33 | if (event.getPointerCount() != 2) { 34 | return false; 35 | } 36 | int pivotX = (int) (event.getX(0) + event.getX(1)) / 2; 37 | int pivotY = (int) (event.getY(0) + event.getY(1)) / 2; 38 | float deltaX = event.getX(0) - event.getX(1); 39 | float deltaY = event.getY(0) - event.getY(1); 40 | int degrees = (int) Math.round(Math.toDegrees(Math.atan2(deltaY, deltaX))); 41 | 42 | switch (event.getActionMasked()) { 43 | case MotionEvent.ACTION_DOWN: 44 | mLastAngle = degrees; 45 | mIsRotate = false; 46 | break; 47 | case MotionEvent.ACTION_UP: 48 | mIsRotate = false; 49 | break; 50 | case MotionEvent.ACTION_POINTER_DOWN: 51 | mLastAngle = degrees; 52 | mIsRotate = false; 53 | break; 54 | case MotionEvent.ACTION_CANCEL: 55 | case MotionEvent.ACTION_POINTER_UP: 56 | mIsRotate = false; 57 | toRightAngle(pivotX, pivotY); 58 | mLastAngle = degrees; 59 | break; 60 | case MotionEvent.ACTION_MOVE: 61 | mIsRotate = true; 62 | int degreesValue = degrees - mLastAngle; 63 | if (degreesValue > 45) { 64 | rotate(-5, pivotX, pivotY); 65 | } else if (degreesValue < -45) { 66 | rotate(5, pivotX, pivotY); 67 | } else { 68 | rotate(degreesValue, pivotX, pivotY); 69 | } 70 | mLastAngle = degrees; 71 | break; 72 | } 73 | 74 | return true; 75 | } 76 | 77 | private void toRightAngle(int pivotX, int pivotY) { 78 | if (mListener != null) { 79 | mListener.onToRightAngle(pivotX, pivotY); 80 | } 81 | } 82 | 83 | private void rotate(int degrees, int pivotX, int pivotY) { 84 | if (mListener != null) { 85 | mListener.onRotate(degrees, pivotX, pivotY); 86 | } 87 | } 88 | 89 | public boolean isRotating() { 90 | return mIsRotate; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /photoviewex/src/main/java/com/wuzy/photoviewex/Util.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex; 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 | } 31 | return true; 32 | } 33 | 34 | static int getPointerIndex(int action) { 35 | return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /photoviewex/src/main/res/anim/dchlib_anim_alpha_out_long_time.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /photoviewex/src/main/res/anim/dchlib_anim_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /photoviewex/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | photoviewex 3 | 4 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.compileSdkVersion 5 | defaultConfig { 6 | applicationId "com.wuzy.photoviewex.sample" 7 | minSdkVersion rootProject.ext.minSdkVersion 8 | targetSdkVersion rootProject.ext.targetSdkVersion 9 | versionCode 1 10 | versionName "1.0" 11 | } 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 16 | } 17 | } 18 | } 19 | 20 | dependencies { 21 | implementation "androidx.appcompat:appcompat:1.0.0" 22 | implementation "androidx.recyclerview:recyclerview:1.0.0" 23 | implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha5' 24 | 25 | implementation "com.google.android.material:material:1.0.0" 26 | 27 | implementation 'com.squareup.picasso:picasso:2.5.2' 28 | implementation project(':photoviewex') 29 | // implementation 'com.github.zywudev:PhotoViewEx:1.0.0' 30 | 31 | } 32 | -------------------------------------------------------------------------------- /sample/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 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /sample/src/main/java/com/wuzy/photoviewex/sample/ActivityTransitionActivity.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 com.wuzy.photoviewex.sample; 17 | 18 | import android.content.Intent; 19 | import android.os.Build; 20 | import android.os.Bundle; 21 | import android.view.View; 22 | import android.widget.Toast; 23 | 24 | import androidx.appcompat.app.AppCompatActivity; 25 | import androidx.core.app.ActivityOptionsCompat; 26 | import androidx.recyclerview.widget.GridLayoutManager; 27 | import androidx.recyclerview.widget.RecyclerView; 28 | 29 | public class ActivityTransitionActivity extends AppCompatActivity { 30 | 31 | @Override 32 | public void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_transition); 35 | 36 | RecyclerView list = findViewById(R.id.list); 37 | list.setLayoutManager(new GridLayoutManager(this, 2)); 38 | ImageAdapter imageAdapter = new ImageAdapter(new ImageAdapter.Listener() { 39 | @Override 40 | public void onImageClicked(View view) { 41 | transition(view); 42 | } 43 | }); 44 | list.setAdapter(imageAdapter); 45 | } 46 | 47 | private void transition(View view) { 48 | if (Build.VERSION.SDK_INT < 21) { 49 | Toast.makeText(ActivityTransitionActivity.this, "21+ only, keep out", Toast.LENGTH_SHORT).show(); 50 | } else { 51 | Intent intent = new Intent(ActivityTransitionActivity.this, ActivityTransitionToActivity.class); 52 | ActivityOptionsCompat options = ActivityOptionsCompat. 53 | makeSceneTransitionAnimation(ActivityTransitionActivity.this, view, getString(R.string.transition_test)); 54 | startActivity(intent, options.toBundle()); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /sample/src/main/java/com/wuzy/photoviewex/sample/ActivityTransitionToActivity.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex.sample; 2 | 3 | import android.os.Bundle; 4 | import android.view.MotionEvent; 5 | 6 | import com.wuzy.photoviewex.DragCloseHelper; 7 | import com.wuzy.photoviewex.OnDragCloseListener; 8 | import com.wuzy.photoviewex.PhotoView; 9 | 10 | import androidx.annotation.Nullable; 11 | import androidx.appcompat.app.AppCompatActivity; 12 | import androidx.constraintlayout.widget.ConstraintLayout; 13 | 14 | /** 15 | * Activity that gets transitioned to 16 | */ 17 | public class ActivityTransitionToActivity extends AppCompatActivity { 18 | 19 | private DragCloseHelper mDragCloseHelper; 20 | private PhotoView mPhotoView; 21 | private ConstraintLayout mConstraintLayout; 22 | 23 | @Override 24 | protected void onCreate(@Nullable Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.activity_transition_to); 27 | mConstraintLayout = findViewById(R.id.iv_preview_cl); 28 | mPhotoView = findViewById(R.id.iv_preview_iv); 29 | 30 | mDragCloseHelper = new DragCloseHelper(this); 31 | mDragCloseHelper.setShareElementMode(true); 32 | mDragCloseHelper.setDragCloseViews(mConstraintLayout,mPhotoView); 33 | mDragCloseHelper.setOnDragCloseListener(new OnDragCloseListener() { 34 | @Override 35 | public void onDragBegin() { 36 | 37 | } 38 | 39 | @Override 40 | public void onDragging(float percent) { 41 | 42 | } 43 | 44 | @Override 45 | public void onDragEnd(boolean isShareElementMode) { 46 | if (isShareElementMode) { 47 | onBackPressed(); 48 | } 49 | } 50 | 51 | @Override 52 | public void onDragCancel() { 53 | 54 | } 55 | 56 | @Override 57 | public boolean intercept() { 58 | // 默认false 59 | return false; 60 | } 61 | }); 62 | } 63 | 64 | @Override 65 | public boolean dispatchTouchEvent(MotionEvent ev) { 66 | if (mDragCloseHelper.handleEvent(ev)) { 67 | return true; 68 | } else { 69 | return super.dispatchTouchEvent(ev); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /sample/src/main/java/com/wuzy/photoviewex/sample/HackyDrawerLayout.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex.sample; 2 | 3 | import android.content.Context; 4 | import android.view.MotionEvent; 5 | 6 | import androidx.drawerlayout.widget.DrawerLayout; 7 | 8 | /** 9 | * Hacky fix for Issue #4 and 10 | * http://code.google.com/p/android/issues/detail?id=18990 11 | *

12 | * ScaleGestureDetector seems to mess up the touch events, which means that 13 | * ViewGroups which make use of onInterceptTouchEvent throw a lot of 14 | * IllegalArgumentException: pointerIndex out of range. 15 | *

16 | * There's not much I can do in my code for now, but we can mask the result by 17 | * just catching the problem and ignoring it. 18 | */ 19 | public class HackyDrawerLayout extends DrawerLayout { 20 | 21 | public HackyDrawerLayout(Context context) { 22 | super(context); 23 | } 24 | 25 | @Override 26 | public boolean onInterceptTouchEvent(MotionEvent ev) { 27 | try { 28 | return super.onInterceptTouchEvent(ev); 29 | } catch (IllegalArgumentException e) { 30 | e.printStackTrace(); 31 | return false; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sample/src/main/java/com/wuzy/photoviewex/sample/HackyViewPager.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex.sample; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.MotionEvent; 6 | 7 | import androidx.viewpager.widget.ViewPager; 8 | 9 | /** 10 | * Hacky fix for Issue #4 and 11 | * http://code.google.com/p/android/issues/detail?id=18990 12 | *

13 | * ScaleGestureDetector seems to mess up the touch events, which means that 14 | * ViewGroups which make use of onInterceptTouchEvent throw a lot of 15 | * IllegalArgumentException: pointerIndex out of range. 16 | *

17 | * There's not much I can do in my code for now, but we can mask the result by 18 | * just catching the problem and ignoring it. 19 | * 20 | * @author Chris Banes 21 | */ 22 | public class HackyViewPager extends ViewPager { 23 | 24 | public HackyViewPager(Context context) { 25 | super(context); 26 | } 27 | 28 | public HackyViewPager(Context context, AttributeSet attrs) { 29 | super(context, attrs); 30 | } 31 | 32 | @Override 33 | public boolean onInterceptTouchEvent(MotionEvent ev) { 34 | try { 35 | return super.onInterceptTouchEvent(ev); 36 | } catch (IllegalArgumentException e) { 37 | return false; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sample/src/main/java/com/wuzy/photoviewex/sample/ImageAdapter.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex.sample; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.recyclerview.widget.RecyclerView; 7 | 8 | /** 9 | * Image adapter 10 | */ 11 | public class ImageAdapter extends RecyclerView.Adapter { 12 | 13 | Listener mListener; 14 | 15 | public ImageAdapter(Listener listener) { 16 | mListener = listener; 17 | } 18 | 19 | @Override 20 | public ImageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 21 | ImageViewHolder holder = ImageViewHolder.inflate(parent); 22 | holder.itemView.setOnClickListener(new View.OnClickListener() { 23 | @Override 24 | public void onClick(View view) { 25 | mListener.onImageClicked(view); 26 | } 27 | }); 28 | return holder; 29 | } 30 | 31 | @Override 32 | public void onBindViewHolder(ImageViewHolder holder, int position) { 33 | 34 | } 35 | 36 | @Override 37 | public int getItemCount() { 38 | return 20; 39 | } 40 | 41 | public interface Listener { 42 | void onImageClicked(View view); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sample/src/main/java/com/wuzy/photoviewex/sample/ImageViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex.sample; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.TextView; 7 | 8 | import androidx.recyclerview.widget.RecyclerView; 9 | 10 | /** 11 | * Image in recyclerview 12 | */ 13 | public class ImageViewHolder extends RecyclerView.ViewHolder { 14 | 15 | public static ImageViewHolder inflate(ViewGroup parent) { 16 | View view = LayoutInflater.from(parent.getContext()) 17 | .inflate(R.layout.item_image, parent, false); 18 | return new ImageViewHolder(view); 19 | } 20 | 21 | public TextView mTextTitle; 22 | 23 | public ImageViewHolder(View view) { 24 | super(view); 25 | mTextTitle = view.findViewById(R.id.title); 26 | } 27 | 28 | private void bind(String title) { 29 | mTextTitle.setText(title); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sample/src/main/java/com/wuzy/photoviewex/sample/ImmersiveActivity.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex.sample; 2 | 3 | import android.os.Build; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | import android.view.View; 7 | import android.widget.ImageView; 8 | 9 | import com.squareup.picasso.Picasso; 10 | import com.wuzy.photoviewex.OnPhotoTapListener; 11 | import com.wuzy.photoviewex.PhotoView; 12 | 13 | import androidx.annotation.Nullable; 14 | import androidx.appcompat.app.AppCompatActivity; 15 | 16 | import static android.R.attr.uiOptions; 17 | 18 | /** 19 | * Shows immersive image viewer 20 | */ 21 | public class ImmersiveActivity extends AppCompatActivity { 22 | 23 | @Override 24 | protected void onCreate(@Nullable Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.activity_immersive); 27 | 28 | PhotoView photoView = findViewById(R.id.photo_view); 29 | Picasso.with(this) 30 | .load("http://pbs.twimg.com/media/Bist9mvIYAAeAyQ.jpg") 31 | .into(photoView); 32 | photoView.setOnPhotoTapListener(new OnPhotoTapListener() { 33 | @Override 34 | public void onPhotoTap(ImageView view, float x, float y) { 35 | //fullScreen(); 36 | } 37 | }); 38 | fullScreen(); 39 | } 40 | 41 | public void fullScreen() { 42 | 43 | // BEGIN_INCLUDE (get_current_ui_flags) 44 | // The UI options currently enabled are represented by a bitfield. 45 | // getSystemUiVisibility() gives us that bitfield. 46 | int uiOptions = getWindow().getDecorView().getSystemUiVisibility(); 47 | int newUiOptions = uiOptions; 48 | // END_INCLUDE (get_current_ui_flags) 49 | // BEGIN_INCLUDE (toggle_ui_flags) 50 | boolean isImmersiveModeEnabled = isImmersiveModeEnabled(); 51 | if (isImmersiveModeEnabled) { 52 | Log.i("TEST", "Turning immersive mode mode off. "); 53 | } else { 54 | Log.i("TEST", "Turning immersive mode mode on."); 55 | } 56 | 57 | // Navigation bar hiding: Backwards compatible to ICS. 58 | if (Build.VERSION.SDK_INT >= 14) { 59 | newUiOptions ^= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; 60 | } 61 | 62 | // Status bar hiding: Backwards compatible to Jellybean 63 | if (Build.VERSION.SDK_INT >= 16) { 64 | newUiOptions ^= View.SYSTEM_UI_FLAG_FULLSCREEN; 65 | } 66 | 67 | // Immersive mode: Backward compatible to KitKat. 68 | // Note that this flag doesn't do anything by itself, it only augments the behavior 69 | // of HIDE_NAVIGATION and FLAG_FULLSCREEN. For the purposes of this sample 70 | // all three flags are being toggled together. 71 | // Note that there are two immersive mode UI flags, one of which is referred to as "sticky". 72 | // Sticky immersive mode differs in that it makes the navigation and status bars 73 | // semi-transparent, and the UI flag does not get cleared when the user interacts with 74 | // the screen. 75 | if (Build.VERSION.SDK_INT >= 18) { 76 | newUiOptions ^= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; 77 | } 78 | 79 | getWindow().getDecorView().setSystemUiVisibility(newUiOptions); 80 | //END_INCLUDE (set_ui_flags) 81 | } 82 | 83 | private boolean isImmersiveModeEnabled() { 84 | return ((uiOptions | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) == uiOptions); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /sample/src/main/java/com/wuzy/photoviewex/sample/LauncherActivity.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 com.wuzy.photoviewex.sample; 17 | 18 | import android.content.Context; 19 | import android.content.Intent; 20 | import android.os.Bundle; 21 | import android.view.LayoutInflater; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | import android.widget.TextView; 25 | 26 | import androidx.appcompat.app.AppCompatActivity; 27 | import androidx.appcompat.widget.Toolbar; 28 | import androidx.recyclerview.widget.LinearLayoutManager; 29 | import androidx.recyclerview.widget.RecyclerView; 30 | 31 | public class LauncherActivity extends AppCompatActivity { 32 | 33 | public static final String[] options = { 34 | "Simple Sample", 35 | "ViewPager Sample", 36 | "Rotation Sample", 37 | "Picasso Sample", 38 | "Activity Transition Sample", 39 | "Immersive Sample" 40 | }; 41 | 42 | @Override 43 | protected void onCreate(Bundle savedInstanceState) { 44 | super.onCreate(savedInstanceState); 45 | setContentView(R.layout.activity_launcher); 46 | Toolbar toolbar = findViewById(R.id.toolbar); 47 | toolbar.setTitle(R.string.app_name); 48 | RecyclerView recyclerView = findViewById(R.id.list); 49 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 50 | recyclerView.setAdapter(new ItemAdapter()); 51 | } 52 | 53 | 54 | private static class ItemAdapter extends RecyclerView.Adapter { 55 | @Override 56 | public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 57 | final ItemViewHolder holder = ItemViewHolder.newInstance(parent); 58 | holder.itemView.setOnClickListener(new View.OnClickListener() { 59 | @Override 60 | public void onClick(View v) { 61 | Class clazz; 62 | 63 | switch (holder.getAdapterPosition()) { 64 | default: 65 | case 0: 66 | clazz = SimpleSampleActivity.class; 67 | break; 68 | case 1: 69 | clazz = ViewPagerActivity.class; 70 | break; 71 | case 2: 72 | clazz = RotationSampleActivity.class; 73 | break; 74 | case 3: 75 | clazz = PicassoSampleActivity.class; 76 | break; 77 | case 4: 78 | clazz = ActivityTransitionActivity.class; 79 | break; 80 | case 5: 81 | clazz = ImmersiveActivity.class; 82 | } 83 | 84 | Context context = holder.itemView.getContext(); 85 | context.startActivity(new Intent(context, clazz)); 86 | } 87 | }); 88 | return holder; 89 | } 90 | 91 | @Override 92 | public void onBindViewHolder(final ItemViewHolder holder, int position) { 93 | holder.bind(options[position]); 94 | } 95 | 96 | @Override 97 | public int getItemCount() { 98 | return options.length; 99 | } 100 | } 101 | 102 | private static class ItemViewHolder extends RecyclerView.ViewHolder { 103 | 104 | public static ItemViewHolder newInstance(ViewGroup parent) { 105 | View view = LayoutInflater.from(parent.getContext()) 106 | .inflate(R.layout.item_sample, parent, false); 107 | return new ItemViewHolder(view); 108 | } 109 | 110 | public TextView mTextTitle; 111 | 112 | public ItemViewHolder(View view) { 113 | super(view); 114 | mTextTitle = view.findViewById(R.id.title); 115 | } 116 | 117 | private void bind(String title) { 118 | mTextTitle.setText(title); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /sample/src/main/java/com/wuzy/photoviewex/sample/PicassoSampleActivity.java: -------------------------------------------------------------------------------- 1 | package com.wuzy.photoviewex.sample; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.squareup.picasso.Picasso; 6 | import com.wuzy.photoviewex.PhotoView; 7 | 8 | import androidx.appcompat.app.AppCompatActivity; 9 | 10 | public class PicassoSampleActivity extends AppCompatActivity { 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | setContentView(R.layout.activity_simple); 16 | 17 | final PhotoView photoView = findViewById(R.id.iv_photo); 18 | 19 | Picasso.with(this) 20 | .load("http://pbs.twimg.com/media/Bist9mvIYAAeAyQ.jpg") 21 | .into(photoView); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample/src/main/java/com/wuzy/photoviewex/sample/RotationSampleActivity.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 com.wuzy.photoviewex.sample; 17 | 18 | import android.os.Bundle; 19 | import android.os.Handler; 20 | import android.view.MenuItem; 21 | 22 | import com.wuzy.photoviewex.PhotoView; 23 | 24 | import androidx.appcompat.app.AppCompatActivity; 25 | import androidx.appcompat.widget.Toolbar; 26 | 27 | public class RotationSampleActivity extends AppCompatActivity { 28 | 29 | private PhotoView photo; 30 | private final Handler handler = new Handler(); 31 | private boolean rotating = false; 32 | 33 | @Override 34 | public void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.activity_rotation_sample); 37 | Toolbar toolbar = findViewById(R.id.toolbar); 38 | toolbar.inflateMenu(R.menu.rotation); 39 | toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { 40 | @Override 41 | public boolean onMenuItemClick(MenuItem item) { 42 | switch (item.getItemId()) { 43 | case R.id.action_rotate_10_right: 44 | photo.setRotationBy(10); 45 | return true; 46 | case R.id.action_rotate_10_left: 47 | photo.setRotationBy(-10); 48 | return true; 49 | case R.id.action_toggle_automatic_rotation: 50 | toggleRotation(); 51 | return true; 52 | case R.id.action_reset_to_0: 53 | photo.setRotationTo(0); 54 | return true; 55 | case R.id.action_reset_to_90: 56 | photo.setRotationTo(90); 57 | return true; 58 | case R.id.action_reset_to_180: 59 | photo.setRotationTo(180); 60 | return true; 61 | case R.id.action_reset_to_270: 62 | photo.setRotationTo(270); 63 | return true; 64 | } 65 | return false; 66 | } 67 | }); 68 | photo = findViewById(R.id.iv_photo); 69 | photo.setImageResource(R.drawable.wallpaper); 70 | } 71 | 72 | @Override 73 | protected void onPause() { 74 | super.onPause(); 75 | handler.removeCallbacksAndMessages(null); 76 | } 77 | 78 | private void toggleRotation() { 79 | if (rotating) { 80 | handler.removeCallbacksAndMessages(null); 81 | } else { 82 | rotateLoop(); 83 | } 84 | rotating = !rotating; 85 | } 86 | 87 | private void rotateLoop() { 88 | handler.postDelayed(new Runnable() { 89 | @Override 90 | public void run() { 91 | photo.setRotationBy(1); 92 | rotateLoop(); 93 | } 94 | }, 15); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /sample/src/main/java/com/wuzy/photoviewex/sample/SimpleSampleActivity.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 com.wuzy.photoviewex.sample; 17 | 18 | import android.graphics.Matrix; 19 | import android.graphics.RectF; 20 | import android.graphics.drawable.Drawable; 21 | import android.os.Bundle; 22 | import android.util.Log; 23 | import android.view.MenuItem; 24 | import android.view.MotionEvent; 25 | import android.view.View; 26 | import android.widget.ImageView; 27 | import android.widget.TextView; 28 | import android.widget.Toast; 29 | 30 | import com.wuzy.photoviewex.OnMatrixChangedListener; 31 | import com.wuzy.photoviewex.OnPhotoTapListener; 32 | import com.wuzy.photoviewex.OnSingleFlingListener; 33 | import com.wuzy.photoviewex.PhotoView; 34 | 35 | import java.util.Random; 36 | 37 | import androidx.appcompat.app.AppCompatActivity; 38 | import androidx.appcompat.widget.Toolbar; 39 | import androidx.core.content.ContextCompat; 40 | 41 | public class SimpleSampleActivity extends AppCompatActivity { 42 | 43 | static final String PHOTO_TAP_TOAST_STRING = "Photo Tap! X: %.2f %% Y:%.2f %% ID: %d"; 44 | static final String SCALE_TOAST_STRING = "Scaled to: %.2ff"; 45 | static final String FLING_LOG_STRING = "Fling velocityX: %.2f, velocityY: %.2f"; 46 | 47 | private PhotoView mPhotoView; 48 | private TextView mCurrMatrixTv; 49 | 50 | private Toast mCurrentToast; 51 | 52 | private Matrix mCurrentDisplayMatrix = null; 53 | 54 | @Override 55 | public void onCreate(Bundle savedInstanceState) { 56 | super.onCreate(savedInstanceState); 57 | setContentView(R.layout.activity_simple_sample); 58 | 59 | Toolbar toolbar = findViewById(R.id.toolbar); 60 | toolbar.setTitle("Simple Sample"); 61 | toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp); 62 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 63 | @Override 64 | public void onClick(View v) { 65 | onBackPressed(); 66 | } 67 | }); 68 | toolbar.inflateMenu(R.menu.main_menu); 69 | toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { 70 | @Override 71 | public boolean onMenuItemClick(MenuItem item) { 72 | switch (item.getItemId()) { 73 | case R.id.menu_zoom_toggle: 74 | mPhotoView.setZoomable(!mPhotoView.isZoomable()); 75 | item.setTitle(mPhotoView.isZoomable() ? R.string.menu_zoom_disable : R.string.menu_zoom_enable); 76 | return true; 77 | 78 | case R.id.menu_scale_fit_center: 79 | mPhotoView.setScaleType(ImageView.ScaleType.CENTER); 80 | return true; 81 | 82 | case R.id.menu_scale_fit_start: 83 | mPhotoView.setScaleType(ImageView.ScaleType.FIT_START); 84 | return true; 85 | 86 | case R.id.menu_scale_fit_end: 87 | mPhotoView.setScaleType(ImageView.ScaleType.FIT_END); 88 | return true; 89 | 90 | case R.id.menu_scale_fit_xy: 91 | mPhotoView.setScaleType(ImageView.ScaleType.FIT_XY); 92 | return true; 93 | 94 | case R.id.menu_scale_scale_center: 95 | mPhotoView.setScaleType(ImageView.ScaleType.CENTER); 96 | return true; 97 | 98 | case R.id.menu_scale_scale_center_crop: 99 | mPhotoView.setScaleType(ImageView.ScaleType.CENTER_CROP); 100 | return true; 101 | 102 | case R.id.menu_scale_scale_center_inside: 103 | mPhotoView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 104 | return true; 105 | 106 | case R.id.menu_scale_random_animate: 107 | case R.id.menu_scale_random: 108 | Random r = new Random(); 109 | 110 | float minScale = mPhotoView.getMinimumScale(); 111 | float maxScale = mPhotoView.getMaximumScale(); 112 | float randomScale = minScale + (r.nextFloat() * (maxScale - minScale)); 113 | mPhotoView.setScale(randomScale, item.getItemId() == R.id.menu_scale_random_animate); 114 | 115 | showToast(String.format(SCALE_TOAST_STRING, randomScale)); 116 | 117 | return true; 118 | case R.id.menu_matrix_restore: 119 | if (mCurrentDisplayMatrix == null) 120 | showToast("You need to capture display matrix first"); 121 | else 122 | mPhotoView.setDisplayMatrix(mCurrentDisplayMatrix); 123 | return true; 124 | case R.id.menu_matrix_capture: 125 | mCurrentDisplayMatrix = new Matrix(); 126 | mPhotoView.getDisplayMatrix(mCurrentDisplayMatrix); 127 | return true; 128 | } 129 | return false; 130 | } 131 | }); 132 | mPhotoView = findViewById(R.id.iv_photo); 133 | mCurrMatrixTv = findViewById(R.id.tv_current_matrix); 134 | 135 | Drawable bitmap = ContextCompat.getDrawable(this, R.drawable.wallpaper); 136 | mPhotoView.setImageDrawable(bitmap); 137 | 138 | // Lets attach some listeners, not required though! 139 | mPhotoView.setOnMatrixChangeListener(new MatrixChangeListener()); 140 | mPhotoView.setOnPhotoTapListener(new PhotoTapListener()); 141 | mPhotoView.setOnSingleFlingListener(new SingleFlingListener()); 142 | } 143 | 144 | private class PhotoTapListener implements OnPhotoTapListener { 145 | 146 | @Override 147 | public void onPhotoTap(ImageView view, float x, float y) { 148 | float xPercentage = x * 100f; 149 | float yPercentage = y * 100f; 150 | 151 | showToast(String.format(PHOTO_TAP_TOAST_STRING, xPercentage, yPercentage, view == null ? 0 : view.getId())); 152 | } 153 | } 154 | 155 | private void showToast(CharSequence text) { 156 | if (mCurrentToast != null) { 157 | mCurrentToast.cancel(); 158 | } 159 | 160 | mCurrentToast = Toast.makeText(SimpleSampleActivity.this, text, Toast.LENGTH_SHORT); 161 | mCurrentToast.show(); 162 | } 163 | 164 | private class MatrixChangeListener implements OnMatrixChangedListener { 165 | 166 | @Override 167 | public void onMatrixChanged(RectF rect) { 168 | mCurrMatrixTv.setText(rect.toString()); 169 | } 170 | } 171 | 172 | private class SingleFlingListener implements OnSingleFlingListener { 173 | 174 | @Override 175 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 176 | Log.d("PhotoView", String.format(FLING_LOG_STRING, velocityX, velocityY)); 177 | return true; 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /sample/src/main/java/com/wuzy/photoviewex/sample/ViewPagerActivity.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 com.wuzy.photoviewex.sample; 17 | 18 | import android.os.Bundle; 19 | import android.view.View; 20 | import android.view.ViewGroup; 21 | import android.view.ViewGroup.LayoutParams; 22 | 23 | import com.wuzy.photoviewex.PhotoView; 24 | 25 | import androidx.appcompat.app.AppCompatActivity; 26 | import androidx.viewpager.widget.PagerAdapter; 27 | import androidx.viewpager.widget.ViewPager; 28 | 29 | public class ViewPagerActivity extends AppCompatActivity { 30 | 31 | @Override 32 | public void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_view_pager); 35 | ViewPager viewPager = findViewById(R.id.view_pager); 36 | viewPager.setAdapter(new SamplePagerAdapter()); 37 | } 38 | 39 | static class SamplePagerAdapter extends PagerAdapter { 40 | 41 | private static final int[] sDrawables = {R.drawable.wallpaper, R.drawable.wallpaper, R.drawable.wallpaper, 42 | R.drawable.wallpaper, R.drawable.wallpaper, R.drawable.wallpaper}; 43 | 44 | @Override 45 | public int getCount() { 46 | return sDrawables.length; 47 | } 48 | 49 | @Override 50 | public View instantiateItem(ViewGroup container, int position) { 51 | PhotoView photoView = new PhotoView(container.getContext()); 52 | photoView.setImageResource(sDrawables[position]); 53 | // Now just add PhotoView to ViewPager and return it 54 | container.addView(photoView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 55 | return photoView; 56 | } 57 | 58 | @Override 59 | public void destroyItem(ViewGroup container, int position, Object object) { 60 | container.removeView((View) object); 61 | } 62 | 63 | @Override 64 | public boolean isViewFromObject(View view, Object object) { 65 | return view == object; 66 | } 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/wallpaper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zywudev/PhotoViewEx/c26a1108c22bf0e7ddf40e56bdfffaca647ab94b/sample/src/main/res/drawable-nodpi/wallpaper.jpg -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_arrow_back_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_immersive.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_rotation_sample.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 16 | 17 | 18 | 19 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_simple.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_simple_sample.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 16 | 17 | 18 | 19 | 23 | 24 | 32 | 33 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_transition.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_transition_to.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_view_pager.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/item_image.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/item_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 23 | 24 | 29 | 30 | -------------------------------------------------------------------------------- /sample/src/main/res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 2 |

5 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 43 | -------------------------------------------------------------------------------- /sample/src/main/res/menu/rotation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | 15 | 16 | 19 | 20 | 23 | 24 | 27 | 28 | 31 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zywudev/PhotoViewEx/c26a1108c22bf0e7ddf40e56bdfffaca647ab94b/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zywudev/PhotoViewEx/c26a1108c22bf0e7ddf40e56bdfffaca647ab94b/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zywudev/PhotoViewEx/c26a1108c22bf0e7ddf40e56bdfffaca647ab94b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zywudev/PhotoViewEx/c26a1108c22bf0e7ddf40e56bdfffaca647ab94b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zywudev/PhotoViewEx/c26a1108c22bf0e7ddf40e56bdfffaca647ab94b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #AACE30 4 | #142D3E 5 | #001425 6 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | PhotoView Sample 4 | Enable Zoom 5 | Disable Zoom 6 | Change to FIT_CENTER 7 | Change to FIT_START 8 | Change to FIT_END 9 | Change to FIT_XY 10 | Change to CENTER 11 | Change to CENTER_INSIDE 12 | Change to CENTER_CROP 13 | Animate scale to random value 14 | Set scale to random value 15 | Restore Display Matrix 16 | Capture Display Matrix 17 | Extract visible bitmap 18 | 19 | 20 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 21 | -------------------------------------------------------------------------------- /sample/src/main/res/values/transitions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | test 4 | -------------------------------------------------------------------------------- /screenshoots/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zywudev/PhotoViewEx/c26a1108c22bf0e7ddf40e56bdfffaca647ab94b/screenshoots/1.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':sample', ':photoviewex' 2 | --------------------------------------------------------------------------------