diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..82ca65e6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,47 @@
+# svn
+*.svn*
+
+# built application files
+*.apk
+*.ap_
+
+# files for the dex VM
+*.dex
+
+# Java class files
+*.class
+
+# generated GUI files
+*/R.java
+
+# generated folder
+bin
+gen
+
+# local
+local.properties
+
+proguard_logs/
+
+# log files
+log*.txt
+
+# archives
+*.gz
+*.tar
+*.zip
+
+# eclipse
+*.metadata
+*.settings
+*.prefs
+
+#idea
+*.idea
+*.iml
+out/
+
+build/
+.gradle/
+
+.DS_Store
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..48a72dd1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,79 @@
+# uCrop - Image Cropping Library for Android
+
+#### This project aims to provide an ultimate and flexible image cropping experience. Made in [Yalantis] (https://yalantis.com/?utm_source=github)
+
+# Usage
+
+*For a working implementation, please have a look at the Sample Project - sample*
+
+1. Include the library as local library project.
+
+ ``` compile 'com.yalantis:ucrop:[latest version]' ```
+
+
+2. The uCrop configuration is created using the builder pattern.
+
+ ```java
+ UCrop.of(sourceUri, destinationUri)
+ .withAspectRatio(16, 9)
+ .withMaxResultSize(maxWidth, maxHeight)
+ .start(context);
+ ```
+
+
+3. Override `onActivityResult` method and handle uCrop result.
+
+ ```java
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == RESULT_OK && requestCode == UCrop.REQUEST_CROP) {
+ final Uri resultUri = UCrop.getOutput(data);
+ } else if (resultCode == UCrop.RESULT_ERROR) {
+ final Throwable cropError = UCrop.getError(data);
+ }
+ }
+ ```
+
+
+# Customization
+
+uCrop builder class has method `withOptions(UCrop.Options option)` which extends library configurations.
+
+Currently you can change:
+
+ * image compression format (e.g. PNG, JPEG, WEBP), compression
+ * image compression quality [0 - 100]. PNG which is lossless, will ignore the quality setting.
+ * whether all gestures are enabled simultaneously
+ * maximum size for Bitmap that is decoded from source Uri and used within crop view. If you want to override default behaviour.
+ * more coming... (e.g. color pallet)
+
+# Compatibility
+
+ * Library - Android GINGERBREAD 2.3+
+ * Sample - Android ICS 4.0+
+
+# Changelog
+
+### Version: 1.0
+
+ * Initial Build
+
+### Let us know!
+
+We’d be really happy if you sent us links to your projects where you use our component. Just send an email to github@yalantis.com And do let us know if you have any questions or suggestion regarding the library.
+
+## License
+
+ Copyright 2016, Yalantis
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..eba60cc0
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.0.0-alpha5'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+def isReleaseBuild() {
+ return version.contains("SNAPSHOT") == false
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/captures/com.yalantis.ucrop.sample_2016.01.15_14.06.alloc b/captures/com.yalantis.ucrop.sample_2016.01.15_14.06.alloc
new file mode 100644
index 00000000..780825b8
Binary files /dev/null and b/captures/com.yalantis.ucrop.sample_2016.01.15_14.06.alloc differ
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000..120529d0
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,33 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+VERSION_NAME=1.0.0
+VERSION_CODE=1
+GROUP=com.yalantis
+
+POM_DESCRIPTION=Android Library for cropping images
+POM_URL=https://github.com/Yalantis/uCrop
+POM_SCM_URL=https://github.com/Yalantis/uCrop
+POM_SCM_CONNECTION=scm:git@github.com/Yalantis/uCrop.git
+POM_SCM_DEV_CONNECTION=scm:git@github.com/Yalantis/uCrop.git
+POM_LICENCE_NAME=The Apache Software License, Version 2.0
+POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0
+POM_LICENCE_DIST=repo
+POM_DEVELOPER_ID=yalantis
+POM_DEVELOPER_NAME=Yalantis
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..8c0fb64a
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..838084e4
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jan 15 00:55:26 EET 2016
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..91a7e269
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..8a0b282a
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/mavenpush.gradle b/mavenpush.gradle
new file mode 100644
index 00000000..b741e61b
--- /dev/null
+++ b/mavenpush.gradle
@@ -0,0 +1,92 @@
+apply plugin: 'maven'
+apply plugin: 'signing'
+
+def sonatypeRepositoryUrl
+if (isReleaseBuild()) {
+ println 'RELEASE BUILD'
+ sonatypeRepositoryUrl = hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
+ : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
+} else {
+ println 'DEBUG BUILD'
+ sonatypeRepositoryUrl = hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
+ : "https://oss.sonatype.org/content/repositories/snapshots/"
+}
+
+def getRepositoryUsername() {
+ return hasProperty('nexusUsername') ? nexusUsername : ""
+}
+
+def getRepositoryPassword() {
+ return hasProperty('nexusPassword') ? nexusPassword : ""
+}
+
+afterEvaluate { project ->
+ uploadArchives {
+ repositories {
+ mavenDeployer {
+ beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
+
+ pom.artifactId = POM_ARTIFACT_ID
+
+ repository(url: sonatypeRepositoryUrl) {
+ authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
+ }
+
+ pom.project {
+ name POM_NAME
+ packaging POM_PACKAGING
+ description POM_DESCRIPTION
+ url POM_URL
+
+ scm {
+ url POM_SCM_URL
+ connection POM_SCM_CONNECTION
+ developerConnection POM_SCM_DEV_CONNECTION
+ }
+
+ licenses {
+ license {
+ name POM_LICENCE_NAME
+ url POM_LICENCE_URL
+ distribution POM_LICENCE_DIST
+ }
+ }
+
+ developers {
+ developer {
+ id POM_DEVELOPER_ID
+ name POM_DEVELOPER_NAME
+ }
+ }
+ }
+ }
+ }
+ }
+
+ signing {
+ required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
+ sign configurations.archives
+ }
+
+ task androidJavadocs(type: Javadoc) {
+ source = android.sourceSets.main.java.sourceFiles
+ }
+
+ task androidJavadocsJar(type: Jar) {
+ classifier = 'javadoc'
+ //basename = artifact_id
+ from androidJavadocs.destinationDir
+ }
+
+ task androidSourcesJar(type: Jar) {
+ classifier = 'sources'
+ //basename = artifact_id
+ from android.sourceSets.main.java.sourceFiles
+ }
+
+ artifacts {
+ //archives packageReleaseJar
+ archives androidSourcesJar
+ archives androidJavadocsJar
+ }
+}
\ No newline at end of file
diff --git a/sample/.gitignore b/sample/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/sample/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/sample/build.gradle b/sample/build.gradle
new file mode 100644
index 00000000..3651e3e7
--- /dev/null
+++ b/sample/build.gradle
@@ -0,0 +1,30 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion "23.0.2"
+
+ defaultConfig {
+ applicationId "com.yalantis.ucrop.sample"
+ minSdkVersion 15
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+}
+
+dependencies {
+ compile 'com.android.support:appcompat-v7:23.1.1'
+
+ compile project (':ucrop')
+}
\ No newline at end of file
diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro
new file mode 100644
index 00000000..0cd55489
--- /dev/null
+++ b/sample/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/oleksii/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..3006bb5a
--- /dev/null
+++ b/sample/src/main/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample/src/main/java/com/yalantis/ucrop/sample/BaseActivity.java b/sample/src/main/java/com/yalantis/ucrop/sample/BaseActivity.java
new file mode 100644
index 00000000..e754819e
--- /dev/null
+++ b/sample/src/main/java/com/yalantis/ucrop/sample/BaseActivity.java
@@ -0,0 +1,76 @@
+package com.yalantis.ucrop.sample;
+
+import android.content.DialogInterface;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ActivityCompat;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ */
+public class BaseActivity extends AppCompatActivity {
+
+ protected static final int REQUEST_STORAGE_READ_ACCESS_PERMISSION = 101;
+ protected static final int REQUEST_STORAGE_WRITE_ACCESS_PERMISSION = 102;
+
+ private AlertDialog mAlertDialog;
+
+ /**
+ * Hide alert dialog if any.
+ */
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mAlertDialog != null && mAlertDialog.isShowing()) {
+ mAlertDialog.dismiss();
+ }
+ }
+
+
+ /**
+ * Requests given permission.
+ * If the permission has been denied previously, a Dialog will prompt the user to grant the
+ * permission, otherwise it is requested directly.
+ */
+ protected void requestPermission(final String permission, String rationale, final int requestCode) {
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
+ showAlertDialog(getString(R.string.permission_title_rationale), rationale,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ActivityCompat.requestPermissions(BaseActivity.this,
+ new String[]{permission}, requestCode);
+ }
+ }, getString(R.string.label_ok), null, getString(R.string.label_cancel));
+ } else {
+ ActivityCompat.requestPermissions(this, new String[]{permission}, requestCode);
+ }
+ }
+
+ /**
+ * This method shows dialog with given title & message.
+ * Also there is an option to pass onClickListener for positive & negative button.
+ *
+ * @param title - dialog title
+ * @param message - dialog message
+ * @param onPositiveButtonClickListener - listener for positive button
+ * @param positiveText - positive button text
+ * @param onNegativeButtonClickListener - listener for negative button
+ * @param negativeText - negative button text
+ */
+ protected void showAlertDialog(@Nullable String title, @Nullable String message,
+ @Nullable DialogInterface.OnClickListener onPositiveButtonClickListener,
+ @NonNull String positiveText,
+ @Nullable DialogInterface.OnClickListener onNegativeButtonClickListener,
+ @NonNull String negativeText) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(title);
+ builder.setMessage(message);
+ builder.setPositiveButton(positiveText, onPositiveButtonClickListener);
+ builder.setNegativeButton(negativeText, onNegativeButtonClickListener);
+ mAlertDialog = builder.show();
+ }
+
+}
diff --git a/sample/src/main/java/com/yalantis/ucrop/sample/ResultActivity.java b/sample/src/main/java/com/yalantis/ucrop/sample/ResultActivity.java
new file mode 100644
index 00000000..f93c4bd3
--- /dev/null
+++ b/sample/src/main/java/com/yalantis/ucrop/sample/ResultActivity.java
@@ -0,0 +1,155 @@
+package com.yalantis.ucrop.sample;
+
+import android.Manifest;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.NotificationCompat;
+import android.support.v7.app.ActionBar;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.nio.channels.FileChannel;
+import java.util.Calendar;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ */
+public class ResultActivity extends BaseActivity {
+
+ private static final String TAG = "ResultActivity";
+ private static final int DOWNLOAD_NOTIFICATION_ID_DONE = 911;
+
+ public static void startWithUri(@NonNull Context context, @NonNull Uri uri) {
+ Intent intent = new Intent(context, ResultActivity.class);
+ intent.setData(uri);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_result);
+
+ ((ImageView) findViewById(R.id.image_view_preview)).setImageURI(getIntent().getData());
+
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(new File(getIntent().getData().getPath()).getAbsolutePath(), options);
+
+ setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
+ final ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setTitle(getString(R.string.format_crop_result_d_d, options.outWidth, options.outHeight));
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_result, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ case R.id.menu_download:
+ saveCroppedImage();
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+
+ /**
+ * Callback received when a permissions request has been completed.
+ */
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ switch (requestCode) {
+ case REQUEST_STORAGE_WRITE_ACCESS_PERMISSION:
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ saveCroppedImage();
+ }
+ break;
+ default:
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ private void saveCroppedImage() {
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ getString(R.string.permission_write_storage_rationale),
+ REQUEST_STORAGE_WRITE_ACCESS_PERMISSION);
+ } else {
+ Uri imageUri = getIntent().getData();
+ if (imageUri != null && imageUri.getScheme().equals("file")) {
+ try {
+ copyFileToDownloads(getIntent().getData());
+ } catch (Exception e) {
+ Toast.makeText(ResultActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show();
+ Log.e(TAG, imageUri.toString(), e);
+ }
+ } else {
+ Toast.makeText(ResultActivity.this, getString(R.string.toast_unexpected_error), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ private void copyFileToDownloads(Uri croppedFileUri) throws Exception {
+ String downloadsDirectoryPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
+ String filename = String.format("%d_%s", Calendar.getInstance().getTimeInMillis(), croppedFileUri.getLastPathSegment());
+
+ File saveFile = new File(downloadsDirectoryPath, filename);
+
+ FileInputStream inStream = new FileInputStream(new File(croppedFileUri.getPath()));
+ FileOutputStream outStream = new FileOutputStream(saveFile);
+ FileChannel inChannel = inStream.getChannel();
+ FileChannel outChannel = outStream.getChannel();
+ inChannel.transferTo(0, inChannel.size(), outChannel);
+ inStream.close();
+ outStream.close();
+
+ showNotification(saveFile);
+ }
+
+ private void showNotification(@NonNull File file) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setDataAndType(Uri.fromFile(file), "image/*");
+
+ NotificationCompat.Builder mNotification = new NotificationCompat.Builder(this);
+
+ mNotification
+ .setContentTitle(getString(R.string.app_name))
+ .setContentText(getString(R.string.notification_image_saved_click_to_preview))
+ .setTicker(getString(R.string.notification_image_saved))
+ .setSmallIcon(R.drawable.ic_done)
+ .setOngoing(false)
+ .setContentIntent(PendingIntent.getActivity(this, 0, intent, 0))
+ .setAutoCancel(true);
+ ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).notify(DOWNLOAD_NOTIFICATION_ID_DONE, mNotification.build());
+ }
+
+}
diff --git a/sample/src/main/java/com/yalantis/ucrop/sample/SampleActivity.java b/sample/src/main/java/com/yalantis/ucrop/sample/SampleActivity.java
new file mode 100644
index 00000000..65f55ddc
--- /dev/null
+++ b/sample/src/main/java/com/yalantis/ucrop/sample/SampleActivity.java
@@ -0,0 +1,254 @@
+package com.yalantis.ucrop.sample;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.util.Log;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.RadioGroup;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.yalantis.ucrop.UCrop;
+import com.yalantis.ucrop.UCropActivity;
+
+import java.io.File;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ */
+public class SampleActivity extends BaseActivity {
+
+ private static final String TAG = "SampleActivity";
+
+ private static final int REQUEST_SELECT_PICTURE = 0x01;
+
+ private static final int SAMPLE_IMAGE_MAX_SIZE_WIDTH = 200;
+ private static final int SAMPLE_IMAGE_MAX_SIZE_HEIGHT = 300;
+ private static final String SAMPLE_CROPPED_IMAGE_NAME = "SampleCropImage.jpeg";
+
+ private RadioGroup mRadioGroupAspectRatio, mRadioGroupCompressionSettings;
+ private EditText mEditTextMaxWidth, mEditTextMaxHeight;
+ private CheckBox mCheckBoxMaxSize;
+ private SeekBar mSeekBarQuality;
+ private TextView mTextViewQuality;
+
+ private Uri mDestinationUri;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_sample);
+
+ mDestinationUri = Uri.fromFile(new File(getCacheDir(), SAMPLE_CROPPED_IMAGE_NAME));
+
+ setupUI();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == RESULT_OK) {
+ if (requestCode == REQUEST_SELECT_PICTURE) {
+ final Uri selectedUri = data.getData();
+ if (selectedUri != null) {
+ startCropActivity(data.getData());
+ } else {
+ Toast.makeText(SampleActivity.this, R.string.toast_cannot_retrieve_selected_image, Toast.LENGTH_SHORT).show();
+ }
+ } else if (requestCode == UCrop.REQUEST_CROP) {
+ handleCropResult(data);
+ }
+ }
+ if (resultCode == UCrop.RESULT_ERROR) {
+ handleCropError(data);
+ }
+ }
+
+ /**
+ * Callback received when a permissions request has been completed.
+ */
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ switch (requestCode) {
+ case REQUEST_STORAGE_READ_ACCESS_PERMISSION:
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ pickFromGallery();
+ }
+ break;
+ default:
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ private void setupUI() {
+ findViewById(R.id.button_crop).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ pickFromGallery();
+ }
+ });
+
+ mRadioGroupAspectRatio = ((RadioGroup) findViewById(R.id.radio_group_aspect_ratio));
+ mRadioGroupCompressionSettings = ((RadioGroup) findViewById(R.id.radio_group_compression_settings));
+ mCheckBoxMaxSize = ((CheckBox) findViewById(R.id.checkbox_max_size));
+ mEditTextMaxWidth = ((EditText) findViewById(R.id.edit_text_max_width));
+ mEditTextMaxHeight = ((EditText) findViewById(R.id.edit_text_max_height));
+ mSeekBarQuality = ((SeekBar) findViewById(R.id.seekbar_quality));
+ mTextViewQuality = ((TextView) findViewById(R.id.text_view_quality));
+
+ mRadioGroupAspectRatio.check(R.id.radio_dynamic);
+ mRadioGroupCompressionSettings.check(R.id.radio_jpeg);
+ mSeekBarQuality.setProgress(UCropActivity.DEFAULT_COMPRESS_QUALITY);
+ mTextViewQuality.setText(String.format(getString(R.string.format_quality_d), mSeekBarQuality.getProgress()));
+ mEditTextMaxWidth.setText(String.valueOf(SAMPLE_IMAGE_MAX_SIZE_WIDTH));
+ mEditTextMaxHeight.setText(String.valueOf(SAMPLE_IMAGE_MAX_SIZE_HEIGHT));
+ mSeekBarQuality.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ mTextViewQuality.setText(String.format(getString(R.string.format_quality_d), progress));
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ });
+ }
+
+ private void pickFromGallery() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN // Permission was added in API Level 16
+ && ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ requestPermission(Manifest.permission.READ_EXTERNAL_STORAGE,
+ getString(R.string.permission_read_storage_rationale),
+ REQUEST_STORAGE_READ_ACCESS_PERMISSION);
+ } else {
+ Intent intent = new Intent();
+ intent.setType("image/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ startActivityForResult(Intent.createChooser(intent, getString(R.string.label_select_picture)), REQUEST_SELECT_PICTURE);
+ }
+ }
+
+ private void startCropActivity(@NonNull Uri uri) {
+ UCrop uCrop = UCrop.of(uri, mDestinationUri);
+
+ uCrop = basisConfig(uCrop);
+ uCrop = advancedConfig(uCrop);
+
+ uCrop.start(SampleActivity.this);
+ }
+
+ /**
+ * In most cases you need only to set crop aspect ration and max size for resulting image.
+ *
+ * @param uCrop - ucrop builder instance
+ * @return - ucrop builder instance
+ */
+ private UCrop basisConfig(@NonNull UCrop uCrop) {
+ switch (mRadioGroupAspectRatio.getCheckedRadioButtonId()) {
+ case R.id.radio_origin:
+ uCrop = uCrop.useSourceImageAspectRatio();
+ break;
+ case R.id.radio_square:
+ uCrop = uCrop.withAspectRatio(1, 1);
+ break;
+ case R.id.radio_16_9:
+ uCrop = uCrop.withAspectRatio(16, 9);
+ break;
+ case R.id.radio_dynamic:
+ default:
+ // do nothing
+ break;
+ }
+
+ if (mCheckBoxMaxSize.isChecked()) {
+ try {
+ int maxWidth = Integer.valueOf(mEditTextMaxWidth.getText().toString().trim());
+ int maxHeight = Integer.valueOf(mEditTextMaxHeight.getText().toString().trim());
+ if (maxWidth > 0 && maxHeight > 0) {
+ uCrop = uCrop.withMaxResultSize(maxWidth, maxHeight);
+ }
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Number please", e);
+ }
+ }
+
+ return uCrop;
+ }
+
+ /**
+ * Sometimes you want to adjust more options, it's done via {@link com.yalantis.ucrop.UCrop.Options} class.
+ *
+ * @param uCrop - ucrop builder instance
+ * @return - ucrop builder instance
+ */
+ private UCrop advancedConfig(@NonNull UCrop uCrop) {
+ UCrop.Options options = new UCrop.Options();
+
+ switch (mRadioGroupCompressionSettings.getCheckedRadioButtonId()) {
+ case R.id.radio_png:
+ options.setCompressionFormat(Bitmap.CompressFormat.PNG);
+ break;
+ case R.id.radio_webp:
+ options.setCompressionFormat(Bitmap.CompressFormat.WEBP);
+ break;
+ case R.id.radio_jpeg:
+ default:
+ options.setCompressionFormat(Bitmap.CompressFormat.JPEG);
+ break;
+ }
+ options.setCompressionQuality(mSeekBarQuality.getProgress());
+
+ /*
+ If you want to unlock all gestures for all UCropActivity tabs
+
+ options.setGesturesAlwaysEnabled(true);
+ * */
+
+ /*
+ This sets max size for bitmap that will be decoded from source Uri.
+ More size - more memory allocation, default implementation uses screen diagonal.
+
+ options.setMaxBitmapSize(640);
+ * */
+
+ return uCrop.withOptions(options);
+ }
+
+ private void handleCropResult(@NonNull Intent result) {
+ final Uri resultUri = UCrop.getOutput(result);
+ if (resultUri != null) {
+ ResultActivity.startWithUri(SampleActivity.this, resultUri);
+ } else {
+ Toast.makeText(SampleActivity.this, R.string.toast_cannot_retrieve_cropped_image, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
+ private void handleCropError(@NonNull Intent result) {
+ final Throwable cropError = UCrop.getError(result);
+ if (cropError != null) {
+ Log.e(TAG, "handleCropError: ", cropError);
+ Toast.makeText(SampleActivity.this, cropError.getMessage(), Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(SampleActivity.this, R.string.toast_unexpected_error, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+}
diff --git a/sample/src/main/res/drawable/bg_rounded_rectangle.xml b/sample/src/main/res/drawable/bg_rounded_rectangle.xml
new file mode 100644
index 00000000..abe9809b
--- /dev/null
+++ b/sample/src/main/res/drawable/bg_rounded_rectangle.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/sample/src/main/res/drawable/ic_done.xml b/sample/src/main/res/drawable/ic_done.xml
new file mode 100644
index 00000000..7affe9ba
--- /dev/null
+++ b/sample/src/main/res/drawable/ic_done.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/sample/src/main/res/drawable/ic_file_download.xml b/sample/src/main/res/drawable/ic_file_download.xml
new file mode 100644
index 00000000..99aa581f
--- /dev/null
+++ b/sample/src/main/res/drawable/ic_file_download.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/sample/src/main/res/layout/activity_result.xml b/sample/src/main/res/layout/activity_result.xml
new file mode 100644
index 00000000..f645ef02
--- /dev/null
+++ b/sample/src/main/res/layout/activity_result.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/layout/activity_sample.xml b/sample/src/main/res/layout/activity_sample.xml
new file mode 100644
index 00000000..a57cb9a7
--- /dev/null
+++ b/sample/src/main/res/layout/activity_sample.xml
@@ -0,0 +1,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample/src/main/res/menu/menu_result.xml b/sample/src/main/res/menu/menu_result.xml
new file mode 100644
index 00000000..c55ec99f
--- /dev/null
+++ b/sample/src/main/res/menu/menu_result.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..d855f131
Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher.png b/sample/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..560423dc
Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..ab7af7d8
Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..1337f785
Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..b32912be
Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml
new file mode 100644
index 00000000..6531d5c0
--- /dev/null
+++ b/sample/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #FF6E40
+ #CC5833
+ #FF6E40
+
diff --git a/sample/src/main/res/values/dimens.xml b/sample/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..fe991af4
--- /dev/null
+++ b/sample/src/main/res/values/dimens.xml
@@ -0,0 +1,4 @@
+
+ 16dp
+ 16dp
+
diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml
new file mode 100644
index 00000000..9bba3fa0
--- /dev/null
+++ b/sample/src/main/res/values/strings.xml
@@ -0,0 +1,40 @@
+
+
+ uCrop
+
+ sample
+ Aspect ratio
+ Dynamic
+ Image source
+ Square
+ Max cropped image size
+ Compression settings
+ Width
+ Height
+ Resize image to max size
+ Select Picture
+ Cancel
+ OK
+
+ Image saved
+ Image saved. Click to preview.
+
+ Download
+
+
+
+ Cropped image
+
+ Crop Result (%1$dx%2$d)
+ Quality: %d
+ %1$dx%2$d px
+
+ Permission needed
+ Storage read permission is needed to pick files.
+ Storage write permission is needed to save the image.
+
+ Cannot retrieve selected image
+ Cannot retrieve cropped image
+ Unexpected error
+
+
diff --git a/sample/src/main/res/values/themes.xml b/sample/src/main/res/values/themes.xml
new file mode 100644
index 00000000..5dcfad1d
--- /dev/null
+++ b/sample/src/main/res/values/themes.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..cfdcacdc
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':ucrop', ':sample'
diff --git a/ucrop/.gitignore b/ucrop/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/ucrop/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/ucrop/build.gradle b/ucrop/build.gradle
new file mode 100644
index 00000000..bae66f3a
--- /dev/null
+++ b/ucrop/build.gradle
@@ -0,0 +1,30 @@
+apply plugin: 'com.android.library'
+apply from: '../mavenpush.gradle'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion '23.0.2'
+
+ defaultConfig {
+ minSdkVersion 10
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+
+ resourcePrefix 'ucrop_'
+}
+
+dependencies {
+ compile 'com.android.support:appcompat-v7:23.1.1'
+}
diff --git a/ucrop/gradle.properties b/ucrop/gradle.properties
new file mode 100644
index 00000000..839db2f0
--- /dev/null
+++ b/ucrop/gradle.properties
@@ -0,0 +1,3 @@
+POM_NAME=uCrop
+POM_ARTIFACT_ID=ucrop
+POM_PACKAGING=aar
\ No newline at end of file
diff --git a/ucrop/proguard-rules.pro b/ucrop/proguard-rules.pro
new file mode 100644
index 00000000..0cd55489
--- /dev/null
+++ b/ucrop/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/oleksii/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/ucrop/src/main/AndroidManifest.xml b/ucrop/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..f06ec81a
--- /dev/null
+++ b/ucrop/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/ucrop/src/main/java/com/yalantis/ucrop/UCrop.java b/ucrop/src/main/java/com/yalantis/ucrop/UCrop.java
new file mode 100644
index 00000000..c94bdad0
--- /dev/null
+++ b/ucrop/src/main/java/com/yalantis/ucrop/UCrop.java
@@ -0,0 +1,277 @@
+package com.yalantis.ucrop;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ *
+ * Builder class to ease Intent setup.
+ */
+public class UCrop {
+
+ public static final int REQUEST_CROP = 69;
+ public static final int RESULT_ERROR = 96;
+
+ public static final String EXTRA_INPUT_URI = "InputUri";
+ public static final String EXTRA_OUTPUT_URI = "OutputUri";
+ public static final String EXTRA_ERROR = "Error";
+
+ public static final String EXTRA_ASPECT_RATIO_SET = "AspectRatioSet";
+ public static final String EXTRA_ASPECT_RATIO_X = "AspectRatioX";
+ public static final String EXTRA_ASPECT_RATIO_Y = "AspectRatioY";
+
+ public static final String EXTRA_MAX_SIZE_SET = "MaxSizeSet";
+ public static final String EXTRA_MAX_SIZE_X = "MaxSizeX";
+ public static final String EXTRA_MAX_SIZE_Y = "MaxSizeY";
+
+ public static final String EXTRA_OPTIONS = "Options";
+
+ private Intent mCropIntent;
+
+ /**
+ * This method creates new Intent builder and sets both source and destination image URIs.
+ *
+ * @param source Uri for image to crop
+ * @param destination Uri for saving the cropped image
+ */
+ public static UCrop of(@NonNull Uri source, @NonNull Uri destination) {
+ return new UCrop(source, destination);
+ }
+
+ private UCrop(@NonNull Uri source, @NonNull Uri destination) {
+ mCropIntent = new Intent();
+ mCropIntent.putExtra(EXTRA_INPUT_URI, source);
+ mCropIntent.putExtra(EXTRA_OUTPUT_URI, destination);
+ }
+
+ /**
+ * Set an aspect ratio for crop bounds.
+ * User won't see the menu with other ratios options.
+ *
+ * @param x aspect ratio X
+ * @param y aspect ratio Y
+ */
+ public UCrop withAspectRatio(@IntRange(from = 1) int x, @IntRange(from = 1) int y) {
+ mCropIntent.putExtra(EXTRA_ASPECT_RATIO_SET, true);
+ mCropIntent.putExtra(EXTRA_ASPECT_RATIO_X, x);
+ mCropIntent.putExtra(EXTRA_ASPECT_RATIO_Y, y);
+ return this;
+ }
+
+ /**
+ * Set an aspect ratio for crop bounds that is evaluated from source image width and height.
+ * User won't see the menu with other ratios options.
+ */
+ public UCrop useSourceImageAspectRatio() {
+ mCropIntent.putExtra(EXTRA_ASPECT_RATIO_SET, true);
+ mCropIntent.putExtra(EXTRA_ASPECT_RATIO_X, 0);
+ mCropIntent.putExtra(EXTRA_ASPECT_RATIO_Y, 0);
+ return this;
+ }
+
+ /**
+ * Set maximum size for result cropped image.
+ *
+ * @param width max cropped image width
+ * @param height max cropped image height
+ */
+ public UCrop withMaxResultSize(@IntRange(from = 100) int width, @IntRange(from = 100) int height) {
+ mCropIntent.putExtra(EXTRA_MAX_SIZE_SET, true);
+ mCropIntent.putExtra(EXTRA_MAX_SIZE_X, width);
+ mCropIntent.putExtra(EXTRA_MAX_SIZE_Y, height);
+ return this;
+ }
+
+ public UCrop withOptions(@NonNull Options options) {
+ mCropIntent.putExtra(EXTRA_OPTIONS, options);
+ return this;
+ }
+
+ /**
+ * Send the crop Intent from an Activity
+ *
+ * @param activity Activity to receive result
+ */
+ public void start(@NonNull Activity activity) {
+ start(activity, REQUEST_CROP);
+ }
+
+ /**
+ * Send the crop Intent from an Activity with a custom request code
+ *
+ * @param activity Activity to receive result
+ * @param requestCode requestCode for result
+ */
+ public void start(@NonNull Activity activity, int requestCode) {
+ activity.startActivityForResult(getIntent(activity), requestCode);
+ }
+
+ /**
+ * Send the crop Intent from a Fragment
+ *
+ * @param fragment Fragment to receive result
+ */
+ public void start(@NonNull Context context, @NonNull Fragment fragment) {
+ start(context, fragment, REQUEST_CROP);
+ }
+
+ /**
+ * Send the crop Intent from a support library Fragment
+ *
+ * @param fragment Fragment to receive result
+ */
+ public void start(@NonNull Context context, @NonNull android.support.v4.app.Fragment fragment) {
+ start(context, fragment, REQUEST_CROP);
+ }
+
+ /**
+ * Send the crop Intent with a custom request code
+ *
+ * @param fragment Fragment to receive result
+ * @param requestCode requestCode for result
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public void start(@NonNull Context context, @NonNull Fragment fragment, int requestCode) {
+ fragment.startActivityForResult(getIntent(context), requestCode);
+ }
+
+ /**
+ * Send the crop Intent with a custom request code
+ *
+ * @param fragment Fragment to receive result
+ * @param requestCode requestCode for result
+ */
+ public void start(@NonNull Context context, @NonNull android.support.v4.app.Fragment fragment, int requestCode) {
+ fragment.startActivityForResult(getIntent(context), requestCode);
+ }
+
+ /**
+ * Get Intent to start {@link UCropActivity}
+ *
+ * @return Intent for {@link UCropActivity}
+ */
+ public Intent getIntent(@NonNull Context context) {
+ mCropIntent.setClass(context, UCropActivity.class);
+ return mCropIntent;
+ }
+
+ /**
+ * Retrieve cropped image Uri from the result Intent
+ *
+ * @param intent crop result intent
+ */
+ @Nullable
+ public static Uri getOutput(@NonNull Intent intent) {
+ return intent.getParcelableExtra(EXTRA_OUTPUT_URI);
+ }
+
+ /**
+ * Method retrieves error from the result intent.
+ *
+ * @param result crop result Intent
+ * @return Throwable that could happen while image processing
+ */
+ @Nullable
+ public static Throwable getError(@NonNull Intent result) {
+ return (Throwable) result.getSerializableExtra(EXTRA_ERROR);
+ }
+
+
+ /**
+ * Class that helps to setup advanced configs that are not commonly used.
+ * Use it with method {@link #withOptions(Options)}
+ */
+ public static class Options implements Parcelable {
+
+ private int mMaxBitmapSize;
+ private String mCompressionFormatName;
+ private int mCompressionQuality;
+ private boolean mGesturesAlwaysEnabled;
+
+ public Options() {
+ // Set default values
+ mMaxBitmapSize = 0;
+ mCompressionFormatName = UCropActivity.DEFAULT_COMPRESS_FORMAT.name();
+ mCompressionQuality = UCropActivity.DEFAULT_COMPRESS_QUALITY;
+ mGesturesAlwaysEnabled = false;
+ }
+
+ public void setMaxBitmapSize(@IntRange(from = 100) int maxBitmapSize) {
+ mMaxBitmapSize = maxBitmapSize;
+ }
+
+ public void setCompressionFormat(@NonNull Bitmap.CompressFormat format) {
+ mCompressionFormatName = format.name();
+ }
+
+ public void setCompressionQuality(@IntRange(from = 1) int compressQuality) {
+ mCompressionQuality = compressQuality;
+ }
+
+ public void setGesturesAlwaysEnabled(boolean gesturesAlwaysEnabled) {
+ mGesturesAlwaysEnabled = gesturesAlwaysEnabled;
+ }
+
+ public int getMaxBitmapSize() {
+ return mMaxBitmapSize;
+ }
+
+ public String getCompressionFormatName() {
+ return mCompressionFormatName;
+ }
+
+ public int getCompressionQuality() {
+ return mCompressionQuality;
+ }
+
+ public boolean isGesturesAlwaysEnabled() {
+ return mGesturesAlwaysEnabled;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mMaxBitmapSize);
+ dest.writeString(mCompressionFormatName);
+ dest.writeInt(mCompressionQuality);
+ dest.writeByte((byte) (mGesturesAlwaysEnabled ? 1 : 0));
+ }
+
+ protected Options(Parcel in) {
+ mMaxBitmapSize = in.readInt();
+ mCompressionFormatName = in.readString();
+ mCompressionQuality = in.readInt();
+ mGesturesAlwaysEnabled = in.readByte() != 0;
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public Options createFromParcel(Parcel in) {
+ return new Options(in);
+ }
+
+ @Override
+ public Options[] newArray(int size) {
+ return new Options[size];
+ }
+ };
+
+ }
+
+}
\ No newline at end of file
diff --git a/ucrop/src/main/java/com/yalantis/ucrop/UCropActivity.java b/ucrop/src/main/java/com/yalantis/ucrop/UCropActivity.java
new file mode 100644
index 00000000..ce104002
--- /dev/null
+++ b/ucrop/src/main/java/com/yalantis/ucrop/UCropActivity.java
@@ -0,0 +1,374 @@
+package com.yalantis.ucrop;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.yalantis.ucrop.util.BitmapLoadUtils;
+import com.yalantis.ucrop.view.CropImageView;
+import com.yalantis.ucrop.view.GestureCropImageView;
+import com.yalantis.ucrop.view.TransformImageView;
+import com.yalantis.ucrop.view.widget.AspectRatioTextView;
+import com.yalantis.ucrop.view.widget.HorizontalProgressWheelView;
+
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ */
+public class UCropActivity extends AppCompatActivity {
+
+ public static final int DEFAULT_MAX_BITMAP_SIZE = 0;
+ public static final int DEFAULT_COMPRESS_QUALITY = 90;
+ public static final Bitmap.CompressFormat DEFAULT_COMPRESS_FORMAT = Bitmap.CompressFormat.JPEG;
+
+ private static final String TAG = "UCropActivity";
+
+ private static final int SCALE_WIDGET_SENSITIVITY_COEFFICIENT = 15000;
+ private static final int ROTATE_WIDGET_SENSITIVITY_COEFFICIENT = 42;
+
+ private GestureCropImageView mGestureCropImageView;
+ private ViewGroup mWrapperStateAspectRatio, mWrapperStateRotate, mWrapperStateScale;
+ private ViewGroup mLayoutAspectRatio, mLayoutRotate, mLayoutScale;
+ private List mCropAspectRatioViews = new ArrayList<>();
+ private TextView mTextViewRotateAngle, mTextViewScalePercent;
+
+ private Uri mOutputUri;
+
+ private int mMaxBitmapSize = DEFAULT_MAX_BITMAP_SIZE;
+ private Bitmap.CompressFormat mCompressFormat = DEFAULT_COMPRESS_FORMAT;
+ private int mCompressQuality = DEFAULT_COMPRESS_QUALITY;
+ private boolean mGesturesAlwaysEnabled = false;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.ucrop_activity_photobox);
+
+ setupViews();
+ setImageData();
+ setInitialState();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ getMenuInflater().inflate(R.menu.ucrop_menu_activity, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.menu_next) {
+ cropAndSaveImage();
+ } else if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mGestureCropImageView != null) {
+ mGestureCropImageView.cancelAllAnimations();
+ }
+ }
+
+ private void setImageData() {
+ final Intent intent = getIntent();
+
+ Uri inputUri = intent.getParcelableExtra(UCrop.EXTRA_INPUT_URI);
+ mOutputUri = intent.getParcelableExtra(UCrop.EXTRA_OUTPUT_URI);
+ processOptions(intent);
+
+ if (inputUri != null && mOutputUri != null) {
+ try {
+ mGestureCropImageView.setMaxBitmapSize(mMaxBitmapSize);
+ mGestureCropImageView.setImageUri(inputUri);
+ } catch (Exception e) {
+ setResultException(e);
+ finish();
+ }
+ } else {
+ setResultException(new NullPointerException(getString(R.string.ucrop_error_input_data_is_absent)));
+ finish();
+ }
+
+ if (intent.getBooleanExtra(UCrop.EXTRA_ASPECT_RATIO_SET, false)) {
+ mWrapperStateAspectRatio.setVisibility(View.GONE);
+
+ int aspectRatioX = intent.getIntExtra(UCrop.EXTRA_ASPECT_RATIO_X, 0);
+ int aspectRatioY = intent.getIntExtra(UCrop.EXTRA_ASPECT_RATIO_Y, 0);
+
+ if (aspectRatioX > 0 && aspectRatioY > 0) {
+ mGestureCropImageView.setTargetAspectRatio(aspectRatioX / (float) aspectRatioY);
+ } else {
+ mGestureCropImageView.setTargetAspectRatio(CropImageView.SOURCE_IMAGE_ASPECT_RATIO);
+ }
+ }
+
+ if (intent.getBooleanExtra(UCrop.EXTRA_MAX_SIZE_SET, false)) {
+ int maxSizeX = intent.getIntExtra(UCrop.EXTRA_MAX_SIZE_X, 0);
+ int maxSizeY = intent.getIntExtra(UCrop.EXTRA_MAX_SIZE_Y, 0);
+
+ if (maxSizeX > 0 && maxSizeY > 0) {
+ mGestureCropImageView.setMaxResultImageSizeX(maxSizeX);
+ mGestureCropImageView.setMaxResultImageSizeY(maxSizeY);
+ } else {
+ Log.w(TAG, "EXTRA_MAX_SIZE_X and EXTRA_MAX_SIZE_Y must be greater than 0");
+ }
+ }
+ }
+
+ private void processOptions(@NonNull Intent intent) {
+ UCrop.Options options = intent.getParcelableExtra(UCrop.EXTRA_OPTIONS);
+ if (options != null) {
+ mMaxBitmapSize = options.getMaxBitmapSize();
+
+ String compressionFormatName = options.getCompressionFormatName();
+ Bitmap.CompressFormat compressFormat = null;
+ if (!TextUtils.isEmpty(compressionFormatName)) {
+ compressFormat = Bitmap.CompressFormat.valueOf(compressionFormatName);
+ }
+ mCompressFormat = (compressFormat == null) ? DEFAULT_COMPRESS_FORMAT : compressFormat;
+
+ mCompressQuality = options.getCompressionQuality();
+ mGesturesAlwaysEnabled = options.isGesturesAlwaysEnabled();
+ }
+ }
+
+ private void setupViews() {
+ final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ toolbar.setNavigationIcon(R.drawable.ucrop_ic_cross);
+ final ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(false);
+ }
+ setStatusBarColor(getResources().getColor(R.color.ucrop_color_statusbar));
+
+ mGestureCropImageView = (GestureCropImageView) findViewById(R.id.image_view_crop);
+ mGestureCropImageView.setTransformImageListener(new TransformImageView.TransformImageListener() {
+ @Override
+ public void onRotate(float currentAngle) {
+ setAngleText(currentAngle);
+ }
+
+ @Override
+ public void onScale(float currentScale) {
+ setScaleText(currentScale);
+ }
+ });
+
+ mWrapperStateAspectRatio = (ViewGroup) findViewById(R.id.state_aspect_ratio);
+ mWrapperStateAspectRatio.setOnClickListener(mStateClickListener);
+ mWrapperStateRotate = (ViewGroup) findViewById(R.id.state_rotate);
+ mWrapperStateRotate.setOnClickListener(mStateClickListener);
+ mWrapperStateScale = (ViewGroup) findViewById(R.id.state_scale);
+ mWrapperStateScale.setOnClickListener(mStateClickListener);
+
+ mLayoutAspectRatio = (ViewGroup) findViewById(R.id.layout_aspect_ratio);
+ mLayoutRotate = (ViewGroup) findViewById(R.id.layout_rotate_wheel);
+ mLayoutScale = (ViewGroup) findViewById(R.id.layout_scale_wheel);
+
+ setupAspectRatioWidget();
+ setupRotateWidget();
+ setupScaleWidget();
+ }
+
+ /**
+ * Sets status-bar color for L devices.
+ *
+ * @param color - status-bar color
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public void setStatusBarColor(@ColorInt int color) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ if (getWindow() != null) {
+ getWindow().setStatusBarColor(color);
+ }
+ }
+ }
+
+ private void setupAspectRatioWidget() {
+ mCropAspectRatioViews.add((ViewGroup) findViewById(R.id.crop_aspect_ratio_1_1));
+ mCropAspectRatioViews.add((ViewGroup) findViewById(R.id.crop_aspect_ratio_3_4));
+ mCropAspectRatioViews.add((ViewGroup) findViewById(R.id.crop_aspect_ratio_original));
+ mCropAspectRatioViews.add((ViewGroup) findViewById(R.id.crop_aspect_ratio_3_2));
+ mCropAspectRatioViews.add((ViewGroup) findViewById(R.id.crop_aspect_ratio_16_9));
+ mCropAspectRatioViews.get(2).setSelected(true);
+
+ for (ViewGroup cropAspectRatioView : mCropAspectRatioViews) {
+ cropAspectRatioView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mGestureCropImageView.setTargetAspectRatio(
+ ((AspectRatioTextView) ((ViewGroup) v).getChildAt(0)).getAspectRatio(v.isSelected()));
+ mGestureCropImageView.setImageToWrapCropBounds();
+ if (!v.isSelected()) {
+ for (ViewGroup cropAspectRatioView : mCropAspectRatioViews) {
+ cropAspectRatioView.setSelected(cropAspectRatioView == v);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ private void setupRotateWidget() {
+ mTextViewRotateAngle = ((TextView) findViewById(R.id.text_view_rotate));
+ ((HorizontalProgressWheelView) findViewById(R.id.rotate_scroll_wheel))
+ .setScrollingListener(new HorizontalProgressWheelView.ScrollingListener() {
+ @Override
+ public void onScroll(float delta, float totalDistance) {
+ mGestureCropImageView.postRotate(delta / ROTATE_WIDGET_SENSITIVITY_COEFFICIENT);
+ }
+
+ @Override
+ public void onScrollEnd() {
+ mGestureCropImageView.setImageToWrapCropBounds();
+ }
+
+ @Override
+ public void onScrollStart() {
+ mGestureCropImageView.cancelAllAnimations();
+ }
+ });
+
+
+ findViewById(R.id.wrapper_reset_rotate).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ resetRotation();
+ }
+ });
+ findViewById(R.id.wrapper_rotate_by_angle).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ rotateByAngle(90);
+ }
+ });
+ }
+
+ private void setupScaleWidget() {
+ mTextViewScalePercent = ((TextView) findViewById(R.id.text_view_scale));
+ ((HorizontalProgressWheelView) findViewById(R.id.scale_scroll_wheel))
+ .setScrollingListener(new HorizontalProgressWheelView.ScrollingListener() {
+ @Override
+ public void onScroll(float delta, float totalDistance) {
+ if (delta > 0) {
+ mGestureCropImageView.zoomInImage(mGestureCropImageView.getCurrentScale()
+ + delta * ((mGestureCropImageView.getMaxScale() - mGestureCropImageView.getMinScale()) / SCALE_WIDGET_SENSITIVITY_COEFFICIENT));
+ } else {
+ mGestureCropImageView.zoomOutImage(mGestureCropImageView.getCurrentScale()
+ + delta * ((mGestureCropImageView.getMaxScale() - mGestureCropImageView.getMinScale()) / SCALE_WIDGET_SENSITIVITY_COEFFICIENT));
+ }
+ }
+
+ @Override
+ public void onScrollEnd() {
+ mGestureCropImageView.setImageToWrapCropBounds();
+ }
+
+ @Override
+ public void onScrollStart() {
+ mGestureCropImageView.cancelAllAnimations();
+ }
+ });
+ }
+
+ private void setAngleText(float angle) {
+ if (mTextViewRotateAngle != null) {
+ mTextViewRotateAngle.setText(String.format("%.1f°", angle));
+ }
+ }
+
+ private void setScaleText(float scale) {
+ if (mTextViewScalePercent != null) {
+ mTextViewScalePercent.setText(String.format("%d%%", (int) (scale * 100)));
+ }
+ }
+
+ private void resetRotation() {
+ mGestureCropImageView.postRotate(-mGestureCropImageView.getCurrentAngle());
+ mGestureCropImageView.setImageToWrapCropBounds();
+ }
+
+ private void rotateByAngle(int angle) {
+ mGestureCropImageView.postRotate(angle);
+ mGestureCropImageView.setImageToWrapCropBounds();
+ }
+
+ private final View.OnClickListener mStateClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!v.isSelected()) {
+ setWidgetState(v.getId());
+ }
+ }
+ };
+
+ private void setInitialState() {
+ setWidgetState(R.id.state_scale);
+ }
+
+ private void setWidgetState(@IdRes int stateViewId) {
+ mWrapperStateAspectRatio.setSelected(stateViewId == R.id.state_aspect_ratio);
+ mWrapperStateRotate.setSelected(stateViewId == R.id.state_rotate);
+ mWrapperStateScale.setSelected(stateViewId == R.id.state_scale);
+
+ mLayoutAspectRatio.setVisibility(stateViewId == R.id.state_aspect_ratio ? View.VISIBLE : View.GONE);
+ mLayoutRotate.setVisibility(stateViewId == R.id.state_rotate ? View.VISIBLE : View.GONE);
+ mLayoutScale.setVisibility(stateViewId == R.id.state_scale ? View.VISIBLE : View.GONE);
+
+ mGestureCropImageView.setRotateEnabled(mGesturesAlwaysEnabled || stateViewId != R.id.state_scale);
+ mGestureCropImageView.setScaleEnabled(mGesturesAlwaysEnabled || stateViewId != R.id.state_rotate);
+ }
+
+ private void cropAndSaveImage() {
+ OutputStream outputStream = null;
+ try {
+ final Bitmap croppedBitmap = mGestureCropImageView.cropImage();
+ if (croppedBitmap != null) {
+ outputStream = getContentResolver().openOutputStream(mOutputUri);
+ croppedBitmap.compress(mCompressFormat, mCompressQuality, outputStream);
+ croppedBitmap.recycle();
+
+ setResultUri(mOutputUri);
+ finish();
+ }
+ } catch (Exception e) {
+ setResultException(e);
+ finish();
+ } finally {
+ BitmapLoadUtils.close(outputStream);
+ }
+ }
+
+ private void setResultUri(Uri uri) {
+ setResult(RESULT_OK, new Intent().putExtra(UCrop.EXTRA_OUTPUT_URI, uri));
+ }
+
+ private void setResultException(Throwable throwable) {
+ setResult(UCrop.RESULT_ERROR, new Intent().putExtra(UCrop.EXTRA_ERROR, throwable));
+ }
+
+}
diff --git a/ucrop/src/main/java/com/yalantis/ucrop/util/BitmapLoadUtils.java b/ucrop/src/main/java/com/yalantis/ucrop/util/BitmapLoadUtils.java
new file mode 100644
index 00000000..947dd1a1
--- /dev/null
+++ b/ucrop/src/main/java/com/yalantis/ucrop/util/BitmapLoadUtils.java
@@ -0,0 +1,126 @@
+package com.yalantis.ucrop.util;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ */
+public class BitmapLoadUtils {
+
+ private static final String TAG = "BitmapLoadUtils";
+
+ @Nullable
+ public static Bitmap decode(@NonNull Context context, @Nullable Uri uri,
+ int requiredWidth, int requiredHeight) throws Exception {
+ if (uri == null) {
+ return null;
+ }
+
+ final ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r");
+ FileDescriptor fileDescriptor;
+ if (parcelFileDescriptor != null) {
+ fileDescriptor = parcelFileDescriptor.getFileDescriptor();
+ } else {
+ return null;
+ }
+
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
+ options.inSampleSize = calculateInSampleSize(options, requiredWidth, requiredHeight);
+ options.inJustDecodeBounds = false;
+
+ Bitmap decodeSampledBitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ close(parcelFileDescriptor);
+ }
+
+ ExifInterface exif = getExif(uri);
+ if (exif != null) {
+ int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
+ // TODO Should not rotate bitmap but initially apply needed angle to the matrix
+ return rotateBitmap(decodeSampledBitmap, exifToDegrees(exifOrientation));
+ } else {
+ return decodeSampledBitmap;
+ }
+ }
+
+ public static Bitmap rotateBitmap(@Nullable Bitmap bitmap, int degrees) {
+ if (bitmap != null && degrees != 0) {
+ Matrix rotateMatrix = new Matrix();
+ rotateMatrix.setRotate(degrees, bitmap.getWidth() / (float) 2, bitmap.getHeight() / (float) 2);
+
+ Bitmap converted = Bitmap.createBitmap(bitmap, 0, 0,
+ bitmap.getWidth(), bitmap.getHeight(), rotateMatrix, true);
+ if (bitmap != converted) {
+ bitmap.recycle();
+ bitmap = converted;
+ }
+ }
+ return bitmap;
+ }
+
+ public static int calculateInSampleSize(@NonNull BitmapFactory.Options options, int reqWidth, int reqHeight) {
+ // Raw height and width of image
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+ // Calculate the largest inSampleSize value that is a power of 2 and keeps both
+ // height and width lower or equal to the requested height and width.
+ while ((height / inSampleSize) > reqHeight || (width / inSampleSize) > reqWidth) {
+ inSampleSize *= 2;
+ }
+ }
+ return inSampleSize;
+ }
+
+ @Nullable
+ private static ExifInterface getExif(@NonNull Uri imageUri) {
+ try {
+ return new ExifInterface(imageUri.getPath());
+ } catch (IOException e) {
+ Log.w(TAG, "getExif: ", e);
+ }
+ return null;
+ }
+
+ private static int exifToDegrees(int exifOrientation) {
+ if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) {
+ return 90;
+ } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_180) {
+ return 180;
+ } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
+ return 270;
+ }
+ return 0;
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ public static void close(@Nullable Closeable c) {
+ if (c != null && c instanceof Closeable) { // java.lang.IncompatibleClassChangeError: interface not implemented
+ try {
+ c.close();
+ } catch (IOException e) {
+ // silence
+ }
+ }
+ }
+
+}
diff --git a/ucrop/src/main/java/com/yalantis/ucrop/util/CubicEasing.java b/ucrop/src/main/java/com/yalantis/ucrop/util/CubicEasing.java
new file mode 100644
index 00000000..3c81b8ec
--- /dev/null
+++ b/ucrop/src/main/java/com/yalantis/ucrop/util/CubicEasing.java
@@ -0,0 +1,17 @@
+package com.yalantis.ucrop.util;
+
+public final class CubicEasing {
+
+ public static float easeOut(float time, float start, float end, float duration) {
+ return end * ((time = time / duration - 1.0f) * time * time + 1.0f) + start;
+ }
+
+ public static float easeIn(float time, float start, float end, float duration) {
+ return end * (time /= duration) * time * time + start;
+ }
+
+ public static float easeInOut(float time, float start, float end, float duration) {
+ return (time /= duration / 2.0f) < 1.0f ? end / 2.0f * time * time * time + start : end / 2.0f * ((time -= 2.0f) * time * time + 2.0f) + start;
+ }
+
+}
diff --git a/ucrop/src/main/java/com/yalantis/ucrop/util/FastBitmapDrawable.java b/ucrop/src/main/java/com/yalantis/ucrop/util/FastBitmapDrawable.java
new file mode 100644
index 00000000..6a783efe
--- /dev/null
+++ b/ucrop/src/main/java/com/yalantis/ucrop/util/FastBitmapDrawable.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.yalantis.ucrop.util;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+public class FastBitmapDrawable extends Drawable {
+
+ private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+
+ private Bitmap mBitmap;
+ private int mAlpha;
+ private int mWidth, mHeight;
+
+ public FastBitmapDrawable(Bitmap b) {
+ mAlpha = 255;
+ setBitmap(b);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final Rect r = getBounds();
+ canvas.drawBitmap(mBitmap, null, r, mPaint);
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ public void setFilterBitmap(boolean filterBitmap) {
+ mPaint.setFilterBitmap(filterBitmap);
+ }
+
+ public int getAlpha() {
+ return mAlpha;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mAlpha = alpha;
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public int getMinimumWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getMinimumHeight() {
+ return mHeight;
+ }
+
+ public Bitmap getBitmap() {
+ return mBitmap;
+ }
+
+ public void setBitmap(Bitmap b) {
+ mBitmap = b;
+ if (b != null) {
+ mWidth = mBitmap.getWidth();
+ mHeight = mBitmap.getHeight();
+ } else {
+ mWidth = mHeight = 0;
+ }
+ }
+
+}
diff --git a/ucrop/src/main/java/com/yalantis/ucrop/util/RectUtils.java b/ucrop/src/main/java/com/yalantis/ucrop/util/RectUtils.java
new file mode 100644
index 00000000..867f8e06
--- /dev/null
+++ b/ucrop/src/main/java/com/yalantis/ucrop/util/RectUtils.java
@@ -0,0 +1,72 @@
+package com.yalantis.ucrop.util;
+
+import android.graphics.RectF;
+
+public class RectUtils {
+
+ /**
+ * Gets a float array of the 2D coordinates representing a rectangles
+ * corners.
+ * The order of the corners in the float array is:
+ * 0------->1
+ * ^ |
+ * | |
+ * | v
+ * 3<-------2
+ *
+ * @param r the rectangle to get the corners of
+ * @return the float array of corners (8 floats)
+ */
+ public static float[] getCornersFromRect(RectF r) {
+ return new float[]{
+ r.left, r.top,
+ r.right, r.top,
+ r.right, r.bottom,
+ r.left, r.bottom
+ };
+ }
+
+ /**
+ * Gets a float array of two lengths representing a rectangles width and height
+ * The order of the corners in the input float array is:
+ * 0------->1
+ * ^ |
+ * | |
+ * | v
+ * 3<-------2
+ *
+ * @param corners the float array of corners (8 floats)
+ * @return the float array of width and height (2 floats)
+ */
+ public static float[] getRectSidesFromCorners(float[] corners) {
+ return new float[]{(float) Math.sqrt(Math.pow(corners[0] - corners[2], 2) + Math.pow(corners[1] - corners[3], 2)),
+ (float) Math.sqrt(Math.pow(corners[2] - corners[4], 2) + Math.pow(corners[3] - corners[5], 2))};
+ }
+
+ public static float[] getCenterFromRect(RectF r) {
+ return new float[]{r.centerX(), r.centerY()};
+ }
+
+ /**
+ * Takes an array of 2D coordinates representing corners and returns the
+ * smallest rectangle containing those coordinates.
+ *
+ * @param array array of 2D coordinates
+ * @return smallest rectangle containing coordinates
+ */
+ public static RectF trapToRect(float[] array) {
+ RectF r = new RectF(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
+ Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
+ for (int i = 1; i < array.length; i += 2) {
+ float x = array[i - 1];
+ float y = array[i];
+ r.left = (x < r.left) ? x : r.left;
+ r.top = (y < r.top) ? y : r.top;
+ r.right = (x > r.right) ? x : r.right;
+ r.bottom = (y > r.bottom) ? y : r.bottom;
+ }
+ r.sort();
+ return r;
+ }
+
+}
\ No newline at end of file
diff --git a/ucrop/src/main/java/com/yalantis/ucrop/util/RotationGestureDetector.java b/ucrop/src/main/java/com/yalantis/ucrop/util/RotationGestureDetector.java
new file mode 100644
index 00000000..4fd8e5c3
--- /dev/null
+++ b/ucrop/src/main/java/com/yalantis/ucrop/util/RotationGestureDetector.java
@@ -0,0 +1,111 @@
+package com.yalantis.ucrop.util;
+
+import android.support.annotation.NonNull;
+import android.view.MotionEvent;
+
+public class RotationGestureDetector {
+
+ private static final int INVALID_POINTER_INDEX = -1;
+
+ private float fX, fY, sX, sY;
+
+ private int mPointerIndex1, mPointerIndex2;
+ private float mAngle;
+ private boolean mIsFirstTouch;
+
+ private OnRotationGestureListener mListener;
+
+ public RotationGestureDetector(OnRotationGestureListener listener) {
+ mListener = listener;
+ mPointerIndex1 = INVALID_POINTER_INDEX;
+ mPointerIndex2 = INVALID_POINTER_INDEX;
+ }
+
+ public float getAngle() {
+ return mAngle;
+ }
+
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ sX = event.getX();
+ sY = event.getY();
+ mPointerIndex1 = event.findPointerIndex(event.getPointerId(0));
+ mAngle = 0;
+ mIsFirstTouch = true;
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ fX = event.getX();
+ fY = event.getY();
+ mPointerIndex2 = event.findPointerIndex(event.getPointerId(event.getActionIndex()));
+ mAngle = 0;
+ mIsFirstTouch = true;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (mPointerIndex1 != INVALID_POINTER_INDEX && mPointerIndex2 != INVALID_POINTER_INDEX && event.getPointerCount() > mPointerIndex2) {
+ float nfX, nfY, nsX, nsY;
+
+ nsX = event.getX(mPointerIndex1);
+ nsY = event.getY(mPointerIndex1);
+ nfX = event.getX(mPointerIndex2);
+ nfY = event.getY(mPointerIndex2);
+
+ if (mIsFirstTouch) {
+ mAngle = 0;
+ mIsFirstTouch = false;
+ } else {
+ calculateAngleBetweenLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY);
+ }
+
+ if (mListener != null) {
+ mListener.onRotation(this);
+ }
+ fX = nfX;
+ fY = nfY;
+ sX = nsX;
+ sY = nsY;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ mPointerIndex1 = INVALID_POINTER_INDEX;
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ mPointerIndex2 = INVALID_POINTER_INDEX;
+ break;
+ }
+ return true;
+ }
+
+ private float calculateAngleBetweenLines(float fx1, float fy1, float fx2, float fy2,
+ float sx1, float sy1, float sx2, float sy2) {
+ return calculateAngleDelta(
+ (float) Math.toDegrees((float) Math.atan2((fy1 - fy2), (fx1 - fx2))),
+ (float) Math.toDegrees((float) Math.atan2((sy1 - sy2), (sx1 - sx2))));
+ }
+
+ private float calculateAngleDelta(float angleFrom, float angleTo) {
+ mAngle = angleTo % 360.0f - angleFrom % 360.0f;
+
+ if (mAngle < -180.0f) {
+ mAngle += 360.0f;
+ } else if (mAngle > 180.0f) {
+ mAngle -= 360.0f;
+ }
+
+ return mAngle;
+ }
+
+ public static class SimpleOnRotationGestureListener implements OnRotationGestureListener {
+
+ @Override
+ public boolean onRotation(RotationGestureDetector rotationDetector) {
+ return false;
+ }
+ }
+
+ public interface OnRotationGestureListener {
+
+ boolean onRotation(RotationGestureDetector rotationDetector);
+ }
+
+}
\ No newline at end of file
diff --git a/ucrop/src/main/java/com/yalantis/ucrop/view/CropImageView.java b/ucrop/src/main/java/com/yalantis/ucrop/view/CropImageView.java
new file mode 100644
index 00000000..fcd2a255
--- /dev/null
+++ b/ucrop/src/main/java/com/yalantis/ucrop/view/CropImageView.java
@@ -0,0 +1,698 @@
+package com.yalantis.ucrop.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+
+import com.yalantis.ucrop.R;
+import com.yalantis.ucrop.util.CubicEasing;
+import com.yalantis.ucrop.util.RectUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ *
+ * This class adds crop feature, methods to draw crop guidelines, and keep image in correct state.
+ * Also it extends parent class methods to add checks for scale; animating zoom in/out.
+ */
+public abstract class CropImageView extends TransformImageView {
+
+ public static final float SOURCE_IMAGE_ASPECT_RATIO = 0f;
+
+ private static final boolean DEFAULT_SHOW_CROP_FRAME = false;
+ private static final boolean DEFAULT_SHOW_CROP_GRID = true;
+ private static final int DEFAULT_CROP_GRID_ROW_COUNT = 3;
+ private static final int DEFAULT_CROP_GRID_COLUMN_COUNT = 3;
+ private static final int DEFAULT_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION = 777;
+
+ private static final float DEFAULT_ASPECT_RATIO = SOURCE_IMAGE_ASPECT_RATIO;
+
+ private static final float DEFAULT_MAX_SCALE_MULTIPLIER = 10.0f;
+
+ private final RectF mCropRect = new RectF();
+ private final RectF mCropViewRect = new RectF();
+
+ private final Matrix mTempMatrix = new Matrix();
+
+ private int mCropGridRowCount, mCropGridColumnCount;
+ private float mTargetAspectRatio;
+ private float[] mGridPoints = null;
+ private boolean mShowCropFrame, mShowCropGrid;
+ private Paint mDimmedPaint, mGridInnerLinePaint, mGridOuterLinePaint;
+ private float mMaxScaleMultiplier;
+
+ private Runnable mWrapCropBoundsRunnable, mZoomImageToPositionRunnable = null;
+
+ private float mMaxScale, mMinScale;
+ private int mMaxResultImageSizeX = 0, mMaxResultImageSizeY = 0;
+ private long mImageToWrapCropBoundsAnimDuration = DEFAULT_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION;
+
+ public CropImageView(Context context) {
+ this(context, null);
+ }
+
+ public CropImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CropImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context, attrs, defStyle);
+ }
+
+ /**
+ * This method crops part of image that fills the crop bounds.
+ *
+ * First image is downscaled if max size was set and if resulting image is larger that max size.
+ * Then image is rotated accordingly.
+ * Finally new Bitmap object is created and returned.
+ *
+ * @return - cropped Bitmap object or null if any error occurs.
+ */
+ @Nullable
+ public Bitmap cropImage() throws Exception {
+ Bitmap viewBitmap = getViewBitmap();
+ RectF currentImageRect = RectUtils.trapToRect(mCurrentImageCorners);
+ if (viewBitmap == null || currentImageRect.isEmpty() || !isImageWrapCropBounds()) {
+ return null;
+ }
+
+ float currentScale = getCurrentScale();
+ float currentAngle = getCurrentAngle();
+
+ if (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0) {
+ float cropWidth = mCropRect.width() / currentScale;
+ float cropHeight = mCropRect.height() / currentScale;
+
+ if (cropWidth > mMaxResultImageSizeX || cropHeight > mMaxResultImageSizeY) {
+
+ float scaleX = mMaxResultImageSizeX / cropWidth;
+ float scaleY = mMaxResultImageSizeY / cropHeight;
+ float resizeScale = Math.min(scaleX, scaleY);
+
+ Bitmap resizedBitmap = Bitmap.createScaledBitmap(viewBitmap,
+ (int) (viewBitmap.getWidth() * resizeScale),
+ (int) (viewBitmap.getHeight() * resizeScale), false);
+ viewBitmap.recycle();
+ viewBitmap = resizedBitmap;
+
+ currentScale /= resizeScale;
+ }
+ }
+
+ if (currentAngle != 0) {
+ mTempMatrix.reset();
+ mTempMatrix.setRotate(currentAngle, viewBitmap.getWidth() / 2, viewBitmap.getHeight() / 2);
+
+ Bitmap rotatedBitmap = Bitmap.createBitmap(viewBitmap, 0, 0, viewBitmap.getWidth(), viewBitmap.getHeight(),
+ mTempMatrix, true);
+ viewBitmap.recycle();
+ viewBitmap = rotatedBitmap;
+ }
+
+ int top = (int) ((mCropRect.top - currentImageRect.top) / currentScale);
+ int left = (int) ((mCropRect.left - currentImageRect.left) / currentScale);
+ int width = (int) (mCropRect.width() / currentScale);
+ int height = (int) (mCropRect.height() / currentScale);
+
+ Bitmap croppedBitmap = Bitmap.createBitmap(viewBitmap, left, top, width, height);
+ viewBitmap.recycle();
+
+ return croppedBitmap;
+ }
+
+ /**
+ * @return - maximum scale value for current image and crop ratio
+ */
+ public float getMaxScale() {
+ return mMaxScale;
+ }
+
+ /**
+ * @return - minimum scale value for current image and crop ratio
+ */
+ public float getMinScale() {
+ return mMinScale;
+ }
+
+ /**
+ * @return - aspect ratio for crop bounds
+ */
+ public float getTargetAspectRatio() {
+ return mTargetAspectRatio;
+ }
+
+ /**
+ * This method sets aspect ratio for crop bounds.
+ * If {@link #SOURCE_IMAGE_ASPECT_RATIO} value is passed - aspect ratio is calculated
+ * based on current image width and height.
+ *
+ * @param targetAspectRatio - aspect ratio for image crop (e.g. 1.77(7) for 16:9)
+ */
+ public void setTargetAspectRatio(float targetAspectRatio) {
+ final Drawable drawable = getDrawable();
+ if (drawable == null) {
+ mTargetAspectRatio = targetAspectRatio;
+ return;
+ }
+
+ if (targetAspectRatio == SOURCE_IMAGE_ASPECT_RATIO) {
+ mTargetAspectRatio = drawable.getIntrinsicWidth() / (float) drawable.getIntrinsicHeight();
+ } else {
+ mTargetAspectRatio = targetAspectRatio;
+ }
+
+ setupCropBounds();
+ mGridPoints = null;
+
+ postInvalidate();
+ }
+
+ /**
+ * This method sets maximum width for resulting cropped image
+ *
+ * @param maxResultImageSizeX - size in pixels
+ */
+ public void setMaxResultImageSizeX(@IntRange(from = 10) int maxResultImageSizeX) {
+ mMaxResultImageSizeX = maxResultImageSizeX;
+ }
+
+ /**
+ * This method sets maximum width for resulting cropped image
+ *
+ * @param maxResultImageSizeY - size in pixels
+ */
+ public void setMaxResultImageSizeY(@IntRange(from = 10) int maxResultImageSizeY) {
+ mMaxResultImageSizeY = maxResultImageSizeY;
+ }
+
+ /**
+ * This method sets animation duration for image to wrap the crop bounds
+ *
+ * @param imageToWrapCropBoundsAnimDuration - duration in milliseconds
+ */
+ public void setImageToWrapCropBoundsAnimDuration(@IntRange(from = 100) long imageToWrapCropBoundsAnimDuration) {
+ if (imageToWrapCropBoundsAnimDuration > 0) {
+ mImageToWrapCropBoundsAnimDuration = imageToWrapCropBoundsAnimDuration;
+ } else {
+ throw new IllegalArgumentException("Animation duration cannot be negative value.");
+ }
+ }
+
+ /**
+ * This method scales image down for given value related to image center.
+ */
+ public void zoomOutImage(float deltaScale) {
+ zoomOutImage(deltaScale, mCropRect.centerX(), mCropRect.centerY());
+ }
+
+ /**
+ * This method scales image down for given value related given coords (x, y).
+ */
+ public void zoomOutImage(float scale, float centerX, float centerY) {
+ if (scale >= getMinScale()) {
+ postScale(scale / getCurrentScale(), centerX, centerY);
+ }
+ }
+
+ /**
+ * This method scales image up for given value related to image center.
+ */
+ public void zoomInImage(float deltaScale) {
+ zoomInImage(deltaScale, mCropRect.centerX(), mCropRect.centerY());
+ }
+
+ /**
+ * This method scales image up for given value related to given coords (x, y).
+ */
+ public void zoomInImage(float scale, float centerX, float centerY) {
+ if (scale <= getMaxScale()) {
+ postScale(scale / getCurrentScale(), centerX, centerY);
+ }
+ }
+
+ /**
+ * This method changes image scale for given value related to point (px, py) but only if
+ * resulting scale is in min/max bounds.
+ *
+ * @param deltaScale - scale value
+ * @param px - scale center X
+ * @param py - scale center Y
+ */
+ public void postScale(float deltaScale, float px, float py) {
+ if (deltaScale > 1 && getCurrentScale() * deltaScale <= getMaxScale()) {
+ super.postScale(deltaScale, px, py);
+ } else if (deltaScale < 1 && getCurrentScale() * deltaScale >= getMinScale()) {
+ super.postScale(deltaScale, px, py);
+ }
+ }
+
+ /**
+ * This method rotates image for given angle related to the image center.
+ *
+ * @param deltaAngle - angle to rotate
+ */
+ public void postRotate(float deltaAngle) {
+ postRotate(deltaAngle, mCropRect.centerX(), mCropRect.centerY());
+ }
+
+ /**
+ * This method cancels all current Runnable objects that represent animations.
+ */
+ public void cancelAllAnimations() {
+ removeCallbacks(mWrapCropBoundsRunnable);
+ removeCallbacks(mZoomImageToPositionRunnable);
+ }
+
+ /**
+ * If image doesn't fill the crop bounds it must be translated and scaled properly to fill those.
+ *
+ * Therefore this method calculates delta X, Y and scale values and passes them to the
+ * {@link WrapCropBoundsRunnable} which animates image.
+ * Scale value must be calculated only if image won't fill the crop bounds after it's translated to the
+ * crop bounds rectangle center. Using temporary variables this method checks this case.
+ */
+ public void setImageToWrapCropBounds() {
+ if (!isImageWrapCropBounds()) {
+
+ float currentX = mCurrentImageCenter[0];
+ float currentY = mCurrentImageCenter[1];
+ float currentScale = getCurrentScale();
+
+ float deltaX = mCropRect.centerX() - currentX;
+ float deltaY = mCropRect.centerY() - currentY;
+ float deltaScale = 0;
+
+ mTempMatrix.reset();
+ mTempMatrix.setTranslate(deltaX, deltaY);
+
+ float[] tempCurrentImageCorners = Arrays.copyOf(mCurrentImageCorners, mCurrentImageCorners.length);
+ mTempMatrix.mapPoints(tempCurrentImageCorners);
+
+ boolean willImageWrapCropBoundsAfterTranslate = isImageWrapCropBounds(tempCurrentImageCorners);
+
+ if (!willImageWrapCropBoundsAfterTranslate) {
+ RectF tempCropRect = new RectF(mCropRect);
+ mTempMatrix.reset();
+ mTempMatrix.setRotate(getCurrentAngle());
+ mTempMatrix.mapRect(tempCropRect);
+
+ float[] currentImageSides = RectUtils.getRectSidesFromCorners(mCurrentImageCorners);
+
+ deltaScale = Math.max(tempCropRect.width() / currentImageSides[0],
+ tempCropRect.height() / currentImageSides[1]);
+ // Ugly but there are always couple pixels that want to hide because of all these calculations
+ deltaScale *= 1.01;
+ deltaScale = deltaScale * currentScale - currentScale;
+ }
+ post(mWrapCropBoundsRunnable = new WrapCropBoundsRunnable(
+ CropImageView.this, mImageToWrapCropBoundsAnimDuration, currentX, currentY, deltaX, deltaY,
+ currentScale, deltaScale, willImageWrapCropBoundsAfterTranslate));
+ }
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyle) {
+ super.init(context, attrs, defStyle);
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ucrop_CropImageView);
+ processStyledAttributes(a);
+ a.recycle();
+ }
+
+ /**
+ * Along with image there are dimmed layer, crop bounds and crop guidelines that must be drawn.
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ drawDimmedLayer(canvas);
+ drawCropGrid(canvas);
+ }
+
+ /**
+ * Could use
+ *
+ * canvas.save();
+ * canvas.clipRect(mCropViewRect, Region.Op.DIFFERENCE);
+ * canvas.drawColor(mOverlayColor);
+ * canvas.restore();
+ *
+ * but this won't properly work on number of devices with HW acceleration enabled.
+ * So lets just draw rectangles...
+ *
+ * @param canvas - current canvas
+ */
+ protected void drawDimmedLayer(@NonNull Canvas canvas) {
+ canvas.drawRect(0, mCropViewRect.top, mCropViewRect.left, mCropViewRect.bottom, mDimmedPaint);
+ canvas.drawRect(0, 0, canvas.getWidth(), mCropViewRect.top, mDimmedPaint);
+ canvas.drawRect(mCropViewRect.right, mCropViewRect.top, canvas.getWidth(), mCropViewRect.bottom, mDimmedPaint);
+ canvas.drawRect(0, mCropViewRect.bottom, canvas.getWidth(), canvas.getHeight(), mDimmedPaint);
+ }
+
+ /**
+ * This method draws crop bounds (empty rectangle)
+ * and crop guidelines (vertical and horizontal lines inside the crop bounds) if needed.
+ *
+ * @param canvas - valid canvas object
+ */
+ protected void drawCropGrid(@NonNull Canvas canvas) {
+ if (mShowCropGrid) {
+ if (mGridPoints == null && !mCropViewRect.isEmpty()) {
+
+ mGridPoints = new float[(mCropGridRowCount - 1) * 4 + (mCropGridColumnCount - 1) * 4];
+
+ int index = 0;
+ for (int i = 0; i < mCropGridRowCount - 1; i++) {
+ mGridPoints[index++] = mCropViewRect.left;
+ mGridPoints[index++] = (mCropViewRect.height() * (((float) i + 1.0f) / (float) mCropGridRowCount)) + mCropViewRect.top;
+ mGridPoints[index++] = mCropViewRect.right;
+ mGridPoints[index++] = (mCropViewRect.height() * (((float) i + 1.0f) / (float) mCropGridRowCount)) + mCropViewRect.top;
+ }
+
+ for (int i = 0; i < mCropGridColumnCount - 1; i++) {
+ mGridPoints[index++] = (mCropViewRect.width() * (((float) i + 1.0f) / (float) mCropGridColumnCount)) + mCropViewRect.left;
+ mGridPoints[index++] = mCropViewRect.top;
+ mGridPoints[index++] = (mCropViewRect.width() * (((float) i + 1.0f) / (float) mCropGridColumnCount)) + mCropViewRect.left;
+ mGridPoints[index++] = mCropViewRect.bottom;
+ }
+ }
+
+ if (mGridPoints != null) {
+ canvas.drawLines(mGridPoints, mGridInnerLinePaint);
+ }
+ }
+
+ if (mShowCropFrame) {
+ canvas.drawRect(mCropViewRect, mGridOuterLinePaint);
+ }
+ }
+
+ /**
+ * When image is laid out it must be centered properly to fit current crop bounds.
+ */
+ @Override
+ protected void onImageLaidOut() {
+ super.onImageLaidOut();
+ final Drawable drawable = getDrawable();
+ if (drawable == null) {
+ return;
+ }
+
+ float drawableWidth = drawable.getIntrinsicWidth();
+ float drawableHeight = drawable.getIntrinsicHeight();
+
+ if (mTargetAspectRatio == SOURCE_IMAGE_ASPECT_RATIO) {
+ mTargetAspectRatio = drawableWidth / drawableHeight;
+ }
+
+ setupCropBounds();
+ setupInitialImagePosition(drawableWidth, drawableHeight);
+ setImageMatrix(mCurrentImageMatrix);
+
+ if (mTransformImageListener != null) {
+ mTransformImageListener.onScale(getCurrentScale());
+ mTransformImageListener.onRotate(getCurrentAngle());
+ }
+ }
+
+ /**
+ * This method checks whether current image fills the crop bounds.
+ */
+ protected boolean isImageWrapCropBounds() {
+ return isImageWrapCropBounds(mCurrentImageCorners);
+ }
+
+ /**
+ * This methods checks whether a rectangle that is represented as 4 corner points (8 floats)
+ * fills the crop bounds rectangle.
+ *
+ * @param imageCorners - corners of a rectangle
+ * @return - true if it wraps crop bounds, false - otherwise
+ */
+ protected boolean isImageWrapCropBounds(float[] imageCorners) {
+ mTempMatrix.reset();
+ mTempMatrix.setRotate(-getCurrentAngle());
+
+ float[] unrotatedImageCorners = Arrays.copyOf(imageCorners, imageCorners.length);
+ mTempMatrix.mapPoints(unrotatedImageCorners);
+
+ float[] unrotatedCropBoundsCorners = RectUtils.getCornersFromRect(mCropRect);
+ mTempMatrix.mapPoints(unrotatedCropBoundsCorners);
+
+ return RectUtils.trapToRect(unrotatedImageCorners).contains(RectUtils.trapToRect(unrotatedCropBoundsCorners));
+ }
+
+ /**
+ * This method changes image scale (animating zoom for given duration), related to given center (x,y).
+ *
+ * @param scale - target scale
+ * @param centerX - scale center X
+ * @param centerY - scale center Y
+ * @param durationMs - zoom animation duration
+ */
+ protected void zoomImageToPosition(float scale, float centerX, float centerY, long durationMs) {
+ if (scale > getMaxScale()) {
+ scale = getMaxScale();
+ }
+
+ final float oldScale = getCurrentScale();
+ final float deltaScale = scale - oldScale;
+
+ post(mZoomImageToPositionRunnable = new ZoomImageToPosition(CropImageView.this,
+ durationMs, oldScale, deltaScale, centerX, centerY));
+ }
+
+ /**
+ * This method calculates initial image position so it fits the crop bounds properly.
+ * Then it sets those values to the current image matrix.
+ *
+ * @param drawableWidth - original image width
+ * @param drawableHeight - original image height
+ */
+ private void setupInitialImagePosition(float drawableWidth, float drawableHeight) {
+ float cropRectWidth = mCropRect.width();
+ float cropRectHeight = mCropRect.height();
+
+ float widthScale = cropRectWidth / drawableWidth;
+ float heightScale = cropRectHeight / drawableHeight;
+
+ mMinScale = Math.max(widthScale, heightScale);
+ mMaxScale = mMinScale * mMaxScaleMultiplier;
+
+ float tw = (cropRectWidth - drawableWidth * mMinScale) / 2.0f + mCropRect.left;
+ float th = (cropRectHeight - drawableHeight * mMinScale) / 2.0f + mCropRect.top;
+
+ mCurrentImageMatrix.reset();
+ mCurrentImageMatrix.postScale(mMinScale, mMinScale);
+ mCurrentImageMatrix.postTranslate(tw, th);
+ }
+
+ /**
+ * This method extracts all needed values from the styled attributes.
+ * Those are used to configure the view.
+ */
+ @SuppressWarnings("deprecation")
+ private void processStyledAttributes(@NonNull TypedArray a) {
+ float targetAspectRatioX = Math.abs(a.getFloat(R.styleable.ucrop_CropImageView_ucrop_aspect_ratio_x, DEFAULT_ASPECT_RATIO));
+ float targetAspectRatioY = Math.abs(a.getFloat(R.styleable.ucrop_CropImageView_ucrop_aspect_ratio_y, DEFAULT_ASPECT_RATIO));
+
+ if (targetAspectRatioX == SOURCE_IMAGE_ASPECT_RATIO || targetAspectRatioY == SOURCE_IMAGE_ASPECT_RATIO) {
+ mTargetAspectRatio = SOURCE_IMAGE_ASPECT_RATIO;
+ } else {
+ mTargetAspectRatio = targetAspectRatioX / targetAspectRatioY;
+ }
+
+ mMaxScaleMultiplier = a.getFloat(R.styleable.ucrop_CropImageView_ucrop_max_scale_multiplier, DEFAULT_MAX_SCALE_MULTIPLIER);
+
+ int overlayColor = a.getColor(R.styleable.ucrop_CropImageView_ucrop_overlay_color,
+ getResources().getColor(R.color.ucrop_color_default_overlay));
+ mDimmedPaint = new Paint();
+ mDimmedPaint.setColor(overlayColor);
+ mDimmedPaint.setStyle(Paint.Style.FILL);
+
+ initCropFrameStyle(a);
+ mShowCropFrame = a.getBoolean(R.styleable.ucrop_CropImageView_ucrop_show_frame, DEFAULT_SHOW_CROP_FRAME);
+
+ initCropGridStyle(a);
+ mShowCropGrid = a.getBoolean(R.styleable.ucrop_CropImageView_ucrop_show_grid, DEFAULT_SHOW_CROP_GRID);
+ }
+
+ /**
+ * This method setups Paint object for the crop bounds.
+ */
+ @SuppressWarnings("deprecation")
+ private void initCropFrameStyle(@NonNull TypedArray a) {
+ int cropFrameStrokeSize = a.getDimensionPixelSize(R.styleable.ucrop_CropImageView_ucrop_frame_stroke_size,
+ getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_frame_stoke_size));
+ int cropFrameColor = a.getColor(R.styleable.ucrop_CropImageView_ucrop_frame_color,
+ getResources().getColor(R.color.ucrop_color_default_crop_frame));
+ mGridOuterLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mGridOuterLinePaint.setStrokeWidth(cropFrameStrokeSize);
+ mGridOuterLinePaint.setColor(cropFrameColor);
+ mGridOuterLinePaint.setStyle(Paint.Style.STROKE);
+ }
+
+ /**
+ * This method setups Paint object for the crop guidelines.
+ */
+ @SuppressWarnings("deprecation")
+ private void initCropGridStyle(@NonNull TypedArray a) {
+ int cropGridStrokeSize = a.getDimensionPixelSize(R.styleable.ucrop_CropImageView_ucrop_grid_stroke_size,
+ getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_grid_stoke_size));
+ int cropGridColor = a.getColor(R.styleable.ucrop_CropImageView_ucrop_grid_color,
+ getResources().getColor(R.color.ucrop_color_default_crop_grid));
+ mGridInnerLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mGridInnerLinePaint.setStrokeWidth(cropGridStrokeSize);
+ mGridInnerLinePaint.setColor(cropGridColor);
+
+ mCropGridRowCount = a.getInt(R.styleable.ucrop_CropImageView_ucrop_grid_row_count, DEFAULT_CROP_GRID_ROW_COUNT);
+ mCropGridColumnCount = a.getInt(R.styleable.ucrop_CropImageView_ucrop_grid_column_count, DEFAULT_CROP_GRID_COLUMN_COUNT);
+ }
+
+ /**
+ * This method setups crop bounds rectangles for given aspect ratio and view size.
+ * {@link #mCropViewRect} is used to draw crop bounds - uses padding.
+ * {@link #mCropRect} is used for crop calculations - doesn't use padding.
+ */
+ private void setupCropBounds() {
+ int height = (int) (mThisWidth / mTargetAspectRatio);
+ if (height > mThisHeight) {
+ int width = (int) (mThisHeight * mTargetAspectRatio);
+ int halfDiff = (mThisWidth - width) / 2;
+ mCropRect.set(halfDiff, 0, width + halfDiff, mThisHeight);
+ mCropViewRect.set(getPaddingLeft() + halfDiff, getPaddingTop(),
+ getPaddingLeft() + width + halfDiff, getPaddingTop() + mThisHeight);
+ } else {
+ int halfDiff = (mThisHeight - height) / 2;
+ mCropRect.set(0, halfDiff, mThisWidth, height + halfDiff);
+ mCropViewRect.set(getPaddingLeft(), getPaddingTop() + halfDiff,
+ getPaddingLeft() + mThisWidth, getPaddingTop() + height + halfDiff);
+ }
+ }
+
+ /**
+ * This Runnable is used to animate an image so it fills the crop bounds entirely.
+ * Given values are interpolated during the animation time.
+ * Runnable can be terminated either vie {@link #cancelAllAnimations()} method
+ * or when certain conditions inside {@link WrapCropBoundsRunnable#run()} method are triggered.
+ */
+ private static class WrapCropBoundsRunnable implements Runnable {
+
+ private final WeakReference mCropImageView;
+
+ private final long mDurationMs, mStartTime;
+ private final float mOldX, mOldY;
+ private final float mCenterDiffX, mCenterDiffY;
+ private final float mOldScale;
+ private final float mDeltaScale;
+ private final boolean mWillBeImageInBoundsAfterTranslate;
+
+ public WrapCropBoundsRunnable(CropImageView cropImageView,
+ long durationMs,
+ float oldX, float oldY,
+ float centerDiffX, float centerDiffY,
+ float oldScale, float deltaScale,
+ boolean willBeImageInBoundsAfterTranslate) {
+
+ mCropImageView = new WeakReference<>(cropImageView);
+
+ mDurationMs = durationMs;
+ mStartTime = System.currentTimeMillis();
+ mOldX = oldX;
+ mOldY = oldY;
+ mCenterDiffX = centerDiffX;
+ mCenterDiffY = centerDiffY;
+ mOldScale = oldScale;
+ mDeltaScale = deltaScale;
+ mWillBeImageInBoundsAfterTranslate = willBeImageInBoundsAfterTranslate;
+ }
+
+ @Override
+ public void run() {
+ CropImageView cropImageView = mCropImageView.get();
+ if (cropImageView == null) {
+ return;
+ }
+
+ long now = System.currentTimeMillis();
+ float currentMs = Math.min(mDurationMs, now - mStartTime);
+
+ float newX = CubicEasing.easeOut(currentMs, 0, mCenterDiffX, mDurationMs);
+ float newY = CubicEasing.easeOut(currentMs, 0, mCenterDiffY, mDurationMs);
+ float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);
+
+ if (currentMs < mDurationMs) {
+ cropImageView.postTranslate(newX - (cropImageView.mCurrentImageCenter[0] - mOldX), newY - (cropImageView.mCurrentImageCenter[1] - mOldY));
+ if (!mWillBeImageInBoundsAfterTranslate) {
+ cropImageView.zoomInImage(mOldScale + newScale, cropImageView.mCropRect.centerX(), cropImageView.mCropRect.centerY());
+ }
+ if (!cropImageView.isImageWrapCropBounds()) {
+ cropImageView.post(this);
+ }
+ }
+ }
+ }
+
+ /**
+ * This Runnable is used to animate an image zoom.
+ * Given values are interpolated during the animation time.
+ * Runnable can be terminated either vie {@link #cancelAllAnimations()} method
+ * or when certain conditions inside {@link ZoomImageToPosition#run()} method are triggered.
+ */
+ private static class ZoomImageToPosition implements Runnable {
+
+ private final WeakReference mCropImageView;
+
+ private final long mDurationMs, mStartTime;
+ private final float mOldScale;
+ private final float mDeltaScale;
+ private final float mDestX;
+ private final float mDestY;
+
+ public ZoomImageToPosition(CropImageView cropImageView,
+ long durationMs,
+ float oldScale, float deltaScale,
+ float destX, float destY) {
+
+ mCropImageView = new WeakReference<>(cropImageView);
+
+ mStartTime = System.currentTimeMillis();
+ mDurationMs = durationMs;
+ mOldScale = oldScale;
+ mDeltaScale = deltaScale;
+ mDestX = destX;
+ mDestY = destY;
+ }
+
+ @Override
+ public void run() {
+ CropImageView cropImageView = mCropImageView.get();
+ if (cropImageView == null) {
+ return;
+ }
+
+ long now = System.currentTimeMillis();
+ float currentMs = Math.min(mDurationMs, now - mStartTime);
+ float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);
+
+ if (currentMs < mDurationMs) {
+ cropImageView.zoomInImage(mOldScale + newScale, mDestX, mDestY);
+ cropImageView.post(this);
+ } else {
+ cropImageView.setImageToWrapCropBounds();
+ }
+ }
+
+ }
+
+}
diff --git a/ucrop/src/main/java/com/yalantis/ucrop/view/GestureCropImageView.java b/ucrop/src/main/java/com/yalantis/ucrop/view/GestureCropImageView.java
new file mode 100644
index 00000000..39a77273
--- /dev/null
+++ b/ucrop/src/main/java/com/yalantis/ucrop/view/GestureCropImageView.java
@@ -0,0 +1,152 @@
+package com.yalantis.ucrop.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+import com.yalantis.ucrop.util.RotationGestureDetector;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ */
+public class GestureCropImageView extends CropImageView {
+
+ private static final int DOUBLE_TAP_ZOOM_DURATION = 200;
+
+ private ScaleGestureDetector mScaleDetector;
+ private RotationGestureDetector mRotateDetector;
+ private GestureDetector mGestureDetector;
+
+ private float mMidPntX, mMidPntY;
+
+ private boolean mIsRotateEnabled = true, mIsScaleEnabled = true;
+ private int mDoubleTapScaleSteps = 5;
+
+ public GestureCropImageView(Context context) {
+ super(context);
+ }
+
+ public GestureCropImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public GestureCropImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setScaleEnabled(boolean scaleEnabled) {
+ mIsScaleEnabled = scaleEnabled;
+ }
+
+ public boolean isScaleEnabled() {
+ return mIsScaleEnabled;
+ }
+
+ public void setRotateEnabled(boolean rotateEnabled) {
+ mIsRotateEnabled = rotateEnabled;
+ }
+
+ public boolean isRotateEnabled() {
+ return mIsRotateEnabled;
+ }
+
+ public void setDoubleTapScaleSteps(int doubleTapScaleSteps) {
+ mDoubleTapScaleSteps = doubleTapScaleSteps;
+ }
+
+ public int getDoubleTapScaleSteps() {
+ return mDoubleTapScaleSteps;
+ }
+
+ /**
+ * If it's ACTION_DOWN event - user touches the screen and all current animation must be canceled.
+ * If it's ACTION_UP event - user removed all fingers from the screen and current image position must be corrected.
+ * If there are more than 2 fingers - update focal point coordinates.
+ * Pass the event to the gesture detectors if those are enabled.
+ */
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
+ cancelAllAnimations();
+ }
+
+ if (event.getPointerCount() > 1) {
+ mMidPntX = (event.getX(0) + event.getX(1)) / 2;
+ mMidPntY = (event.getY(0) + event.getY(1)) / 2;
+ }
+
+ mGestureDetector.onTouchEvent(event);
+
+ if (mIsScaleEnabled) {
+ mScaleDetector.onTouchEvent(event);
+ }
+
+ if (mIsRotateEnabled) {
+ mRotateDetector.onTouchEvent(event);
+ }
+
+ if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
+ setImageToWrapCropBounds();
+ }
+ return true;
+ }
+
+ @Override
+ protected void init(Context context, AttributeSet attrs, int defStyle) {
+ super.init(context, attrs, defStyle);
+ setupGestureListeners();
+ }
+
+ /**
+ * This method calculates target scale value for double tap gesture.
+ * User is able to zoom the image from min scale value
+ * to the max scale value with {@link #mDoubleTapScaleSteps} double taps.
+ */
+ protected float getDoubleTapTargetScale() {
+ return getCurrentScale() * (float) Math.pow(getMaxScale() / getMinScale(), 1.0f / mDoubleTapScaleSteps);
+ }
+
+ private void setupGestureListeners() {
+ mGestureDetector = new GestureDetector(getContext(), new GestureListener(), null, true);
+ mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
+ mRotateDetector = new RotationGestureDetector(new RotateListener());
+ }
+
+ private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ postScale(detector.getScaleFactor(), mMidPntX, mMidPntY);
+ return true;
+ }
+ }
+
+ private class GestureListener extends GestureDetector.SimpleOnGestureListener {
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ zoomImageToPosition(getDoubleTapTargetScale(), e.getX(), e.getY(), DOUBLE_TAP_ZOOM_DURATION);
+ return super.onDoubleTap(e);
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ postTranslate(-distanceX, -distanceY);
+ return true;
+ }
+
+ }
+
+ private class RotateListener extends RotationGestureDetector.SimpleOnRotationGestureListener {
+
+ @Override
+ public boolean onRotation(RotationGestureDetector rotationDetector) {
+ postRotate(rotationDetector.getAngle(), mMidPntX, mMidPntY);
+ return true;
+ }
+
+ }
+
+}
diff --git a/ucrop/src/main/java/com/yalantis/ucrop/view/TransformImageView.java b/ucrop/src/main/java/com/yalantis/ucrop/view/TransformImageView.java
new file mode 100644
index 00000000..50787942
--- /dev/null
+++ b/ucrop/src/main/java/com/yalantis/ucrop/view/TransformImageView.java
@@ -0,0 +1,321 @@
+package com.yalantis.ucrop.view;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Display;
+import android.view.WindowManager;
+import android.widget.ImageView;
+
+import com.yalantis.ucrop.util.BitmapLoadUtils;
+import com.yalantis.ucrop.util.FastBitmapDrawable;
+import com.yalantis.ucrop.util.RectUtils;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ *
+ * This class provides base logic to setup the image, transform it with matrix (move, scale, rotate),
+ * and methods to get current matrix state.
+ */
+public class TransformImageView extends ImageView {
+
+ private static final String TAG = "TransformImageView";
+
+ private static final int RECT_CORNER_POINTS_COORDS = 8;
+ private static final int RECT_CENTER_POINT_COORDS = 2;
+ private static final int MATRIX_VALUES_COUNT = 9;
+
+ protected final float[] mCurrentImageCorners = new float[RECT_CORNER_POINTS_COORDS];
+ protected final float[] mCurrentImageCenter = new float[RECT_CENTER_POINT_COORDS];
+
+ private final float[] mMatrixValues = new float[MATRIX_VALUES_COUNT];
+
+ protected Matrix mCurrentImageMatrix = new Matrix();
+ protected int mThisWidth, mThisHeight;
+
+ protected TransformImageListener mTransformImageListener;
+
+ private float[] mInitialImageCorners;
+ private float[] mInitialImageCenter;
+
+ private int mMaxBitmapSize = 0;
+ private Uri mImageUri;
+
+ public TransformImageView(Context context) {
+ this(context, null);
+ }
+
+ public TransformImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TransformImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context, attrs, defStyle);
+ }
+
+ public void setTransformImageListener(TransformImageListener transformImageListener) {
+ mTransformImageListener = transformImageListener;
+ }
+
+ @Override
+ public void setScaleType(ScaleType scaleType) {
+ if (scaleType == ScaleType.MATRIX) {
+ super.setScaleType(scaleType);
+ } else {
+ Log.w(TAG, "Invalid ScaleType. Only ScaleType.MATRIX can be used");
+ }
+ }
+
+ /**
+ * Setter for {@link #mMaxBitmapSize} value.
+ * Be sure to call it before {@link #setImageURI(Uri)} or other image setters.
+ *
+ * @param maxBitmapSize - max size for both width and height of bitmap that will be used in the view.
+ */
+ public void setMaxBitmapSize(int maxBitmapSize) {
+ mMaxBitmapSize = maxBitmapSize;
+ }
+
+ public int getMaxBitmapSize() {
+ if (mMaxBitmapSize <= 0) {
+ mMaxBitmapSize = calculateMaxBitmapSize();
+ }
+ return mMaxBitmapSize;
+ }
+
+ @Override
+ public void setImageBitmap(final Bitmap bitmap) {
+ setImageDrawable(new FastBitmapDrawable(bitmap));
+ }
+
+ @Nullable
+ public Uri getImageUri() {
+ return mImageUri;
+ }
+
+ /**
+ * This method takes an Uri as a parameter, then calls method to decode it into Bitmap with specified size.
+ *
+ * @param imageUri - image Uri
+ * @throws Exception - can throw exception if having problems with decoding Uri or OOM.
+ */
+ public void setImageUri(@NonNull Uri imageUri) throws Exception {
+ mImageUri = imageUri;
+ int maxBitmapSize = getMaxBitmapSize();
+ setImageBitmap(BitmapLoadUtils.decode(getContext(), imageUri, maxBitmapSize, maxBitmapSize));
+ }
+
+ /**
+ * @return - current image scale value.
+ * [1.0f - for original image, 2.0f - for 200% scaled image, etc.]
+ */
+ public float getCurrentScale() {
+ return getMatrixScale(mCurrentImageMatrix);
+ }
+
+ /**
+ * This method calculates scale value for given Matrix object.
+ */
+ public float getMatrixScale(@NonNull Matrix matrix) {
+ return (float) Math.sqrt(Math.pow(getMatrixValue(matrix, Matrix.MSCALE_X), 2)
+ + Math.pow(getMatrixValue(matrix, Matrix.MSKEW_Y), 2));
+ }
+
+ /**
+ * @return - current image rotation angle.
+ */
+ public float getCurrentAngle() {
+ return getMatrixAngle(mCurrentImageMatrix);
+ }
+
+ /**
+ * This method calculates rotation angle for given Matrix object.
+ */
+ public float getMatrixAngle(@NonNull Matrix matrix) {
+ return (float) -(Math.atan2(getMatrixValue(matrix, Matrix.MSKEW_X),
+ getMatrixValue(matrix, Matrix.MSCALE_X)) * (180 / Math.PI));
+ }
+
+ @Override
+ public void setImageMatrix(Matrix matrix) {
+ super.setImageMatrix(matrix);
+ updateCurrentImagePoints();
+ }
+
+ @Nullable
+ public Bitmap getViewBitmap() {
+ if (getDrawable() == null || !(getDrawable() instanceof FastBitmapDrawable)) {
+ return null;
+ } else {
+ return ((FastBitmapDrawable) getDrawable()).getBitmap();
+ }
+ }
+
+ /**
+ * This method translates current image.
+ *
+ * @param deltaX - horizontal shift
+ * @param deltaY - vertical shift
+ */
+ public void postTranslate(float deltaX, float deltaY) {
+ if (deltaX != 0 || deltaY != 0) {
+ mCurrentImageMatrix.postTranslate(deltaX, deltaY);
+ setImageMatrix(mCurrentImageMatrix);
+ }
+ }
+
+ /**
+ * This method scales current image.
+ *
+ * @param deltaScale - scale value
+ * @param px - scale center X
+ * @param py - scale center Y
+ */
+ public void postScale(float deltaScale, float px, float py) {
+ if (deltaScale != 0) {
+ mCurrentImageMatrix.postScale(deltaScale, deltaScale, px, py);
+ setImageMatrix(mCurrentImageMatrix);
+ if (mTransformImageListener != null) {
+ mTransformImageListener.onScale(getMatrixScale(mCurrentImageMatrix));
+ }
+ }
+ }
+
+ /**
+ * This method rotates current image.
+ *
+ * @param deltaAngle - rotation angle
+ * @param px - rotation center X
+ * @param py - rotation center Y
+ */
+ public void postRotate(float deltaAngle, float px, float py) {
+ if (deltaAngle != 0) {
+ mCurrentImageMatrix.postRotate(deltaAngle, px, py);
+ setImageMatrix(mCurrentImageMatrix);
+ if (mTransformImageListener != null) {
+ mTransformImageListener.onRotate(getMatrixAngle(mCurrentImageMatrix));
+ }
+ }
+ }
+
+ protected void init(Context context, AttributeSet attrs, int defStyle) {
+ setScaleType(ScaleType.MATRIX);
+ }
+
+ /**
+ * This method calculates maximum size of both width and height of bitmap.
+ * It is the device screen diagonal for default implementation.
+ *
+ * @return - max bitmap size in pixels.
+ */
+ @SuppressWarnings({"SuspiciousNameCombination", "deprecation"})
+ protected int calculateMaxBitmapSize() {
+ WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+
+ Point size = new Point();
+ int width, height;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
+ display.getSize(size);
+ width = size.x;
+ height = size.y;
+ } else {
+ width = display.getWidth();
+ height = display.getHeight();
+ }
+ return (int) Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (changed) {
+ left = getPaddingLeft();
+ top = getPaddingTop();
+ right = getWidth() - getPaddingRight();
+ bottom = getHeight() - getPaddingBottom();
+ mThisWidth = right - left;
+ mThisHeight = bottom - top;
+
+ onImageLaidOut();
+ }
+ }
+
+ /**
+ * When image is laid out {@link #mInitialImageCenter} and {@link #mInitialImageCenter}
+ * must be set.
+ */
+ protected void onImageLaidOut() {
+ final Drawable drawable = getDrawable();
+ if (drawable == null) {
+ return;
+ }
+
+ float w = drawable.getIntrinsicWidth();
+ float h = drawable.getIntrinsicHeight();
+
+ Log.d(TAG, String.format("Image size: [%d:%d]", (int) w, (int) h));
+
+ RectF initialImageRect = new RectF(0, 0, w, h);
+ mInitialImageCorners = RectUtils.getCornersFromRect(initialImageRect);
+ mInitialImageCenter = RectUtils.getCenterFromRect(initialImageRect);
+ }
+
+ /**
+ * This method returns Matrix value for given index.
+ *
+ * @param matrix - valid Matrix object
+ * @param valueIndex - index of needed value. See {@link Matrix#MSCALE_X} and others.
+ * @return - matrix value for index
+ */
+ protected float getMatrixValue(@NonNull Matrix matrix, @IntRange(from = 0, to = MATRIX_VALUES_COUNT) int valueIndex) {
+ matrix.getValues(mMatrixValues);
+ return mMatrixValues[valueIndex];
+ }
+
+ /**
+ * This method logs given matrix X, Y, scale, and angle values.
+ * Can be used for debug.
+ */
+ @SuppressWarnings("unused")
+ protected void printMatrix(@NonNull String logPrefix, @NonNull Matrix matrix) {
+ float x = getMatrixValue(matrix, Matrix.MTRANS_X);
+ float y = getMatrixValue(matrix, Matrix.MTRANS_Y);
+ float rScale = getMatrixScale(matrix);
+ float rAngle = getMatrixAngle(matrix);
+ Log.d(TAG, logPrefix + ": matrix: { x: " + x + ", y: " + y + ", scale: " + rScale + ", angle: " + rAngle + " }");
+ }
+
+ /**
+ * This method updates current image corners and center points that are stored in
+ * {@link #mCurrentImageCorners} and {@link #mCurrentImageCenter} arrays.
+ * Those are used for several calculations.
+ */
+ private void updateCurrentImagePoints() {
+ mCurrentImageMatrix.mapPoints(mCurrentImageCorners, mInitialImageCorners);
+ mCurrentImageMatrix.mapPoints(mCurrentImageCenter, mInitialImageCenter);
+ }
+
+ /**
+ * Interface for rotation and scale change notifying.
+ */
+ public interface TransformImageListener {
+
+ void onRotate(float currentAngle);
+
+ void onScale(float currentScale);
+
+ }
+
+}
diff --git a/ucrop/src/main/java/com/yalantis/ucrop/view/widget/AspectRatioTextView.java b/ucrop/src/main/java/com/yalantis/ucrop/view/widget/AspectRatioTextView.java
new file mode 100644
index 00000000..3806e02e
--- /dev/null
+++ b/ucrop/src/main/java/com/yalantis/ucrop/view/widget/AspectRatioTextView.java
@@ -0,0 +1,112 @@
+package com.yalantis.ucrop.view.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.widget.TextView;
+
+import com.yalantis.ucrop.R;
+import com.yalantis.ucrop.view.CropImageView;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ */
+public class AspectRatioTextView extends TextView {
+
+ private final Rect mCanvasClipBounds = new Rect();
+ private Paint mDotPaint;
+ private int mDotSize;
+ private float mAspectRatio;
+
+ private String mAspectRatioTitle;
+ private float mAspectRatioX, mAspectRatioY;
+
+ public AspectRatioTextView(Context context) {
+ this(context, null);
+ }
+
+ public AspectRatioTextView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AspectRatioTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ucrop_AspectRatioTextView);
+ init(a);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public AspectRatioTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ucrop_AspectRatioTextView);
+ init(a);
+ }
+
+ public float getAspectRatio(boolean toggleRatio) {
+ if (toggleRatio) {
+ toggleAspectRatio();
+ setTitle();
+ }
+ return mAspectRatio;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (isSelected()) {
+ canvas.getClipBounds(mCanvasClipBounds);
+ canvas.drawCircle((mCanvasClipBounds.right - mCanvasClipBounds.left) / 2.0f, mCanvasClipBounds.bottom - mDotSize,
+ mDotSize / 2, mDotPaint);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void init(@NonNull TypedArray a) {
+ setGravity(Gravity.CENTER_HORIZONTAL);
+
+ mAspectRatioTitle = a.getString(R.styleable.ucrop_AspectRatioTextView_ucrop_artv_ratio_title);
+ mAspectRatioX = a.getFloat(R.styleable.ucrop_AspectRatioTextView_ucrop_artv_ratio_x, CropImageView.SOURCE_IMAGE_ASPECT_RATIO);
+ mAspectRatioY = a.getFloat(R.styleable.ucrop_AspectRatioTextView_ucrop_artv_ratio_y, CropImageView.SOURCE_IMAGE_ASPECT_RATIO);
+
+ if (mAspectRatioX == CropImageView.SOURCE_IMAGE_ASPECT_RATIO || mAspectRatioY == CropImageView.SOURCE_IMAGE_ASPECT_RATIO) {
+ mAspectRatio = CropImageView.SOURCE_IMAGE_ASPECT_RATIO;
+ } else {
+ mAspectRatio = mAspectRatioX / mAspectRatioY;
+ }
+
+ mDotSize = getContext().getResources().getDimensionPixelSize(R.dimen.ucrop_size_dot_scale_text_view);
+ mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mDotPaint.setStyle(Paint.Style.FILL);
+ mDotPaint.setColor(getResources().getColor(R.color.ucrop_color_widget_active));
+
+ setTitle();
+ }
+
+ private void toggleAspectRatio() {
+ if (mAspectRatio != CropImageView.SOURCE_IMAGE_ASPECT_RATIO) {
+ float tempRatioW = mAspectRatioX;
+ mAspectRatioX = mAspectRatioY;
+ mAspectRatioY = tempRatioW;
+
+ mAspectRatio = mAspectRatioX / mAspectRatioY;
+ }
+ }
+
+ private void setTitle() {
+ if (!TextUtils.isEmpty(mAspectRatioTitle)) {
+ setText(mAspectRatioTitle);
+ } else {
+ setText(String.format("%d:%d", (int) mAspectRatioX, (int) mAspectRatioY));
+ }
+ }
+
+}
diff --git a/ucrop/src/main/java/com/yalantis/ucrop/view/widget/HorizontalProgressWheelView.java b/ucrop/src/main/java/com/yalantis/ucrop/view/widget/HorizontalProgressWheelView.java
new file mode 100644
index 00000000..395977f6
--- /dev/null
+++ b/ucrop/src/main/java/com/yalantis/ucrop/view/widget/HorizontalProgressWheelView.java
@@ -0,0 +1,141 @@
+package com.yalantis.ucrop.view.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.yalantis.ucrop.R;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ */
+public class HorizontalProgressWheelView extends View {
+
+ private final Rect mCanvasClipBounds = new Rect();
+
+ private ScrollingListener mScrollingListener;
+ private float mLastTouchedPosition;
+
+ private Paint mProgressLinePaint;
+ private int mProgressLineWidth, mProgressLineHeight;
+ private int mProgressLineMargin;
+
+ private boolean mScrollStarted;
+ private float mTotalScrollDistance;
+
+ public HorizontalProgressWheelView(Context context) {
+ this(context, null);
+ }
+
+ public HorizontalProgressWheelView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public HorizontalProgressWheelView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public HorizontalProgressWheelView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public void setScrollingListener(ScrollingListener scrollingListener) {
+ mScrollingListener = scrollingListener;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mLastTouchedPosition = event.getX();
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mScrollingListener != null) {
+ mScrollStarted = false;
+ mScrollingListener.onScrollEnd();
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ float distance = event.getX() - mLastTouchedPosition;
+ if (distance != 0) {
+ if (!mScrollStarted) {
+ mScrollStarted = true;
+ if (mScrollingListener != null) {
+ mScrollingListener.onScrollStart();
+ }
+ }
+ onScrollEvent(event, distance);
+ }
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ canvas.getClipBounds(mCanvasClipBounds);
+
+ int linesCount = mCanvasClipBounds.width() / (mProgressLineWidth + mProgressLineMargin);
+ float deltaX = (mTotalScrollDistance) % (float) (mProgressLineMargin + mProgressLineWidth);
+
+ mProgressLinePaint.setColor(getResources().getColor(R.color.ucrop_color_progress_wheel_line));
+ for (int i = 0; i < linesCount; i++) {
+ if (i < (linesCount / 4)) {
+ mProgressLinePaint.setAlpha((int) (255 * (i / (float) (linesCount / 4))));
+ } else if (i > (linesCount * 3 / 4)) {
+ mProgressLinePaint.setAlpha((int) (255 * ((linesCount - i) / (float) (linesCount / 4))));
+ } else {
+ mProgressLinePaint.setAlpha(255);
+ }
+ canvas.drawLine(
+ -deltaX + mCanvasClipBounds.left + i * (mProgressLineWidth + mProgressLineMargin),
+ mCanvasClipBounds.centerY() - mProgressLineHeight / 4.0f,
+ -deltaX + mCanvasClipBounds.left + i * (mProgressLineWidth + mProgressLineMargin),
+ mCanvasClipBounds.centerY() + mProgressLineHeight / 4.0f, mProgressLinePaint);
+ }
+
+ mProgressLinePaint.setColor(getResources().getColor(R.color.ucrop_color_widget_active));
+ canvas.drawLine(mCanvasClipBounds.centerX(), mCanvasClipBounds.centerY() - mProgressLineHeight / 2.0f, mCanvasClipBounds.centerX(), mCanvasClipBounds.centerY() + mProgressLineHeight / 2.0f, mProgressLinePaint);
+
+ }
+
+ private void onScrollEvent(MotionEvent event, float distance) {
+ mTotalScrollDistance -= distance;
+ postInvalidate();
+ mLastTouchedPosition = event.getX();
+ if (mScrollingListener != null) {
+ mScrollingListener.onScroll(-distance, mTotalScrollDistance);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void init() {
+
+ mProgressLineWidth = getContext().getResources().getDimensionPixelSize(R.dimen.ucrop_width_horizontal_wheel_progress_line);
+ mProgressLineHeight = getContext().getResources().getDimensionPixelSize(R.dimen.ucrop_height_horizontal_wheel_progress_line);
+ mProgressLineMargin = getContext().getResources().getDimensionPixelSize(R.dimen.ucrop_margin_horizontal_wheel_progress_line);
+
+ mProgressLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mProgressLinePaint.setStyle(Paint.Style.STROKE);
+ mProgressLinePaint.setStrokeWidth(mProgressLineWidth);
+ }
+
+ public interface ScrollingListener {
+
+ void onScrollStart();
+
+ void onScroll(float delta, float totalDistance);
+
+ void onScrollEnd();
+ }
+
+}
diff --git a/ucrop/src/main/res/color/ucrop_scale_text_view_selector.xml b/ucrop/src/main/res/color/ucrop_scale_text_view_selector.xml
new file mode 100644
index 00000000..db0aa99b
--- /dev/null
+++ b/ucrop/src/main/res/color/ucrop_scale_text_view_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/ucrop/src/main/res/drawable-hdpi/ucrop_ic_angle.png b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_angle.png
new file mode 100644
index 00000000..218d5609
Binary files /dev/null and b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_angle.png differ
diff --git a/ucrop/src/main/res/drawable-hdpi/ucrop_ic_crop.png b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_crop.png
new file mode 100644
index 00000000..a12fdfc7
Binary files /dev/null and b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_crop.png differ
diff --git a/ucrop/src/main/res/drawable-hdpi/ucrop_ic_crop_active.png b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_crop_active.png
new file mode 100644
index 00000000..e57de508
Binary files /dev/null and b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_crop_active.png differ
diff --git a/ucrop/src/main/res/drawable-hdpi/ucrop_ic_cross.png b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_cross.png
new file mode 100644
index 00000000..c0e8de80
Binary files /dev/null and b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_cross.png differ
diff --git a/ucrop/src/main/res/drawable-hdpi/ucrop_ic_next.png b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_next.png
new file mode 100644
index 00000000..d1b1cddb
Binary files /dev/null and b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_next.png differ
diff --git a/ucrop/src/main/res/drawable-hdpi/ucrop_ic_reset.png b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_reset.png
new file mode 100644
index 00000000..2b6692bb
Binary files /dev/null and b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_reset.png differ
diff --git a/ucrop/src/main/res/drawable-hdpi/ucrop_ic_rotate.png b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_rotate.png
new file mode 100644
index 00000000..ba20cf35
Binary files /dev/null and b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_rotate.png differ
diff --git a/ucrop/src/main/res/drawable-hdpi/ucrop_ic_rotate_active.png b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_rotate_active.png
new file mode 100644
index 00000000..db84d4ce
Binary files /dev/null and b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_rotate_active.png differ
diff --git a/ucrop/src/main/res/drawable-hdpi/ucrop_ic_scale.png b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_scale.png
new file mode 100644
index 00000000..0be967cd
Binary files /dev/null and b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_scale.png differ
diff --git a/ucrop/src/main/res/drawable-hdpi/ucrop_ic_scale_active.png b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_scale_active.png
new file mode 100644
index 00000000..dbbda4ec
Binary files /dev/null and b/ucrop/src/main/res/drawable-hdpi/ucrop_ic_scale_active.png differ
diff --git a/ucrop/src/main/res/drawable-ldpi/ucrop_ic_angle.png b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_angle.png
new file mode 100644
index 00000000..41af9796
Binary files /dev/null and b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_angle.png differ
diff --git a/ucrop/src/main/res/drawable-ldpi/ucrop_ic_crop.png b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_crop.png
new file mode 100644
index 00000000..19daec37
Binary files /dev/null and b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_crop.png differ
diff --git a/ucrop/src/main/res/drawable-ldpi/ucrop_ic_crop_active.png b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_crop_active.png
new file mode 100644
index 00000000..33350ef6
Binary files /dev/null and b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_crop_active.png differ
diff --git a/ucrop/src/main/res/drawable-ldpi/ucrop_ic_cross.png b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_cross.png
new file mode 100644
index 00000000..69bb6ec8
Binary files /dev/null and b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_cross.png differ
diff --git a/ucrop/src/main/res/drawable-ldpi/ucrop_ic_next.png b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_next.png
new file mode 100644
index 00000000..732e9a78
Binary files /dev/null and b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_next.png differ
diff --git a/ucrop/src/main/res/drawable-ldpi/ucrop_ic_reset.png b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_reset.png
new file mode 100644
index 00000000..dfdb5bc9
Binary files /dev/null and b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_reset.png differ
diff --git a/ucrop/src/main/res/drawable-ldpi/ucrop_ic_rotate.png b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_rotate.png
new file mode 100644
index 00000000..d812eada
Binary files /dev/null and b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_rotate.png differ
diff --git a/ucrop/src/main/res/drawable-ldpi/ucrop_ic_rotate_active.png b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_rotate_active.png
new file mode 100644
index 00000000..a8dbdfbd
Binary files /dev/null and b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_rotate_active.png differ
diff --git a/ucrop/src/main/res/drawable-ldpi/ucrop_ic_scale.png b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_scale.png
new file mode 100644
index 00000000..2522ac7d
Binary files /dev/null and b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_scale.png differ
diff --git a/ucrop/src/main/res/drawable-ldpi/ucrop_ic_scale_active.png b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_scale_active.png
new file mode 100644
index 00000000..771608fb
Binary files /dev/null and b/ucrop/src/main/res/drawable-ldpi/ucrop_ic_scale_active.png differ
diff --git a/ucrop/src/main/res/drawable-mdpi/ucrop_ic_angle.png b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_angle.png
new file mode 100644
index 00000000..9945b062
Binary files /dev/null and b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_angle.png differ
diff --git a/ucrop/src/main/res/drawable-mdpi/ucrop_ic_crop.png b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_crop.png
new file mode 100644
index 00000000..36e4f362
Binary files /dev/null and b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_crop.png differ
diff --git a/ucrop/src/main/res/drawable-mdpi/ucrop_ic_crop_active.png b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_crop_active.png
new file mode 100644
index 00000000..91666946
Binary files /dev/null and b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_crop_active.png differ
diff --git a/ucrop/src/main/res/drawable-mdpi/ucrop_ic_cross.png b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_cross.png
new file mode 100644
index 00000000..a2da5b96
Binary files /dev/null and b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_cross.png differ
diff --git a/ucrop/src/main/res/drawable-mdpi/ucrop_ic_next.png b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_next.png
new file mode 100644
index 00000000..8f972bb0
Binary files /dev/null and b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_next.png differ
diff --git a/ucrop/src/main/res/drawable-mdpi/ucrop_ic_reset.png b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_reset.png
new file mode 100644
index 00000000..5431cd34
Binary files /dev/null and b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_reset.png differ
diff --git a/ucrop/src/main/res/drawable-mdpi/ucrop_ic_rotate.png b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_rotate.png
new file mode 100644
index 00000000..f7890066
Binary files /dev/null and b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_rotate.png differ
diff --git a/ucrop/src/main/res/drawable-mdpi/ucrop_ic_rotate_active.png b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_rotate_active.png
new file mode 100644
index 00000000..62688677
Binary files /dev/null and b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_rotate_active.png differ
diff --git a/ucrop/src/main/res/drawable-mdpi/ucrop_ic_scale.png b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_scale.png
new file mode 100644
index 00000000..ac363b77
Binary files /dev/null and b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_scale.png differ
diff --git a/ucrop/src/main/res/drawable-mdpi/ucrop_ic_scale_active.png b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_scale_active.png
new file mode 100644
index 00000000..f4d65b9b
Binary files /dev/null and b/ucrop/src/main/res/drawable-mdpi/ucrop_ic_scale_active.png differ
diff --git a/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_angle.png b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_angle.png
new file mode 100644
index 00000000..27d13187
Binary files /dev/null and b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_angle.png differ
diff --git a/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_crop.png b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_crop.png
new file mode 100644
index 00000000..f18a41c3
Binary files /dev/null and b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_crop.png differ
diff --git a/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_crop_active.png b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_crop_active.png
new file mode 100644
index 00000000..b9b61f43
Binary files /dev/null and b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_crop_active.png differ
diff --git a/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_cross.png b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_cross.png
new file mode 100644
index 00000000..443430be
Binary files /dev/null and b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_cross.png differ
diff --git a/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_next.png b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_next.png
new file mode 100644
index 00000000..6cd8e294
Binary files /dev/null and b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_next.png differ
diff --git a/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_reset.png b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_reset.png
new file mode 100644
index 00000000..15f8a7b2
Binary files /dev/null and b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_reset.png differ
diff --git a/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_rotate.png b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_rotate.png
new file mode 100644
index 00000000..d10f0c99
Binary files /dev/null and b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_rotate.png differ
diff --git a/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_rotate_active.png b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_rotate_active.png
new file mode 100644
index 00000000..ef0f4ea5
Binary files /dev/null and b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_rotate_active.png differ
diff --git a/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_scale.png b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_scale.png
new file mode 100644
index 00000000..cdb82257
Binary files /dev/null and b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_scale.png differ
diff --git a/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_scale_active.png b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_scale_active.png
new file mode 100644
index 00000000..1ff58613
Binary files /dev/null and b/ucrop/src/main/res/drawable-xhdpi/ucrop_ic_scale_active.png differ
diff --git a/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_angle.png b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_angle.png
new file mode 100644
index 00000000..30d941d3
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_angle.png differ
diff --git a/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_crop.png b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_crop.png
new file mode 100644
index 00000000..7decf1a3
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_crop.png differ
diff --git a/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_crop_active.png b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_crop_active.png
new file mode 100644
index 00000000..2b03286e
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_crop_active.png differ
diff --git a/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_cross.png b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_cross.png
new file mode 100644
index 00000000..169e0c37
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_cross.png differ
diff --git a/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_next.png b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_next.png
new file mode 100644
index 00000000..d6f68f37
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_next.png differ
diff --git a/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_reset.png b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_reset.png
new file mode 100644
index 00000000..52870ba0
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_reset.png differ
diff --git a/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_rotate.png b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_rotate.png
new file mode 100644
index 00000000..91b1c8ed
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_rotate.png differ
diff --git a/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_rotate_active.png b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_rotate_active.png
new file mode 100644
index 00000000..18c0eb37
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_rotate_active.png differ
diff --git a/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_scale.png b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_scale.png
new file mode 100644
index 00000000..158335cc
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_scale.png differ
diff --git a/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_scale_active.png b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_scale_active.png
new file mode 100644
index 00000000..40e6f223
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxhdpi/ucrop_ic_scale_active.png differ
diff --git a/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_angle.png b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_angle.png
new file mode 100644
index 00000000..68eb67e0
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_angle.png differ
diff --git a/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_crop.png b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_crop.png
new file mode 100644
index 00000000..8dfce883
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_crop.png differ
diff --git a/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_crop_active.png b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_crop_active.png
new file mode 100644
index 00000000..979b894c
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_crop_active.png differ
diff --git a/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_cross.png b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_cross.png
new file mode 100644
index 00000000..9e3226e1
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_cross.png differ
diff --git a/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_next.png b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_next.png
new file mode 100644
index 00000000..9f645340
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_next.png differ
diff --git a/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_reset.png b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_reset.png
new file mode 100644
index 00000000..75a92c0d
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_reset.png differ
diff --git a/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_rotate.png b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_rotate.png
new file mode 100644
index 00000000..7525f926
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_rotate.png differ
diff --git a/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_rotate_active.png b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_rotate_active.png
new file mode 100644
index 00000000..f781d499
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_rotate_active.png differ
diff --git a/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_scale.png b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_scale.png
new file mode 100644
index 00000000..21fc505b
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_scale.png differ
diff --git a/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_scale_active.png b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_scale_active.png
new file mode 100644
index 00000000..229b777c
Binary files /dev/null and b/ucrop/src/main/res/drawable-xxxhdpi/ucrop_ic_scale_active.png differ
diff --git a/ucrop/src/main/res/drawable/ucrop_ic_selector_aspect_ratio.xml b/ucrop/src/main/res/drawable/ucrop_ic_selector_aspect_ratio.xml
new file mode 100644
index 00000000..8aff570a
--- /dev/null
+++ b/ucrop/src/main/res/drawable/ucrop_ic_selector_aspect_ratio.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/ucrop/src/main/res/drawable/ucrop_ic_selector_rotate.xml b/ucrop/src/main/res/drawable/ucrop_ic_selector_rotate.xml
new file mode 100644
index 00000000..59f5b7e6
--- /dev/null
+++ b/ucrop/src/main/res/drawable/ucrop_ic_selector_rotate.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/ucrop/src/main/res/drawable/ucrop_ic_selector_scale.xml b/ucrop/src/main/res/drawable/ucrop_ic_selector_scale.xml
new file mode 100644
index 00000000..21051c82
--- /dev/null
+++ b/ucrop/src/main/res/drawable/ucrop_ic_selector_scale.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/ucrop/src/main/res/drawable/ucrop_shadow_upside.xml b/ucrop/src/main/res/drawable/ucrop_shadow_upside.xml
new file mode 100644
index 00000000..0be54272
--- /dev/null
+++ b/ucrop/src/main/res/drawable/ucrop_shadow_upside.xml
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/ucrop/src/main/res/layout/ucrop_activity_photobox.xml b/ucrop/src/main/res/layout/ucrop_activity_photobox.xml
new file mode 100644
index 00000000..b3be90a7
--- /dev/null
+++ b/ucrop/src/main/res/layout/ucrop_activity_photobox.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ucrop/src/main/res/layout/ucrop_layout_aspect_ratio.xml b/ucrop/src/main/res/layout/ucrop_layout_aspect_ratio.xml
new file mode 100644
index 00000000..a755cf30
--- /dev/null
+++ b/ucrop/src/main/res/layout/ucrop_layout_aspect_ratio.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ucrop/src/main/res/layout/ucrop_layout_rotate_wheel.xml b/ucrop/src/main/res/layout/ucrop_layout_rotate_wheel.xml
new file mode 100644
index 00000000..603269d2
--- /dev/null
+++ b/ucrop/src/main/res/layout/ucrop_layout_rotate_wheel.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ucrop/src/main/res/layout/ucrop_layout_scale_wheel.xml b/ucrop/src/main/res/layout/ucrop_layout_scale_wheel.xml
new file mode 100644
index 00000000..8c7bb104
--- /dev/null
+++ b/ucrop/src/main/res/layout/ucrop_layout_scale_wheel.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ucrop/src/main/res/menu/ucrop_menu_activity.xml b/ucrop/src/main/res/menu/ucrop_menu_activity.xml
new file mode 100644
index 00000000..d23121d9
--- /dev/null
+++ b/ucrop/src/main/res/menu/ucrop_menu_activity.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/ucrop/src/main/res/values/attrs.xml b/ucrop/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..12bbed71
--- /dev/null
+++ b/ucrop/src/main/res/values/attrs.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ucrop/src/main/res/values/colors.xml b/ucrop/src/main/res/values/colors.xml
new file mode 100644
index 00000000..6fd8eeb7
--- /dev/null
+++ b/ucrop/src/main/res/values/colors.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ #FF6E40
+ #CC5833
+ #fff
+ #FF6E40
+ #fff
+ #808080
+
+
+ #ffffff
+ #ffffff
+ #a0000000
+
+
\ No newline at end of file
diff --git a/ucrop/src/main/res/values/dimens.xml b/ucrop/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..d54e7189
--- /dev/null
+++ b/ucrop/src/main/res/values/dimens.xml
@@ -0,0 +1,12 @@
+
+
+ 1dp
+ 1dp
+
+ 8dp
+
+ 20dp
+ 2dp
+ 10dp
+
+
diff --git a/ucrop/src/main/res/values/public.xml b/ucrop/src/main/res/values/public.xml
new file mode 100644
index 00000000..b4ab285e
--- /dev/null
+++ b/ucrop/src/main/res/values/public.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/ucrop/src/main/res/values/strings.xml b/ucrop/src/main/res/values/strings.xml
new file mode 100644
index 00000000..8bf5d51a
--- /dev/null
+++ b/ucrop/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+
+
+ Original
+ Edit Photo
+
+ Next
+
+ Both input and output Uri must be specified
+
+
diff --git a/ucrop/src/main/res/values/styles.xml b/ucrop/src/main/res/values/styles.xml
new file mode 100644
index 00000000..cdacfd30
--- /dev/null
+++ b/ucrop/src/main/res/values/styles.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file