├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── yashoid │ │ └── instacropper │ │ └── sample │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── yashoid │ │ │ └── instacropper │ │ │ └── sample │ │ │ └── MainActivity.java │ └── res │ │ ├── layout │ │ └── activity_main.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-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── yashoid │ └── instacropper │ └── sample │ └── ExampleUnitTest.java ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── instacropper ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── yashoid │ │ └── instacropper │ │ ├── GridDrawable.java │ │ ├── InstaCropperActivity.java │ │ ├── InstaCropperView.java │ │ ├── InstaCropperViewNew.java │ │ ├── MakeDrawableTask.java │ │ ├── MultipleCropActivity.java │ │ └── MultipleUris.java │ └── res │ ├── drawable-hdpi │ └── ic_instacropper_crop.png │ ├── drawable-mdpi │ └── ic_instacropper_crop.png │ ├── drawable-xhdpi │ └── ic_instacropper_crop.png │ ├── drawable-xxhdpi │ └── ic_instacropper_crop.png │ ├── drawable-xxxhdpi │ └── ic_instacropper_crop.png │ ├── layout │ └── activity_instacropper.xml │ ├── menu │ └── menu_instacropper.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | gradle.properties 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### NOTICE: I build custom views "for free". Just send me a description of what you need and I will send it to you the next day! Email me at yasharpm[at]gmail[dot]com with the subject "CustomView request". 2 | 3 | # InstaCropper 4 | A View for cropping images that is similar to Instagram's crop. Also an Activity for cropping is included. 5 | 6 | ![alt tag](https://cloud.githubusercontent.com/assets/4597931/23830368/724ddf70-071e-11e7-9d7e-65615be8d5e6.gif) 7 | 8 | ## Usage 9 | 10 | Add the dependency: 11 | ```Groovy 12 | dependencies { 13 | implementation 'com.yashoid:instacropper:1.2.0' 14 | } 15 | ``` 16 | 17 | ## How to use this library 18 | 19 | Add `InstaCropperView` to your xml layout 20 | 21 | ```xml 22 | 23 | 27 | 28 | 33 | 34 | 35 | ``` 36 | 37 | InstaCropperView receives only Uri but any Uri is possible. 38 | ```java 39 | mInstaCropper.setImageUri(imageUri); 40 | ``` 41 | 42 | You can specify three size ratios for the crop. The narrowest allowed, the widest allowed and the ideal ratio. The View will adjust its size by the ideal ratio. 43 | ```java 44 | mInstaCropper.setRatios(defaultRatio, minimumRatio, maximumRatio); 45 | ``` 46 | 47 | The cropped image is returned in a callback. You can specify MeasureSpec to adjust the width and height of the returned Bitmap. 48 | ```java 49 | mInstaCropper.crop( 50 | View.MeasureSpec.makeMeasureSpec(1024, View.MeasureSpec.AT_MOST), 51 | View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), 52 | new InstaCropperView.BitmapCallback() { 53 | 54 | @Override 55 | public void onBitmapReady(Bitmap bitmap) { 56 | // Do something. 57 | } 58 | 59 | } 60 | ); 61 | ``` 62 | 63 | It is also possible to use the crop feature via an Intent call. There are various `getIntent()` methods defined on `InstaCropperActivity`. You will then receive the crop result in `data.getData()`. 64 | ```java 65 | Intent intent = InstaCropperActivity.getIntent(context, srcUri, dstUri, maxWidth, outputQuality); 66 | startActivityForResult(intent, REQUEST_CROP); 67 | ``` 68 | 69 | Cropping of multiple images is also possible. Use `MultipleCropActivity.getIntent()` methods to access. If all the images are cropped you will receive `RESULT_OK` otherwise `RESULT_CANCELED`. `EXTRA_COUNT` will contain the number of images cropped. 70 | ```java 71 | Intent intent = MultipleCropActivity.getIntent(context, srcUris, dstUris, maxWidth, maxHeight, aspectRatio); 72 | startActivityForResult(intent, REQUEST_MULTIPLE_CROP); 73 | ``` 74 | 75 | You can modify the crop Activity's apprearance by overriding the following resouce values: 76 | ```xml 77 | Crop 78 | Crop 79 | 80 | @android:color/white 81 | @android:color/black 82 | 83 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/yashoid/instacropper/sample/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.yashoid.instacropper.sample; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /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 | dependencies { 9 | classpath ('com.android.tools.build:gradle:4.0.1') { force=true } 10 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7' 11 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1' 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasharpm/InstaCropper/5585693ecc8fbcc2629f19a346d19c0b32436443/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jul 31 13:14:16 IRDT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /instacropper/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /instacropper/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | ext { 4 | bintrayRepo = 'YashoidLibraries' 5 | bintrayName = 'instacropper' 6 | 7 | publishedGroupId = 'com.yashoid' 8 | libraryName = 'InstaCropper' 9 | artifact = 'instacropper' 10 | 11 | libraryDescription = 'A View for cropping images that is similar to Instagram\'s crop which allows a range of spect ratios. Also an Activity for cropping is included.' 12 | 13 | siteUrl = 'https://github.com/yasharpm/InstaCropper' 14 | gitUrl = 'https://github.com/yasharpm/InstaCropper.git' 15 | 16 | libraryVersion = '1.2.0' 17 | 18 | developerId = 'yasharpm' 19 | developerName = 'Yashar PourMohammad' 20 | developerEmail = 'yasharpm@gmail.com' 21 | 22 | licenseName = 'The Apache Software License, Version 2.0' 23 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 24 | allLicenses = ["Apache-2.0"] 25 | } 26 | 27 | android { 28 | compileSdkVersion 29 29 | 30 | defaultConfig { 31 | minSdkVersion 16 32 | targetSdkVersion 29 33 | versionCode 1 34 | versionName "1.0" 35 | 36 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 37 | 38 | } 39 | buildTypes { 40 | release { 41 | minifyEnabled false 42 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 43 | } 44 | 45 | apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle' 46 | apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle' 47 | } 48 | } -------------------------------------------------------------------------------- /instacropper/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Not installed\Programming\Libraries\Android\android-sdk-windows/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /instacropper/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /instacropper/src/main/java/com/yashoid/instacropper/GridDrawable.java: -------------------------------------------------------------------------------- 1 | package com.yashoid.instacropper; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.ColorFilter; 7 | import android.graphics.Paint; 8 | import android.graphics.PixelFormat; 9 | import android.graphics.Rect; 10 | import android.graphics.drawable.Drawable; 11 | import android.os.Handler; 12 | import android.view.animation.LinearInterpolator; 13 | 14 | /** 15 | * Created by Yashar on 3/8/2017. 16 | */ 17 | 18 | public class GridDrawable extends Drawable { 19 | 20 | private static final int LINE_COLOR = Color.WHITE; 21 | private static final int LINE_BORDER_COLOR = 0x44888888; 22 | private static final float LINE_STROKE_WIDTH = 1; 23 | private static final long TIME_BEFORE_FADE = 300; 24 | private static final long TIME_TO_FADE = 300; 25 | 26 | private Handler mHandler; 27 | 28 | private Rect mPreviousBounds = new Rect(); 29 | 30 | private Paint mLinePaint; 31 | private Paint mLineBorderPaint; 32 | 33 | private ValueAnimator mAnimator = new ValueAnimator(); 34 | 35 | private float mAlpha = 1; 36 | 37 | protected GridDrawable() { 38 | mHandler = new Handler(); 39 | 40 | mLinePaint = new Paint(); 41 | mLinePaint.setStyle(Paint.Style.STROKE); 42 | mLinePaint.setColor(LINE_COLOR); 43 | mLinePaint.setStrokeWidth(LINE_STROKE_WIDTH); 44 | 45 | mLineBorderPaint = new Paint(); 46 | mLineBorderPaint.setStyle(Paint.Style.STROKE); 47 | mLineBorderPaint.setColor(LINE_BORDER_COLOR); 48 | mLineBorderPaint.setStrokeWidth(LINE_STROKE_WIDTH); 49 | 50 | mAnimator.setDuration(TIME_TO_FADE); 51 | mAnimator.setStartDelay(TIME_BEFORE_FADE); 52 | mAnimator.setFloatValues(1, 0); 53 | mAnimator.addUpdateListener(mAnimatorUpdateListener); 54 | mAnimator.setInterpolator(new LinearInterpolator()); 55 | 56 | mAnimator.start(); 57 | } 58 | 59 | @Override 60 | public void setBounds(int left, int top, int right, int bottom) { 61 | super.setBounds(left, top, right, bottom); 62 | 63 | mAlpha = 1; 64 | invalidateSelf(); 65 | 66 | mAnimator.cancel(); 67 | mAnimator.start(); 68 | } 69 | 70 | @Override 71 | public void draw(Canvas canvas) { 72 | mLinePaint.setAlpha(Math.round(mAlpha*255)); 73 | mLineBorderPaint.setAlpha(Math.round(mAlpha*0x44)); 74 | 75 | Rect bounds = getBounds(); 76 | 77 | int width = bounds.width(); 78 | int height = bounds.height(); 79 | 80 | int left = bounds.left + width / 3; 81 | int right = left + width / 3; 82 | int top = bounds.top + height / 3; 83 | int bottom = top + height / 3; 84 | 85 | canvas.drawLine(left - 1, bounds.top, left - 1, bounds.bottom, mLineBorderPaint); 86 | canvas.drawLine(left + 1, bounds.top, left + 1, bounds.bottom, mLineBorderPaint); 87 | 88 | canvas.drawLine(right - 1, bounds.top, right - 1, bounds.bottom, mLineBorderPaint); 89 | canvas.drawLine(right + 1, bounds.top, right + 1, bounds.bottom, mLineBorderPaint); 90 | 91 | canvas.drawLine(bounds.left, top - 1, bounds.right, top - 1, mLineBorderPaint); 92 | canvas.drawLine(bounds.left, top + 1, bounds.right, top + 1, mLineBorderPaint); 93 | 94 | canvas.drawLine(bounds.left, bottom - 1, bounds.right, bottom - 1, mLineBorderPaint); 95 | canvas.drawLine(bounds.left, bottom + 1, bounds.right, bottom + 1, mLineBorderPaint); 96 | 97 | canvas.drawLine(left, bounds.top, left, bounds.bottom, mLinePaint); 98 | canvas.drawLine(right, bounds.top, right, bounds.bottom, mLinePaint); 99 | canvas.drawLine(bounds.left, top, bounds.right, top, mLinePaint); 100 | canvas.drawLine(bounds.left, bottom, bounds.right, bottom, mLinePaint); 101 | } 102 | 103 | @Override 104 | public void setAlpha(int alpha) { } 105 | 106 | @Override 107 | public void setColorFilter(ColorFilter colorFilter) { } 108 | 109 | @Override 110 | public int getOpacity() { 111 | return PixelFormat.OPAQUE; 112 | } 113 | 114 | private ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() { 115 | 116 | @Override 117 | public void onAnimationUpdate(ValueAnimator animation) { 118 | mAlpha = (float) animation.getAnimatedValue(); 119 | 120 | invalidateSelf(); 121 | } 122 | 123 | }; 124 | 125 | } 126 | -------------------------------------------------------------------------------- /instacropper/src/main/java/com/yashoid/instacropper/InstaCropperActivity.java: -------------------------------------------------------------------------------- 1 | package com.yashoid.instacropper; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.graphics.Bitmap; 7 | import android.graphics.PorterDuff; 8 | import android.graphics.drawable.Drawable; 9 | import android.net.Uri; 10 | import android.os.AsyncTask; 11 | import android.os.Build; 12 | import android.os.Bundle; 13 | import android.provider.MediaStore; 14 | import android.view.Menu; 15 | import android.view.MenuItem; 16 | import android.view.View; 17 | 18 | import java.io.IOException; 19 | import java.io.OutputStream; 20 | 21 | /** 22 | * Created by Yashar on 3/11/2017. 23 | */ 24 | 25 | public class InstaCropperActivity extends Activity { 26 | 27 | public static final int DEFAULT_OUTPUT_QUALITY = 80; 28 | 29 | public static final String EXTRA_OUTPUT = MediaStore.EXTRA_OUTPUT; 30 | 31 | public static final String EXTRA_PREFERRED_RATIO = "preferred_ratio"; 32 | public static final String EXTRA_MINIMUM_RATIO = "minimum_ratio"; 33 | public static final String EXTRA_MAXIMUM_RATIO = "maximum_ratio"; 34 | 35 | public static final String EXTRA_WIDTH_SPEC = "width_spec"; 36 | public static final String EXTRA_HEIGHT_SPEC = "height_spec"; 37 | 38 | public static final String EXTRA_OUTPUT_QUALITY = "output_quality"; 39 | 40 | public static Intent getIntent(Context context, Uri src, Uri dst, int maxWidth, int outputQuality) { 41 | return getIntent( 42 | context, 43 | src, 44 | dst, 45 | View.MeasureSpec.makeMeasureSpec(maxWidth, View.MeasureSpec.AT_MOST), 46 | View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), 47 | outputQuality 48 | ); 49 | } 50 | 51 | public static Intent getIntent(Context context, Uri src, Uri dst, int widthSpec, int heightSpec, int outputQuality) { 52 | return getIntent( 53 | context, 54 | src, 55 | dst, 56 | InstaCropperView.DEFAULT_RATIO, 57 | InstaCropperView.DEFAULT_MINIMUM_RATIO, 58 | InstaCropperView.DEFAULT_MAXIMUM_RATIO, 59 | widthSpec, 60 | heightSpec, 61 | outputQuality 62 | ); 63 | } 64 | 65 | public static Intent getIntent(Context context, Uri src, Uri dst, 66 | float preferredRatio, float minimumRatio, float maximumRatio, 67 | int widthSpec, int heightSpec, int outputQuality) { 68 | Intent intent = new Intent(context, InstaCropperActivity.class); 69 | 70 | intent.setData(src); 71 | 72 | intent.putExtra(EXTRA_OUTPUT, dst); 73 | 74 | intent.putExtra(EXTRA_PREFERRED_RATIO, preferredRatio); 75 | intent.putExtra(EXTRA_MINIMUM_RATIO, minimumRatio); 76 | intent.putExtra(EXTRA_MAXIMUM_RATIO, maximumRatio); 77 | 78 | intent.putExtra(EXTRA_WIDTH_SPEC, widthSpec); 79 | intent.putExtra(EXTRA_HEIGHT_SPEC, heightSpec); 80 | intent.putExtra(EXTRA_OUTPUT_QUALITY, outputQuality); 81 | 82 | return intent; 83 | } 84 | 85 | private InstaCropperView mInstaCropper; 86 | 87 | private int mWidthSpec; 88 | private int mHeightSpec; 89 | private int mOutputQuality; 90 | 91 | private Uri mOutputUri; 92 | 93 | @Override 94 | protected void onCreate(Bundle savedInstanceState) { 95 | super.onCreate(savedInstanceState); 96 | setContentView(R.layout.activity_instacropper); 97 | 98 | mInstaCropper = (InstaCropperView) findViewById(R.id.instacropper); 99 | 100 | Intent intent = getIntent(); 101 | 102 | Uri uri = intent.getData(); 103 | 104 | float defaultRatio = intent.getFloatExtra(EXTRA_PREFERRED_RATIO, InstaCropperView.DEFAULT_RATIO); 105 | float minimumRatio = intent.getFloatExtra(EXTRA_MINIMUM_RATIO, InstaCropperView.DEFAULT_MINIMUM_RATIO); 106 | float maximumRatio = intent.getFloatExtra(EXTRA_MAXIMUM_RATIO, InstaCropperView.DEFAULT_MAXIMUM_RATIO); 107 | 108 | mInstaCropper.setRatios(defaultRatio, minimumRatio, maximumRatio); 109 | mInstaCropper.setImageUri(uri); 110 | 111 | mWidthSpec = intent.getIntExtra(EXTRA_WIDTH_SPEC, View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); 112 | mHeightSpec = intent.getIntExtra(EXTRA_HEIGHT_SPEC, View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); 113 | mOutputQuality = intent.getIntExtra(EXTRA_OUTPUT_QUALITY, DEFAULT_OUTPUT_QUALITY); 114 | 115 | mOutputUri = intent.getParcelableExtra(EXTRA_OUTPUT); 116 | } 117 | 118 | @Override 119 | public boolean onCreateOptionsMenu(Menu menu) { 120 | getMenuInflater().inflate(R.menu.menu_instacropper, menu); 121 | 122 | MenuItem menuItem = menu.findItem(R.id.menu_crop); 123 | 124 | Drawable d = menuItem.getIcon().mutate(); 125 | 126 | int color; 127 | 128 | if (Build.VERSION.SDK_INT < 23) { 129 | color = getResources().getColor(R.color.instacropper_crop_color); 130 | } 131 | else { 132 | color = getResources().getColor(R.color.instacropper_crop_color, getTheme()); 133 | } 134 | 135 | d.setColorFilter(color, PorterDuff.Mode.SRC_IN); 136 | 137 | menuItem.setIcon(d); 138 | 139 | return true; 140 | } 141 | 142 | @Override 143 | public boolean onOptionsItemSelected(MenuItem item) { 144 | mInstaCropper.crop(mWidthSpec, mHeightSpec, mBitmapCallback); 145 | 146 | return true; 147 | } 148 | 149 | private InstaCropperView.BitmapCallback mBitmapCallback = new InstaCropperView.BitmapCallback() { 150 | 151 | @Override 152 | public void onBitmapReady(final Bitmap bitmap) { 153 | if (bitmap == null) { 154 | setResult(RESULT_CANCELED); 155 | finish(); 156 | return; 157 | } 158 | 159 | new AsyncTask() { 160 | 161 | @Override 162 | protected Boolean doInBackground(Void... params) { 163 | try { 164 | OutputStream os = getContentResolver().openOutputStream(mOutputUri); 165 | 166 | bitmap.compress(Bitmap.CompressFormat.JPEG, mOutputQuality, os); 167 | 168 | os.flush(); 169 | os.close(); 170 | 171 | return true; 172 | } catch (IOException e) { } 173 | 174 | return false; 175 | } 176 | 177 | @Override 178 | protected void onPostExecute(Boolean success) { 179 | if (success) { 180 | Intent data = new Intent(); 181 | data.setData(mOutputUri); 182 | setResult(RESULT_OK, data); 183 | } 184 | else { 185 | setResult(RESULT_CANCELED); 186 | } 187 | 188 | finish(); 189 | } 190 | 191 | }.execute(); 192 | } 193 | 194 | }; 195 | 196 | } 197 | -------------------------------------------------------------------------------- /instacropper/src/main/java/com/yashoid/instacropper/InstaCropperView.java: -------------------------------------------------------------------------------- 1 | package com.yashoid.instacropper; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.annotation.TargetApi; 5 | import android.content.Context; 6 | import android.graphics.Bitmap; 7 | import android.graphics.BitmapFactory; 8 | import android.graphics.Canvas; 9 | import android.graphics.Color; 10 | import android.graphics.Paint; 11 | import android.graphics.RectF; 12 | import android.graphics.drawable.Drawable; 13 | import android.net.Uri; 14 | import android.os.AsyncTask; 15 | import android.os.Build; 16 | import android.util.AttributeSet; 17 | import android.util.Log; 18 | import android.view.GestureDetector; 19 | import android.view.MotionEvent; 20 | import android.view.ScaleGestureDetector; 21 | import android.view.View; 22 | import android.view.animation.DecelerateInterpolator; 23 | 24 | import java.io.FileNotFoundException; 25 | 26 | /** 27 | * Created by Yashar on 3/8/2017. 28 | */ 29 | 30 | public class InstaCropperView extends View { 31 | 32 | public static final float DEFAULT_MINIMUM_RATIO = 4F/5F; 33 | public static final float DEFAULT_MAXIMUM_RATIO = 1.91F; 34 | public static final float DEFAULT_RATIO = 1F; 35 | 36 | private static final float MAXIMUM_OVER_SCROLL = 144F; 37 | private static final float MAXIMUM_OVER_SCALE = 0.7F; 38 | 39 | private static final long SET_BACK_DURATION = 400; 40 | 41 | public interface BitmapCallback { 42 | 43 | void onBitmapReady(Bitmap bitmap); 44 | 45 | } 46 | 47 | private float mMinimumRatio = DEFAULT_MINIMUM_RATIO; 48 | private float mMaximumRatio = DEFAULT_MAXIMUM_RATIO; 49 | private float mDefaultRatio = DEFAULT_RATIO; 50 | 51 | private Uri mImageUri = null; 52 | private int mImageRawWidth; 53 | private int mImageRawHeight; 54 | 55 | private MakeDrawableTask mMakeDrawableTask = null; 56 | 57 | private int mWidth; 58 | private int mHeight; 59 | 60 | private GridDrawable mGridDrawable = new GridDrawable(); 61 | 62 | private Drawable mDrawable = null; 63 | 64 | private float mDrawableScale; 65 | private float mScaleFocusX; 66 | private float mScaleFocusY; 67 | 68 | private float mDisplayDrawableLeft; 69 | private float mDisplayDrawableTop; 70 | 71 | private RectF mHelperRect = new RectF(); 72 | 73 | private GestureDetector mGestureDetector; 74 | private ScaleGestureDetector mScaleGestureDetector; 75 | 76 | private float mMaximumOverScroll; 77 | 78 | private ValueAnimator mAnimator; 79 | 80 | private OnClickListener mOnClickListener = null; 81 | 82 | public InstaCropperView(Context context) { 83 | super(context); 84 | initialize(context, null, 0, 0); 85 | } 86 | 87 | public InstaCropperView(Context context, AttributeSet attrs) { 88 | super(context, attrs); 89 | initialize(context, attrs, 0, 0); 90 | } 91 | 92 | public InstaCropperView(Context context, AttributeSet attrs, int defStyleAttr) { 93 | super(context, attrs, defStyleAttr); 94 | initialize(context, attrs, defStyleAttr, 0); 95 | } 96 | 97 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 98 | public InstaCropperView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 99 | super(context, attrs, defStyleAttr, defStyleRes); 100 | initialize(context, attrs, defStyleAttr, defStyleRes); 101 | } 102 | 103 | private void initialize(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 104 | mGestureDetector = new GestureDetector(context, mOnGestureListener); 105 | mScaleGestureDetector = new ScaleGestureDetector(context, mOnScaleGestureListener); 106 | 107 | mMaximumOverScroll = getResources().getDisplayMetrics().density * MAXIMUM_OVER_SCROLL; 108 | 109 | mAnimator = new ValueAnimator(); 110 | mAnimator.setDuration(SET_BACK_DURATION); 111 | mAnimator.setFloatValues(0, 1); 112 | mAnimator.setInterpolator(new DecelerateInterpolator(0.25F)); 113 | mAnimator.addUpdateListener(mSettleAnimatorUpdateListener); 114 | 115 | mGridDrawable.setCallback(mGridCallback); 116 | } 117 | 118 | private Drawable.Callback mGridCallback = new Drawable.Callback() { 119 | 120 | @Override 121 | public void invalidateDrawable(Drawable who) { 122 | invalidate(); 123 | } 124 | 125 | @Override 126 | public void scheduleDrawable(Drawable who, Runnable what, long when) { } 127 | 128 | @Override 129 | public void unscheduleDrawable(Drawable who, Runnable what) { } 130 | 131 | }; 132 | 133 | public void setRatios(float defaultRatio, float minimumRatio, float maximumRatio) { 134 | mDefaultRatio = defaultRatio; 135 | mMinimumRatio = minimumRatio; 136 | mMaximumRatio = maximumRatio; 137 | 138 | if (mAnimator.isRunning()) { 139 | mAnimator.cancel(); 140 | } 141 | 142 | cancelMakingDrawableProcessIfExists(); 143 | 144 | mDrawable = null; 145 | 146 | requestLayout(); 147 | } 148 | 149 | public void setImageUri(Uri uri) { 150 | cancelMakingDrawableProcessIfExists(); 151 | 152 | mImageUri = uri; 153 | mDrawable = null; 154 | 155 | requestLayout(); 156 | invalidate(); 157 | } 158 | 159 | public void crop(final int widthSpec, final int heightSpec, final BitmapCallback callback) { 160 | if (mImageUri == null) { 161 | throw new IllegalStateException("Image uri is not set."); 162 | } 163 | 164 | if (mDrawable == null || mAnimator.isRunning()) { 165 | postDelayed(new Runnable() { 166 | 167 | @Override 168 | public void run() { 169 | crop(widthSpec, heightSpec, callback); 170 | } 171 | 172 | }, SET_BACK_DURATION / 2); 173 | return; 174 | } 175 | 176 | RectF gridBounds = new RectF(mGridDrawable.getBounds()); 177 | gridBounds.offset(-mDisplayDrawableLeft, -mDisplayDrawableTop); 178 | 179 | getDisplayDrawableBounds(mHelperRect); 180 | 181 | float leftRatio = gridBounds.left / mHelperRect.width(); 182 | float topRatio = gridBounds.top / mHelperRect.height(); 183 | float rightRatio = gridBounds.right / mHelperRect.width(); 184 | float bottomRatio = gridBounds.bottom / mHelperRect.height(); 185 | 186 | final int actualLeft = Math.max(0, (int) (leftRatio * mImageRawWidth)); 187 | final int actualTop = Math.max(0, (int) (topRatio * mImageRawHeight)); 188 | final int actualRight = Math.min(mImageRawWidth, (int) (rightRatio * mImageRawWidth)); 189 | final int actualBottom = Math.min(mImageRawHeight, (int) (bottomRatio * mImageRawHeight)); 190 | 191 | final Context context = getContext(); 192 | 193 | new AsyncTask() { 194 | 195 | @Override 196 | protected Bitmap doInBackground(Void... params) { 197 | int actualWidth = actualRight - actualLeft; 198 | int actualHeight = actualBottom - actualTop; 199 | float actualRatio = (float) actualWidth / (float) actualHeight; 200 | 201 | if (actualRatio < mMinimumRatio) { 202 | actualRatio = mMinimumRatio; 203 | } 204 | 205 | if (actualRatio > mMaximumRatio) { 206 | actualRatio = mMaximumRatio; 207 | } 208 | 209 | int widthMode = MeasureSpec.getMode(widthSpec); 210 | int widthSize = MeasureSpec.getSize(widthSpec); 211 | int heightMode = MeasureSpec.getMode(heightSpec); 212 | int heightSize = MeasureSpec.getSize(heightSpec); 213 | 214 | int targetWidth = actualWidth; 215 | int targetHeight = actualHeight; 216 | 217 | switch (widthMode) { 218 | case MeasureSpec.EXACTLY: 219 | targetWidth = widthSize; 220 | 221 | switch (heightMode) { 222 | case MeasureSpec.EXACTLY: 223 | targetHeight = heightSize; 224 | break; 225 | case MeasureSpec.AT_MOST: 226 | targetHeight = Math.min(heightSize, (int) (targetWidth / actualRatio)); 227 | break; 228 | case MeasureSpec.UNSPECIFIED: 229 | targetHeight = (int) (targetWidth / actualRatio); 230 | break; 231 | } 232 | break; 233 | case MeasureSpec.AT_MOST: 234 | switch (heightMode) { 235 | case MeasureSpec.EXACTLY: 236 | targetHeight = heightSize; 237 | targetWidth = Math.min(widthSize, (int) (targetHeight * actualRatio)); 238 | break; 239 | case MeasureSpec.AT_MOST: 240 | if (actualWidth <= widthSize && actualHeight <= heightSize) { 241 | targetWidth = actualWidth; 242 | targetHeight = actualHeight; 243 | } 244 | else { 245 | float specRatio = (float) widthSize / (float) heightSize; 246 | 247 | if (specRatio == actualRatio) { 248 | targetWidth = widthSize; 249 | targetHeight = heightSize; 250 | } 251 | else if (specRatio > actualRatio) { 252 | targetHeight = heightSize; 253 | targetWidth = (int) (targetHeight * actualRatio); 254 | } 255 | else { 256 | targetWidth = widthSize; 257 | targetHeight = (int) (targetWidth / actualRatio); 258 | } 259 | } 260 | break; 261 | case MeasureSpec.UNSPECIFIED: 262 | if (actualWidth <= widthSize) { 263 | targetWidth = actualWidth; 264 | targetHeight = actualHeight; 265 | } 266 | else { 267 | targetWidth = widthSize; 268 | targetHeight = (int) (targetWidth / actualRatio); 269 | } 270 | break; 271 | } 272 | break; 273 | case MeasureSpec.UNSPECIFIED: 274 | switch (heightMode) { 275 | case MeasureSpec.EXACTLY: 276 | targetHeight = heightSize; 277 | targetWidth = (int) (targetHeight * actualRatio); 278 | break; 279 | case MeasureSpec.AT_MOST: 280 | if (actualHeight <= heightSize) { 281 | targetHeight = actualHeight; 282 | targetWidth = actualWidth; 283 | } 284 | else { 285 | targetHeight = heightSize; 286 | targetWidth = (int) (targetHeight * actualRatio); 287 | } 288 | break; 289 | case MeasureSpec.UNSPECIFIED: 290 | targetWidth = actualWidth; 291 | targetHeight = actualHeight; 292 | break; 293 | } 294 | break; 295 | } 296 | 297 | return cropImageAndResize(context, actualLeft, actualTop, actualRight, actualBottom, targetWidth, targetHeight); 298 | } 299 | 300 | @Override 301 | protected void onPostExecute(Bitmap bitmap) { 302 | callback.onBitmapReady(bitmap); 303 | } 304 | 305 | }.execute(); 306 | } 307 | 308 | private Bitmap cropImageAndResize(Context context, int left, int top, int right, int bottom, int width, int height) { 309 | BitmapFactory.Options options = new BitmapFactory.Options(); 310 | options.inSampleSize = 1; 311 | 312 | int rawArea = (right - left) * (bottom - top); 313 | int targetArea = width * height * 4; 314 | 315 | int resultArea = rawArea; 316 | 317 | while (resultArea > targetArea) { 318 | options.inSampleSize *= 2; 319 | resultArea = rawArea / (options.inSampleSize * options.inSampleSize) ; 320 | } 321 | 322 | if (options.inSampleSize > 1) { 323 | options.inSampleSize /= 2; 324 | } 325 | 326 | try { 327 | Bitmap rawBitmap = MakeDrawableTask.getBitmap(context, mImageUri, options); 328 | 329 | if (rawBitmap == null) { 330 | return null; 331 | } 332 | 333 | left /= options.inSampleSize; 334 | top /= options.inSampleSize; 335 | right /= options.inSampleSize; 336 | bottom /= options.inSampleSize; 337 | 338 | int croppedWidth = right - left; 339 | int croppedHeight = bottom - top; 340 | 341 | Bitmap croppedBitmap = Bitmap.createBitmap(rawBitmap, left, top, croppedWidth, croppedHeight); 342 | 343 | if (rawBitmap != croppedBitmap) { 344 | rawBitmap.recycle(); 345 | } 346 | 347 | if (croppedWidth <= width && croppedHeight <= height) { 348 | return croppedBitmap; 349 | } 350 | 351 | Bitmap resizedBitmap = MakeDrawableTask.resizeBitmap(croppedBitmap, width, height); 352 | 353 | croppedBitmap.recycle(); 354 | 355 | return resizedBitmap; 356 | } catch (Throwable t) { 357 | return null; 358 | } 359 | } 360 | 361 | @Override 362 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 363 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); 364 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 365 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 366 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 367 | 368 | int targetWidth = 1; 369 | int targetHeight = 1; 370 | 371 | switch (widthMode) { 372 | case MeasureSpec.EXACTLY: 373 | targetWidth = widthSize; 374 | 375 | switch (heightMode) { 376 | case MeasureSpec.EXACTLY: 377 | targetHeight = heightSize; 378 | break; 379 | case MeasureSpec.AT_MOST: 380 | targetHeight = Math.min(heightSize, (int) (targetWidth / mDefaultRatio)); 381 | break; 382 | case MeasureSpec.UNSPECIFIED: 383 | targetHeight = (int) (targetWidth / mDefaultRatio); 384 | break; 385 | } 386 | break; 387 | case MeasureSpec.AT_MOST: 388 | switch (heightMode) { 389 | case MeasureSpec.EXACTLY: 390 | targetHeight = heightSize; 391 | targetWidth = Math.min(widthSize, (int) (targetHeight * mDefaultRatio)); 392 | break; 393 | case MeasureSpec.AT_MOST: 394 | float specRatio = (float) widthSize / (float) heightSize; 395 | 396 | if (specRatio == mDefaultRatio) { 397 | targetWidth = widthSize; 398 | targetHeight = heightSize; 399 | } 400 | else if (specRatio > mDefaultRatio) { 401 | targetHeight = heightSize; 402 | targetWidth = (int) (targetHeight * mDefaultRatio); 403 | } 404 | else { 405 | targetWidth = widthSize; 406 | targetHeight = (int) (targetWidth / mDefaultRatio); 407 | } 408 | break; 409 | case MeasureSpec.UNSPECIFIED: 410 | targetWidth = widthSize; 411 | targetHeight = (int) (targetWidth / mDefaultRatio); 412 | break; 413 | } 414 | break; 415 | case MeasureSpec.UNSPECIFIED: 416 | switch (heightMode) { 417 | case MeasureSpec.EXACTLY: 418 | targetHeight = heightSize; 419 | targetWidth = (int) (targetHeight * mDefaultRatio); 420 | break; 421 | case MeasureSpec.AT_MOST: 422 | targetHeight = heightSize; 423 | targetWidth = (int) (targetHeight * mDefaultRatio); 424 | break; 425 | case MeasureSpec.UNSPECIFIED: 426 | targetWidth = (int) mMaximumOverScroll; 427 | targetHeight = (int) mMaximumOverScroll; 428 | break; 429 | } 430 | break; 431 | } 432 | 433 | setMeasuredDimension(targetWidth, targetHeight); 434 | } 435 | 436 | @Override 437 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 438 | super.onLayout(changed, left, top, right, bottom); 439 | 440 | mWidth = right - left; 441 | mHeight = bottom - top; 442 | 443 | if (mWidth == 0 || mHeight == 0) { 444 | return; 445 | } 446 | 447 | if (mImageUri == null) { 448 | return; 449 | } 450 | 451 | if (currentDrawableIsSuitableForView()) { 452 | cancelMakingDrawableProcessIfExists(); 453 | return; 454 | } 455 | 456 | if (isMakingDrawableForView()) { 457 | if (drawableBeingMadeIsSuitableForView()) { 458 | return; 459 | } 460 | 461 | cancelMakingDrawableProcessIfExists(); 462 | } 463 | 464 | startMakingSuitableDrawable(); 465 | } 466 | 467 | private boolean currentDrawableIsSuitableForView() { 468 | if (mDrawable == null) { 469 | return false; 470 | } 471 | 472 | int drawableWidth = mDrawable.getIntrinsicWidth(); 473 | int drawableHeight = mDrawable.getIntrinsicHeight(); 474 | 475 | return isSizeSuitableForView(drawableWidth, drawableHeight); 476 | } 477 | 478 | private void cancelMakingDrawableProcessIfExists() { 479 | if (mMakeDrawableTask != null) { 480 | mMakeDrawableTask.cancel(true); 481 | mMakeDrawableTask = null; 482 | } 483 | } 484 | 485 | private boolean isMakingDrawableForView() { 486 | return mMakeDrawableTask != null; 487 | } 488 | 489 | private boolean drawableBeingMadeIsSuitableForView() { 490 | return isSizeSuitableForView(mMakeDrawableTask.getTargetWidth(), mMakeDrawableTask.getTargetHeight()); 491 | } 492 | 493 | private boolean isSizeSuitableForView(int width, int height) { 494 | int viewArea = mWidth * mHeight; 495 | int drawableArea = width * height; 496 | 497 | float areaRatio = (float) viewArea / (float) drawableArea; 498 | 499 | return areaRatio >= 0.5F && areaRatio <= 2F; 500 | } 501 | 502 | private void startMakingSuitableDrawable() { 503 | mMakeDrawableTask = new MakeDrawableTask(getContext(), mImageUri, mWidth, mHeight) { 504 | 505 | @Override 506 | protected void onPostExecute(Drawable drawable) { 507 | mDrawable = drawable; 508 | 509 | mImageRawWidth = getRawWidth(); 510 | mImageRawHeight = getRawHeight(); 511 | 512 | onDrawableChanged(); 513 | } 514 | 515 | }; 516 | 517 | mMakeDrawableTask.execute(); 518 | } 519 | 520 | private void onDrawableChanged() { 521 | reset(); 522 | } 523 | 524 | private void reset() { 525 | if (mAnimator.isRunning()) { 526 | mAnimator.cancel(); 527 | } 528 | 529 | scaleDrawableToFitWithinViewWithValidRatio(); 530 | 531 | placeDrawableInTheCenter(); 532 | 533 | updateGrid(); 534 | 535 | invalidate(); 536 | } 537 | 538 | private boolean isImageSizeRatioValid(float imageSizeRatio) { 539 | return imageSizeRatio >= mMinimumRatio && imageSizeRatio <= mMaximumRatio; 540 | } 541 | 542 | private float getImageSizeRatio() { 543 | return (float) mImageRawWidth / (float) mImageRawHeight; 544 | } 545 | 546 | private void scaleDrawableToFitWithinViewWithValidRatio() { 547 | float scale = getDrawableScaleToFitWithValidRatio(); 548 | 549 | setDrawableScale(scale); 550 | } 551 | 552 | private float getDrawableScaleToFitWithValidRatio() { 553 | float scale; 554 | 555 | float drawableSizeRatio = getImageSizeRatio(); 556 | boolean imageSizeRatioIsValid = isImageSizeRatioValid(drawableSizeRatio); 557 | 558 | if (imageSizeRatioIsValid) { 559 | float viewRatio = (float) mWidth / (float) mHeight; 560 | float drawableRatio = (float) mImageRawWidth / (float) mImageRawHeight; 561 | 562 | boolean drawableIsWiderThanView = drawableRatio > viewRatio; 563 | 564 | if (drawableIsWiderThanView) { 565 | scale = (float) mWidth / (float) mImageRawWidth; 566 | } 567 | else { 568 | scale = (float) mHeight / (float) mImageRawHeight; 569 | } 570 | } 571 | else { 572 | if (drawableSizeRatio < mMinimumRatio) { 573 | getBoundsForHeightAndRatio(mHeight, mMinimumRatio, mHelperRect); 574 | scale = mHelperRect.width() / mImageRawWidth; 575 | } 576 | else { 577 | getBoundsForWidthAndRatio(mWidth, mMaximumRatio, mHelperRect); 578 | scale = mHelperRect.height() / mImageRawHeight; 579 | } 580 | } 581 | 582 | return scale; 583 | } 584 | 585 | private void setDrawableScale(float scale) { 586 | mDrawableScale = scale; 587 | 588 | invalidate(); 589 | } 590 | 591 | private void placeDrawableInTheCenter() { 592 | mDisplayDrawableLeft = (mWidth - getDisplayDrawableWidth()) / 2; 593 | mDisplayDrawableTop = (mHeight - getDisplayDrawableHeight()) / 2; 594 | 595 | invalidate(); 596 | } 597 | 598 | private float getDisplayDrawableWidth() { 599 | return mDrawableScale * mImageRawWidth; 600 | } 601 | 602 | private float getDisplayDrawableHeight() { 603 | return mDrawableScale * mImageRawHeight; 604 | } 605 | 606 | private void updateGrid() { 607 | getDisplayDrawableBounds(mHelperRect); 608 | 609 | mHelperRect.intersect(0, 0, mWidth, mHeight); 610 | 611 | float gridLeft = mHelperRect.left; 612 | float gridTop = mHelperRect.top; 613 | 614 | float gridWidth = mHelperRect.width(); 615 | float gridHeight = mHelperRect.height(); 616 | 617 | mHelperRect.set(gridLeft, gridTop, gridLeft + gridWidth, gridTop + gridHeight); 618 | setGridBounds(mHelperRect); 619 | 620 | invalidate(); 621 | } 622 | 623 | private void getBoundsForWidthAndRatio(float width, float ratio, RectF rect) { 624 | float height = width / ratio; 625 | 626 | rect.set(0, 0, width, height); 627 | } 628 | 629 | private void getBoundsForHeightAndRatio(float height, float ratio, RectF rect) { 630 | float width = height * ratio; 631 | 632 | rect.set(0, 0, width, height); 633 | } 634 | 635 | private void setGridBounds(RectF bounds) { 636 | mGridDrawable.setBounds((int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom); 637 | 638 | invalidate(); 639 | } 640 | 641 | private void getDisplayDrawableBounds(RectF bounds) { 642 | bounds.left = mDisplayDrawableLeft; 643 | bounds.top = mDisplayDrawableTop; 644 | bounds.right = bounds.left + getDisplayDrawableWidth(); 645 | bounds.bottom = bounds.top + getDisplayDrawableHeight(); 646 | } 647 | 648 | @Override 649 | protected void onDraw(Canvas canvas) { 650 | super.onDraw(canvas); 651 | 652 | if (mDrawable == null) { 653 | return; 654 | } 655 | 656 | getDisplayDrawableBounds(mHelperRect); 657 | 658 | mDrawable.setBounds((int) mHelperRect.left, (int) mHelperRect.top, (int) mHelperRect.right, (int) mHelperRect.bottom); 659 | mDrawable.draw(canvas); 660 | 661 | mGridDrawable.draw(canvas); 662 | } 663 | 664 | @Override 665 | public void setOnClickListener(OnClickListener l) { 666 | mOnClickListener = l; 667 | } 668 | 669 | @Override 670 | public boolean onTouchEvent(MotionEvent event) { 671 | if (mDrawable == null) { 672 | return false; 673 | } 674 | 675 | mGestureDetector.onTouchEvent(event); 676 | mScaleGestureDetector.onTouchEvent(event); 677 | 678 | int action = event.getAction(); 679 | 680 | if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_OUTSIDE) { 681 | mAnimator.start(); 682 | } 683 | 684 | return true; 685 | } 686 | 687 | private GestureDetector.OnGestureListener mOnGestureListener = new GestureDetector.OnGestureListener() { 688 | 689 | @Override 690 | public boolean onDown(MotionEvent motionEvent) { 691 | return true; 692 | } 693 | 694 | @Override 695 | public void onShowPress(MotionEvent motionEvent) { } 696 | 697 | @Override 698 | public boolean onSingleTapUp(MotionEvent motionEvent) { 699 | if (mOnClickListener != null) { 700 | mOnClickListener.onClick(InstaCropperView.this); 701 | 702 | return true; 703 | } 704 | 705 | return false; 706 | } 707 | 708 | @Override 709 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 710 | distanceX = - distanceX; 711 | distanceY = - distanceY; 712 | 713 | getDisplayDrawableBounds(mHelperRect); 714 | 715 | float overScrollX = measureOverScrollX(mHelperRect); 716 | float overScrollY = measureOverScrollY(mHelperRect); 717 | 718 | distanceX = applyOverScrollFix(distanceX, overScrollX); 719 | distanceY = applyOverScrollFix(distanceY, overScrollY); 720 | 721 | mDisplayDrawableLeft += distanceX; 722 | mDisplayDrawableTop += distanceY; 723 | 724 | updateGrid(); 725 | invalidate(); 726 | 727 | return true; 728 | } 729 | 730 | @Override 731 | public void onLongPress(MotionEvent motionEvent) { } 732 | 733 | @Override 734 | public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) { 735 | return false; 736 | } 737 | 738 | }; 739 | 740 | private float measureOverScrollX(RectF displayDrawableBounds) { 741 | boolean drawableIsSmallerThanView = displayDrawableBounds.width() <= mWidth; 742 | 743 | if (drawableIsSmallerThanView) { 744 | return displayDrawableBounds.centerX() - mWidth/2; 745 | } 746 | 747 | if (displayDrawableBounds.left <= 0 && displayDrawableBounds.right >= mWidth) { 748 | return 0; 749 | } 750 | 751 | if (displayDrawableBounds.left < 0) { 752 | return displayDrawableBounds.right - mWidth; 753 | } 754 | 755 | if (displayDrawableBounds.right > mWidth) { 756 | return displayDrawableBounds.left; 757 | } 758 | 759 | return 0; 760 | } 761 | 762 | private float measureOverScrollY(RectF displayDrawableBounds) { 763 | boolean drawableIsSmallerThanView = displayDrawableBounds.height() < mHeight; 764 | 765 | if (drawableIsSmallerThanView) { 766 | return displayDrawableBounds.centerY() - mHeight/2; 767 | } 768 | 769 | if (displayDrawableBounds.top <= 0 && displayDrawableBounds.bottom >= mHeight) { 770 | return 0; 771 | } 772 | 773 | if (displayDrawableBounds.top < 0) { 774 | return displayDrawableBounds.bottom - mHeight; 775 | } 776 | 777 | if (displayDrawableBounds.bottom > mHeight) { 778 | return displayDrawableBounds.top; 779 | } 780 | 781 | return 0; 782 | } 783 | 784 | private float applyOverScrollFix(float distance, float overScroll) { 785 | if (overScroll * distance <= 0) { 786 | return distance; 787 | } 788 | 789 | float offRatio = Math.abs(overScroll) / mMaximumOverScroll; 790 | 791 | distance -= distance * Math.sqrt(offRatio); 792 | 793 | return distance; 794 | } 795 | 796 | private ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener = 797 | new ScaleGestureDetector.OnScaleGestureListener() { 798 | 799 | @Override 800 | public boolean onScale(ScaleGestureDetector detector) { 801 | float overScale = measureOverScale(); 802 | float scale = applyOverScaleFix(detector.getScaleFactor(), overScale); 803 | 804 | mScaleFocusX = detector.getFocusX(); 805 | mScaleFocusY = detector.getFocusY(); 806 | 807 | setScaleKeepingFocus(mDrawableScale * scale, mScaleFocusX, mScaleFocusY); 808 | 809 | return true; 810 | } 811 | 812 | @Override 813 | public boolean onScaleBegin(ScaleGestureDetector detector) { 814 | return true; 815 | } 816 | 817 | @Override 818 | public void onScaleEnd(ScaleGestureDetector detector) { } 819 | 820 | }; 821 | 822 | private float measureOverScale() { 823 | float maximumAllowedScale = getMaximumAllowedScale(); 824 | float minimumAllowedScale = getMinimumAllowedScale(); 825 | 826 | if (maximumAllowedScale < minimumAllowedScale) { 827 | maximumAllowedScale = minimumAllowedScale; 828 | } 829 | 830 | if (mDrawableScale < minimumAllowedScale) { 831 | return mDrawableScale / minimumAllowedScale; 832 | } 833 | else if (mDrawableScale > maximumAllowedScale) { 834 | return mDrawableScale / maximumAllowedScale; 835 | } 836 | else { 837 | return 1; 838 | } 839 | } 840 | 841 | private float getMaximumAllowedScale() { 842 | float maximumAllowedWidth = mImageRawWidth; 843 | float maximumAllowedHeight = mImageRawHeight; 844 | 845 | return Math.min(maximumAllowedWidth / (float) mWidth, maximumAllowedHeight / (float) mHeight); 846 | } 847 | 848 | private float getMinimumAllowedScale() { 849 | return getDrawableScaleToFitWithValidRatio(); 850 | } 851 | 852 | private float applyOverScaleFix(float scale, float overScale) { 853 | if (overScale == 1) { 854 | return scale; 855 | } 856 | 857 | if (overScale > 1) { 858 | overScale = 1F / overScale; 859 | } 860 | 861 | float wentOverScaleRatio = (overScale - MAXIMUM_OVER_SCALE) / (1 - MAXIMUM_OVER_SCALE); 862 | 863 | if (wentOverScaleRatio < 0) { 864 | wentOverScaleRatio = 0; 865 | } 866 | 867 | // 1 -> scale , 0 -> 1 868 | // scale * f(1) = scale 869 | // scale * f(0) = 1 870 | 871 | // f(1) = 1 872 | // f(0) = 1/scale 873 | 874 | scale *= wentOverScaleRatio + (1 - wentOverScaleRatio) / scale; 875 | 876 | return scale; 877 | } 878 | 879 | private void setScaleKeepingFocus(float scale, float focusX, float focusY) { 880 | getDisplayDrawableBounds(mHelperRect); 881 | 882 | float focusRatioX = (focusX - mHelperRect.left) / mHelperRect.width(); 883 | float focusRatioY = (focusY - mHelperRect.top) / mHelperRect.height(); 884 | 885 | mDrawableScale = scale; 886 | 887 | getDisplayDrawableBounds(mHelperRect); 888 | 889 | float scaledFocusX = mHelperRect.left + focusRatioX * mHelperRect.width(); 890 | float scaledFocusY = mHelperRect.top + focusRatioY * mHelperRect.height(); 891 | 892 | mDisplayDrawableLeft += focusX - scaledFocusX; 893 | mDisplayDrawableTop += focusY - scaledFocusY; 894 | 895 | updateGrid(); 896 | invalidate(); 897 | } 898 | 899 | private ValueAnimator.AnimatorUpdateListener mSettleAnimatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() { 900 | 901 | @Override 902 | public void onAnimationUpdate(ValueAnimator animation) { 903 | float animatedValue = (float) animation.getAnimatedValue(); 904 | 905 | getDisplayDrawableBounds(mHelperRect); 906 | 907 | float overScrollX = measureOverScrollX(mHelperRect); 908 | float overScrollY = measureOverScrollY(mHelperRect); 909 | float overScale = measureOverScale(); 910 | 911 | mDisplayDrawableLeft -= overScrollX * animatedValue; 912 | mDisplayDrawableTop -= overScrollY * animatedValue; 913 | 914 | float targetScale = mDrawableScale / overScale; 915 | float newScale = (1 - animatedValue) * mDrawableScale + animatedValue * targetScale; 916 | 917 | setScaleKeepingFocus(newScale, mScaleFocusX, mScaleFocusY); 918 | 919 | updateGrid(); 920 | invalidate(); 921 | } 922 | 923 | }; 924 | 925 | } 926 | -------------------------------------------------------------------------------- /instacropper/src/main/java/com/yashoid/instacropper/InstaCropperViewNew.java: -------------------------------------------------------------------------------- 1 | package com.yashoid.instacropper; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.annotation.TargetApi; 5 | import android.content.Context; 6 | import android.graphics.Bitmap; 7 | import android.graphics.Canvas; 8 | import android.graphics.PointF; 9 | import android.graphics.RectF; 10 | import android.graphics.drawable.Drawable; 11 | import android.net.Uri; 12 | import android.os.Build; 13 | import android.util.AttributeSet; 14 | import android.util.Log; 15 | import android.view.GestureDetector; 16 | import android.view.MotionEvent; 17 | import android.view.ScaleGestureDetector; 18 | import android.view.View; 19 | import android.view.animation.DecelerateInterpolator; 20 | 21 | /** 22 | * Created by Yashar on 3/8/2017. 23 | */ 24 | 25 | public class InstaCropperViewNew extends View { 26 | 27 | public static final float DEFAULT_MINIMUM_RATIO = 4F/5F; 28 | public static final float DEFAULT_MAXIMUM_RATIO = 1.91F; 29 | public static final float DEFAULT_RATIO = 1F; 30 | 31 | private static final float MAXIMUM_OVER_SCROLL = 144F; 32 | private static final float MAXIMUM_OVER_SCALE = 0.7F; 33 | 34 | private static final long SET_BACK_DURATION = 400; 35 | 36 | public interface BitmapCallback { 37 | 38 | void onBitmapReady(Bitmap bitmap); 39 | 40 | } 41 | 42 | private float mMinimumRatio = DEFAULT_MINIMUM_RATIO; 43 | private float mMaximumRatio = DEFAULT_MAXIMUM_RATIO; 44 | private float mDefaultRatio = DEFAULT_RATIO; 45 | 46 | private Uri mImageUri = null; 47 | private int mImageRawWidth; 48 | private int mImageRawHeight; 49 | 50 | private MakeDrawableTask mMakeDrawableTask = null; 51 | 52 | private int mWidth; 53 | private int mHeight; 54 | 55 | private GridDrawable mGridDrawable = new GridDrawable(); 56 | 57 | private Drawable mDrawable = null; 58 | 59 | private float mFocusX; 60 | private float mFocusY; 61 | 62 | private Rectangle mRectangle = null; 63 | private RectF mViewBounds = new RectF(); 64 | private Fitness mFitness = new Fitness(); 65 | private Fix mHelperFix = new Fix(); 66 | 67 | private RectF mHelperRect = new RectF(); 68 | 69 | private GestureDetector mGestureDetector; 70 | private ScaleGestureDetector mScaleGestureDetector; 71 | 72 | private float mMaximumOverScroll; 73 | 74 | private ValueAnimator mAnimator; 75 | 76 | public InstaCropperViewNew(Context context) { 77 | super(context); 78 | initialize(context, null, 0, 0); 79 | } 80 | 81 | public InstaCropperViewNew(Context context, AttributeSet attrs) { 82 | super(context, attrs); 83 | initialize(context, attrs, 0, 0); 84 | } 85 | 86 | public InstaCropperViewNew(Context context, AttributeSet attrs, int defStyleAttr) { 87 | super(context, attrs, defStyleAttr); 88 | initialize(context, attrs, defStyleAttr, 0); 89 | } 90 | 91 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 92 | public InstaCropperViewNew(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 93 | super(context, attrs, defStyleAttr, defStyleRes); 94 | initialize(context, attrs, defStyleAttr, defStyleRes); 95 | } 96 | 97 | private void initialize(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 98 | mGestureDetector = new GestureDetector(context, mOnGestureListener); 99 | mScaleGestureDetector = new ScaleGestureDetector(context, mOnScaleGestureListener); 100 | 101 | mMaximumOverScroll = getResources().getDisplayMetrics().density * MAXIMUM_OVER_SCROLL; 102 | 103 | mAnimator = new ValueAnimator(); 104 | mAnimator.setDuration(SET_BACK_DURATION); 105 | mAnimator.setFloatValues(0, 1); 106 | mAnimator.setInterpolator(new DecelerateInterpolator(0.25F)); 107 | mAnimator.addUpdateListener(mSettleAnimatorUpdateListener); 108 | 109 | mGridDrawable.setCallback(mGridCallback); 110 | } 111 | 112 | private Drawable.Callback mGridCallback = new Drawable.Callback() { 113 | 114 | @Override 115 | public void invalidateDrawable(Drawable who) { 116 | invalidate(); 117 | } 118 | 119 | @Override 120 | public void scheduleDrawable(Drawable who, Runnable what, long when) { } 121 | 122 | @Override 123 | public void unscheduleDrawable(Drawable who, Runnable what) { } 124 | 125 | }; 126 | 127 | public void setRatios(float defaultRatio, float minimumRatio, float maximumRatio) { 128 | mDefaultRatio = defaultRatio; 129 | mMinimumRatio = minimumRatio; 130 | mMaximumRatio = maximumRatio; 131 | 132 | if (mAnimator.isRunning()) { 133 | mAnimator.cancel(); 134 | } 135 | 136 | cancelMakingDrawableProcessIfExists(); 137 | 138 | mDrawable = null; 139 | 140 | requestLayout(); 141 | } 142 | 143 | public void setImageUri(Uri uri) { 144 | cancelMakingDrawableProcessIfExists(); 145 | 146 | mImageUri = uri; 147 | 148 | requestLayout(); 149 | invalidate(); 150 | } 151 | 152 | public void setDrawableRotation(float rotation) { 153 | if (rotation == mRectangle.getRotation()) { 154 | return; 155 | } 156 | 157 | // TODO 158 | 159 | invalidate(); 160 | } 161 | 162 | public float getDrawableRotation() { 163 | return mRectangle.getRotation(); 164 | } 165 | 166 | // public void crop(final int widthSpec, final int heightSpec, final BitmapCallback callback) { 167 | // if (mImageUri == null) { 168 | // throw new IllegalStateException("Image uri is not set."); 169 | // } 170 | // 171 | // if (mDrawable == null || mAnimator.isRunning()) { 172 | // postDelayed(new Runnable() { 173 | // 174 | // @Override 175 | // public void run() { 176 | // crop(widthSpec, heightSpec, callback); 177 | // } 178 | // 179 | // }, SET_BACK_DURATION / 2); 180 | // return; 181 | // } 182 | // 183 | // RectF gridBounds = new RectF(mGridDrawable.getBounds()); 184 | // gridBounds.offset(-mDrawableLeft, -mDrawableTop); 185 | // 186 | // getDisplayDrawableBounds(mHelperRect); 187 | // 188 | // float leftRatio = gridBounds.left / mHelperRect.width(); 189 | // float topRatio = gridBounds.top / mHelperRect.height(); 190 | // float rightRatio = gridBounds.right / mHelperRect.width(); 191 | // float bottomRatio = gridBounds.bottom / mHelperRect.height(); 192 | // 193 | // final int actualLeft = Math.max(0, (int) (leftRatio * mImageRawWidth)); 194 | // final int actualTop = Math.max(0, (int) (topRatio * mImageRawHeight)); 195 | // final int actualRight = Math.min(mImageRawWidth, (int) (rightRatio * mImageRawWidth)); 196 | // final int actualBottom = Math.min(mImageRawHeight, (int) (bottomRatio * mImageRawHeight)); 197 | // 198 | // final Context context = getContext(); 199 | // 200 | // new AsyncTask() { 201 | // 202 | // @Override 203 | // protected Bitmap doInBackground(Void... params) { 204 | // int actualWidth = actualRight - actualLeft; 205 | // int actualHeight = actualBottom - actualTop; 206 | // float actualRatio = (float) actualWidth / (float) actualHeight; 207 | // 208 | // if (actualRatio < mMinimumRatio) { 209 | // actualRatio = mMinimumRatio; 210 | // } 211 | // 212 | // if (actualRatio > mMaximumRatio) { 213 | // actualRatio = mMaximumRatio; 214 | // } 215 | // 216 | // int widthMode = MeasureSpec.getMode(widthSpec); 217 | // int widthSize = MeasureSpec.getSize(widthSpec); 218 | // int heightMode = MeasureSpec.getMode(heightSpec); 219 | // int heightSize = MeasureSpec.getSize(heightSpec); 220 | // 221 | // int targetWidth = actualWidth; 222 | // int targetHeight = actualHeight; 223 | // 224 | // switch (widthMode) { 225 | // case MeasureSpec.EXACTLY: 226 | // targetWidth = widthSize; 227 | // 228 | // switch (heightMode) { 229 | // case MeasureSpec.EXACTLY: 230 | // targetHeight = heightSize; 231 | // break; 232 | // case MeasureSpec.AT_MOST: 233 | // targetHeight = Math.min(heightSize, (int) (targetWidth / actualRatio)); 234 | // break; 235 | // case MeasureSpec.UNSPECIFIED: 236 | // targetHeight = (int) (targetWidth / actualRatio); 237 | // break; 238 | // } 239 | // break; 240 | // case MeasureSpec.AT_MOST: 241 | // switch (heightMode) { 242 | // case MeasureSpec.EXACTLY: 243 | // targetHeight = heightSize; 244 | // targetWidth = Math.min(widthSize, (int) (targetHeight * actualRatio)); 245 | // break; 246 | // case MeasureSpec.AT_MOST: 247 | // if (actualWidth <= widthSize && actualHeight <= heightSize) { 248 | // targetWidth = actualWidth; 249 | // targetHeight = actualHeight; 250 | // } 251 | // else { 252 | // float specRatio = (float) widthSize / (float) heightSize; 253 | // 254 | // if (specRatio == actualRatio) { 255 | // targetWidth = widthSize; 256 | // targetHeight = heightSize; 257 | // } 258 | // else if (specRatio > actualRatio) { 259 | // targetHeight = heightSize; 260 | // targetWidth = (int) (targetHeight * actualRatio); 261 | // } 262 | // else { 263 | // targetWidth = widthSize; 264 | // targetHeight = (int) (targetWidth / actualRatio); 265 | // } 266 | // } 267 | // break; 268 | // case MeasureSpec.UNSPECIFIED: 269 | // if (actualWidth <= widthSize) { 270 | // targetWidth = actualWidth; 271 | // targetHeight = actualHeight; 272 | // } 273 | // else { 274 | // targetWidth = widthSize; 275 | // targetHeight = (int) (targetWidth / actualRatio); 276 | // } 277 | // break; 278 | // } 279 | // break; 280 | // case MeasureSpec.UNSPECIFIED: 281 | // switch (heightMode) { 282 | // case MeasureSpec.EXACTLY: 283 | // targetHeight = heightSize; 284 | // targetWidth = (int) (targetHeight * actualRatio); 285 | // break; 286 | // case MeasureSpec.AT_MOST: 287 | // if (actualHeight <= heightSize) { 288 | // targetHeight = actualHeight; 289 | // targetWidth = actualWidth; 290 | // } 291 | // else { 292 | // targetHeight = heightSize; 293 | // targetWidth = (int) (targetHeight * actualRatio); 294 | // } 295 | // break; 296 | // case MeasureSpec.UNSPECIFIED: 297 | // targetWidth = actualWidth; 298 | // targetHeight = actualHeight; 299 | // break; 300 | // } 301 | // break; 302 | // } 303 | // 304 | // return cropImageAndResize(context, actualLeft, actualTop, actualRight, actualBottom, targetWidth, targetHeight); 305 | // } 306 | // 307 | // @Override 308 | // protected void onPostExecute(Bitmap bitmap) { 309 | // callback.onBitmapReady(bitmap); 310 | // } 311 | // 312 | // }.execute(); 313 | // } 314 | // 315 | // private Bitmap cropImageAndResize(Context context, int left, int top, int right, int bottom, int width, int height) { 316 | // BitmapFactory.Options options = new BitmapFactory.Options(); 317 | // options.inSampleSize = 1; 318 | // 319 | // int rawArea = (right - left) * (bottom - top); 320 | // int targetArea = width * height; 321 | // 322 | // int resultArea = rawArea; 323 | // 324 | // while (resultArea > targetArea) { 325 | // options.inSampleSize *= 2; 326 | // resultArea = rawArea / (options.inSampleSize * options.inSampleSize) ; 327 | // } 328 | // 329 | // if (options.inSampleSize > 1) { 330 | // options.inSampleSize /= 2; 331 | // } 332 | // 333 | // try { 334 | // Bitmap rawBitmap = MakeDrawableTask.getBitmap(context, mImageUri, options); 335 | // 336 | // if (rawBitmap == null) { 337 | // return null; 338 | // } 339 | // 340 | // left /= options.inSampleSize; 341 | // top /= options.inSampleSize; 342 | // right /= options.inSampleSize; 343 | // bottom /= options.inSampleSize; 344 | // 345 | // int croppedWidth = right - left; 346 | // int croppedHeight = bottom - top; 347 | // 348 | // Bitmap croppedBitmap = Bitmap.createBitmap(rawBitmap, left, top, croppedWidth, croppedHeight); 349 | // 350 | // if (rawBitmap != croppedBitmap) { 351 | // rawBitmap.recycle(); 352 | // } 353 | // 354 | // if (croppedWidth <= width && croppedHeight <= height) { 355 | // return croppedBitmap; 356 | // } 357 | // 358 | // Bitmap resizedBitmap = Bitmap.createScaledBitmap(croppedBitmap, width, height, false); 359 | // 360 | // croppedBitmap.recycle(); 361 | // 362 | // return resizedBitmap; 363 | // } catch (Throwable t) { 364 | // return null; 365 | // } 366 | // } 367 | 368 | @Override 369 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 370 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); 371 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); 372 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 373 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); 374 | 375 | int targetWidth = 1; 376 | int targetHeight = 1; 377 | 378 | switch (widthMode) { 379 | case MeasureSpec.EXACTLY: 380 | targetWidth = widthSize; 381 | 382 | switch (heightMode) { 383 | case MeasureSpec.EXACTLY: 384 | targetHeight = heightSize; 385 | break; 386 | case MeasureSpec.AT_MOST: 387 | targetHeight = Math.min(heightSize, (int) (targetWidth / mDefaultRatio)); 388 | break; 389 | case MeasureSpec.UNSPECIFIED: 390 | targetHeight = (int) (targetWidth / mDefaultRatio); 391 | break; 392 | } 393 | break; 394 | case MeasureSpec.AT_MOST: 395 | switch (heightMode) { 396 | case MeasureSpec.EXACTLY: 397 | targetHeight = heightSize; 398 | targetWidth = Math.min(widthSize, (int) (targetHeight * mDefaultRatio)); 399 | break; 400 | case MeasureSpec.AT_MOST: 401 | float specRatio = (float) widthSize / (float) heightSize; 402 | 403 | if (specRatio == mDefaultRatio) { 404 | targetWidth = widthSize; 405 | targetHeight = heightSize; 406 | } 407 | else if (specRatio > mDefaultRatio) { 408 | targetHeight = heightSize; 409 | targetWidth = (int) (targetHeight * mDefaultRatio); 410 | } 411 | else { 412 | targetWidth = widthSize; 413 | targetHeight = (int) (targetWidth / mDefaultRatio); 414 | } 415 | break; 416 | case MeasureSpec.UNSPECIFIED: 417 | targetWidth = widthSize; 418 | targetHeight = (int) (targetWidth / mDefaultRatio); 419 | break; 420 | } 421 | break; 422 | case MeasureSpec.UNSPECIFIED: 423 | switch (heightMode) { 424 | case MeasureSpec.EXACTLY: 425 | targetHeight = heightSize; 426 | targetWidth = (int) (targetHeight * mDefaultRatio); 427 | break; 428 | case MeasureSpec.AT_MOST: 429 | targetHeight = heightSize; 430 | targetWidth = (int) (targetHeight * mDefaultRatio); 431 | break; 432 | case MeasureSpec.UNSPECIFIED: 433 | targetWidth = (int) mMaximumOverScroll; 434 | targetHeight = (int) mMaximumOverScroll; 435 | break; 436 | } 437 | break; 438 | } 439 | 440 | setMeasuredDimension(targetWidth, targetHeight); 441 | } 442 | 443 | @Override 444 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 445 | super.onLayout(changed, left, top, right, bottom); 446 | 447 | mWidth = right - left; 448 | mHeight = bottom - top; 449 | 450 | if (mWidth == 0 || mHeight == 0) { 451 | return; 452 | } 453 | 454 | if (mImageUri == null) { 455 | return; 456 | } 457 | 458 | mViewBounds.set(0, 0, mWidth, mHeight); 459 | 460 | if (currentDrawableIsSuitableForView()) { 461 | cancelMakingDrawableProcessIfExists(); 462 | return; 463 | } 464 | 465 | if (isMakingDrawableForView()) { 466 | if (drawableBeingMadeIsSuitableForView()) { 467 | return; 468 | } 469 | 470 | cancelMakingDrawableProcessIfExists(); 471 | } 472 | 473 | startMakingSuitableDrawable(); 474 | } 475 | 476 | private boolean currentDrawableIsSuitableForView() { 477 | if (mDrawable == null) { 478 | return false; 479 | } 480 | 481 | int drawableWidth = mDrawable.getIntrinsicWidth(); 482 | int drawableHeight = mDrawable.getIntrinsicHeight(); 483 | 484 | return isSizeSuitableForView(drawableWidth, drawableHeight); 485 | } 486 | 487 | private void cancelMakingDrawableProcessIfExists() { 488 | if (mMakeDrawableTask != null) { 489 | mMakeDrawableTask.cancel(true); 490 | mMakeDrawableTask = null; 491 | } 492 | } 493 | 494 | private boolean isMakingDrawableForView() { 495 | return mMakeDrawableTask != null; 496 | } 497 | 498 | private boolean drawableBeingMadeIsSuitableForView() { 499 | return isSizeSuitableForView(mMakeDrawableTask.getTargetWidth(), mMakeDrawableTask.getTargetHeight()); 500 | } 501 | 502 | private boolean isSizeSuitableForView(int width, int height) { 503 | int viewArea = mWidth * mHeight; 504 | int drawableArea = width * height; 505 | 506 | float areaRatio = (float) viewArea / (float) drawableArea; 507 | 508 | return areaRatio >= 0.5F && areaRatio <= 2F; 509 | } 510 | 511 | private void startMakingSuitableDrawable() { 512 | mMakeDrawableTask = new MakeDrawableTask(getContext(), mImageUri, mWidth, mHeight) { 513 | 514 | @Override 515 | protected void onPostExecute(Drawable drawable) { 516 | mDrawable = drawable; 517 | 518 | mImageRawWidth = getRawWidth(); 519 | mImageRawHeight = getRawHeight(); 520 | 521 | onDrawableChanged(); 522 | } 523 | 524 | }; 525 | 526 | mMakeDrawableTask.execute(); 527 | } 528 | 529 | private void onDrawableChanged() { 530 | reset(); 531 | } 532 | 533 | private void reset() { 534 | if (mAnimator.isRunning()) { 535 | mAnimator.cancel(); 536 | } 537 | 538 | mRectangle = new Rectangle(mImageRawWidth, mImageRawHeight, mViewBounds.centerX(), mViewBounds.centerY()); 539 | mRectangle.getFitness(mViewBounds, mFitness); 540 | mFitness.getFittingFix(mHelperFix); 541 | mHelperFix.apply(mRectangle, mViewBounds, mMinimumRatio, mMaximumRatio); 542 | 543 | updateGrid(); 544 | 545 | invalidate(); 546 | } 547 | 548 | private void updateGrid() { 549 | mHelperRect.set(mViewBounds); 550 | mHelperRect.intersect(0, 0, mWidth, mHeight); 551 | 552 | setGridBounds(mHelperRect); 553 | 554 | invalidate(); 555 | } 556 | 557 | private void setGridBounds(RectF bounds) { 558 | mGridDrawable.setBounds((int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom); 559 | 560 | invalidate(); 561 | } 562 | 563 | @Override 564 | protected void onDraw(Canvas canvas) { 565 | super.onDraw(canvas); 566 | 567 | if (mDrawable == null) { 568 | return; 569 | } 570 | 571 | canvas.save(); 572 | 573 | canvas.translate(- mRectangle.getCenterX(), - mRectangle.getCenterY()); 574 | 575 | mGridDrawable.draw(canvas); 576 | 577 | canvas.scale(mRectangle.getScale(), mRectangle.getScale(), 0, 0); 578 | canvas.rotate(mRectangle.getRotation(), 0, 0); 579 | 580 | mDrawable.setBounds(0, 0, (int) mRectangle.getWidth(), (int) mRectangle.getHeight()); 581 | mDrawable.draw(canvas); 582 | 583 | canvas.restore(); 584 | 585 | Log.d("AAA", "raw: " + mImageRawWidth + ", " + mImageRawHeight + " now: " + mRectangle.getWidth() + ", " + mRectangle.getHeight()); 586 | } 587 | 588 | @Override 589 | public boolean onTouchEvent(MotionEvent event) { 590 | if (mDrawable == null) { 591 | return false; 592 | } 593 | 594 | mGestureDetector.onTouchEvent(event); 595 | mScaleGestureDetector.onTouchEvent(event); 596 | 597 | int action = event.getAction(); 598 | 599 | if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_OUTSIDE) { 600 | mAnimator.start(); 601 | } 602 | 603 | return true; 604 | } 605 | 606 | private GestureDetector.OnGestureListener mOnGestureListener = new GestureDetector.OnGestureListener() { 607 | 608 | @Override 609 | public boolean onDown(MotionEvent motionEvent) { 610 | return true; 611 | } 612 | 613 | @Override 614 | public void onShowPress(MotionEvent motionEvent) { } 615 | 616 | @Override 617 | public boolean onSingleTapUp(MotionEvent motionEvent) { 618 | return false; 619 | } 620 | 621 | @Override 622 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 623 | distanceX = - distanceX; 624 | distanceY = - distanceY; 625 | 626 | mRectangle.getFitness(mViewBounds, mFitness); 627 | mFitness.getEssentialFix(mHelperFix); 628 | 629 | float overScrollX = mHelperFix.translateX; 630 | float overScrollY = mHelperFix.translateY; 631 | 632 | distanceX = applyOverScrollFix(distanceX, overScrollX); 633 | distanceY = applyOverScrollFix(distanceY, overScrollY); 634 | 635 | mRectangle.translateBy(distanceX, distanceY); 636 | 637 | updateGrid(); 638 | 639 | invalidate(); 640 | 641 | return true; 642 | } 643 | 644 | @Override 645 | public void onLongPress(MotionEvent motionEvent) { } 646 | 647 | @Override 648 | public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) { 649 | return false; 650 | } 651 | 652 | }; 653 | 654 | private float applyOverScrollFix(float distance, float overScroll) { 655 | if (overScroll * distance <= 0) { 656 | return distance; 657 | } 658 | 659 | float offRatio = Math.abs(overScroll) / mMaximumOverScroll; 660 | 661 | distance -= distance * Math.sqrt(offRatio); 662 | 663 | return distance; 664 | } 665 | 666 | private ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener = 667 | new ScaleGestureDetector.OnScaleGestureListener() { 668 | 669 | @Override 670 | public boolean onScale(ScaleGestureDetector detector) { 671 | float overScale = measureOverScale(); 672 | float scale = applyOverScaleFix(detector.getScaleFactor(), overScale); 673 | 674 | mFocusX = detector.getFocusX(); 675 | mFocusY = detector.getFocusY(); 676 | 677 | mRectangle.scaleBy(scale, mFocusX, mFocusY); 678 | 679 | return true; 680 | } 681 | 682 | @Override 683 | public boolean onScaleBegin(ScaleGestureDetector detector) { 684 | return true; 685 | } 686 | 687 | @Override 688 | public void onScaleEnd(ScaleGestureDetector detector) { } 689 | 690 | }; 691 | 692 | private float measureOverScale() { 693 | mRectangle.getFitness(mViewBounds, mFitness); 694 | mFitness.getEssentialFix(mHelperFix); 695 | 696 | return measureOverScale(mHelperFix); 697 | } 698 | 699 | private float measureOverScale(Fix fix) { 700 | float maximumAllowedScale = getMaximumAllowedScale(); 701 | float minimumAllowedScale = getMinimumAllowedScale(); 702 | 703 | float scale = fix.getScale(mRectangle); 704 | 705 | if (scale < minimumAllowedScale) { 706 | return scale / minimumAllowedScale; 707 | } 708 | else if (scale > maximumAllowedScale) { 709 | return scale / maximumAllowedScale; 710 | } 711 | else { 712 | return 1; 713 | } 714 | } 715 | 716 | private float getMaximumAllowedScale() { 717 | float maximumAllowedWidth = mImageRawWidth; 718 | float maximumAllowedHeight = mImageRawHeight; 719 | // TODO 720 | return Math.min(maximumAllowedWidth / (float) mWidth, maximumAllowedHeight / (float) mHeight); 721 | } 722 | 723 | private float getMinimumAllowedScale() { 724 | mRectangle.getFitness(mViewBounds, mFitness); 725 | mFitness.getFittingFix(mHelperFix); 726 | 727 | return mHelperFix.getScale(mRectangle); 728 | } 729 | 730 | private float applyOverScaleFix(float scale, float overScale) { 731 | if (overScale == 1) { 732 | return scale; 733 | } 734 | 735 | if (overScale > 1) { 736 | overScale = 1F / overScale; 737 | } 738 | 739 | float wentOverScaleRatio = (overScale - MAXIMUM_OVER_SCALE) / (1 - MAXIMUM_OVER_SCALE); 740 | 741 | if (wentOverScaleRatio < 0) { 742 | wentOverScaleRatio = 0; 743 | } 744 | 745 | // 1 -> scale , 0 -> 1 746 | // scale * f(1) = scale 747 | // scale * f(0) = 1 748 | 749 | // f(1) = 1 750 | // f(0) = 1/scale 751 | 752 | scale *= wentOverScaleRatio + (1 - wentOverScaleRatio) / scale; 753 | 754 | return scale; 755 | } 756 | 757 | private ValueAnimator.AnimatorUpdateListener mSettleAnimatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() { 758 | 759 | @Override 760 | public void onAnimationUpdate(ValueAnimator animation) { 761 | float animatedValue = (float) animation.getAnimatedValue(); 762 | 763 | mRectangle.getFitness(mViewBounds, mFitness); 764 | mFitness.getEssentialFix(mHelperFix); 765 | 766 | float overScrollX = mHelperFix.translateX; 767 | float overScrollY = mHelperFix.translateY; 768 | float overScale = measureOverScale(mHelperFix); 769 | 770 | float translateX = - overScrollX * animatedValue; 771 | float translateY = - overScrollY * animatedValue; 772 | mRectangle.translateBy(translateX, translateY); 773 | 774 | float scale = mRectangle.getScale(); 775 | 776 | float targetScale = scale / overScale; 777 | 778 | Log.d("AAA", "scale="+scale + " targetScale=" + targetScale); 779 | 780 | float newScale = (1 - animatedValue) * scale + animatedValue * targetScale; 781 | mRectangle.scaleBy(newScale / scale , mFocusX, mFocusY); 782 | 783 | updateGrid(); 784 | invalidate(); 785 | } 786 | 787 | }; 788 | 789 | private static class Fix { 790 | 791 | protected float translateX; 792 | protected float translateY; 793 | 794 | protected float sizeChangeX; 795 | protected float sizeChangeY; 796 | 797 | protected Fix() { 798 | 799 | } 800 | 801 | protected void apply(Rectangle rectangle, RectF inside, float minimumRatio, float maximumRatio) { 802 | rectangle.translateBy(translateX, translateY); 803 | 804 | float rotation = rectangle.getRotation(); 805 | 806 | if (rotation == 0 || rotation == 180) { 807 | float width = rectangle.getWidth(); 808 | float height = rectangle.getHeight(); 809 | 810 | float newWidth = width + sizeChangeX; 811 | float newHeight = height + sizeChangeY; 812 | 813 | // TODO 814 | } 815 | 816 | float scale = getScale(rectangle); 817 | 818 | rectangle.scaleBy(scale, rectangle.getCenterX(), rectangle.getCenterY()); 819 | } 820 | 821 | protected float getScale(Rectangle rectangle) { 822 | float width = rectangle.getWidth(); 823 | float height = rectangle.getHeight(); 824 | float ratio = width / height; 825 | 826 | float rotation = rectangle.getRotation(); 827 | double r = rotation / (2 * Math.PI); 828 | double sin = Math.sin(-r); 829 | double cos = Math.cos(-r); 830 | 831 | double widthChange = sizeChangeX * cos - sizeChangeY * sin; 832 | double heightChange = sizeChangeX * sin + sizeChangeY * cos; 833 | 834 | float newWidth = (float) (width + widthChange); 835 | float newHeight = (float) (height + heightChange); 836 | float newRatio = newWidth / newHeight; 837 | 838 | if (newRatio < ratio) { 839 | newHeight = newWidth / ratio; 840 | } 841 | else { 842 | newWidth = newHeight * ratio; 843 | } 844 | 845 | return newWidth / width; 846 | } 847 | 848 | @Override 849 | public String toString() { 850 | return "dx=" + translateX + " dy=" + translateY + " dSizeX=" + sizeChangeX + " dSizeY=" + sizeChangeY; 851 | } 852 | } 853 | 854 | private static class Fitness { 855 | 856 | private RectF mEssentialBias = new RectF(); 857 | private RectF mOptionalBias = new RectF(); 858 | 859 | protected Fitness() { 860 | 861 | } 862 | 863 | protected void set(float essentialInPositiveX, float essentialInNegativeX, float essentialInPositiveY, float essentialInNegativeY, 864 | float optionalInPositiveX, float optionalInNegativeX, float optionalInPositiveY, float optionalInNegativeY) { 865 | mEssentialBias.set(essentialInNegativeX, essentialInNegativeY, essentialInPositiveX, essentialInPositiveY); 866 | mOptionalBias.set(optionalInNegativeX, optionalInNegativeY, optionalInPositiveX, optionalInPositiveY); 867 | 868 | Log.d("AAA", "fitness set. " + toString()); 869 | } 870 | 871 | protected void getFittingFix(Fix fix) { 872 | fix.translateX = mEssentialBias.centerX() + mOptionalBias.centerX(); 873 | fix.translateY = mEssentialBias.centerY() + mOptionalBias.centerY(); 874 | 875 | fix.sizeChangeX = mEssentialBias.width() - mOptionalBias.width(); 876 | fix.sizeChangeY = mEssentialBias.height() - mOptionalBias.height(); 877 | 878 | Log.d("AAA", "Fitting fix is: " + fix); 879 | } 880 | 881 | protected void getEssentialFix(Fix fix) { 882 | if (mOptionalBias.left >= mEssentialBias.left && mOptionalBias.right <= mEssentialBias.right) { 883 | fix.translateX = mEssentialBias.centerX(); 884 | fix.sizeChangeX = mEssentialBias.width(); 885 | } 886 | else if (mOptionalBias.left <= mEssentialBias.left && mOptionalBias.right >= mEssentialBias.right) { 887 | fix.translateX = 0; 888 | fix.sizeChangeX = 0; 889 | } 890 | else if (mEssentialBias.left < mOptionalBias.left) { 891 | fix.translateX = mEssentialBias.left; 892 | fix.sizeChangeX = Math.max(0, mEssentialBias.right - mOptionalBias.right); 893 | } 894 | else if (mEssentialBias.right > mOptionalBias.right) { 895 | fix.translateX = mEssentialBias.right; 896 | fix.sizeChangeX = Math.max(0, mOptionalBias.left - mEssentialBias.left); 897 | } 898 | 899 | if (mOptionalBias.top >= mEssentialBias.top && mOptionalBias.bottom <= mEssentialBias.bottom) { 900 | fix.translateY = mEssentialBias.centerY(); 901 | fix.sizeChangeY = mEssentialBias.height(); 902 | } 903 | else if (mOptionalBias.top <= mEssentialBias.top && mOptionalBias.bottom >= mEssentialBias.bottom) { 904 | fix.translateY = 0; 905 | fix.sizeChangeY = 0; 906 | } 907 | else if (mEssentialBias.top < mOptionalBias.top) { 908 | fix.translateY = mEssentialBias.top; 909 | fix.sizeChangeY = Math.max(0, mEssentialBias.bottom - mOptionalBias.bottom); 910 | } 911 | else if (mEssentialBias.bottom > mOptionalBias.bottom) { 912 | fix.translateY = mEssentialBias.bottom; 913 | fix.sizeChangeY = Math.max(0, mOptionalBias.top - mEssentialBias.top); 914 | } 915 | } 916 | 917 | @Override 918 | public String toString() { 919 | return "Essential bias: " + mEssentialBias.toString() + " Optional bias: " + mOptionalBias.toString(); 920 | } 921 | 922 | } 923 | 924 | private static class Rectangle { 925 | 926 | private float mWidth; 927 | private float mHeight; 928 | 929 | private float mCenterX; 930 | private float mCenterY; 931 | 932 | private float mScale = 1; 933 | private float mRotation = 0; 934 | 935 | private Line[] mLines; 936 | 937 | protected Rectangle(float width, float height, float centerX, float centerY) { 938 | mWidth = width; 939 | mHeight = height; 940 | 941 | mCenterX = centerX; 942 | mCenterY = centerY; 943 | 944 | mLines = new Line[4]; 945 | 946 | mLines[0] = new Line(centerX - width/2, centerY - height/2, 1, 0); 947 | mLines[1] = new Line(centerX - width/2, centerY - height/2, 0, 1); 948 | mLines[2] = new Line(centerX + width/2, centerY + height/2, -1, 0); 949 | mLines[3] = new Line(centerX + width/2, centerY + height/2, 0, -1); 950 | } 951 | 952 | protected float getWidth() { 953 | return mWidth; 954 | } 955 | 956 | protected float getHeight() { 957 | return mHeight; 958 | } 959 | 960 | protected float getCenterX() { 961 | return mCenterX; 962 | } 963 | 964 | protected float getCenterY() { 965 | return mCenterY; 966 | } 967 | 968 | protected float getScale() { 969 | return mScale; 970 | } 971 | 972 | protected void getFitness(RectF bounds, Fitness fitness) { 973 | float essentialInPositiveX = 0; 974 | float essentialInNegativeX = 0; 975 | float essentialInPositiveY = 0; 976 | float essentialInNegativeY = 0; 977 | 978 | float optionalInPositiveX = 0; 979 | float optionalInNegativeX = 0; 980 | float optionalInPositiveY = 0; 981 | float optionalInNegativeY = 0; 982 | 983 | for (Line line: mLines) { 984 | float lineFitness = line.getFitness(bounds); 985 | // Log.d("AAA", "Line fitness: " + lineFitness); 986 | 987 | boolean isEssential = lineFitness < 0; 988 | 989 | float dx = lineFitness * line.directionX; 990 | float dy = lineFitness * line.directionY; 991 | 992 | if (isEssential) { 993 | if (dx > 0) { 994 | essentialInPositiveX = Math.max(essentialInPositiveX, dx); 995 | } 996 | else { 997 | essentialInNegativeX = Math.min(essentialInNegativeX, dx); 998 | } 999 | 1000 | if (dy > 0) { 1001 | essentialInPositiveY = Math.max(essentialInPositiveY, dy); 1002 | } 1003 | else { 1004 | essentialInNegativeY = Math.min(essentialInNegativeY, dy); 1005 | } 1006 | } 1007 | else { 1008 | if (dx > 0) { 1009 | optionalInPositiveX = Math.min(optionalInPositiveX, dx); 1010 | } 1011 | else { 1012 | optionalInNegativeX = Math.max(optionalInNegativeX, dx); 1013 | } 1014 | 1015 | if (dy > 0) { 1016 | optionalInPositiveY = Math.min(optionalInPositiveY, dy); 1017 | } 1018 | else { 1019 | optionalInNegativeY = Math.max(optionalInNegativeY, dy); 1020 | } 1021 | } 1022 | } 1023 | 1024 | fitness.set(essentialInPositiveX, essentialInNegativeX, essentialInPositiveY, essentialInNegativeY, 1025 | optionalInPositiveX, optionalInNegativeX, optionalInPositiveY, optionalInNegativeY); 1026 | } 1027 | 1028 | private float getRotation() { 1029 | return mRotation; 1030 | } 1031 | 1032 | private void updateRotation() { 1033 | while (mRotation >= 360) { 1034 | mRotation -= 360; 1035 | } 1036 | 1037 | while (mRotation < 0) { 1038 | mRotation += 360; 1039 | } 1040 | } 1041 | 1042 | // protected void rotateBy(float degrees) { 1043 | // mRotation += degrees; 1044 | // updateRotation(); 1045 | // 1046 | // double r = degrees / (2 * Math.PI); 1047 | // 1048 | // double sin = Math.sin(r); 1049 | // double cos = Math.cos(r); 1050 | // 1051 | // for (Line line: mLines) { 1052 | // line.rotateBy(sin, cos); 1053 | // } 1054 | // } 1055 | 1056 | protected void translateBy(float dx, float dy) { 1057 | mCenterX += dx; 1058 | mCenterY += dy; 1059 | 1060 | for (Line line: mLines) { 1061 | line.translateBy(dx, dy); 1062 | } 1063 | } 1064 | 1065 | protected void scaleBy(float scale, float pivotX, float pivotY) { 1066 | mScale *= scale; 1067 | 1068 | mWidth *= scale; 1069 | mHeight *= scale; 1070 | 1071 | for (Line line: mLines) { 1072 | float fitness = line.getFitness(pivotX, pivotY); 1073 | 1074 | float translateX = (scale - 1) * fitness * line.directionX; 1075 | float translateY = (scale - 1) * fitness * line.directionY; 1076 | 1077 | line.translateBy(translateX, translateY); 1078 | } 1079 | 1080 | calcCenter(); 1081 | } 1082 | 1083 | private void calcCenter() { 1084 | float sumX = 0; 1085 | float sumY = 0; 1086 | 1087 | for (Line line: mLines) { 1088 | sumX += line.x; 1089 | sumY += line.y; 1090 | } 1091 | 1092 | mCenterX = sumX / mLines.length; 1093 | mCenterY = sumY / mLines.length; 1094 | } 1095 | 1096 | } 1097 | 1098 | private static class Line { 1099 | 1100 | private float x; 1101 | private float y; 1102 | private float directionX; 1103 | private float directionY; 1104 | 1105 | protected Line(float x, float y, float directionX, float directionY) { 1106 | this.x = x; 1107 | this.y = y; 1108 | this.directionX = directionX; 1109 | this.directionY = directionY; 1110 | } 1111 | 1112 | protected float getFitness(RectF bounds) { 1113 | float lt = getFitness(bounds.left, bounds.top); 1114 | float rt = getFitness(bounds.right, bounds.top); 1115 | float rb = getFitness(bounds.right, bounds.bottom); 1116 | float lb = getFitness(bounds.left, bounds.bottom); 1117 | 1118 | return Math.min(Math.min(lt, rt), Math.min(rb, lb)); 1119 | } 1120 | 1121 | private float getFitness(float pointX, float pointY) { 1122 | // x = x - dy*t , y = y + dx*t 1123 | // x = pointX + dx*q , y = pointY + dy*q 1124 | 1125 | float q = directionX*(x - pointX) + directionY*(y - pointY); 1126 | 1127 | float crossX = pointX + directionX * q; 1128 | float crossY = pointY + directionY * q; 1129 | 1130 | float distance = PointF.length(crossX - pointX, crossY - pointY); 1131 | 1132 | return - Math.signum(q) * distance; 1133 | } 1134 | 1135 | protected void rotateBy(double sin, double cos) { 1136 | double newDirectionX = directionX * cos - directionY * sin; 1137 | double newDirectionY = directionX * sin + directionY * cos; 1138 | 1139 | directionX = (float) newDirectionX; 1140 | directionY = (float) newDirectionY; 1141 | } 1142 | 1143 | protected void translateBy(float dx, float dy) { 1144 | x += dx; 1145 | y += dy; 1146 | } 1147 | 1148 | } 1149 | 1150 | } 1151 | -------------------------------------------------------------------------------- /instacropper/src/main/java/com/yashoid/instacropper/MakeDrawableTask.java: -------------------------------------------------------------------------------- 1 | package com.yashoid.instacropper; 2 | 3 | import android.content.Context; 4 | import android.database.Cursor; 5 | import android.graphics.Bitmap; 6 | import android.graphics.BitmapFactory; 7 | import android.graphics.Canvas; 8 | import android.graphics.Matrix; 9 | import android.graphics.Paint; 10 | import android.graphics.drawable.BitmapDrawable; 11 | import android.graphics.drawable.Drawable; 12 | import android.media.ExifInterface; 13 | import android.net.Uri; 14 | import android.os.AsyncTask; 15 | import android.os.Build; 16 | import android.provider.MediaStore; 17 | import android.util.Log; 18 | 19 | import java.io.FileNotFoundException; 20 | import java.io.IOException; 21 | 22 | /** 23 | * Created by Yashar on 3/8/2017. 24 | */ 25 | 26 | public class MakeDrawableTask extends AsyncTask { 27 | 28 | private static final String TAG = "MakeDrawableTask"; 29 | 30 | private Context mContext; 31 | 32 | private Uri mUri; 33 | 34 | private int mTargetWidth; 35 | private int mTargetHeight; 36 | 37 | private int mRawWidth; 38 | private int mRawHeight; 39 | 40 | protected MakeDrawableTask(Context context, Uri uri, int targetWidth, int targetHeight) { 41 | mContext = context; 42 | 43 | mUri = uri; 44 | 45 | mTargetWidth = targetWidth; 46 | mTargetHeight = targetHeight; 47 | } 48 | 49 | protected int getTargetWidth() { 50 | return mTargetWidth; 51 | } 52 | 53 | protected int getTargetHeight() { 54 | return mTargetHeight; 55 | } 56 | 57 | @Override 58 | protected Drawable doInBackground(Void... params) { 59 | BitmapFactory.Options options = new BitmapFactory.Options(); 60 | options.inSampleSize = 1; 61 | 62 | options.inJustDecodeBounds = true; 63 | 64 | try { 65 | BitmapFactory.decodeStream(mContext.getContentResolver().openInputStream(mUri), null, options); 66 | 67 | mRawWidth = options.outWidth; 68 | mRawHeight = options.outHeight; 69 | 70 | int resultWidth = mRawWidth; 71 | int resultHeight = mRawHeight; 72 | 73 | Runtime.getRuntime().gc(); 74 | 75 | long totalMemory = Runtime.getRuntime().maxMemory(); 76 | long allowedMemoryToUse = totalMemory / 8; 77 | int maximumAreaPossibleAccordingToAvailableMemory = (int) (allowedMemoryToUse / 4); 78 | 79 | int targetArea = Math.min(mTargetWidth * mTargetHeight * 4, maximumAreaPossibleAccordingToAvailableMemory); 80 | 81 | int resultArea = resultWidth * resultHeight; 82 | 83 | while (resultArea > targetArea) { 84 | options.inSampleSize *= 2; 85 | 86 | resultWidth = mRawWidth / options.inSampleSize; 87 | resultHeight = mRawHeight / options.inSampleSize; 88 | 89 | resultArea = resultWidth * resultHeight; 90 | } 91 | 92 | options.inJustDecodeBounds = false; 93 | 94 | Bitmap bitmap = getBitmap(mContext, mUri, options); 95 | 96 | if (bitmap == null) { 97 | return null; 98 | } 99 | 100 | float beforeRatio = (float) mRawWidth / (float) mRawHeight; 101 | float afterRatio = (float) bitmap.getWidth() / (float) bitmap.getHeight(); 102 | 103 | if ((beforeRatio < 1 && afterRatio > 1) || (beforeRatio > 1 && afterRatio < 1)) { 104 | int rawWidth = mRawWidth; 105 | mRawWidth = mRawHeight; 106 | mRawHeight = rawWidth; 107 | } 108 | 109 | return new BitmapDrawable(mContext.getResources(), bitmap); 110 | } catch (FileNotFoundException e) { 111 | return null; 112 | } 113 | } 114 | 115 | protected int getRawWidth() { 116 | return mRawWidth; 117 | } 118 | 119 | protected int getRawHeight() { 120 | return mRawHeight; 121 | } 122 | 123 | protected static Bitmap getBitmap(Context context, Uri uri, BitmapFactory.Options options) { 124 | Bitmap bitmap = null; 125 | 126 | while (true) { 127 | try { 128 | bitmap = BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri), null, options); 129 | break; 130 | } catch (Throwable t) { 131 | options.inSampleSize *= 2; 132 | 133 | if (options.inSampleSize >= 1024) { 134 | Log.d(TAG, "Failed to optimize RAM to receive Bitmap."); 135 | 136 | break; 137 | } 138 | } 139 | } 140 | 141 | if (bitmap != null) { 142 | int orientation = getRotation(uri, context); 143 | 144 | if (orientation != 0) { 145 | Matrix matrix = new Matrix(); 146 | matrix.postRotate(orientation); 147 | 148 | bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false); 149 | } 150 | } 151 | 152 | return bitmap; 153 | } 154 | 155 | private static int getRotation(Uri uri, Context context) { 156 | if (isUriMatching(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, uri) || isUriMatching(MediaStore.Images.Media.INTERNAL_CONTENT_URI, uri)) { 157 | Cursor c = context.getContentResolver().query(uri, new String[] {MediaStore.Images.Media.ORIENTATION }, null, null, null); 158 | 159 | if (c.getCount() == 1) { 160 | c.moveToFirst(); 161 | 162 | int orientation = c.getInt(0); 163 | 164 | c.close(); 165 | 166 | return orientation; 167 | } 168 | else { 169 | Log.w(TAG, "Failed to get MediaStore image orientation."); 170 | 171 | c.close(); 172 | 173 | return 0; 174 | } 175 | } 176 | 177 | try { 178 | ExifInterface ei; 179 | 180 | if (Build.VERSION.SDK_INT >= 24) { 181 | ei = new ExifInterface(context.getContentResolver().openInputStream(uri)); 182 | } 183 | else { 184 | ei = new ExifInterface(uri.toString()); 185 | } 186 | 187 | int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); 188 | 189 | switch(orientation) { 190 | case ExifInterface.ORIENTATION_ROTATE_90: 191 | return 90; 192 | case ExifInterface.ORIENTATION_ROTATE_180: 193 | return 180; 194 | case ExifInterface.ORIENTATION_ROTATE_270: 195 | return 270; 196 | case ExifInterface.ORIENTATION_NORMAL: 197 | default: 198 | return 0; 199 | } 200 | } catch (IOException e) { 201 | Log.w(TAG, "Failed to get image orientation from file.", e); 202 | 203 | return 0; 204 | } 205 | } 206 | 207 | protected static Bitmap resizeBitmap(Bitmap bitmap, int newWidth, int newHeight) { 208 | Bitmap resizedBitmap = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); 209 | 210 | float scaleX = newWidth / (float) bitmap.getWidth(); 211 | float scaleY = newHeight / (float) bitmap.getHeight(); 212 | float pivotX = 0; 213 | float pivotY = 0; 214 | 215 | Matrix scaleMatrix = new Matrix(); 216 | scaleMatrix.setScale(scaleX, scaleY, pivotX, pivotY); 217 | 218 | Canvas canvas = new Canvas(resizedBitmap); 219 | canvas.setMatrix(scaleMatrix); 220 | canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG)); 221 | 222 | return resizedBitmap; 223 | } 224 | 225 | private static boolean isUriMatching(Uri path, Uri element) { 226 | return Uri.withAppendedPath(path, element.getLastPathSegment()).equals(element); 227 | } 228 | 229 | } 230 | -------------------------------------------------------------------------------- /instacropper/src/main/java/com/yashoid/instacropper/MultipleCropActivity.java: -------------------------------------------------------------------------------- 1 | package com.yashoid.instacropper; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.os.Bundle; 8 | import android.view.View; 9 | 10 | public class MultipleCropActivity extends Activity { 11 | 12 | public static final String EXTRA_OUTPUT = InstaCropperActivity.EXTRA_OUTPUT; 13 | public static final String EXTRA_COUNT = "count"; 14 | 15 | public static final String EXTRA_PREFERRED_RATIO = InstaCropperActivity.EXTRA_PREFERRED_RATIO; 16 | public static final String EXTRA_MINIMUM_RATIO = InstaCropperActivity.EXTRA_MINIMUM_RATIO; 17 | public static final String EXTRA_MAXIMUM_RATIO = InstaCropperActivity.EXTRA_MAXIMUM_RATIO; 18 | 19 | public static final String EXTRA_WIDTH_SPEC = InstaCropperActivity.EXTRA_WIDTH_SPEC; 20 | public static final String EXTRA_HEIGHT_SPEC = InstaCropperActivity.EXTRA_HEIGHT_SPEC; 21 | 22 | public static final String EXTRA_OUTPUT_QUALITY = InstaCropperActivity.EXTRA_OUTPUT_QUALITY; 23 | 24 | private static final String KEY_INDEX = "index"; 25 | 26 | public static Intent getIntent(Context context, MultipleUris src, MultipleUris dst, 27 | int maxWidth, int outputQuality) { 28 | Intent intent = new Intent(context, MultipleCropActivity.class); 29 | 30 | intent.setData(src.toUri()); 31 | 32 | intent.putExtra(EXTRA_OUTPUT, dst.toUri()); 33 | 34 | intent.putExtra(EXTRA_PREFERRED_RATIO, InstaCropperView.DEFAULT_RATIO); 35 | intent.putExtra(EXTRA_MINIMUM_RATIO, InstaCropperView.DEFAULT_MINIMUM_RATIO); 36 | intent.putExtra(EXTRA_MAXIMUM_RATIO, InstaCropperView.DEFAULT_MAXIMUM_RATIO); 37 | 38 | intent.putExtra(EXTRA_WIDTH_SPEC, View.MeasureSpec.makeMeasureSpec(maxWidth, View.MeasureSpec.AT_MOST)); 39 | intent.putExtra(EXTRA_HEIGHT_SPEC, View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); 40 | intent.putExtra(EXTRA_OUTPUT_QUALITY, outputQuality); 41 | 42 | return intent; 43 | } 44 | 45 | public static Intent getIntent(Context context, MultipleUris src, MultipleUris dst, 46 | int maxWidth, int maxHeight, float aspectRatio) { 47 | Intent intent = new Intent(context, MultipleCropActivity.class); 48 | 49 | intent.setData(src.toUri()); 50 | 51 | intent.putExtra(EXTRA_OUTPUT, dst.toUri()); 52 | 53 | intent.putExtra(EXTRA_PREFERRED_RATIO, aspectRatio); 54 | intent.putExtra(EXTRA_MINIMUM_RATIO, aspectRatio); 55 | intent.putExtra(EXTRA_MAXIMUM_RATIO, aspectRatio); 56 | 57 | intent.putExtra(EXTRA_WIDTH_SPEC, View.MeasureSpec.makeMeasureSpec(maxWidth, View.MeasureSpec.AT_MOST)); 58 | intent.putExtra(EXTRA_HEIGHT_SPEC, View.MeasureSpec.makeMeasureSpec(maxHeight, View.MeasureSpec.AT_MOST)); 59 | intent.putExtra(EXTRA_OUTPUT_QUALITY, InstaCropperActivity.DEFAULT_OUTPUT_QUALITY); 60 | 61 | return intent; 62 | } 63 | 64 | private MultipleUris mSources; 65 | private MultipleUris mDestinations; 66 | 67 | private int mIndex; 68 | 69 | @Override 70 | protected void onCreate(Bundle savedInstanceState) { 71 | super.onCreate(savedInstanceState); 72 | 73 | Intent intent = getIntent(); 74 | 75 | Uri srcUri = intent.getData(); 76 | Uri dstUri = intent.getParcelableExtra(EXTRA_OUTPUT); 77 | 78 | if (srcUri == null || dstUri == null) { 79 | throw new IllegalArgumentException("Source or destination is not provided."); 80 | } 81 | 82 | mSources = new MultipleUris(srcUri); 83 | mDestinations = new MultipleUris(dstUri); 84 | 85 | if (mSources.size() != mDestinations.size()) { 86 | throw new IllegalArgumentException("Source and destination URIs must have the same length."); 87 | } 88 | 89 | if (savedInstanceState == null) { 90 | mIndex = 0; 91 | goNext(); 92 | } 93 | else { 94 | mIndex = savedInstanceState.getInt(KEY_INDEX); 95 | } 96 | } 97 | 98 | private void goNext() { 99 | if (mIndex == mSources.size()) { 100 | Intent output = new Intent(); 101 | output.setData(mDestinations.toUri()); 102 | output.putExtra(EXTRA_COUNT, mIndex); 103 | setResult(RESULT_OK, output); 104 | finish(); 105 | return; 106 | } 107 | 108 | Uri source = mSources.getUris().get(mIndex); 109 | Uri destination = mDestinations.getUris().get(mIndex); 110 | 111 | Intent intent = new Intent(getIntent()); 112 | intent.setClass(this, InstaCropperActivity.class); 113 | intent.setData(source); 114 | intent.putExtra(EXTRA_OUTPUT, destination); 115 | 116 | startActivityForResult(intent, 0); 117 | } 118 | 119 | @Override 120 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 121 | if (resultCode == RESULT_CANCELED) { 122 | Intent output = new Intent(); 123 | output.setData(mDestinations.toUri()); 124 | output.putExtra(EXTRA_COUNT, mIndex); 125 | setResult(RESULT_CANCELED, output); 126 | finish(); 127 | return; 128 | } 129 | 130 | mIndex++; 131 | 132 | goNext(); 133 | } 134 | 135 | @Override 136 | protected void onSaveInstanceState(Bundle outState) { 137 | super.onSaveInstanceState(outState); 138 | 139 | outState.putInt(KEY_INDEX, mIndex); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /instacropper/src/main/java/com/yashoid/instacropper/MultipleUris.java: -------------------------------------------------------------------------------- 1 | package com.yashoid.instacropper; 2 | 3 | import android.net.Uri; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | public class MultipleUris { 10 | 11 | private static final String FAKE = "_MultipleUris"; 12 | 13 | private List mUris; 14 | 15 | public MultipleUris(Uri... uris) { 16 | mUris = new ArrayList<>(uris.length); 17 | 18 | Collections.addAll(mUris, uris); 19 | } 20 | 21 | public MultipleUris(List uris) { 22 | mUris = new ArrayList<>(uris); 23 | } 24 | 25 | public MultipleUris(Uri uri) { 26 | mUris = new ArrayList<>(5); 27 | 28 | for (String name: uri.getQueryParameterNames()) { 29 | mUris.add(Uri.parse(uri.getQueryParameter(name))); 30 | } 31 | } 32 | 33 | public MultipleUris() { 34 | mUris = new ArrayList<>(5); 35 | } 36 | 37 | public MultipleUris add(Uri uri) { 38 | mUris.add(uri); 39 | return this; 40 | } 41 | 42 | public MultipleUris remove(Uri uri) { 43 | mUris.remove(uri); 44 | return this; 45 | } 46 | 47 | public List getUris() { 48 | return mUris; 49 | } 50 | 51 | public int size() { 52 | return mUris.size(); 53 | } 54 | 55 | public Uri toUri() { 56 | StringBuilder sb = new StringBuilder(); 57 | 58 | sb.append(FAKE).append("://").append(FAKE).append("/?"); 59 | 60 | for (int i = 0; i < mUris.size(); i++) { 61 | if (i > 0) { 62 | sb.append("&"); 63 | } 64 | 65 | sb.append(i).append("=").append(mUris.get(i)); 66 | } 67 | 68 | return Uri.parse(sb.toString()); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /instacropper/src/main/res/drawable-hdpi/ic_instacropper_crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasharpm/InstaCropper/5585693ecc8fbcc2629f19a346d19c0b32436443/instacropper/src/main/res/drawable-hdpi/ic_instacropper_crop.png -------------------------------------------------------------------------------- /instacropper/src/main/res/drawable-mdpi/ic_instacropper_crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasharpm/InstaCropper/5585693ecc8fbcc2629f19a346d19c0b32436443/instacropper/src/main/res/drawable-mdpi/ic_instacropper_crop.png -------------------------------------------------------------------------------- /instacropper/src/main/res/drawable-xhdpi/ic_instacropper_crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasharpm/InstaCropper/5585693ecc8fbcc2629f19a346d19c0b32436443/instacropper/src/main/res/drawable-xhdpi/ic_instacropper_crop.png -------------------------------------------------------------------------------- /instacropper/src/main/res/drawable-xxhdpi/ic_instacropper_crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasharpm/InstaCropper/5585693ecc8fbcc2629f19a346d19c0b32436443/instacropper/src/main/res/drawable-xxhdpi/ic_instacropper_crop.png -------------------------------------------------------------------------------- /instacropper/src/main/res/drawable-xxxhdpi/ic_instacropper_crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasharpm/InstaCropper/5585693ecc8fbcc2629f19a346d19c0b32436443/instacropper/src/main/res/drawable-xxxhdpi/ic_instacropper_crop.png -------------------------------------------------------------------------------- /instacropper/src/main/res/layout/activity_instacropper.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /instacropper/src/main/res/menu/menu_instacropper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /instacropper/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @android:color/white 5 | 6 | @android:color/black 7 | 8 | -------------------------------------------------------------------------------- /instacropper/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Crop 3 | 4 | Crop 5 | 6 | 7 | -------------------------------------------------------------------------------- /instacropper/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':instacropper' 2 | --------------------------------------------------------------------------------