├── .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 | 
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 |
84 | ```
85 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 29
5 | defaultConfig {
6 | applicationId "com.yashoid.instacropper.sample"
7 | minSdkVersion 16
8 | targetSdkVersion 29
9 | versionCode 1
10 | versionName "1.0"
11 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
12 | }
13 | buildTypes {
14 | release {
15 | minifyEnabled false
16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
17 | }
18 | }
19 | }
20 |
21 | dependencies {
22 | implementation fileTree(dir: 'libs', include: ['*.jar'])
23 | androidTestImplementation('androidx.test.espresso:espresso-core:3.2.0')
24 | implementation 'androidx.appcompat:appcompat:1.1.0'
25 | testImplementation 'junit:junit:4.12'
26 |
27 | implementation project(':instacropper')
28 | }
29 |
--------------------------------------------------------------------------------
/app/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 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/yashoid/instacropper/sample/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.yashoid.instacropper.sample;
2 |
3 | import android.content.Context;
4 | import androidx.test.platform.app.InstrumentationRegistry;
5 | import androidx.test.ext.junit.runners.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.yashoid.instacropper", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yashoid/instacropper/sample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.yashoid.instacropper.sample;
2 |
3 | import android.content.Intent;
4 | import android.graphics.Bitmap;
5 | import android.net.Uri;
6 | import android.os.Bundle;
7 | import android.os.Environment;
8 | import android.provider.MediaStore;
9 | import androidx.appcompat.app.AppCompatActivity;
10 | import android.util.Log;
11 | import android.view.View;
12 | import android.widget.Toast;
13 |
14 | import com.yashoid.instacropper.InstaCropperActivity;
15 | import com.yashoid.instacropper.InstaCropperView;
16 |
17 | import java.io.File;
18 | import java.io.FileOutputStream;
19 | import java.io.IOException;
20 |
21 | public class MainActivity extends AppCompatActivity {
22 |
23 | private static final String TAG = "MainActivity";
24 |
25 | private InstaCropperView mInstaCropper;
26 |
27 | @Override
28 | protected void onCreate(Bundle savedInstanceState) {
29 | super.onCreate(savedInstanceState);
30 | setContentView(R.layout.activity_main);
31 |
32 | mInstaCropper = (InstaCropperView) findViewById(R.id.instacropper);
33 | }
34 |
35 | public void pickPhoto(View v) {
36 | Intent intent = new Intent(Intent.ACTION_PICK);
37 | intent.setData(MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
38 |
39 | startActivityForResult(intent, 1);
40 |
41 | // Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
42 | // intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(getFile()));
43 | // startActivityForResult(intent, 1);
44 | }
45 |
46 | @Override
47 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
48 | super.onActivityResult(requestCode, resultCode, data);
49 |
50 | switch (requestCode) {
51 | case 1:
52 | if (resultCode == RESULT_OK) {
53 | Intent intent = InstaCropperActivity.getIntent(this, data.getData(), Uri.fromFile(new File(getExternalCacheDir(), "test.jpg")), 720, 50);
54 | // Intent intent = InstaCropperActivity.getIntent(this, Uri.fromFile(getFile()), Uri.fromFile(getFile()), 720, 50);
55 | startActivityForResult(intent, 2);
56 | }
57 | return;
58 | case 2:
59 | if (resultCode == RESULT_OK) {
60 | mInstaCropper.setImageUri(data.getData());
61 | }
62 | return;
63 | }
64 | }
65 |
66 | public void rotate(View v) {
67 | // mInstaCropper.setDrawableRotation(mInstaCropper.getDrawableRotation() + 15);
68 | }
69 |
70 | public void crop(View v) {
71 | mInstaCropper.crop(View.MeasureSpec.makeMeasureSpec(720, View.MeasureSpec.AT_MOST), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), new InstaCropperView.BitmapCallback() {
72 |
73 | @Override
74 | public void onBitmapReady(Bitmap bitmap) {
75 | if (bitmap == null) {
76 | Toast.makeText(MainActivity.this, "Returned bitmap is null.", Toast.LENGTH_SHORT).show();
77 | return;
78 | }
79 |
80 | File file = getFile();
81 |
82 | try {
83 | FileOutputStream os = new FileOutputStream(file);
84 |
85 | bitmap.compress(Bitmap.CompressFormat.JPEG, 50, os);
86 |
87 | os.flush();
88 | os.close();
89 |
90 | mInstaCropper.setImageUri(Uri.fromFile(file));
91 |
92 | Log.i(TAG, "Image updated.");
93 | } catch (IOException e) {
94 | Log.e(TAG, "Failed to compress bitmap.", e);
95 | }
96 | }
97 |
98 | });
99 | }
100 |
101 | private File getFile() {
102 | return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "instaCropper.jpg");
103 | }
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
27 |
28 |
34 |
35 |
42 |
43 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasharpm/InstaCropper/5585693ecc8fbcc2629f19a346d19c0b32436443/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasharpm/InstaCropper/5585693ecc8fbcc2629f19a346d19c0b32436443/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasharpm/InstaCropper/5585693ecc8fbcc2629f19a346d19c0b32436443/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasharpm/InstaCropper/5585693ecc8fbcc2629f19a346d19c0b32436443/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yasharpm/InstaCropper/5585693ecc8fbcc2629f19a346d19c0b32436443/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | InstaCropper
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------