├── .gitignore ├── .publishing └── sonatype.gradle ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── build.gradle ├── example ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── soundcloud │ │ └── android │ │ └── crop │ │ └── example │ │ └── MainActivity.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ ├── ic_launcher.png │ └── tile.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── drawable-xxxhdpi │ └── ic_launcher.png │ ├── drawable │ └── texture.xml │ ├── layout │ └── activity_main.xml │ ├── menu │ └── activity_main.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── theme.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib ├── build.gradle └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── soundcloud │ │ └── android │ │ └── crop │ │ ├── BaseTestCase.java │ │ └── CropBuilderTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── soundcloud │ │ └── android │ │ └── crop │ │ ├── Crop.java │ │ ├── CropImageActivity.java │ │ ├── CropImageView.java │ │ ├── CropUtil.java │ │ ├── HighlightView.java │ │ ├── ImageViewTouchBase.java │ │ ├── Log.java │ │ ├── MonitoredActivity.java │ │ └── RotateBitmap.java │ └── res │ ├── drawable-hdpi │ ├── crop__divider.9.png │ ├── crop__ic_cancel.png │ └── crop__ic_done.png │ ├── drawable-mdpi │ ├── crop__divider.9.png │ ├── crop__ic_cancel.png │ └── crop__ic_done.png │ ├── drawable-v21 │ └── crop__selectable_background.xml │ ├── drawable-xhdpi │ ├── crop__divider.9.png │ ├── crop__ic_cancel.png │ ├── crop__ic_done.png │ └── crop__tile.png │ ├── drawable │ ├── crop__selectable_background.xml │ └── crop__texture.xml │ ├── layout │ ├── crop__activity_crop.xml │ └── crop__layout_done_cancel.xml │ ├── values-ar │ └── strings.xml │ ├── values-ca │ └── strings.xml │ ├── values-de │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-fa │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-in │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-ko │ └── strings.xml │ ├── values-land │ └── dimens.xml │ ├── values-large │ └── dimens.xml │ ├── values-pt │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-sv │ └── strings.xml │ ├── values-tr │ └── strings.xml │ ├── values-v21 │ └── colors.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── screenshot.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /local.properties 3 | /.idea 4 | build/ 5 | .gradle 6 | .DS_Store 7 | *.iml -------------------------------------------------------------------------------- /.publishing/sonatype.gradle: -------------------------------------------------------------------------------- 1 | configurations { 2 | archives { 3 | extendsFrom configurations.default 4 | } 5 | } 6 | 7 | signing { 8 | required { has("release") && gradle.taskGraph.hasTask("uploadArchives") } 9 | sign configurations.archives 10 | } 11 | 12 | uploadArchives { 13 | configuration = configurations.archives 14 | 15 | repositories { 16 | mavenDeployer { 17 | beforeDeployment { 18 | MavenDeployment deployment -> signing.signPom(deployment) 19 | } 20 | 21 | repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') { 22 | authentication(userName: sonatypeUsername, password: sonatypePassword) 23 | } 24 | 25 | pom.project { 26 | name 'Android Crop' 27 | packaging 'aar' 28 | description 'An Android library that provides an image cropping Activity' 29 | url 'https://github.com/jdamcd/android-crop' 30 | 31 | scm { 32 | url 'scm:git@github.com:jdamcd/android-crop.git' 33 | connection 'scm:git@github.com:jdamcd/android-crop.git' 34 | developerConnection 'scm:git@github.com:jdamcd/android-crop.git' 35 | } 36 | 37 | licenses { 38 | license { 39 | name 'The Apache Software License, Version 2.0' 40 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 41 | } 42 | } 43 | 44 | organization { 45 | name 'SoundCloud' 46 | url 'http://developers.soundcloud.com' 47 | } 48 | 49 | developers { 50 | developer { 51 | id 'jdamcd' 52 | name 'Jamie McDonald' 53 | email 'mcdonald@soundcloud.com' 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | sudo: false 3 | 4 | android: 5 | components: 6 | - build-tools-23.0.1 7 | - android-23 8 | - extra-android-support 9 | - extra-android-m2repository 10 | 11 | script: 12 | - ./gradlew clean build 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Next 2 | * Fix max size crash when input cannot be decoded 3 | * Translations: German, Chinese (simplified & traditional) 4 | 5 | ## 1.0.1 6 | 7 | * Support image picker helper from Fragments 8 | * Restore support for SDK level 10 9 | * Fix translucent status bar set via app theme 10 | * Fix wrong result code when crop results in IOException 11 | * Fix image "twitching" on zoom out to max bounds 12 | * Translations: Italian, Turkish, Catalan, Swedish 13 | 14 | ## 1.0.0 15 | 16 | * Improved builder interface: `Crop.of(in, out).start(activity)` 17 | * Material styling 18 | * Drop support for SDK level 9 19 | * Start crop from support Fragment 20 | * Fix max size 21 | * Fix issue cropping images from Google Drive 22 | * Optional circle crop guide 23 | * Optional custom request code 24 | * Translations: French, Korean, Chinese, Spanish, Japanese, Arabic, Portuguese, Indonesian, Russian 25 | 26 | ## 0.9.10 27 | 28 | * Fix bug on some devices where image was displayed with 0 size 29 | 30 | ## 0.9.9 31 | 32 | * Downscale source images that are too big to load 33 | * Optional always show crop handles 34 | * Fix shading outside crop area on some API levels 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > I guess people are just cropping out all the sadness 2 | 3 | An Android library project that provides a simple image cropping `Activity`, based on code from AOSP. 4 | 5 | **Deprecated!** This project is not maintained. If it doesn't meet your needs as is, consider creating a fork or picking from these [alternatives](https://android-arsenal.com/tag/45). 6 | 7 | [![maven central](https://img.shields.io/badge/maven%20central-1.0.1-brightgreen.svg)](http://search.maven.org/#artifactdetails%7Ccom.soundcloud.android%7Candroid-crop%7C1.0.1%7Caar.asc) 8 | [![changelog](https://img.shields.io/badge/changelog-1.0.1-lightgrey.svg)](CHANGELOG.md) 9 | 10 | ## Features 11 | 12 | * Gradle build & AAR 13 | * Modern UI 14 | * Backwards compatible to SDK 10 15 | * Simple builder for configuration 16 | * Example project 17 | 18 | ## Usage 19 | 20 | First, declare `CropImageActivity` in your manifest file: 21 | 22 | ```xml 23 | 24 | ``` 25 | 26 | #### Crop 27 | 28 | ```java 29 | Crop.of(inputUri, outputUri).asSquare().start(activity) 30 | ``` 31 | 32 | Listen for the result of the crop (see example project if you want to do some error handling): 33 | 34 | ```java 35 | @Override 36 | protected void onActivityResult(int requestCode, int resultCode, Intent result) { 37 | if (requestCode == Crop.REQUEST_CROP && resultCode == RESULT_OK) { 38 | doSomethingWithCroppedImage(outputUri); 39 | } 40 | } 41 | ``` 42 | 43 | Some attributes are provided to customise the crop screen. See the example project [theme](https://github.com/jdamcd/android-crop/blob/master/example/src/main/res/values/theme.xml). 44 | 45 | #### Pick 46 | 47 | The library provides a utility method to start an image picker: 48 | 49 | ```java 50 | Crop.pickImage(activity) 51 | ``` 52 | 53 | #### Dependency 54 | 55 | The AAR is published on Maven Central: 56 | 57 | ```groovy 58 | compile 'com.soundcloud.android:android-crop:1.0.1@aar' 59 | ``` 60 | 61 | ## How does it look? 62 | 63 | ![android-crop screenshot](screenshot.png) 64 | 65 | ## License 66 | 67 | This project is based on the [AOSP](https://source.android.com) camera image cropper via [android-cropimage](https://github.com/lvillani/android-cropimage). 68 | 69 | Copyright 2016 SoundCloud 70 | 71 | Licensed under the Apache License, Version 2.0 (the "License"); 72 | you may not use this file except in compliance with the License. 73 | You may obtain a copy of the License at 74 | 75 | http://www.apache.org/licenses/LICENSE-2.0 76 | 77 | Unless required by applicable law or agreed to in writing, software 78 | distributed under the License is distributed on an "AS IS" BASIS, 79 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 80 | See the License for the specific language governing permissions and 81 | limitations under the License. 82 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:2.1.0' 7 | } 8 | } 9 | 10 | allprojects { 11 | group = 'com.soundcloud.android' 12 | version = project.VERSION 13 | 14 | repositories { 15 | mavenCentral() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | archivesBaseName = 'android-crop-example' 4 | 5 | android { 6 | compileSdkVersion 23 7 | buildToolsVersion '23.0.1' 8 | 9 | defaultConfig { 10 | minSdkVersion 10 11 | targetSdkVersion 22 12 | versionCode Integer.parseInt(project.VERSION_CODE) 13 | versionName project.VERSION 14 | } 15 | } 16 | 17 | dependencies { 18 | compile project(':lib') 19 | } 20 | -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /example/src/main/java/com/soundcloud/android/crop/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.soundcloud.android.crop.example; 2 | 3 | import com.soundcloud.android.crop.Crop; 4 | 5 | import android.app.Activity; 6 | import android.content.Intent; 7 | import android.net.Uri; 8 | import android.os.Bundle; 9 | import android.view.Menu; 10 | import android.view.MenuItem; 11 | import android.widget.ImageView; 12 | import android.widget.Toast; 13 | 14 | import java.io.File; 15 | 16 | public class MainActivity extends Activity { 17 | 18 | private ImageView resultView; 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | setContentView(R.layout.activity_main); 24 | resultView = (ImageView) findViewById(R.id.result_image); 25 | } 26 | 27 | @Override 28 | public boolean onCreateOptionsMenu(Menu menu) { 29 | getMenuInflater().inflate(R.menu.activity_main, menu); 30 | return super.onCreateOptionsMenu(menu); 31 | } 32 | 33 | @Override 34 | public boolean onOptionsItemSelected(MenuItem item) { 35 | if (item.getItemId() == R.id.action_select) { 36 | resultView.setImageDrawable(null); 37 | Crop.pickImage(this); 38 | return true; 39 | } 40 | return super.onOptionsItemSelected(item); 41 | } 42 | 43 | @Override 44 | protected void onActivityResult(int requestCode, int resultCode, Intent result) { 45 | if (requestCode == Crop.REQUEST_PICK && resultCode == RESULT_OK) { 46 | beginCrop(result.getData()); 47 | } else if (requestCode == Crop.REQUEST_CROP) { 48 | handleCrop(resultCode, result); 49 | } 50 | } 51 | 52 | private void beginCrop(Uri source) { 53 | Uri destination = Uri.fromFile(new File(getCacheDir(), "cropped")); 54 | Crop.of(source, destination).asSquare().start(this); 55 | } 56 | 57 | private void handleCrop(int resultCode, Intent result) { 58 | if (resultCode == RESULT_OK) { 59 | resultView.setImageURI(Crop.getOutput(result)); 60 | } else if (resultCode == Crop.RESULT_ERROR) { 61 | Toast.makeText(this, Crop.getError(result).getMessage(), Toast.LENGTH_SHORT).show(); 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /example/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/example/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/example/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xhdpi/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/example/src/main/res/drawable-xhdpi/tile.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/example/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/example/src/main/res/drawable-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/drawable/texture.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /example/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/src/main/res/menu/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /example/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #f3f3f3 4 | 5 | -------------------------------------------------------------------------------- /example/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Crop Demo 4 | Pick 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/src/main/res/values/theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION=1.0.1 2 | VERSION_CODE=1 3 | 4 | signing.keyId=63A46540 5 | signing.secretKeyRingFile= 6 | signing.password= 7 | 8 | sonatypeUsername=jdamcd 9 | sonatypePassword= 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri May 27 11:24:52 EDT 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /lib/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'maven' 3 | apply plugin: 'signing' 4 | //apply from: '../.publishing/sonatype.gradle' 5 | 6 | archivesBaseName = 'android-crop' 7 | 8 | android { 9 | compileSdkVersion 23 10 | buildToolsVersion '23.0.1' 11 | 12 | defaultConfig { 13 | minSdkVersion 10 14 | targetSdkVersion 22 15 | 16 | testApplicationId 'com.soundcloud.android.crop.test' 17 | testInstrumentationRunner 'android.test.InstrumentationTestRunner' 18 | } 19 | } 20 | 21 | dependencies { 22 | compile 'com.android.support:support-annotations:23.0.1' 23 | compile 'com.android.support:support-v4:23.0.1' 24 | androidTestCompile 'com.squareup:fest-android:1.0.7' 25 | androidTestCompile 'com.android.support:support-v4:23.0.1' 26 | androidTestCompile 'org.mockito:mockito-core:1.9.5' 27 | androidTestCompile 'com.google.dexmaker:dexmaker:1.0' 28 | androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.0' 29 | } 30 | 31 | -------------------------------------------------------------------------------- /lib/src/androidTest/java/com/soundcloud/android/crop/BaseTestCase.java: -------------------------------------------------------------------------------- 1 | package com.soundcloud.android.crop; 2 | 3 | import android.test.InstrumentationTestCase; 4 | 5 | public class BaseTestCase extends InstrumentationTestCase { 6 | 7 | @Override 8 | public void setUp() throws Exception { 9 | super.setUp(); 10 | // Work around dexmaker issue when running tests on Android 4.3 11 | System.setProperty("dexmaker.dexcache", 12 | getInstrumentation().getTargetContext().getCacheDir().getPath()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/androidTest/java/com/soundcloud/android/crop/CropBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.soundcloud.android.crop; 2 | 3 | import static org.fest.assertions.api.Assertions.assertThat; 4 | import static org.mockito.Mockito.mock; 5 | import static org.mockito.Mockito.when; 6 | 7 | import org.fest.assertions.api.ANDROID; 8 | 9 | import android.app.Activity; 10 | import android.content.Intent; 11 | import android.net.Uri; 12 | import android.provider.MediaStore; 13 | 14 | public class CropBuilderTest extends BaseTestCase { 15 | 16 | private Activity activity; 17 | private Crop builder; 18 | 19 | @Override 20 | public void setUp() throws Exception { 21 | super.setUp(); 22 | activity = mock(Activity.class); 23 | when(activity.getPackageName()).thenReturn("com.example"); 24 | 25 | builder = Crop.of(Uri.parse("image:input"), Uri.parse("image:output")); 26 | } 27 | 28 | public void testInputUriSetAsData() { 29 | ANDROID.assertThat(builder.getIntent(activity)).hasData("image:input"); 30 | } 31 | 32 | public void testOutputUriSetAsExtra() { 33 | Intent intent = builder.getIntent(activity); 34 | Uri output = intent.getParcelableExtra(MediaStore.EXTRA_OUTPUT); 35 | 36 | assertThat(output.toString()).isEqualTo("image:output"); 37 | } 38 | 39 | public void testAspectRatioSetAsExtras() { 40 | builder.withAspect(16, 10); 41 | 42 | Intent intent = builder.getIntent(activity); 43 | 44 | assertThat(intent.getIntExtra("aspect_x", 0)).isEqualTo(16); 45 | assertThat(intent.getIntExtra("aspect_y", 0)).isEqualTo(10); 46 | } 47 | 48 | public void testFixedAspectRatioSetAsExtras() { 49 | builder.asSquare(); 50 | 51 | Intent intent = builder.getIntent(activity); 52 | 53 | assertThat(intent.getIntExtra("aspect_x", 0)).isEqualTo(1); 54 | assertThat(intent.getIntExtra("aspect_y", 0)).isEqualTo(1); 55 | } 56 | 57 | public void testMaxSizeSetAsExtras() { 58 | builder.withMaxSize(400, 300); 59 | 60 | Intent intent = builder.getIntent(activity); 61 | 62 | assertThat(intent.getIntExtra("max_x", 0)).isEqualTo(400); 63 | assertThat(intent.getIntExtra("max_y", 0)).isEqualTo(300); 64 | } 65 | 66 | public void testBuildsIntentWithMultipleOptions() { 67 | builder.asSquare().withMaxSize(200, 200); 68 | 69 | Intent intent = builder.getIntent(activity); 70 | 71 | assertThat(intent.getIntExtra("aspect_x", 0)).isEqualTo(1); 72 | assertThat(intent.getIntExtra("aspect_y", 0)).isEqualTo(1); 73 | assertThat(intent.getIntExtra("max_x", 0)).isEqualTo(200); 74 | assertThat(intent.getIntExtra("max_y", 0)).isEqualTo(200); 75 | } 76 | 77 | public void testAsPngSetAsExtras() { 78 | builder.asPng(true); 79 | 80 | Intent intent = builder.getIntent(activity); 81 | 82 | assertThat(intent.getBooleanExtra("as_png", false)).isEqualTo(true); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/src/main/java/com/soundcloud/android/crop/Crop.java: -------------------------------------------------------------------------------- 1 | package com.soundcloud.android.crop; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Activity; 5 | import android.app.Fragment; 6 | import android.content.ActivityNotFoundException; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.net.Uri; 10 | import android.os.Build; 11 | import android.provider.MediaStore; 12 | import android.widget.Toast; 13 | 14 | /** 15 | * Builder for crop Intents and utils for handling result 16 | */ 17 | public class Crop { 18 | 19 | public static final int REQUEST_CROP = 6709; 20 | public static final int REQUEST_PICK = 9162; 21 | public static final int RESULT_ERROR = 404; 22 | 23 | interface Extra { 24 | String ASPECT_X = "aspect_x"; 25 | String ASPECT_Y = "aspect_y"; 26 | String MAX_X = "max_x"; 27 | String MAX_Y = "max_y"; 28 | String AS_PNG = "as_png"; 29 | String ERROR = "error"; 30 | } 31 | 32 | private Intent cropIntent; 33 | 34 | /** 35 | * Create a crop Intent builder with source and destination image Uris 36 | * 37 | * @param source Uri for image to crop 38 | * @param destination Uri for saving the cropped image 39 | */ 40 | public static Crop of(Uri source, Uri destination) { 41 | return new Crop(source, destination); 42 | } 43 | 44 | private Crop(Uri source, Uri destination) { 45 | cropIntent = new Intent(); 46 | cropIntent.setData(source); 47 | cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, destination); 48 | } 49 | 50 | /** 51 | * Set fixed aspect ratio for crop area 52 | * 53 | * @param x Aspect X 54 | * @param y Aspect Y 55 | */ 56 | public Crop withAspect(int x, int y) { 57 | cropIntent.putExtra(Extra.ASPECT_X, x); 58 | cropIntent.putExtra(Extra.ASPECT_Y, y); 59 | return this; 60 | } 61 | 62 | /** 63 | * Crop area with fixed 1:1 aspect ratio 64 | */ 65 | public Crop asSquare() { 66 | cropIntent.putExtra(Extra.ASPECT_X, 1); 67 | cropIntent.putExtra(Extra.ASPECT_Y, 1); 68 | return this; 69 | } 70 | 71 | /** 72 | * Set maximum crop size 73 | * 74 | * @param width Max width 75 | * @param height Max height 76 | */ 77 | public Crop withMaxSize(int width, int height) { 78 | cropIntent.putExtra(Extra.MAX_X, width); 79 | cropIntent.putExtra(Extra.MAX_Y, height); 80 | return this; 81 | } 82 | 83 | /** 84 | * Set whether to save the result as a PNG or not. Helpful to preserve alpha. 85 | * @param asPng whether to save the result as a PNG or not 86 | */ 87 | public Crop asPng(boolean asPng) { 88 | cropIntent.putExtra(Extra.AS_PNG, asPng); 89 | return this; 90 | } 91 | 92 | /** 93 | * Send the crop Intent from an Activity 94 | * 95 | * @param activity Activity to receive result 96 | */ 97 | public void start(Activity activity) { 98 | start(activity, REQUEST_CROP); 99 | } 100 | 101 | /** 102 | * Send the crop Intent from an Activity with a custom request code 103 | * 104 | * @param activity Activity to receive result 105 | * @param requestCode requestCode for result 106 | */ 107 | public void start(Activity activity, int requestCode) { 108 | activity.startActivityForResult(getIntent(activity), requestCode); 109 | } 110 | 111 | /** 112 | * Send the crop Intent from a Fragment 113 | * 114 | * @param context Context 115 | * @param fragment Fragment to receive result 116 | */ 117 | public void start(Context context, Fragment fragment) { 118 | start(context, fragment, REQUEST_CROP); 119 | } 120 | 121 | /** 122 | * Send the crop Intent from a support library Fragment 123 | * 124 | * @param context Context 125 | * @param fragment Fragment to receive result 126 | */ 127 | public void start(Context context, android.support.v4.app.Fragment fragment) { 128 | start(context, fragment, REQUEST_CROP); 129 | } 130 | 131 | /** 132 | * Send the crop Intent with a custom request code 133 | * 134 | * @param context Context 135 | * @param fragment Fragment to receive result 136 | * @param requestCode requestCode for result 137 | */ 138 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 139 | public void start(Context context, Fragment fragment, int requestCode) { 140 | fragment.startActivityForResult(getIntent(context), requestCode); 141 | } 142 | 143 | /** 144 | * Send the crop Intent with a custom request code 145 | * 146 | * @param context Context 147 | * @param fragment Fragment to receive result 148 | * @param requestCode requestCode for result 149 | */ 150 | public void start(Context context, android.support.v4.app.Fragment fragment, int requestCode) { 151 | fragment.startActivityForResult(getIntent(context), requestCode); 152 | } 153 | 154 | /** 155 | * Get Intent to start crop Activity 156 | * 157 | * @param context Context 158 | * @return Intent for CropImageActivity 159 | */ 160 | public Intent getIntent(Context context) { 161 | cropIntent.setClass(context, CropImageActivity.class); 162 | return cropIntent; 163 | } 164 | 165 | /** 166 | * Retrieve URI for cropped image, as set in the Intent builder 167 | * 168 | * @param result Output Image URI 169 | */ 170 | public static Uri getOutput(Intent result) { 171 | return result.getParcelableExtra(MediaStore.EXTRA_OUTPUT); 172 | } 173 | 174 | /** 175 | * Retrieve error that caused crop to fail 176 | * 177 | * @param result Result Intent 178 | * @return Throwable handled in CropImageActivity 179 | */ 180 | public static Throwable getError(Intent result) { 181 | return (Throwable) result.getSerializableExtra(Extra.ERROR); 182 | } 183 | 184 | /** 185 | * Pick image from an Activity 186 | * 187 | * @param activity Activity to receive result 188 | */ 189 | public static void pickImage(Activity activity) { 190 | pickImage(activity, REQUEST_PICK); 191 | } 192 | 193 | /** 194 | * Pick image from a Fragment 195 | * 196 | * @param context Context 197 | * @param fragment Fragment to receive result 198 | */ 199 | public static void pickImage(Context context, Fragment fragment) { 200 | pickImage(context, fragment, REQUEST_PICK); 201 | } 202 | 203 | /** 204 | * Pick image from a support library Fragment 205 | * 206 | * @param context Context 207 | * @param fragment Fragment to receive result 208 | */ 209 | public static void pickImage(Context context, android.support.v4.app.Fragment fragment) { 210 | pickImage(context, fragment, REQUEST_PICK); 211 | } 212 | 213 | /** 214 | * Pick image from an Activity with a custom request code 215 | * 216 | * @param activity Activity to receive result 217 | * @param requestCode requestCode for result 218 | */ 219 | public static void pickImage(Activity activity, int requestCode) { 220 | try { 221 | activity.startActivityForResult(getImagePicker(), requestCode); 222 | } catch (ActivityNotFoundException e) { 223 | showImagePickerError(activity); 224 | } 225 | } 226 | 227 | /** 228 | * Pick image from a Fragment with a custom request code 229 | * 230 | * @param context Context 231 | * @param fragment Fragment to receive result 232 | * @param requestCode requestCode for result 233 | */ 234 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 235 | public static void pickImage(Context context, Fragment fragment, int requestCode) { 236 | try { 237 | fragment.startActivityForResult(getImagePicker(), requestCode); 238 | } catch (ActivityNotFoundException e) { 239 | showImagePickerError(context); 240 | } 241 | } 242 | 243 | /** 244 | * Pick image from a support library Fragment with a custom request code 245 | * 246 | * @param context Context 247 | * @param fragment Fragment to receive result 248 | * @param requestCode requestCode for result 249 | */ 250 | public static void pickImage(Context context, android.support.v4.app.Fragment fragment, int requestCode) { 251 | try { 252 | fragment.startActivityForResult(getImagePicker(), requestCode); 253 | } catch (ActivityNotFoundException e) { 254 | showImagePickerError(context); 255 | } 256 | } 257 | 258 | private static Intent getImagePicker() { 259 | return new Intent(Intent.ACTION_GET_CONTENT).setType("image/*"); 260 | } 261 | 262 | private static void showImagePickerError(Context context) { 263 | Toast.makeText(context.getApplicationContext(), R.string.crop__pick_error, Toast.LENGTH_SHORT).show(); 264 | } 265 | 266 | } 267 | -------------------------------------------------------------------------------- /lib/src/main/java/com/soundcloud/android/crop/CropImageActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2007 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.soundcloud.android.crop; 18 | 19 | import android.annotation.TargetApi; 20 | import android.content.Intent; 21 | import android.graphics.Bitmap; 22 | import android.graphics.BitmapFactory; 23 | import android.graphics.BitmapRegionDecoder; 24 | import android.graphics.Matrix; 25 | import android.graphics.Rect; 26 | import android.graphics.RectF; 27 | import android.net.Uri; 28 | import android.opengl.GLES10; 29 | import android.os.Build; 30 | import android.os.Bundle; 31 | import android.os.Handler; 32 | import android.provider.MediaStore; 33 | import android.view.View; 34 | import android.view.Window; 35 | import android.view.WindowManager; 36 | 37 | import java.io.IOException; 38 | import java.io.InputStream; 39 | import java.io.OutputStream; 40 | import java.util.concurrent.CountDownLatch; 41 | 42 | /* 43 | * Modified from original in AOSP. 44 | */ 45 | public class CropImageActivity extends MonitoredActivity { 46 | 47 | private static final int SIZE_DEFAULT = 2048; 48 | private static final int SIZE_LIMIT = 4096; 49 | 50 | private final Handler handler = new Handler(); 51 | 52 | private int aspectX; 53 | private int aspectY; 54 | 55 | // Output image 56 | private int maxX; 57 | private int maxY; 58 | private int exifRotation; 59 | private boolean saveAsPng; 60 | 61 | private Uri sourceUri; 62 | private Uri saveUri; 63 | 64 | private boolean isSaving; 65 | 66 | private int sampleSize; 67 | private RotateBitmap rotateBitmap; 68 | private CropImageView imageView; 69 | private HighlightView cropView; 70 | 71 | @Override 72 | public void onCreate(Bundle icicle) { 73 | super.onCreate(icicle); 74 | setupWindowFlags(); 75 | setupViews(); 76 | 77 | loadInput(); 78 | if (rotateBitmap == null) { 79 | finish(); 80 | return; 81 | } 82 | startCrop(); 83 | } 84 | 85 | @TargetApi(Build.VERSION_CODES.KITKAT) 86 | private void setupWindowFlags() { 87 | requestWindowFeature(Window.FEATURE_NO_TITLE); 88 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 89 | getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 90 | } 91 | } 92 | 93 | private void setupViews() { 94 | setContentView(R.layout.crop__activity_crop); 95 | 96 | imageView = (CropImageView) findViewById(R.id.crop_image); 97 | imageView.context = this; 98 | imageView.setRecycler(new ImageViewTouchBase.Recycler() { 99 | @Override 100 | public void recycle(Bitmap b) { 101 | b.recycle(); 102 | System.gc(); 103 | } 104 | }); 105 | 106 | findViewById(R.id.btn_cancel).setOnClickListener(new View.OnClickListener() { 107 | public void onClick(View v) { 108 | setResult(RESULT_CANCELED); 109 | finish(); 110 | } 111 | }); 112 | 113 | findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() { 114 | public void onClick(View v) { 115 | onSaveClicked(); 116 | } 117 | }); 118 | } 119 | 120 | private void loadInput() { 121 | Intent intent = getIntent(); 122 | Bundle extras = intent.getExtras(); 123 | 124 | if (extras != null) { 125 | aspectX = extras.getInt(Crop.Extra.ASPECT_X); 126 | aspectY = extras.getInt(Crop.Extra.ASPECT_Y); 127 | maxX = extras.getInt(Crop.Extra.MAX_X); 128 | maxY = extras.getInt(Crop.Extra.MAX_Y); 129 | saveAsPng = extras.getBoolean(Crop.Extra.AS_PNG, false); 130 | saveUri = extras.getParcelable(MediaStore.EXTRA_OUTPUT); 131 | } 132 | 133 | sourceUri = intent.getData(); 134 | if (sourceUri != null) { 135 | exifRotation = CropUtil.getExifRotation(CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri)); 136 | 137 | InputStream is = null; 138 | try { 139 | sampleSize = calculateBitmapSampleSize(sourceUri); 140 | is = getContentResolver().openInputStream(sourceUri); 141 | BitmapFactory.Options option = new BitmapFactory.Options(); 142 | option.inSampleSize = sampleSize; 143 | rotateBitmap = new RotateBitmap(BitmapFactory.decodeStream(is, null, option), exifRotation); 144 | } catch (IOException e) { 145 | Log.e("Error reading image: " + e.getMessage(), e); 146 | setResultException(e); 147 | } catch (OutOfMemoryError e) { 148 | Log.e("OOM reading image: " + e.getMessage(), e); 149 | setResultException(e); 150 | } finally { 151 | CropUtil.closeSilently(is); 152 | } 153 | } 154 | } 155 | 156 | private int calculateBitmapSampleSize(Uri bitmapUri) throws IOException { 157 | InputStream is = null; 158 | BitmapFactory.Options options = new BitmapFactory.Options(); 159 | options.inJustDecodeBounds = true; 160 | try { 161 | is = getContentResolver().openInputStream(bitmapUri); 162 | BitmapFactory.decodeStream(is, null, options); // Just get image size 163 | } finally { 164 | CropUtil.closeSilently(is); 165 | } 166 | 167 | int maxSize = getMaxImageSize(); 168 | int sampleSize = 1; 169 | while (options.outHeight / sampleSize > maxSize || options.outWidth / sampleSize > maxSize) { 170 | sampleSize = sampleSize << 1; 171 | } 172 | return sampleSize; 173 | } 174 | 175 | private int getMaxImageSize() { 176 | int textureLimit = getMaxTextureSize(); 177 | if (textureLimit == 0) { 178 | return SIZE_DEFAULT; 179 | } else { 180 | return Math.min(textureLimit, SIZE_LIMIT); 181 | } 182 | } 183 | 184 | private int getMaxTextureSize() { 185 | // The OpenGL texture size is the maximum size that can be drawn in an ImageView 186 | int[] maxSize = new int[1]; 187 | GLES10.glGetIntegerv(GLES10.GL_MAX_TEXTURE_SIZE, maxSize, 0); 188 | return maxSize[0]; 189 | } 190 | 191 | private void startCrop() { 192 | if (isFinishing()) { 193 | return; 194 | } 195 | imageView.setImageRotateBitmapResetBase(rotateBitmap, true); 196 | CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__wait), 197 | new Runnable() { 198 | public void run() { 199 | final CountDownLatch latch = new CountDownLatch(1); 200 | handler.post(new Runnable() { 201 | public void run() { 202 | if (imageView.getScale() == 1F) { 203 | imageView.center(); 204 | } 205 | latch.countDown(); 206 | } 207 | }); 208 | try { 209 | latch.await(); 210 | } catch (InterruptedException e) { 211 | throw new RuntimeException(e); 212 | } 213 | new Cropper().crop(); 214 | } 215 | }, handler 216 | ); 217 | } 218 | 219 | private class Cropper { 220 | 221 | private void makeDefault() { 222 | if (rotateBitmap == null) { 223 | return; 224 | } 225 | 226 | HighlightView hv = new HighlightView(imageView); 227 | final int width = rotateBitmap.getWidth(); 228 | final int height = rotateBitmap.getHeight(); 229 | 230 | Rect imageRect = new Rect(0, 0, width, height); 231 | 232 | // Make the default size about 4/5 of the width or height 233 | int cropWidth = Math.min(width, height) * 4 / 5; 234 | @SuppressWarnings("SuspiciousNameCombination") 235 | int cropHeight = cropWidth; 236 | 237 | if (aspectX != 0 && aspectY != 0) { 238 | if (aspectX > aspectY) { 239 | cropHeight = cropWidth * aspectY / aspectX; 240 | } else { 241 | cropWidth = cropHeight * aspectX / aspectY; 242 | } 243 | } 244 | 245 | int x = (width - cropWidth) / 2; 246 | int y = (height - cropHeight) / 2; 247 | 248 | RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight); 249 | hv.setup(imageView.getUnrotatedMatrix(), imageRect, cropRect, aspectX != 0 && aspectY != 0); 250 | imageView.add(hv); 251 | } 252 | 253 | public void crop() { 254 | handler.post(new Runnable() { 255 | public void run() { 256 | makeDefault(); 257 | imageView.invalidate(); 258 | if (imageView.highlightViews.size() == 1) { 259 | cropView = imageView.highlightViews.get(0); 260 | cropView.setFocus(true); 261 | } 262 | } 263 | }); 264 | } 265 | } 266 | 267 | private void onSaveClicked() { 268 | if (cropView == null || isSaving) { 269 | return; 270 | } 271 | isSaving = true; 272 | 273 | Bitmap croppedImage; 274 | Rect r = cropView.getScaledCropRect(sampleSize); 275 | int width = r.width(); 276 | int height = r.height(); 277 | 278 | int outWidth = width; 279 | int outHeight = height; 280 | if (maxX > 0 && maxY > 0 && (width > maxX || height > maxY)) { 281 | float ratio = (float) width / (float) height; 282 | if ((float) maxX / (float) maxY > ratio) { 283 | outHeight = maxY; 284 | outWidth = (int) ((float) maxY * ratio + .5f); 285 | } else { 286 | outWidth = maxX; 287 | outHeight = (int) ((float) maxX / ratio + .5f); 288 | } 289 | } 290 | 291 | try { 292 | croppedImage = decodeRegionCrop(r, outWidth, outHeight); 293 | } catch (IllegalArgumentException e) { 294 | setResultException(e); 295 | finish(); 296 | return; 297 | } 298 | 299 | if (croppedImage != null) { 300 | imageView.setImageRotateBitmapResetBase(new RotateBitmap(croppedImage, exifRotation), true); 301 | imageView.center(); 302 | imageView.highlightViews.clear(); 303 | } 304 | saveImage(croppedImage); 305 | } 306 | 307 | private void saveImage(Bitmap croppedImage) { 308 | if (croppedImage != null) { 309 | final Bitmap b = croppedImage; 310 | CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__saving), 311 | new Runnable() { 312 | public void run() { 313 | saveOutput(b); 314 | } 315 | }, handler 316 | ); 317 | } else { 318 | finish(); 319 | } 320 | } 321 | 322 | private Bitmap decodeRegionCrop(Rect rect, int outWidth, int outHeight) { 323 | // Release memory now 324 | clearImageView(); 325 | 326 | InputStream is = null; 327 | Bitmap croppedImage = null; 328 | try { 329 | is = getContentResolver().openInputStream(sourceUri); 330 | BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); 331 | final int width = decoder.getWidth(); 332 | final int height = decoder.getHeight(); 333 | 334 | if (exifRotation != 0) { 335 | // Adjust crop area to account for image rotation 336 | Matrix matrix = new Matrix(); 337 | matrix.setRotate(-exifRotation); 338 | 339 | RectF adjusted = new RectF(); 340 | matrix.mapRect(adjusted, new RectF(rect)); 341 | 342 | // Adjust to account for origin at 0,0 343 | adjusted.offset(adjusted.left < 0 ? width : 0, adjusted.top < 0 ? height : 0); 344 | rect = new Rect((int) adjusted.left, (int) adjusted.top, (int) adjusted.right, (int) adjusted.bottom); 345 | } 346 | 347 | try { 348 | croppedImage = decoder.decodeRegion(rect, new BitmapFactory.Options()); 349 | if (croppedImage != null && (rect.width() > outWidth || rect.height() > outHeight)) { 350 | Matrix matrix = new Matrix(); 351 | matrix.postScale((float) outWidth / rect.width(), (float) outHeight / rect.height()); 352 | croppedImage = Bitmap.createBitmap(croppedImage, 0, 0, croppedImage.getWidth(), croppedImage.getHeight(), matrix, true); 353 | } 354 | } catch (IllegalArgumentException e) { 355 | // Rethrow with some extra information 356 | throw new IllegalArgumentException("Rectangle " + rect + " is outside of the image (" 357 | + width + "," + height + "," + exifRotation + ")", e); 358 | } 359 | 360 | } catch (IOException e) { 361 | Log.e("Error cropping image: " + e.getMessage(), e); 362 | setResultException(e); 363 | } catch (OutOfMemoryError e) { 364 | Log.e("OOM cropping image: " + e.getMessage(), e); 365 | setResultException(e); 366 | } finally { 367 | CropUtil.closeSilently(is); 368 | } 369 | return croppedImage; 370 | } 371 | 372 | private void clearImageView() { 373 | imageView.clear(); 374 | if (rotateBitmap != null) { 375 | rotateBitmap.recycle(); 376 | } 377 | System.gc(); 378 | } 379 | 380 | private void saveOutput(Bitmap croppedImage) { 381 | if (saveUri != null) { 382 | OutputStream outputStream = null; 383 | try { 384 | outputStream = getContentResolver().openOutputStream(saveUri); 385 | if (outputStream != null) { 386 | croppedImage.compress(saveAsPng ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, 387 | 90, // note: quality is ignored when using PNG 388 | outputStream); 389 | } 390 | } catch (IOException e) { 391 | setResultException(e); 392 | Log.e("Cannot open file: " + saveUri, e); 393 | } finally { 394 | CropUtil.closeSilently(outputStream); 395 | } 396 | 397 | CropUtil.copyExifRotation( 398 | CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri), 399 | CropUtil.getFromMediaUri(this, getContentResolver(), saveUri) 400 | ); 401 | 402 | setResultUri(saveUri); 403 | } 404 | 405 | final Bitmap b = croppedImage; 406 | handler.post(new Runnable() { 407 | public void run() { 408 | imageView.clear(); 409 | b.recycle(); 410 | } 411 | }); 412 | 413 | finish(); 414 | } 415 | 416 | @Override 417 | protected void onDestroy() { 418 | super.onDestroy(); 419 | if (rotateBitmap != null) { 420 | rotateBitmap.recycle(); 421 | } 422 | } 423 | 424 | @Override 425 | public boolean onSearchRequested() { 426 | return false; 427 | } 428 | 429 | public boolean isSaving() { 430 | return isSaving; 431 | } 432 | 433 | private void setResultUri(Uri uri) { 434 | setResult(RESULT_OK, new Intent().putExtra(MediaStore.EXTRA_OUTPUT, uri)); 435 | } 436 | 437 | private void setResultException(Throwable throwable) { 438 | setResult(Crop.RESULT_ERROR, new Intent().putExtra(Crop.Extra.ERROR, throwable)); 439 | } 440 | 441 | } 442 | -------------------------------------------------------------------------------- /lib/src/main/java/com/soundcloud/android/crop/CropImageView.java: -------------------------------------------------------------------------------- 1 | package com.soundcloud.android.crop; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Rect; 6 | import android.support.annotation.NonNull; 7 | import android.util.AttributeSet; 8 | import android.view.MotionEvent; 9 | 10 | import java.util.ArrayList; 11 | 12 | public class CropImageView extends ImageViewTouchBase { 13 | 14 | ArrayList highlightViews = new ArrayList(); 15 | HighlightView motionHighlightView; 16 | Context context; 17 | 18 | private float lastX; 19 | private float lastY; 20 | private int motionEdge; 21 | private int validPointerId; 22 | 23 | public CropImageView(Context context) { 24 | super(context); 25 | } 26 | 27 | public CropImageView(Context context, AttributeSet attrs) { 28 | super(context, attrs); 29 | } 30 | 31 | public CropImageView(Context context, AttributeSet attrs, int defStyle) { 32 | super(context, attrs, defStyle); 33 | } 34 | 35 | @Override 36 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 37 | super.onLayout(changed, left, top, right, bottom); 38 | if (bitmapDisplayed.getBitmap() != null) { 39 | for (HighlightView hv : highlightViews) { 40 | 41 | hv.matrix.set(getUnrotatedMatrix()); 42 | hv.invalidate(); 43 | if (hv.hasFocus()) { 44 | centerBasedOnHighlightView(hv); 45 | } 46 | } 47 | } 48 | } 49 | 50 | @Override 51 | protected void zoomTo(float scale, float centerX, float centerY) { 52 | super.zoomTo(scale, centerX, centerY); 53 | for (HighlightView hv : highlightViews) { 54 | hv.matrix.set(getUnrotatedMatrix()); 55 | hv.invalidate(); 56 | } 57 | } 58 | 59 | @Override 60 | protected void zoomIn() { 61 | super.zoomIn(); 62 | for (HighlightView hv : highlightViews) { 63 | hv.matrix.set(getUnrotatedMatrix()); 64 | hv.invalidate(); 65 | } 66 | } 67 | 68 | @Override 69 | protected void zoomOut() { 70 | super.zoomOut(); 71 | for (HighlightView hv : highlightViews) { 72 | hv.matrix.set(getUnrotatedMatrix()); 73 | hv.invalidate(); 74 | } 75 | } 76 | 77 | @Override 78 | protected void postTranslate(float deltaX, float deltaY) { 79 | super.postTranslate(deltaX, deltaY); 80 | for (HighlightView hv : highlightViews) { 81 | hv.matrix.postTranslate(deltaX, deltaY); 82 | hv.invalidate(); 83 | } 84 | } 85 | 86 | @Override 87 | public boolean onTouchEvent(@NonNull MotionEvent event) { 88 | CropImageActivity cropImageActivity = (CropImageActivity) context; 89 | if (cropImageActivity.isSaving()) { 90 | return false; 91 | } 92 | 93 | switch (event.getAction()) { 94 | case MotionEvent.ACTION_DOWN: 95 | for (HighlightView hv : highlightViews) { 96 | int edge = hv.getHit(event.getX(), event.getY()); 97 | if (edge != HighlightView.GROW_NONE) { 98 | motionEdge = edge; 99 | motionHighlightView = hv; 100 | lastX = event.getX(); 101 | lastY = event.getY(); 102 | // Prevent multiple touches from interfering with crop area re-sizing 103 | validPointerId = event.getPointerId(event.getActionIndex()); 104 | motionHighlightView.setMode((edge == HighlightView.MOVE) 105 | ? HighlightView.ModifyMode.Move 106 | : HighlightView.ModifyMode.Grow); 107 | break; 108 | } 109 | } 110 | break; 111 | case MotionEvent.ACTION_UP: 112 | if (motionHighlightView != null) { 113 | centerBasedOnHighlightView(motionHighlightView); 114 | motionHighlightView.setMode(HighlightView.ModifyMode.None); 115 | } 116 | motionHighlightView = null; 117 | center(); 118 | break; 119 | case MotionEvent.ACTION_MOVE: 120 | if (motionHighlightView != null && event.getPointerId(event.getActionIndex()) == validPointerId) { 121 | motionHighlightView.handleMotion(motionEdge, event.getX() 122 | - lastX, event.getY() - lastY); 123 | lastX = event.getX(); 124 | lastY = event.getY(); 125 | } 126 | 127 | // If we're not zoomed then there's no point in even allowing the user to move the image around. 128 | // This call to center puts it back to the normalized location. 129 | if (getScale() == 1F) { 130 | center(); 131 | } 132 | break; 133 | } 134 | 135 | return true; 136 | } 137 | 138 | // Pan the displayed image to make sure the cropping rectangle is visible. 139 | private void ensureVisible(HighlightView hv) { 140 | Rect r = hv.drawRect; 141 | 142 | int panDeltaX1 = Math.max(0, getLeft() - r.left); 143 | int panDeltaX2 = Math.min(0, getRight() - r.right); 144 | 145 | int panDeltaY1 = Math.max(0, getTop() - r.top); 146 | int panDeltaY2 = Math.min(0, getBottom() - r.bottom); 147 | 148 | int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2; 149 | int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2; 150 | 151 | if (panDeltaX != 0 || panDeltaY != 0) { 152 | panBy(panDeltaX, panDeltaY); 153 | } 154 | } 155 | 156 | // If the cropping rectangle's size changed significantly, change the 157 | // view's center and scale according to the cropping rectangle. 158 | private void centerBasedOnHighlightView(HighlightView hv) { 159 | Rect drawRect = hv.drawRect; 160 | 161 | float width = drawRect.width(); 162 | float height = drawRect.height(); 163 | 164 | float thisWidth = getWidth(); 165 | float thisHeight = getHeight(); 166 | 167 | float z1 = thisWidth / width * .6F; 168 | float z2 = thisHeight / height * .6F; 169 | 170 | float zoom = Math.min(z1, z2); 171 | zoom = zoom * this.getScale(); 172 | zoom = Math.max(1F, zoom); 173 | 174 | if ((Math.abs(zoom - getScale()) / zoom) > .1) { 175 | float[] coordinates = new float[] { hv.cropRect.centerX(), hv.cropRect.centerY() }; 176 | getUnrotatedMatrix().mapPoints(coordinates); 177 | zoomTo(zoom, coordinates[0], coordinates[1], 300F); 178 | } 179 | 180 | ensureVisible(hv); 181 | } 182 | 183 | @Override 184 | protected void onDraw(@NonNull Canvas canvas) { 185 | super.onDraw(canvas); 186 | for (HighlightView highlightView : highlightViews) { 187 | highlightView.draw(canvas); 188 | } 189 | } 190 | 191 | public void add(HighlightView hv) { 192 | highlightViews.add(hv); 193 | invalidate(); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/src/main/java/com/soundcloud/android/crop/CropUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.soundcloud.android.crop; 18 | 19 | import android.app.ProgressDialog; 20 | import android.content.ContentResolver; 21 | import android.content.Context; 22 | import android.database.Cursor; 23 | import android.media.ExifInterface; 24 | import android.net.Uri; 25 | import android.os.Handler; 26 | import android.os.ParcelFileDescriptor; 27 | import android.provider.MediaStore; 28 | import android.support.annotation.Nullable; 29 | import android.text.TextUtils; 30 | 31 | import java.io.Closeable; 32 | import java.io.File; 33 | import java.io.FileDescriptor; 34 | import java.io.FileInputStream; 35 | import java.io.FileOutputStream; 36 | import java.io.IOException; 37 | 38 | /* 39 | * Modified from original in AOSP. 40 | */ 41 | class CropUtil { 42 | 43 | private static final String SCHEME_FILE = "file"; 44 | private static final String SCHEME_CONTENT = "content"; 45 | 46 | public static void closeSilently(@Nullable Closeable c) { 47 | if (c == null) return; 48 | try { 49 | c.close(); 50 | } catch (Throwable t) { 51 | // Do nothing 52 | } 53 | } 54 | 55 | public static int getExifRotation(File imageFile) { 56 | if (imageFile == null) return 0; 57 | try { 58 | ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath()); 59 | // We only recognize a subset of orientation tag values 60 | switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)) { 61 | case ExifInterface.ORIENTATION_ROTATE_90: 62 | return 90; 63 | case ExifInterface.ORIENTATION_ROTATE_180: 64 | return 180; 65 | case ExifInterface.ORIENTATION_ROTATE_270: 66 | return 270; 67 | default: 68 | return ExifInterface.ORIENTATION_UNDEFINED; 69 | } 70 | } catch (IOException e) { 71 | Log.e("Error getting Exif data", e); 72 | return 0; 73 | } 74 | } 75 | 76 | public static boolean copyExifRotation(File sourceFile, File destFile) { 77 | if (sourceFile == null || destFile == null) return false; 78 | try { 79 | ExifInterface exifSource = new ExifInterface(sourceFile.getAbsolutePath()); 80 | ExifInterface exifDest = new ExifInterface(destFile.getAbsolutePath()); 81 | exifDest.setAttribute(ExifInterface.TAG_ORIENTATION, exifSource.getAttribute(ExifInterface.TAG_ORIENTATION)); 82 | exifDest.saveAttributes(); 83 | return true; 84 | } catch (IOException e) { 85 | Log.e("Error copying Exif data", e); 86 | return false; 87 | } 88 | } 89 | 90 | @Nullable 91 | public static File getFromMediaUri(Context context, ContentResolver resolver, Uri uri) { 92 | if (uri == null) return null; 93 | 94 | if (SCHEME_FILE.equals(uri.getScheme())) { 95 | return new File(uri.getPath()); 96 | } else if (SCHEME_CONTENT.equals(uri.getScheme())) { 97 | final String[] filePathColumn = { MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME }; 98 | Cursor cursor = null; 99 | try { 100 | cursor = resolver.query(uri, filePathColumn, null, null, null); 101 | if (cursor != null && cursor.moveToFirst()) { 102 | final int columnIndex = (uri.toString().startsWith("content://com.google.android.gallery3d")) ? 103 | cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) : 104 | cursor.getColumnIndex(MediaStore.MediaColumns.DATA); 105 | // Picasa images on API 13+ 106 | if (columnIndex != -1) { 107 | String filePath = cursor.getString(columnIndex); 108 | if (!TextUtils.isEmpty(filePath)) { 109 | return new File(filePath); 110 | } 111 | } 112 | } 113 | } catch (IllegalArgumentException e) { 114 | // Google Drive images 115 | return getFromMediaUriPfd(context, resolver, uri); 116 | } catch (SecurityException ignored) { 117 | // Nothing we can do 118 | } finally { 119 | if (cursor != null) cursor.close(); 120 | } 121 | } 122 | return null; 123 | } 124 | 125 | private static String getTempFilename(Context context) throws IOException { 126 | File outputDir = context.getCacheDir(); 127 | File outputFile = File.createTempFile("image", "tmp", outputDir); 128 | return outputFile.getAbsolutePath(); 129 | } 130 | 131 | @Nullable 132 | private static File getFromMediaUriPfd(Context context, ContentResolver resolver, Uri uri) { 133 | if (uri == null) return null; 134 | 135 | FileInputStream input = null; 136 | FileOutputStream output = null; 137 | try { 138 | ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r"); 139 | FileDescriptor fd = pfd.getFileDescriptor(); 140 | input = new FileInputStream(fd); 141 | 142 | String tempFilename = getTempFilename(context); 143 | output = new FileOutputStream(tempFilename); 144 | 145 | int read; 146 | byte[] bytes = new byte[4096]; 147 | while ((read = input.read(bytes)) != -1) { 148 | output.write(bytes, 0, read); 149 | } 150 | return new File(tempFilename); 151 | } catch (IOException ignored) { 152 | // Nothing we can do 153 | } finally { 154 | closeSilently(input); 155 | closeSilently(output); 156 | } 157 | return null; 158 | } 159 | 160 | public static void startBackgroundJob(MonitoredActivity activity, 161 | String title, String message, Runnable job, Handler handler) { 162 | // Make the progress dialog uncancelable, so that we can guarantee 163 | // the thread will be done before the activity getting destroyed 164 | ProgressDialog dialog = ProgressDialog.show( 165 | activity, title, message, true, false); 166 | new Thread(new BackgroundJob(activity, job, dialog, handler)).start(); 167 | } 168 | 169 | private static class BackgroundJob extends MonitoredActivity.LifeCycleAdapter implements Runnable { 170 | 171 | private final MonitoredActivity activity; 172 | private final ProgressDialog dialog; 173 | private final Runnable job; 174 | private final Handler handler; 175 | private final Runnable cleanupRunner = new Runnable() { 176 | public void run() { 177 | activity.removeLifeCycleListener(BackgroundJob.this); 178 | if (dialog.getWindow() != null) dialog.dismiss(); 179 | } 180 | }; 181 | 182 | public BackgroundJob(MonitoredActivity activity, Runnable job, 183 | ProgressDialog dialog, Handler handler) { 184 | this.activity = activity; 185 | this.dialog = dialog; 186 | this.job = job; 187 | this.activity.addLifeCycleListener(this); 188 | this.handler = handler; 189 | } 190 | 191 | public void run() { 192 | try { 193 | job.run(); 194 | } finally { 195 | handler.post(cleanupRunner); 196 | } 197 | } 198 | 199 | @Override 200 | public void onActivityDestroyed(MonitoredActivity activity) { 201 | // We get here only when the onDestroyed being called before 202 | // the cleanupRunner. So, run it now and remove it from the queue 203 | cleanupRunner.run(); 204 | handler.removeCallbacks(cleanupRunner); 205 | } 206 | 207 | @Override 208 | public void onActivityStopped(MonitoredActivity activity) { 209 | dialog.hide(); 210 | } 211 | 212 | @Override 213 | public void onActivityStarted(MonitoredActivity activity) { 214 | dialog.show(); 215 | } 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /lib/src/main/java/com/soundcloud/android/crop/HighlightView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2007 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.soundcloud.android.crop; 18 | 19 | import android.annotation.SuppressLint; 20 | import android.content.Context; 21 | import android.content.res.TypedArray; 22 | import android.graphics.Canvas; 23 | import android.graphics.Color; 24 | import android.graphics.Matrix; 25 | import android.graphics.Paint; 26 | import android.graphics.Path; 27 | import android.graphics.Rect; 28 | import android.graphics.RectF; 29 | import android.graphics.Region; 30 | import android.os.Build; 31 | import android.util.TypedValue; 32 | import android.view.View; 33 | 34 | /* 35 | * Modified from version in AOSP. 36 | * 37 | * This class is used to display a highlighted cropping rectangle 38 | * overlayed on the image. There are two coordinate spaces in use. One is 39 | * image, another is screen. computeLayout() uses matrix to map from image 40 | * space to screen space. 41 | */ 42 | class HighlightView { 43 | 44 | public static final int GROW_NONE = (1 << 0); 45 | public static final int GROW_LEFT_EDGE = (1 << 1); 46 | public static final int GROW_RIGHT_EDGE = (1 << 2); 47 | public static final int GROW_TOP_EDGE = (1 << 3); 48 | public static final int GROW_BOTTOM_EDGE = (1 << 4); 49 | public static final int MOVE = (1 << 5); 50 | 51 | private static final int DEFAULT_HIGHLIGHT_COLOR = 0xFF33B5E5; 52 | private static final float HANDLE_RADIUS_DP = 12f; 53 | private static final float OUTLINE_DP = 2f; 54 | 55 | enum ModifyMode { None, Move, Grow } 56 | enum HandleMode { Changing, Always, Never } 57 | 58 | RectF cropRect; // Image space 59 | Rect drawRect; // Screen space 60 | Matrix matrix; 61 | private RectF imageRect; // Image space 62 | 63 | private final Paint outsidePaint = new Paint(); 64 | private final Paint outlinePaint = new Paint(); 65 | private final Paint handlePaint = new Paint(); 66 | 67 | private View viewContext; // View displaying image 68 | private boolean showThirds; 69 | private boolean showCircle; 70 | private int highlightColor; 71 | 72 | private ModifyMode modifyMode = ModifyMode.None; 73 | private HandleMode handleMode = HandleMode.Changing; 74 | private boolean maintainAspectRatio; 75 | private float initialAspectRatio; 76 | private float handleRadius; 77 | private float outlineWidth; 78 | private boolean isFocused; 79 | 80 | public HighlightView(View context) { 81 | viewContext = context; 82 | initStyles(context.getContext()); 83 | } 84 | 85 | private void initStyles(Context context) { 86 | TypedValue outValue = new TypedValue(); 87 | context.getTheme().resolveAttribute(R.attr.cropImageStyle, outValue, true); 88 | TypedArray attributes = context.obtainStyledAttributes(outValue.resourceId, R.styleable.CropImageView); 89 | try { 90 | showThirds = attributes.getBoolean(R.styleable.CropImageView_showThirds, false); 91 | showCircle = attributes.getBoolean(R.styleable.CropImageView_showCircle, false); 92 | highlightColor = attributes.getColor(R.styleable.CropImageView_highlightColor, 93 | DEFAULT_HIGHLIGHT_COLOR); 94 | handleMode = HandleMode.values()[attributes.getInt(R.styleable.CropImageView_showHandles, 0)]; 95 | } finally { 96 | attributes.recycle(); 97 | } 98 | } 99 | 100 | public void setup(Matrix m, Rect imageRect, RectF cropRect, boolean maintainAspectRatio) { 101 | matrix = new Matrix(m); 102 | 103 | this.cropRect = cropRect; 104 | this.imageRect = new RectF(imageRect); 105 | this.maintainAspectRatio = maintainAspectRatio; 106 | 107 | initialAspectRatio = this.cropRect.width() / this.cropRect.height(); 108 | drawRect = computeLayout(); 109 | 110 | outsidePaint.setARGB(125, 50, 50, 50); 111 | outlinePaint.setStyle(Paint.Style.STROKE); 112 | outlinePaint.setAntiAlias(true); 113 | outlineWidth = dpToPx(OUTLINE_DP); 114 | 115 | handlePaint.setColor(highlightColor); 116 | handlePaint.setStyle(Paint.Style.FILL); 117 | handlePaint.setAntiAlias(true); 118 | handleRadius = dpToPx(HANDLE_RADIUS_DP); 119 | 120 | modifyMode = ModifyMode.None; 121 | } 122 | 123 | private float dpToPx(float dp) { 124 | return dp * viewContext.getResources().getDisplayMetrics().density; 125 | } 126 | 127 | protected void draw(Canvas canvas) { 128 | canvas.save(); 129 | Path path = new Path(); 130 | outlinePaint.setStrokeWidth(outlineWidth); 131 | if (!hasFocus()) { 132 | outlinePaint.setColor(Color.BLACK); 133 | canvas.drawRect(drawRect, outlinePaint); 134 | } else { 135 | Rect viewDrawingRect = new Rect(); 136 | viewContext.getDrawingRect(viewDrawingRect); 137 | 138 | path.addRect(new RectF(drawRect), Path.Direction.CW); 139 | outlinePaint.setColor(highlightColor); 140 | 141 | if (isClipPathSupported(canvas)) { 142 | canvas.clipPath(path, Region.Op.DIFFERENCE); 143 | canvas.drawRect(viewDrawingRect, outsidePaint); 144 | } else { 145 | drawOutsideFallback(canvas); 146 | } 147 | 148 | canvas.restore(); 149 | canvas.drawPath(path, outlinePaint); 150 | 151 | if (showThirds) { 152 | drawThirds(canvas); 153 | } 154 | 155 | if (showCircle) { 156 | drawCircle(canvas); 157 | } 158 | 159 | if (handleMode == HandleMode.Always || 160 | (handleMode == HandleMode.Changing && modifyMode == ModifyMode.Grow)) { 161 | drawHandles(canvas); 162 | } 163 | } 164 | } 165 | 166 | /* 167 | * Fall back to naive method for darkening outside crop area 168 | */ 169 | private void drawOutsideFallback(Canvas canvas) { 170 | canvas.drawRect(0, 0, canvas.getWidth(), drawRect.top, outsidePaint); 171 | canvas.drawRect(0, drawRect.bottom, canvas.getWidth(), canvas.getHeight(), outsidePaint); 172 | canvas.drawRect(0, drawRect.top, drawRect.left, drawRect.bottom, outsidePaint); 173 | canvas.drawRect(drawRect.right, drawRect.top, canvas.getWidth(), drawRect.bottom, outsidePaint); 174 | } 175 | 176 | /* 177 | * Clip path is broken, unreliable or not supported on: 178 | * - JellyBean MR1 179 | * - ICS & ICS MR1 with hardware acceleration turned on 180 | */ 181 | @SuppressLint("NewApi") 182 | private boolean isClipPathSupported(Canvas canvas) { 183 | if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { 184 | return false; 185 | } else if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) 186 | || Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { 187 | return true; 188 | } else { 189 | return !canvas.isHardwareAccelerated(); 190 | } 191 | } 192 | 193 | private void drawHandles(Canvas canvas) { 194 | int xMiddle = drawRect.left + ((drawRect.right - drawRect.left) / 2); 195 | int yMiddle = drawRect.top + ((drawRect.bottom - drawRect.top) / 2); 196 | 197 | canvas.drawCircle(drawRect.left, yMiddle, handleRadius, handlePaint); 198 | canvas.drawCircle(xMiddle, drawRect.top, handleRadius, handlePaint); 199 | canvas.drawCircle(drawRect.right, yMiddle, handleRadius, handlePaint); 200 | canvas.drawCircle(xMiddle, drawRect.bottom, handleRadius, handlePaint); 201 | } 202 | 203 | private void drawThirds(Canvas canvas) { 204 | outlinePaint.setStrokeWidth(1); 205 | float xThird = (drawRect.right - drawRect.left) / 3; 206 | float yThird = (drawRect.bottom - drawRect.top) / 3; 207 | 208 | canvas.drawLine(drawRect.left + xThird, drawRect.top, 209 | drawRect.left + xThird, drawRect.bottom, outlinePaint); 210 | canvas.drawLine(drawRect.left + xThird * 2, drawRect.top, 211 | drawRect.left + xThird * 2, drawRect.bottom, outlinePaint); 212 | canvas.drawLine(drawRect.left, drawRect.top + yThird, 213 | drawRect.right, drawRect.top + yThird, outlinePaint); 214 | canvas.drawLine(drawRect.left, drawRect.top + yThird * 2, 215 | drawRect.right, drawRect.top + yThird * 2, outlinePaint); 216 | } 217 | 218 | private void drawCircle(Canvas canvas) { 219 | outlinePaint.setStrokeWidth(1); 220 | canvas.drawOval(new RectF(drawRect), outlinePaint); 221 | } 222 | 223 | public void setMode(ModifyMode mode) { 224 | if (mode != modifyMode) { 225 | modifyMode = mode; 226 | viewContext.invalidate(); 227 | } 228 | } 229 | 230 | // Determines which edges are hit by touching at (x, y) 231 | public int getHit(float x, float y) { 232 | Rect r = computeLayout(); 233 | final float hysteresis = 20F; 234 | int retval = GROW_NONE; 235 | 236 | // verticalCheck makes sure the position is between the top and 237 | // the bottom edge (with some tolerance). Similar for horizCheck. 238 | boolean verticalCheck = (y >= r.top - hysteresis) 239 | && (y < r.bottom + hysteresis); 240 | boolean horizCheck = (x >= r.left - hysteresis) 241 | && (x < r.right + hysteresis); 242 | 243 | // Check whether the position is near some edge(s) 244 | if ((Math.abs(r.left - x) < hysteresis) && verticalCheck) { 245 | retval |= GROW_LEFT_EDGE; 246 | } 247 | if ((Math.abs(r.right - x) < hysteresis) && verticalCheck) { 248 | retval |= GROW_RIGHT_EDGE; 249 | } 250 | if ((Math.abs(r.top - y) < hysteresis) && horizCheck) { 251 | retval |= GROW_TOP_EDGE; 252 | } 253 | if ((Math.abs(r.bottom - y) < hysteresis) && horizCheck) { 254 | retval |= GROW_BOTTOM_EDGE; 255 | } 256 | 257 | // Not near any edge but inside the rectangle: move 258 | if (retval == GROW_NONE && r.contains((int) x, (int) y)) { 259 | retval = MOVE; 260 | } 261 | return retval; 262 | } 263 | 264 | // Handles motion (dx, dy) in screen space. 265 | // The "edge" parameter specifies which edges the user is dragging. 266 | void handleMotion(int edge, float dx, float dy) { 267 | Rect r = computeLayout(); 268 | if (edge == MOVE) { 269 | // Convert to image space before sending to moveBy() 270 | moveBy(dx * (cropRect.width() / r.width()), 271 | dy * (cropRect.height() / r.height())); 272 | } else { 273 | if (((GROW_LEFT_EDGE | GROW_RIGHT_EDGE) & edge) == 0) { 274 | dx = 0; 275 | } 276 | 277 | if (((GROW_TOP_EDGE | GROW_BOTTOM_EDGE) & edge) == 0) { 278 | dy = 0; 279 | } 280 | 281 | // Convert to image space before sending to growBy() 282 | float xDelta = dx * (cropRect.width() / r.width()); 283 | float yDelta = dy * (cropRect.height() / r.height()); 284 | growBy((((edge & GROW_LEFT_EDGE) != 0) ? -1 : 1) * xDelta, 285 | (((edge & GROW_TOP_EDGE) != 0) ? -1 : 1) * yDelta); 286 | } 287 | } 288 | 289 | // Grows the cropping rectangle by (dx, dy) in image space 290 | void moveBy(float dx, float dy) { 291 | Rect invalRect = new Rect(drawRect); 292 | 293 | cropRect.offset(dx, dy); 294 | 295 | // Put the cropping rectangle inside image rectangle 296 | cropRect.offset( 297 | Math.max(0, imageRect.left - cropRect.left), 298 | Math.max(0, imageRect.top - cropRect.top)); 299 | 300 | cropRect.offset( 301 | Math.min(0, imageRect.right - cropRect.right), 302 | Math.min(0, imageRect.bottom - cropRect.bottom)); 303 | 304 | drawRect = computeLayout(); 305 | invalRect.union(drawRect); 306 | invalRect.inset(-(int) handleRadius, -(int) handleRadius); 307 | viewContext.invalidate(invalRect); 308 | } 309 | 310 | // Grows the cropping rectangle by (dx, dy) in image space. 311 | void growBy(float dx, float dy) { 312 | if (maintainAspectRatio) { 313 | if (dx != 0) { 314 | dy = dx / initialAspectRatio; 315 | } else if (dy != 0) { 316 | dx = dy * initialAspectRatio; 317 | } 318 | } 319 | 320 | // Don't let the cropping rectangle grow too fast. 321 | // Grow at most half of the difference between the image rectangle and 322 | // the cropping rectangle. 323 | RectF r = new RectF(cropRect); 324 | if (dx > 0F && r.width() + 2 * dx > imageRect.width()) { 325 | dx = (imageRect.width() - r.width()) / 2F; 326 | if (maintainAspectRatio) { 327 | dy = dx / initialAspectRatio; 328 | } 329 | } 330 | if (dy > 0F && r.height() + 2 * dy > imageRect.height()) { 331 | dy = (imageRect.height() - r.height()) / 2F; 332 | if (maintainAspectRatio) { 333 | dx = dy * initialAspectRatio; 334 | } 335 | } 336 | 337 | r.inset(-dx, -dy); 338 | 339 | // Don't let the cropping rectangle shrink too fast 340 | final float widthCap = 25F; 341 | if (r.width() < widthCap) { 342 | r.inset(-(widthCap - r.width()) / 2F, 0F); 343 | } 344 | float heightCap = maintainAspectRatio 345 | ? (widthCap / initialAspectRatio) 346 | : widthCap; 347 | if (r.height() < heightCap) { 348 | r.inset(0F, -(heightCap - r.height()) / 2F); 349 | } 350 | 351 | // Put the cropping rectangle inside the image rectangle 352 | if (r.left < imageRect.left) { 353 | r.offset(imageRect.left - r.left, 0F); 354 | } else if (r.right > imageRect.right) { 355 | r.offset(-(r.right - imageRect.right), 0F); 356 | } 357 | if (r.top < imageRect.top) { 358 | r.offset(0F, imageRect.top - r.top); 359 | } else if (r.bottom > imageRect.bottom) { 360 | r.offset(0F, -(r.bottom - imageRect.bottom)); 361 | } 362 | 363 | cropRect.set(r); 364 | drawRect = computeLayout(); 365 | viewContext.invalidate(); 366 | } 367 | 368 | // Returns the cropping rectangle in image space with specified scale 369 | public Rect getScaledCropRect(float scale) { 370 | return new Rect((int) (cropRect.left * scale), (int) (cropRect.top * scale), 371 | (int) (cropRect.right * scale), (int) (cropRect.bottom * scale)); 372 | } 373 | 374 | // Maps the cropping rectangle from image space to screen space 375 | private Rect computeLayout() { 376 | RectF r = new RectF(cropRect.left, cropRect.top, 377 | cropRect.right, cropRect.bottom); 378 | matrix.mapRect(r); 379 | return new Rect(Math.round(r.left), Math.round(r.top), 380 | Math.round(r.right), Math.round(r.bottom)); 381 | } 382 | 383 | public void invalidate() { 384 | drawRect = computeLayout(); 385 | } 386 | 387 | public boolean hasFocus() { 388 | return isFocused; 389 | } 390 | 391 | public void setFocus(boolean isFocused) { 392 | this.isFocused = isFocused; 393 | } 394 | 395 | } 396 | -------------------------------------------------------------------------------- /lib/src/main/java/com/soundcloud/android/crop/ImageViewTouchBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.soundcloud.android.crop; 18 | 19 | import android.content.Context; 20 | import android.graphics.Bitmap; 21 | import android.graphics.Matrix; 22 | import android.graphics.RectF; 23 | import android.graphics.drawable.Drawable; 24 | import android.os.Handler; 25 | import android.util.AttributeSet; 26 | import android.view.KeyEvent; 27 | import android.widget.ImageView; 28 | 29 | /* 30 | * Modified from original in AOSP. 31 | */ 32 | abstract class ImageViewTouchBase extends ImageView { 33 | 34 | private static final float SCALE_RATE = 1.25F; 35 | 36 | // This is the base transformation which is used to show the image 37 | // initially. The current computation for this shows the image in 38 | // it's entirety, letterboxing as needed. One could choose to 39 | // show the image as cropped instead. 40 | // 41 | // This matrix is recomputed when we go from the thumbnail image to 42 | // the full size image. 43 | protected Matrix baseMatrix = new Matrix(); 44 | 45 | // This is the supplementary transformation which reflects what 46 | // the user has done in terms of zooming and panning. 47 | // 48 | // This matrix remains the same when we go from the thumbnail image 49 | // to the full size image. 50 | protected Matrix suppMatrix = new Matrix(); 51 | 52 | // This is the final matrix which is computed as the concatentation 53 | // of the base matrix and the supplementary matrix. 54 | private final Matrix displayMatrix = new Matrix(); 55 | 56 | // Temporary buffer used for getting the values out of a matrix. 57 | private final float[] matrixValues = new float[9]; 58 | 59 | // The current bitmap being displayed. 60 | protected final RotateBitmap bitmapDisplayed = new RotateBitmap(null, 0); 61 | 62 | int thisWidth = -1; 63 | int thisHeight = -1; 64 | 65 | float maxZoom; 66 | 67 | private Runnable onLayoutRunnable; 68 | 69 | protected Handler handler = new Handler(); 70 | 71 | // ImageViewTouchBase will pass a Bitmap to the Recycler if it has finished 72 | // its use of that Bitmap 73 | public interface Recycler { 74 | public void recycle(Bitmap b); 75 | } 76 | 77 | private Recycler recycler; 78 | 79 | public ImageViewTouchBase(Context context) { 80 | super(context); 81 | init(); 82 | } 83 | 84 | public ImageViewTouchBase(Context context, AttributeSet attrs) { 85 | super(context, attrs); 86 | init(); 87 | } 88 | 89 | public ImageViewTouchBase(Context context, AttributeSet attrs, int defStyle) { 90 | super(context, attrs, defStyle); 91 | init(); 92 | } 93 | 94 | public void setRecycler(Recycler recycler) { 95 | this.recycler = recycler; 96 | } 97 | 98 | @Override 99 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 100 | super.onLayout(changed, left, top, right, bottom); 101 | thisWidth = right - left; 102 | thisHeight = bottom - top; 103 | Runnable r = onLayoutRunnable; 104 | if (r != null) { 105 | onLayoutRunnable = null; 106 | r.run(); 107 | } 108 | if (bitmapDisplayed.getBitmap() != null) { 109 | getProperBaseMatrix(bitmapDisplayed, baseMatrix, true); 110 | setImageMatrix(getImageViewMatrix()); 111 | } 112 | } 113 | 114 | @Override 115 | public boolean onKeyDown(int keyCode, KeyEvent event) { 116 | if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { 117 | event.startTracking(); 118 | return true; 119 | } 120 | return super.onKeyDown(keyCode, event); 121 | } 122 | 123 | @Override 124 | public boolean onKeyUp(int keyCode, KeyEvent event) { 125 | if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) { 126 | if (getScale() > 1.0f) { 127 | // If we're zoomed in, pressing Back jumps out to show the 128 | // entire image, otherwise Back returns the user to the gallery 129 | zoomTo(1.0f); 130 | return true; 131 | } 132 | } 133 | return super.onKeyUp(keyCode, event); 134 | } 135 | 136 | @Override 137 | public void setImageBitmap(Bitmap bitmap) { 138 | setImageBitmap(bitmap, 0); 139 | } 140 | 141 | private void setImageBitmap(Bitmap bitmap, int rotation) { 142 | super.setImageBitmap(bitmap); 143 | Drawable d = getDrawable(); 144 | if (d != null) { 145 | d.setDither(true); 146 | } 147 | 148 | Bitmap old = bitmapDisplayed.getBitmap(); 149 | bitmapDisplayed.setBitmap(bitmap); 150 | bitmapDisplayed.setRotation(rotation); 151 | 152 | if (old != null && old != bitmap && recycler != null) { 153 | recycler.recycle(old); 154 | } 155 | } 156 | 157 | public void clear() { 158 | setImageBitmapResetBase(null, true); 159 | } 160 | 161 | 162 | // This function changes bitmap, reset base matrix according to the size 163 | // of the bitmap, and optionally reset the supplementary matrix 164 | public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp) { 165 | setImageRotateBitmapResetBase(new RotateBitmap(bitmap, 0), resetSupp); 166 | } 167 | 168 | public void setImageRotateBitmapResetBase(final RotateBitmap bitmap, final boolean resetSupp) { 169 | final int viewWidth = getWidth(); 170 | 171 | if (viewWidth <= 0) { 172 | onLayoutRunnable = new Runnable() { 173 | public void run() { 174 | setImageRotateBitmapResetBase(bitmap, resetSupp); 175 | } 176 | }; 177 | return; 178 | } 179 | 180 | if (bitmap.getBitmap() != null) { 181 | getProperBaseMatrix(bitmap, baseMatrix, true); 182 | setImageBitmap(bitmap.getBitmap(), bitmap.getRotation()); 183 | } else { 184 | baseMatrix.reset(); 185 | setImageBitmap(null); 186 | } 187 | 188 | if (resetSupp) { 189 | suppMatrix.reset(); 190 | } 191 | setImageMatrix(getImageViewMatrix()); 192 | maxZoom = calculateMaxZoom(); 193 | } 194 | 195 | // Center as much as possible in one or both axis. Centering is defined as follows: 196 | // * If the image is scaled down below the view's dimensions then center it. 197 | // * If the image is scaled larger than the view and is translated out of view then translate it back into view. 198 | protected void center() { 199 | final Bitmap bitmap = bitmapDisplayed.getBitmap(); 200 | if (bitmap == null) { 201 | return; 202 | } 203 | Matrix m = getImageViewMatrix(); 204 | 205 | RectF rect = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()); 206 | m.mapRect(rect); 207 | 208 | float height = rect.height(); 209 | float width = rect.width(); 210 | 211 | float deltaX = 0, deltaY = 0; 212 | 213 | deltaY = centerVertical(rect, height, deltaY); 214 | deltaX = centerHorizontal(rect, width, deltaX); 215 | 216 | postTranslate(deltaX, deltaY); 217 | setImageMatrix(getImageViewMatrix()); 218 | } 219 | 220 | private float centerVertical(RectF rect, float height, float deltaY) { 221 | int viewHeight = getHeight(); 222 | if (height < viewHeight) { 223 | deltaY = (viewHeight - height) / 2 - rect.top; 224 | } else if (rect.top > 0) { 225 | deltaY = -rect.top; 226 | } else if (rect.bottom < viewHeight) { 227 | deltaY = getHeight() - rect.bottom; 228 | } 229 | return deltaY; 230 | } 231 | 232 | private float centerHorizontal(RectF rect, float width, float deltaX) { 233 | int viewWidth = getWidth(); 234 | if (width < viewWidth) { 235 | deltaX = (viewWidth - width) / 2 - rect.left; 236 | } else if (rect.left > 0) { 237 | deltaX = -rect.left; 238 | } else if (rect.right < viewWidth) { 239 | deltaX = viewWidth - rect.right; 240 | } 241 | return deltaX; 242 | } 243 | 244 | private void init() { 245 | setScaleType(ImageView.ScaleType.MATRIX); 246 | } 247 | 248 | protected float getValue(Matrix matrix, int whichValue) { 249 | matrix.getValues(matrixValues); 250 | return matrixValues[whichValue]; 251 | } 252 | 253 | // Get the scale factor out of the matrix. 254 | protected float getScale(Matrix matrix) { 255 | return getValue(matrix, Matrix.MSCALE_X); 256 | } 257 | 258 | protected float getScale() { 259 | return getScale(suppMatrix); 260 | } 261 | 262 | // Setup the base matrix so that the image is centered and scaled properly. 263 | private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix, boolean includeRotation) { 264 | float viewWidth = getWidth(); 265 | float viewHeight = getHeight(); 266 | 267 | float w = bitmap.getWidth(); 268 | float h = bitmap.getHeight(); 269 | matrix.reset(); 270 | 271 | // We limit up-scaling to 3x otherwise the result may look bad if it's a small icon 272 | float widthScale = Math.min(viewWidth / w, 3.0f); 273 | float heightScale = Math.min(viewHeight / h, 3.0f); 274 | float scale = Math.min(widthScale, heightScale); 275 | 276 | if (includeRotation) { 277 | matrix.postConcat(bitmap.getRotateMatrix()); 278 | } 279 | matrix.postScale(scale, scale); 280 | matrix.postTranslate((viewWidth - w * scale) / 2F, (viewHeight - h * scale) / 2F); 281 | } 282 | 283 | // Combine the base matrix and the supp matrix to make the final matrix 284 | protected Matrix getImageViewMatrix() { 285 | // The final matrix is computed as the concatentation of the base matrix 286 | // and the supplementary matrix 287 | displayMatrix.set(baseMatrix); 288 | displayMatrix.postConcat(suppMatrix); 289 | return displayMatrix; 290 | } 291 | 292 | public Matrix getUnrotatedMatrix(){ 293 | Matrix unrotated = new Matrix(); 294 | getProperBaseMatrix(bitmapDisplayed, unrotated, false); 295 | unrotated.postConcat(suppMatrix); 296 | return unrotated; 297 | } 298 | 299 | protected float calculateMaxZoom() { 300 | if (bitmapDisplayed.getBitmap() == null) { 301 | return 1F; 302 | } 303 | 304 | float fw = (float) bitmapDisplayed.getWidth() / (float) thisWidth; 305 | float fh = (float) bitmapDisplayed.getHeight() / (float) thisHeight; 306 | return Math.max(fw, fh) * 4; // 400% 307 | } 308 | 309 | protected void zoomTo(float scale, float centerX, float centerY) { 310 | if (scale > maxZoom) { 311 | scale = maxZoom; 312 | } 313 | 314 | float oldScale = getScale(); 315 | float deltaScale = scale / oldScale; 316 | 317 | suppMatrix.postScale(deltaScale, deltaScale, centerX, centerY); 318 | setImageMatrix(getImageViewMatrix()); 319 | center(); 320 | } 321 | 322 | protected void zoomTo(final float scale, final float centerX, 323 | final float centerY, final float durationMs) { 324 | final float incrementPerMs = (scale - getScale()) / durationMs; 325 | final float oldScale = getScale(); 326 | final long startTime = System.currentTimeMillis(); 327 | 328 | handler.post(new Runnable() { 329 | public void run() { 330 | long now = System.currentTimeMillis(); 331 | float currentMs = Math.min(durationMs, now - startTime); 332 | float target = oldScale + (incrementPerMs * currentMs); 333 | zoomTo(target, centerX, centerY); 334 | 335 | if (currentMs < durationMs) { 336 | handler.post(this); 337 | } 338 | } 339 | }); 340 | } 341 | 342 | protected void zoomTo(float scale) { 343 | float cx = getWidth() / 2F; 344 | float cy = getHeight() / 2F; 345 | zoomTo(scale, cx, cy); 346 | } 347 | 348 | protected void zoomIn() { 349 | zoomIn(SCALE_RATE); 350 | } 351 | 352 | protected void zoomOut() { 353 | zoomOut(SCALE_RATE); 354 | } 355 | 356 | protected void zoomIn(float rate) { 357 | if (getScale() >= maxZoom) { 358 | return; // Don't let the user zoom into the molecular level 359 | } 360 | if (bitmapDisplayed.getBitmap() == null) { 361 | return; 362 | } 363 | 364 | float cx = getWidth() / 2F; 365 | float cy = getHeight() / 2F; 366 | 367 | suppMatrix.postScale(rate, rate, cx, cy); 368 | setImageMatrix(getImageViewMatrix()); 369 | } 370 | 371 | protected void zoomOut(float rate) { 372 | if (bitmapDisplayed.getBitmap() == null) { 373 | return; 374 | } 375 | 376 | float cx = getWidth() / 2F; 377 | float cy = getHeight() / 2F; 378 | 379 | // Zoom out to at most 1x 380 | Matrix tmp = new Matrix(suppMatrix); 381 | tmp.postScale(1F / rate, 1F / rate, cx, cy); 382 | 383 | if (getScale(tmp) < 1F) { 384 | suppMatrix.setScale(1F, 1F, cx, cy); 385 | } else { 386 | suppMatrix.postScale(1F / rate, 1F / rate, cx, cy); 387 | } 388 | setImageMatrix(getImageViewMatrix()); 389 | center(); 390 | } 391 | 392 | protected void postTranslate(float dx, float dy) { 393 | suppMatrix.postTranslate(dx, dy); 394 | } 395 | 396 | protected void panBy(float dx, float dy) { 397 | postTranslate(dx, dy); 398 | setImageMatrix(getImageViewMatrix()); 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /lib/src/main/java/com/soundcloud/android/crop/Log.java: -------------------------------------------------------------------------------- 1 | package com.soundcloud.android.crop; 2 | 3 | class Log { 4 | 5 | private static final String TAG = "android-crop"; 6 | 7 | public static void e(String msg) { 8 | android.util.Log.e(TAG, msg); 9 | } 10 | 11 | public static void e(String msg, Throwable e) { 12 | android.util.Log.e(TAG, msg, e); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/main/java/com/soundcloud/android/crop/MonitoredActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.soundcloud.android.crop; 18 | 19 | import android.app.Activity; 20 | import android.os.Bundle; 21 | 22 | import java.util.ArrayList; 23 | 24 | /* 25 | * Modified from original in AOSP. 26 | */ 27 | abstract class MonitoredActivity extends Activity { 28 | 29 | private final ArrayList listeners = new ArrayList(); 30 | 31 | public static interface LifeCycleListener { 32 | public void onActivityCreated(MonitoredActivity activity); 33 | public void onActivityDestroyed(MonitoredActivity activity); 34 | public void onActivityStarted(MonitoredActivity activity); 35 | public void onActivityStopped(MonitoredActivity activity); 36 | } 37 | 38 | public static class LifeCycleAdapter implements LifeCycleListener { 39 | public void onActivityCreated(MonitoredActivity activity) {} 40 | public void onActivityDestroyed(MonitoredActivity activity) {} 41 | public void onActivityStarted(MonitoredActivity activity) {} 42 | public void onActivityStopped(MonitoredActivity activity) {} 43 | } 44 | 45 | public void addLifeCycleListener(LifeCycleListener listener) { 46 | if (listeners.contains(listener)) return; 47 | listeners.add(listener); 48 | } 49 | 50 | public void removeLifeCycleListener(LifeCycleListener listener) { 51 | listeners.remove(listener); 52 | } 53 | 54 | @Override 55 | protected void onCreate(Bundle savedInstanceState) { 56 | super.onCreate(savedInstanceState); 57 | for (LifeCycleListener listener : listeners) { 58 | listener.onActivityCreated(this); 59 | } 60 | } 61 | 62 | @Override 63 | protected void onDestroy() { 64 | super.onDestroy(); 65 | for (LifeCycleListener listener : listeners) { 66 | listener.onActivityDestroyed(this); 67 | } 68 | } 69 | 70 | @Override 71 | protected void onStart() { 72 | super.onStart(); 73 | for (LifeCycleListener listener : listeners) { 74 | listener.onActivityStarted(this); 75 | } 76 | } 77 | 78 | @Override 79 | protected void onStop() { 80 | super.onStop(); 81 | for (LifeCycleListener listener : listeners) { 82 | listener.onActivityStopped(this); 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /lib/src/main/java/com/soundcloud/android/crop/RotateBitmap.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.soundcloud.android.crop; 18 | 19 | import android.graphics.Bitmap; 20 | import android.graphics.Matrix; 21 | 22 | /* 23 | * Modified from original in AOSP. 24 | */ 25 | class RotateBitmap { 26 | 27 | private Bitmap bitmap; 28 | private int rotation; 29 | 30 | public RotateBitmap(Bitmap bitmap, int rotation) { 31 | this.bitmap = bitmap; 32 | this.rotation = rotation % 360; 33 | } 34 | 35 | public void setRotation(int rotation) { 36 | this.rotation = rotation; 37 | } 38 | 39 | public int getRotation() { 40 | return rotation; 41 | } 42 | 43 | public Bitmap getBitmap() { 44 | return bitmap; 45 | } 46 | 47 | public void setBitmap(Bitmap bitmap) { 48 | this.bitmap = bitmap; 49 | } 50 | 51 | public Matrix getRotateMatrix() { 52 | // By default this is an identity matrix 53 | Matrix matrix = new Matrix(); 54 | if (bitmap != null && rotation != 0) { 55 | // We want to do the rotation at origin, but since the bounding 56 | // rectangle will be changed after rotation, so the delta values 57 | // are based on old & new width/height respectively. 58 | int cx = bitmap.getWidth() / 2; 59 | int cy = bitmap.getHeight() / 2; 60 | matrix.preTranslate(-cx, -cy); 61 | matrix.postRotate(rotation); 62 | matrix.postTranslate(getWidth() / 2, getHeight() / 2); 63 | } 64 | return matrix; 65 | } 66 | 67 | public boolean isOrientationChanged() { 68 | return (rotation / 90) % 2 != 0; 69 | } 70 | 71 | public int getHeight() { 72 | if (bitmap == null) return 0; 73 | if (isOrientationChanged()) { 74 | return bitmap.getWidth(); 75 | } else { 76 | return bitmap.getHeight(); 77 | } 78 | } 79 | 80 | public int getWidth() { 81 | if (bitmap == null) return 0; 82 | if (isOrientationChanged()) { 83 | return bitmap.getHeight(); 84 | } else { 85 | return bitmap.getWidth(); 86 | } 87 | } 88 | 89 | public void recycle() { 90 | if (bitmap != null) { 91 | bitmap.recycle(); 92 | bitmap = null; 93 | } 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /lib/src/main/res/drawable-hdpi/crop__divider.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/lib/src/main/res/drawable-hdpi/crop__divider.9.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-hdpi/crop__ic_cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/lib/src/main/res/drawable-hdpi/crop__ic_cancel.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-hdpi/crop__ic_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/lib/src/main/res/drawable-hdpi/crop__ic_done.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-mdpi/crop__divider.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/lib/src/main/res/drawable-mdpi/crop__divider.9.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-mdpi/crop__ic_cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/lib/src/main/res/drawable-mdpi/crop__ic_cancel.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-mdpi/crop__ic_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/lib/src/main/res/drawable-mdpi/crop__ic_done.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-v21/crop__selectable_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /lib/src/main/res/drawable-xhdpi/crop__divider.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/lib/src/main/res/drawable-xhdpi/crop__divider.9.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-xhdpi/crop__ic_cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/lib/src/main/res/drawable-xhdpi/crop__ic_cancel.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-xhdpi/crop__ic_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/lib/src/main/res/drawable-xhdpi/crop__ic_done.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-xhdpi/crop__tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/lib/src/main/res/drawable-xhdpi/crop__tile.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable/crop__selectable_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /lib/src/main/res/drawable/crop__texture.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /lib/src/main/res/layout/crop__activity_crop.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /lib/src/main/res/layout/crop__layout_done_cancel.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /lib/src/main/res/values-ar/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | جارى حفظ الصورة … 4 | رجاء الأنتظار … 5 | الصورة غير متاحة 6 | 7 | تم 8 | الغاء 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-ca/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Guardant imatge… 4 | Si us plau esperi… 5 | No hi ha imatges disponibles 6 | 7 | ACCEPTAR 8 | CANCEL·LAR 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bild speichern… 4 | Bitte warten… 5 | Keine Bildquellen verfügbar 6 | 7 | übernehmen 8 | abbrechen 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Guardando imagen… 4 | Por favor espere… 5 | No hay imágenes disponibles 6 | 7 | ACEPTAR 8 | CANCELAR 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-fa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | در حال ذخیره سازی 4 | لطفاً صبر کنید ... 5 | تصویری در دسترس نیست 6 | 7 | تأیید 8 | انصراف 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Enregistrement de l\'image… 4 | Veuillez patienter… 5 | Aucune image disponible 6 | 7 | ACCEPTER 8 | ANNULER 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-in/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Menyimpan gambar… 4 | Silakan tunggu… 5 | Tidak ada sumber gambar yang tersedia 6 | 7 | SELESAI 8 | BATAL 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-it/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Salvataggio immagine… 4 | Attendere prego… 5 | Nessuna immagine disponibile 6 | 7 | ACCETTA 8 | ANNULLA 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 保存中… 4 | お待ちください… 5 | 画像が見つかりません 6 | 7 | 決定 8 | キャンセル 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-ko/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 사진을 저장중입니다… 4 | 잠시만 기다려주세요… 5 | 이미지가 존재하지 않습니다. 6 | 7 | 확인 8 | 취소 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-land/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 48dp 4 | 5 | -------------------------------------------------------------------------------- /lib/src/main/res/values-large/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64dp 4 | 5 | -------------------------------------------------------------------------------- /lib/src/main/res/values-pt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Salvando imagem… 4 | Por favor, aguarde… 5 | Sem fontes de imagem disponíveis 6 | 7 | FINALIZADO 8 | CANCELAR 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Изображение сохраняется… 4 | Пожалуйста, подождите… 5 | Нет доступных изображений 6 | 7 | ГОТОВО 8 | ОТМЕНА 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-sv/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sparar bild… 4 | Var god vänta… 5 | Inga bildkällor tillgängliga 6 | 7 | KLAR 8 | AVBRYT 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-tr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Fotoğraf kaydediliyor… 4 | Lütfen bekleyin… 5 | Fotoğraf bulunamadı 6 | 7 | TAMAM 8 | ÇIKIŞ 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-v21/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #aaaaaa 4 | 5 | -------------------------------------------------------------------------------- /lib/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 正在保存照片… 4 | 请等待… 5 | 无效的图片 6 | 7 | 完成 8 | 取消 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 正在儲存相片… 4 | 請稍候… 5 | 沒有可用的圖片來源 6 | 7 | 完成 8 | 取消 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #f3f3f3 4 | #666666 5 | #1a000000 6 | #77000000 7 | 8 | -------------------------------------------------------------------------------- /lib/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56dp 4 | 5 | -------------------------------------------------------------------------------- /lib/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Saving picture… 4 | Please wait… 5 | No image sources available 6 | 7 | DONE 8 | CANCEL 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 21 | 22 | 33 | 34 | 38 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdamcd/android-crop/f4b2d25a0508f45b1b961064a9b3e4a8ae30227c/screenshot.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':lib', ':example' 2 | --------------------------------------------------------------------------------