├── .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-es │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-id │ └── 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-v21 │ └── colors.xml │ ├── values-zh │ └── strings.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── screenshot.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | /*/build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | bin 30 | gen 31 | .project 32 | .classpath 33 | .settings 34 | .idea 35 | *.iml 36 | *.ipr 37 | *.iws 38 | out 39 | target 40 | release.properties 41 | pom.xml.* 42 | build.xml 43 | local.properties 44 | proguard.cfg 45 | .DS_Store 46 | .gradle 47 | build 48 | app/*/build -------------------------------------------------------------------------------- /.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 | 3 | android: 4 | components: 5 | - build-tools-21.1.2 6 | - android-21 7 | - extra-android-support 8 | - sys-img-armeabi-v7a-android-21 9 | 10 | install: 11 | - ./gradlew :lib:build 12 | 13 | before_script: 14 | - echo no | android create avd --force -n test -t android-21 --abi armeabi-v7a 15 | - emulator -avd test -no-skin -no-audio -no-window & 16 | - android-wait-for-emulator 17 | - adb shell input keyevent 82 & 18 | 19 | script: 20 | - ./gradlew :lib:connectedAndroidTest 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | * Improved builder interface: `Crop.of(in, out).start(activity)` 4 | * Material styling 5 | * Drop support for Gingerbread 6 | * Start crop from support Fragment 7 | * Translations: French, Korean, Chinese, Spanish, Japanese, Arabic, Portuguese, Indonesian, Russian 8 | * Fix max size 9 | * Fix issue cropping images from Google Drive 10 | * Optional circle crop guide 11 | * Optional custom request code 12 | 13 | ## 0.9.10 14 | 15 | * Fix bug on some devices where image was displayed with 0 size 16 | 17 | ## 0.9.9 18 | 19 | * Downscale source images that are too big to load 20 | * Fix shading outside crop area on some API levels 21 | * Add option to always show crop handles 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > I guess people are just cropping out all the sadness 2 | 3 | An Android library project to provide a simple image cropping `Activity`, based on code from AOSP. 4 | 5 | [![build status](https://travis-ci.org/jdamcd/android-crop.png)](https://travis-ci.org/jdamcd/android-crop) 6 | [![maven central](https://img.shields.io/badge/maven%20central-1.0.0-brightgreen.svg)](http://search.maven.org/#artifactdetails%7Ccom.soundcloud.android%7Candroid-crop%7C1.0.0%7Caar.asc) 7 | [![changelog](https://img.shields.io/badge/changelog-1.0.0-lightgrey.svg)](CHANGELOG.md) 8 | 9 | ## Goals 10 | 11 | * Gradle build with AAR 12 | * Modern UI 13 | * Backwards compatible to SDK 14 14 | * Simple builder for configuration 15 | * Example project 16 | * More tests, less unused complexity 17 | 18 | ## Usage 19 | 20 | First, declare `CropImageActivity` in your manifest file: 21 | 22 | `` 23 | 24 | #### Crop 25 | 26 | `Crop.of(inputUri, outputUri).asSquare().start(activity)` 27 | 28 | Listen for the result of the crop (see example project if you want to do some error handling): 29 | 30 | @Override 31 | protected void onActivityResult(int requestCode, int resultCode, Intent result) { 32 | if (requestCode == Crop.REQUEST_CROP && resultCode == RESULT_OK) { 33 | doSomethingWithCroppedImage(outputUri); 34 | } 35 | } 36 | 37 | Some options are provided to style the crop screen. See example project theme. 38 | 39 | #### Pick 40 | 41 | The library provides a utility method to start an image picker: 42 | 43 | `Crop.pickImage(activity)` 44 | 45 | #### Dependency 46 | 47 | The AAR is published on Maven Central: 48 | 49 | `compile 'com.soundcloud.android:android-crop:1.0.0@aar'` 50 | 51 | #### Users 52 | 53 | Apps that use this library include: [SoundCloud](https://play.google.com/store/apps/details?id=com.soundcloud.android), [Depop](https://play.google.com/store/apps/details?id=com.depop) 54 | 55 | ## How does it look? 56 | 57 | ![android-crop screenshot](screenshot.png) 58 | 59 | ## License 60 | 61 | This project is based on the [AOSP](https://source.android.com) camera image cropper via [android-cropimage](https://github.com/lvillani/android-cropimage). 62 | 63 | Copyright 2015 SoundCloud 64 | 65 | Licensed under the Apache License, Version 2.0 (the "License"); 66 | you may not use this file except in compliance with the License. 67 | You may obtain a copy of the License at 68 | 69 | http://www.apache.org/licenses/LICENSE-2.0 70 | 71 | Unless required by applicable law or agreed to in writing, software 72 | distributed under the License is distributed on an "AS IS" BASIS, 73 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 74 | See the License for the specific language governing permissions and 75 | limitations under the License. 76 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:1.2.3' 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 21 7 | buildToolsVersion '21.1.2' 8 | 9 | defaultConfig { 10 | minSdkVersion 14 11 | targetSdkVersion 21 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 | import android.app.Activity; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.os.Bundle; 8 | import android.view.Menu; 9 | import android.view.MenuItem; 10 | import android.widget.ImageView; 11 | import android.widget.Toast; 12 | 13 | import java.io.File; 14 | 15 | public class MainActivity extends Activity { 16 | 17 | private ImageView resultView; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_main); 23 | resultView = (ImageView) findViewById(R.id.result_image); 24 | } 25 | 26 | @Override 27 | public boolean onCreateOptionsMenu(Menu menu) { 28 | getMenuInflater().inflate(R.menu.activity_main, menu); 29 | return super.onCreateOptionsMenu(menu); 30 | } 31 | 32 | @Override 33 | public boolean onOptionsItemSelected(MenuItem item) { 34 | if (item.getItemId() == R.id.action_select) { 35 | resultView.setImageDrawable(null); 36 | Crop.pickImage(this); 37 | return true; 38 | } 39 | return super.onOptionsItemSelected(item); 40 | } 41 | 42 | @Override 43 | protected void onActivityResult(int requestCode, int resultCode, Intent result) { 44 | if (requestCode == Crop.REQUEST_PICK && resultCode == RESULT_OK) { 45 | beginCrop(result.getData()); 46 | } else if (requestCode == Crop.REQUEST_CROP) { 47 | handleCrop(resultCode, result); 48 | } 49 | } 50 | 51 | private void beginCrop(Uri source) { 52 | Uri destination = Uri.fromFile(new File(getCacheDir(), "cropped")); 53 | Crop.of(source, destination).asSquare().start(this); 54 | } 55 | 56 | private void handleCrop(int resultCode, Intent result) { 57 | if (resultCode == RESULT_OK) { 58 | resultView.setImageURI(Crop.getOutput(result)); 59 | } else if (resultCode == Crop.RESULT_ERROR) { 60 | Toast.makeText(this, Crop.getError(result).getMessage(), Toast.LENGTH_SHORT).show(); 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /example/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/example/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/example/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xhdpi/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/example/src/main/res/drawable-xhdpi/tile.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/example/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/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.0 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/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 30 00:50:07 CET 2014 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.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # 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 21 10 | buildToolsVersion '21.1.2' 11 | 12 | defaultConfig { 13 | minSdkVersion 14 14 | targetSdkVersion 21 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:21.0.0' 23 | compile 'com.android.support:support-v4:21.0.3' 24 | androidTestCompile 'com.squareup:fest-android:1.0.7' 25 | androidTestCompile 'com.android.support:support-v4:21.0.3' 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 | } 78 | -------------------------------------------------------------------------------- /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.app.Activity; 4 | import android.app.Fragment; 5 | import android.content.ActivityNotFoundException; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.net.Uri; 9 | import android.provider.MediaStore; 10 | import android.widget.Toast; 11 | 12 | /** 13 | * Builder for crop Intents and utils for handling result 14 | */ 15 | public class Crop { 16 | 17 | public static final int REQUEST_CROP = 6709; 18 | public static final int REQUEST_PICK = 9162; 19 | public static final int RESULT_ERROR = 404; 20 | 21 | static interface Extra { 22 | String ASPECT_X = "aspect_x"; 23 | String ASPECT_Y = "aspect_y"; 24 | String MAX_X = "max_x"; 25 | String MAX_Y = "max_y"; 26 | String ERROR = "error"; 27 | } 28 | 29 | private Intent cropIntent; 30 | 31 | /** 32 | * Create a crop Intent builder with source and destination image Uris 33 | * 34 | * @param source Uri for image to crop 35 | * @param destination Uri for saving the cropped image 36 | */ 37 | public static Crop of(Uri source, Uri destination) { 38 | return new Crop(source, destination); 39 | } 40 | 41 | private Crop(Uri source, Uri destination) { 42 | cropIntent = new Intent(); 43 | cropIntent.setData(source); 44 | cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, destination); 45 | } 46 | 47 | /** 48 | * Set fixed aspect ratio for crop area 49 | * 50 | * @param x Aspect X 51 | * @param y Aspect Y 52 | */ 53 | public Crop withAspect(int x, int y) { 54 | cropIntent.putExtra(Extra.ASPECT_X, x); 55 | cropIntent.putExtra(Extra.ASPECT_Y, y); 56 | return this; 57 | } 58 | 59 | /** 60 | * Crop area with fixed 1:1 aspect ratio 61 | */ 62 | public Crop asSquare() { 63 | cropIntent.putExtra(Extra.ASPECT_X, 1); 64 | cropIntent.putExtra(Extra.ASPECT_Y, 1); 65 | return this; 66 | } 67 | 68 | /** 69 | * Set maximum crop size 70 | * 71 | * @param width Max width 72 | * @param height Max height 73 | */ 74 | public Crop withMaxSize(int width, int height) { 75 | cropIntent.putExtra(Extra.MAX_X, width); 76 | cropIntent.putExtra(Extra.MAX_Y, height); 77 | return this; 78 | } 79 | 80 | /** 81 | * Send the crop Intent from an Activity 82 | * 83 | * @param activity Activity to receive result 84 | */ 85 | public void start(Activity activity) { 86 | start(activity, REQUEST_CROP); 87 | } 88 | 89 | /** 90 | * Send the crop Intent from an Activity with a custom requestCode 91 | * 92 | * @param activity Activity to receive result 93 | * @param requestCode requestCode for result 94 | */ 95 | public void start(Activity activity, int requestCode) { 96 | activity.startActivityForResult(getIntent(activity), requestCode); 97 | } 98 | 99 | /** 100 | * Send the crop Intent from a Fragment 101 | * 102 | * @param context Context 103 | * @param fragment Fragment to receive result 104 | */ 105 | public void start(Context context, Fragment fragment) { 106 | start(context, fragment, REQUEST_CROP); 107 | } 108 | 109 | /** 110 | * Send the crop Intent from a support library Fragment 111 | * 112 | * @param context Context 113 | * @param fragment Fragment to receive result 114 | */ 115 | public void start(Context context, android.support.v4.app.Fragment fragment) { 116 | start(context, fragment, REQUEST_CROP); 117 | } 118 | 119 | /** 120 | * Send the crop Intent with a custom requestCode 121 | * 122 | * @param context Context 123 | * @param fragment Fragment to receive result 124 | * @param requestCode requestCode for result 125 | */ 126 | public void start(Context context, Fragment fragment, int requestCode) { 127 | fragment.startActivityForResult(getIntent(context), requestCode); 128 | } 129 | 130 | /** 131 | * Send the crop Intent with a custom requestCode 132 | * 133 | * @param context Context 134 | * @param fragment Fragment to receive result 135 | * @param requestCode requestCode for result 136 | */ 137 | public void start(Context context, android.support.v4.app.Fragment fragment, int requestCode) { 138 | fragment.startActivityForResult(getIntent(context), requestCode); 139 | } 140 | 141 | /** 142 | * Get Intent to start crop Activity 143 | * 144 | * @param context Context 145 | * @return Intent for CropImageActivity 146 | */ 147 | public Intent getIntent(Context context) { 148 | cropIntent.setClass(context, CropImageActivity.class); 149 | return cropIntent; 150 | } 151 | 152 | /** 153 | * Retrieve URI for cropped image, as set in the Intent builder 154 | * 155 | * @param result Output Image URI 156 | */ 157 | public static Uri getOutput(Intent result) { 158 | return result.getParcelableExtra(MediaStore.EXTRA_OUTPUT); 159 | } 160 | 161 | /** 162 | * Retrieve error that caused crop to fail 163 | * 164 | * @param result Result Intent 165 | * @return Throwable handled in CropImageActivity 166 | */ 167 | public static Throwable getError(Intent result) { 168 | return (Throwable) result.getSerializableExtra(Extra.ERROR); 169 | } 170 | 171 | /** 172 | * Utility to start an image picker 173 | * 选择图片 174 | * 175 | * @param activity Activity that will receive result 176 | */ 177 | public static void pickImage(Activity activity) { 178 | pickImage(activity, REQUEST_PICK); 179 | } 180 | 181 | /** 182 | * Utility to start an image picker with request code 183 | * 184 | * @param activity Activity that will receive result 185 | * @param requestCode requestCode for result 186 | */ 187 | public static void pickImage(Activity activity, int requestCode) { 188 | Intent intent = new Intent(Intent.ACTION_GET_CONTENT).setType("image/*"); 189 | try { 190 | activity.startActivityForResult(intent, requestCode); 191 | } catch (ActivityNotFoundException e) { 192 | Toast.makeText(activity, R.string.crop__pick_error, Toast.LENGTH_SHORT).show(); 193 | } 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /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.content.Intent; 20 | import android.graphics.Bitmap; 21 | import android.graphics.BitmapFactory; 22 | import android.graphics.BitmapRegionDecoder; 23 | import android.graphics.Matrix; 24 | import android.graphics.Rect; 25 | import android.graphics.RectF; 26 | import android.net.Uri; 27 | import android.opengl.GLES10; 28 | import android.os.Bundle; 29 | import android.os.Handler; 30 | import android.provider.MediaStore; 31 | import android.view.View; 32 | import android.view.Window; 33 | 34 | import java.io.IOException; 35 | import java.io.InputStream; 36 | import java.io.OutputStream; 37 | import java.util.concurrent.CountDownLatch; 38 | 39 | /* 40 | * Modified from original in AOSP. 41 | */ 42 | public class CropImageActivity extends MonitoredActivity { 43 | 44 | private static final int SIZE_DEFAULT = 2048; 45 | private static final int SIZE_LIMIT = 4096; 46 | 47 | private final Handler handler = new Handler(); 48 | 49 | private int aspectX; 50 | private int aspectY; 51 | 52 | // Output image 53 | private int maxX; 54 | private int maxY; 55 | private int exifRotation; 56 | 57 | private Uri sourceUri; 58 | private Uri saveUri; 59 | 60 | private boolean isSaving; 61 | 62 | private int sampleSize; 63 | private RotateBitmap rotateBitmap; 64 | private CropImageView imageView; 65 | private HighlightView cropView; 66 | 67 | @Override 68 | public void onCreate(Bundle icicle) { 69 | super.onCreate(icicle); 70 | requestWindowFeature(Window.FEATURE_NO_TITLE); 71 | setContentView(R.layout.crop__activity_crop); 72 | initViews(); 73 | 74 | setupFromIntent(); 75 | if (rotateBitmap == null) { 76 | finish(); 77 | return; 78 | } 79 | startCrop(); 80 | } 81 | 82 | private void initViews() { 83 | imageView = (CropImageView) findViewById(R.id.crop_image); 84 | imageView.context = this; 85 | imageView.setRecycler(new ImageViewTouchBase.Recycler() { 86 | @Override 87 | public void recycle(Bitmap b) { 88 | b.recycle(); 89 | System.gc(); 90 | } 91 | }); 92 | 93 | findViewById(R.id.btn_cancel).setOnClickListener(new View.OnClickListener() { 94 | public void onClick(View v) { 95 | setResult(RESULT_CANCELED); 96 | finish(); 97 | } 98 | }); 99 | 100 | findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() { 101 | public void onClick(View v) { 102 | onSaveClicked(); 103 | } 104 | }); 105 | } 106 | 107 | private void setupFromIntent() { 108 | Intent intent = getIntent(); // Intent { dat=content://media/external/images/media/261750 cmp=com.soundcloud.android.crop.example/com.soundcloud.android.crop.CropImageActivity (has extras) } 109 | Bundle extras = intent.getExtras(); // Bundle[{output=file:///data/data/com.soundcloud.android.crop.example/cache/cropped, aspect_x=1, aspect_y=1}] 110 | 111 | if (extras != null) { 112 | aspectX = extras.getInt(Crop.Extra.ASPECT_X); 113 | aspectY = extras.getInt(Crop.Extra.ASPECT_Y); 114 | maxX = extras.getInt(Crop.Extra.MAX_X); 115 | maxY = extras.getInt(Crop.Extra.MAX_Y); 116 | saveUri = extras.getParcelable(MediaStore.EXTRA_OUTPUT); // file:///data/data/com.soundcloud.android.crop.example/cache/cropped 117 | } 118 | 119 | sourceUri = intent.getData(); 120 | if (sourceUri != null) { 121 | exifRotation = CropUtil.getExifRotation(CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri)); 122 | 123 | InputStream is = null; 124 | try { 125 | sampleSize = calculateBitmapSampleSize(sourceUri); 126 | is = getContentResolver().openInputStream(sourceUri); 127 | BitmapFactory.Options option = new BitmapFactory.Options(); 128 | option.inSampleSize = sampleSize; 129 | rotateBitmap = new RotateBitmap(BitmapFactory.decodeStream(is, null, option), exifRotation); 130 | } catch (IOException e) { 131 | Log.e("Error reading image: " + e.getMessage(), e); 132 | setResultException(e); 133 | } catch (OutOfMemoryError e) { 134 | Log.e("OOM reading image: " + e.getMessage(), e); 135 | setResultException(e); 136 | } finally { 137 | CropUtil.closeSilently(is); 138 | } 139 | } 140 | } 141 | 142 | private int calculateBitmapSampleSize(Uri bitmapUri) throws IOException { 143 | InputStream is = null; 144 | BitmapFactory.Options options = new BitmapFactory.Options(); 145 | options.inJustDecodeBounds = true; 146 | try { 147 | is = getContentResolver().openInputStream(bitmapUri); 148 | BitmapFactory.decodeStream(is, null, options); // Just get image size 149 | } finally { 150 | CropUtil.closeSilently(is); 151 | } 152 | //maxSize 4096 153 | int maxSize = getMaxImageSize(); 154 | int sampleSize = 1; 155 | while (options.outHeight / sampleSize > maxSize || options.outWidth / sampleSize > maxSize) { 156 | //如果图片过大则需要缩放 1<<1 = 2; 157 | sampleSize = sampleSize << 1; 158 | } 159 | return sampleSize; 160 | } 161 | 162 | private int getMaxImageSize() { 163 | int textureLimit = getMaxTextureSize(); 164 | if (textureLimit == 0) { 165 | return SIZE_DEFAULT; 166 | } else { 167 | return Math.min(textureLimit, SIZE_LIMIT); 168 | } 169 | } 170 | 171 | private int getMaxTextureSize() { 172 | // The OpenGL texture size is the maximum size that can be drawn in an ImageView 173 | int[] maxSize = new int[1]; 174 | GLES10.glGetIntegerv(GLES10.GL_MAX_TEXTURE_SIZE, maxSize, 0); 175 | return maxSize[0]; 176 | } 177 | //开始裁剪 178 | private void startCrop() { 179 | if (isFinishing()) { 180 | return; 181 | } 182 | //显示图片 183 | imageView.setImageRotateBitmapResetBase(rotateBitmap, true); 184 | CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__wait), 185 | new Runnable() { 186 | public void run() { 187 | /** 188 | * http://www.cnblogs.com/yezhenhan/archive/2012/01/07/2315652.html 189 | * CountDownLatch类是一个同步计数器,构造时传入int参数,该参数就是计数器的初始值,每调用一次countDown()方法, 190 | * 计数器减1,计数器大于0 时,await()方法会阻塞程序继续执行 191 | * CountDownLatch如其所写,是一个倒计数的锁存器,当计数减至0时触发特定的事件。利用这种特性, 192 | * 可以让主线程等待子线程的结束。 193 | */ 194 | final CountDownLatch latch = new CountDownLatch(1); 195 | handler.post(new Runnable() { 196 | public void run() { 197 | if (imageView.getScale() == 1F) { 198 | imageView.center(true, true); 199 | } 200 | latch.countDown(); 201 | } 202 | }); 203 | try { 204 | latch.await(); 205 | } catch (InterruptedException e) { 206 | throw new RuntimeException(e); 207 | } 208 | new Cropper().crop(); 209 | } 210 | }, handler 211 | ); 212 | } 213 | 214 | private class Cropper { 215 | 216 | private void makeDefault() { 217 | if (rotateBitmap == null) { 218 | return; 219 | } 220 | 221 | HighlightView hv = new HighlightView(imageView); 222 | final int width = rotateBitmap.getWidth(); 223 | final int height = rotateBitmap.getHeight(); 224 | 225 | Rect imageRect = new Rect(0, 0, width, height); //Rect(0, 0 - 1968, 2624) 226 | 227 | // Make the default size about 4/5 of the width or height 228 | int cropWidth = Math.min(width, height) * 4 / 5; 229 | @SuppressWarnings("SuspiciousNameCombination") 230 | int cropHeight = cropWidth; //1574 231 | 232 | if (aspectX != 0 && aspectY != 0) { 233 | if (aspectX > aspectY) { 234 | cropHeight = cropWidth * aspectY / aspectX; 235 | } else { 236 | cropWidth = cropHeight * aspectX / aspectY; 237 | } 238 | } 239 | 240 | int x = (width - cropWidth) / 2; 241 | int y = (height - cropHeight) / 2; 242 | 243 | RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight); //RectF(197.0, 525.0, 1771.0, 2099.0) 244 | hv.setup(imageView.getUnrotatedMatrix(), imageRect, cropRect, aspectX != 0 && aspectY != 0); 245 | imageView.add(hv); 246 | } 247 | 248 | public void crop() { 249 | handler.post(new Runnable() { 250 | public void run() { //设置默认的裁剪框 251 | makeDefault(); 252 | imageView.invalidate(); 253 | if (imageView.highlightViews.size() == 1) { 254 | cropView = imageView.highlightViews.get(0); 255 | cropView.setFocus(true); 256 | } 257 | } 258 | }); 259 | } 260 | } 261 | 262 | private void onSaveClicked() { 263 | if (cropView == null || isSaving) { 264 | return; 265 | } 266 | isSaving = true; 267 | 268 | Bitmap croppedImage; 269 | Rect r = cropView.getScaledCropRect(sampleSize); 270 | int width = r.width(); 271 | int height = r.height(); 272 | 273 | int outWidth = width; 274 | int outHeight = height; 275 | if (maxX > 0 && maxY > 0 && (width > maxX || height > maxY)) { 276 | float ratio = (float) width / (float) height; 277 | if ((float) maxX / (float) maxY > ratio) { 278 | outHeight = maxY; 279 | outWidth = (int) ((float) maxY * ratio + .5f); 280 | } else { 281 | outWidth = maxX; 282 | outHeight = (int) ((float) maxX / ratio + .5f); 283 | } 284 | } 285 | 286 | try { 287 | croppedImage = decodeRegionCrop(r, outWidth, outHeight); 288 | } catch (IllegalArgumentException e) { 289 | setResultException(e); 290 | finish(); 291 | return; 292 | } 293 | 294 | if (croppedImage != null) { 295 | imageView.setImageRotateBitmapResetBase(new RotateBitmap(croppedImage, exifRotation), true); 296 | imageView.center(true, true); 297 | imageView.highlightViews.clear(); 298 | } 299 | saveImage(croppedImage); 300 | } 301 | 302 | private void saveImage(Bitmap croppedImage) { 303 | if (croppedImage != null) { 304 | final Bitmap b = croppedImage; 305 | CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__saving), 306 | new Runnable() { 307 | public void run() { 308 | saveOutput(b); 309 | } 310 | }, handler 311 | ); 312 | } else { 313 | finish(); 314 | } 315 | } 316 | 317 | private Bitmap decodeRegionCrop(Rect rect, int outWidth, int outHeight) { 318 | // Release memory now 319 | clearImageView(); 320 | 321 | InputStream is = null; 322 | Bitmap croppedImage = null; 323 | try { 324 | is = getContentResolver().openInputStream(sourceUri); 325 | BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); 326 | final int width = decoder.getWidth(); 327 | final int height = decoder.getHeight(); 328 | 329 | if (exifRotation != 0) { 330 | // Adjust crop area to account for image rotation 331 | Matrix matrix = new Matrix(); 332 | matrix.setRotate(-exifRotation); 333 | 334 | RectF adjusted = new RectF(); 335 | matrix.mapRect(adjusted, new RectF(rect)); 336 | 337 | // Adjust to account for origin at 0,0 338 | adjusted.offset(adjusted.left < 0 ? width : 0, adjusted.top < 0 ? height : 0); 339 | rect = new Rect((int) adjusted.left, (int) adjusted.top, (int) adjusted.right, (int) adjusted.bottom); 340 | } 341 | 342 | try { 343 | croppedImage = decoder.decodeRegion(rect, new BitmapFactory.Options()); 344 | if (rect.width() > outWidth || rect.height() > outHeight) { 345 | Matrix matrix = new Matrix(); 346 | matrix.postScale((float) outWidth / rect.width(), (float) outHeight / rect.height()); 347 | croppedImage = Bitmap.createBitmap(croppedImage, 0, 0, croppedImage.getWidth(), croppedImage.getHeight(), matrix, true); 348 | } 349 | } catch (IllegalArgumentException e) { 350 | // Rethrow with some extra information 351 | throw new IllegalArgumentException("Rectangle " + rect + " is outside of the image (" 352 | + width + "," + height + "," + exifRotation + ")", e); 353 | } 354 | 355 | } catch (IOException e) { 356 | Log.e("Error cropping image: " + e.getMessage(), e); 357 | finish(); 358 | } catch (OutOfMemoryError e) { 359 | Log.e("OOM cropping image: " + e.getMessage(), e); 360 | setResultException(e); 361 | } finally { 362 | CropUtil.closeSilently(is); 363 | } 364 | return croppedImage; 365 | } 366 | 367 | private void clearImageView() { 368 | imageView.clear(); 369 | if (rotateBitmap != null) { 370 | rotateBitmap.recycle(); 371 | } 372 | System.gc(); 373 | } 374 | 375 | private void saveOutput(Bitmap croppedImage) { 376 | if (saveUri != null) { 377 | OutputStream outputStream = null; 378 | try { 379 | outputStream = getContentResolver().openOutputStream(saveUri); 380 | if (outputStream != null) { 381 | croppedImage.compress(Bitmap.CompressFormat.JPEG, 90, outputStream); 382 | } 383 | } catch (IOException e) { 384 | setResultException(e); 385 | Log.e("Cannot open file: " + saveUri, e); 386 | } finally { 387 | CropUtil.closeSilently(outputStream); 388 | } 389 | 390 | CropUtil.copyExifRotation( 391 | CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri), 392 | CropUtil.getFromMediaUri(this, getContentResolver(), saveUri) 393 | ); 394 | 395 | setResultUri(saveUri); 396 | } 397 | 398 | final Bitmap b = croppedImage; 399 | handler.post(new Runnable() { 400 | public void run() { 401 | imageView.clear(); 402 | b.recycle(); 403 | } 404 | }); 405 | 406 | finish(); 407 | } 408 | 409 | @Override 410 | protected void onDestroy() { 411 | super.onDestroy(); 412 | if (rotateBitmap != null) { 413 | rotateBitmap.recycle(); 414 | } 415 | } 416 | 417 | @Override 418 | public boolean onSearchRequested() { 419 | return false; 420 | } 421 | 422 | public boolean isSaving() { 423 | return isSaving; 424 | } 425 | 426 | private void setResultUri(Uri uri) { 427 | setResult(RESULT_OK, new Intent().putExtra(MediaStore.EXTRA_OUTPUT, uri)); 428 | } 429 | 430 | private void setResultException(Throwable throwable) { 431 | setResult(Crop.RESULT_ERROR, new Intent().putExtra(Crop.Extra.ERROR, throwable)); 432 | } 433 | 434 | } 435 | -------------------------------------------------------------------------------- /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 | //用于裁剪的CropImageView 继承自ImageViewTouchBase 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 | 22 | public CropImageView(Context context) { 23 | super(context); 24 | } 25 | 26 | public CropImageView(Context context, AttributeSet attrs) { 27 | super(context, attrs); 28 | } 29 | 30 | public CropImageView(Context context, AttributeSet attrs, int defStyle) { 31 | super(context, attrs, defStyle); 32 | } 33 | 34 | @Override 35 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 36 | super.onLayout(changed, left, top, right, bottom); 37 | if (bitmapDisplayed.getBitmap() != null) { 38 | for (HighlightView hv : highlightViews) { 39 | 40 | hv.matrix.set(getUnrotatedMatrix()); 41 | hv.invalidate(); 42 | if (hv.hasFocus()) { 43 | centerBasedOnHighlightView(hv); 44 | } 45 | } 46 | } 47 | } 48 | 49 | @Override 50 | protected void zoomTo(float scale, float centerX, float centerY) { 51 | super.zoomTo(scale, centerX, centerY); 52 | for (HighlightView hv : highlightViews) { 53 | hv.matrix.set(getUnrotatedMatrix()); 54 | hv.invalidate(); 55 | } 56 | } 57 | 58 | @Override 59 | protected void zoomIn() { 60 | super.zoomIn(); 61 | for (HighlightView hv : highlightViews) { 62 | hv.matrix.set(getUnrotatedMatrix()); 63 | hv.invalidate(); 64 | } 65 | } 66 | 67 | @Override 68 | protected void zoomOut() { 69 | super.zoomOut(); 70 | for (HighlightView hv : highlightViews) { 71 | hv.matrix.set(getUnrotatedMatrix()); 72 | hv.invalidate(); 73 | } 74 | } 75 | 76 | @Override 77 | protected void postTranslate(float deltaX, float deltaY) { 78 | super.postTranslate(deltaX, deltaY); 79 | for (HighlightView hv : highlightViews) { 80 | hv.matrix.postTranslate(deltaX, deltaY); 81 | hv.invalidate(); 82 | } 83 | } 84 | 85 | @Override 86 | public boolean onTouchEvent(MotionEvent event) { 87 | CropImageActivity cropImageActivity = (CropImageActivity) context; 88 | if (cropImageActivity.isSaving()) { 89 | return false; 90 | } 91 | 92 | switch (event.getAction()) { 93 | case MotionEvent.ACTION_DOWN: 94 | for (HighlightView hv : highlightViews) { 95 | int edge = hv.getHit(event.getX(), event.getY()); 96 | if (edge != HighlightView.GROW_NONE) { 97 | motionEdge = edge; 98 | motionHighlightView = hv; 99 | lastX = event.getX(); 100 | lastY = event.getY(); 101 | motionHighlightView.setMode((edge == HighlightView.MOVE) 102 | ? HighlightView.ModifyMode.Move 103 | : HighlightView.ModifyMode.Grow); 104 | break; 105 | } 106 | } 107 | break; 108 | case MotionEvent.ACTION_UP: 109 | if (motionHighlightView != null) { 110 | centerBasedOnHighlightView(motionHighlightView); 111 | motionHighlightView.setMode(HighlightView.ModifyMode.None); 112 | } 113 | motionHighlightView = null; 114 | break; 115 | case MotionEvent.ACTION_MOVE: 116 | if (motionHighlightView != null) { 117 | motionHighlightView.handleMotion(motionEdge, event.getX() 118 | - lastX, event.getY() - lastY); 119 | lastX = event.getX(); 120 | lastY = event.getY(); 121 | ensureVisible(motionHighlightView); 122 | } 123 | break; 124 | } 125 | 126 | switch (event.getAction()) { 127 | case MotionEvent.ACTION_UP: 128 | center(true, true); 129 | break; 130 | case MotionEvent.ACTION_MOVE: 131 | // if we're not zoomed then there's no point in even allowing 132 | // the user to move the image around. This call to center puts 133 | // it back to the normalized location (with false meaning don't 134 | // animate). 135 | if (getScale() == 1F) { 136 | center(true, true); 137 | } 138 | break; 139 | } 140 | 141 | return true; 142 | } 143 | 144 | // Pan the displayed image to make sure the cropping rectangle is visible. 145 | private void ensureVisible(HighlightView hv) { 146 | Rect r = hv.drawRect; 147 | 148 | int panDeltaX1 = Math.max(0, getLeft() - r.left); 149 | int panDeltaX2 = Math.min(0, getRight() - r.right); 150 | 151 | int panDeltaY1 = Math.max(0, getTop() - r.top); 152 | int panDeltaY2 = Math.min(0, getBottom() - r.bottom); 153 | 154 | int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2; 155 | int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2; 156 | 157 | if (panDeltaX != 0 || panDeltaY != 0) { 158 | panBy(panDeltaX, panDeltaY); 159 | } 160 | } 161 | 162 | // If the cropping rectangle's size changed significantly, change the 163 | // view's center and scale according to the cropping rectangle. 164 | private void centerBasedOnHighlightView(HighlightView hv) { 165 | Rect drawRect = hv.drawRect; 166 | 167 | float width = drawRect.width(); 168 | float height = drawRect.height(); 169 | 170 | float thisWidth = getWidth(); 171 | float thisHeight = getHeight(); 172 | 173 | float z1 = thisWidth / width * .6F; 174 | float z2 = thisHeight / height * .6F; 175 | 176 | float zoom = Math.min(z1, z2); 177 | zoom = zoom * this.getScale(); 178 | zoom = Math.max(1F, zoom); 179 | 180 | if ((Math.abs(zoom - getScale()) / zoom) > .1) { 181 | float[] coordinates = new float[] { hv.cropRect.centerX(), hv.cropRect.centerY() }; 182 | getUnrotatedMatrix().mapPoints(coordinates); 183 | zoomTo(zoom, coordinates[0], coordinates[1], 300F); 184 | } 185 | 186 | ensureVisible(hv); 187 | } 188 | 189 | @Override 190 | protected void onDraw(@NonNull Canvas canvas) { 191 | super.onDraw(canvas); 192 | for (HighlightView mHighlightView : highlightViews) { 193 | mHighlightView.draw(canvas); 194 | } 195 | } 196 | 197 | public void add(HighlightView hv) { 198 | highlightViews.add(hv); 199 | invalidate(); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /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 | * //覆盖在Image上的裁剪框,同时被图片和屏幕使用 42 | * computeLayout() 它使用matrix映射从图像空间到屏幕空间 43 | */ 44 | class HighlightView { 45 | 46 | public static final int GROW_NONE = (1 << 0); 47 | public static final int GROW_LEFT_EDGE = (1 << 1); 48 | public static final int GROW_RIGHT_EDGE = (1 << 2); 49 | public static final int GROW_TOP_EDGE = (1 << 3); 50 | public static final int GROW_BOTTOM_EDGE = (1 << 4); 51 | public static final int MOVE = (1 << 5); 52 | 53 | private static final int DEFAULT_HIGHLIGHT_COLOR = 0xFF33B5E5; 54 | private static final float HANDLE_RADIUS_DP = 12f; 55 | private static final float OUTLINE_DP = 2f; 56 | 57 | enum ModifyMode { None, Move, Grow } 58 | enum HandleMode { Changing, Always, Never } 59 | 60 | RectF cropRect; // Image space 61 | Rect drawRect; // Screen space 62 | Matrix matrix; 63 | private RectF imageRect; // Image space 64 | 65 | private final Paint outsidePaint = new Paint(); 66 | private final Paint outlinePaint = new Paint(); 67 | private final Paint handlePaint = new Paint(); 68 | 69 | private View viewContext; // View displaying image 70 | private boolean showThirds; 71 | private boolean showCircle; 72 | private int highlightColor; 73 | 74 | private ModifyMode modifyMode = ModifyMode.None; 75 | private HandleMode handleMode = HandleMode.Changing; 76 | private boolean maintainAspectRatio; 77 | private float initialAspectRatio; 78 | private float handleRadius; 79 | private float outlineWidth; 80 | private boolean isFocused; 81 | 82 | public HighlightView(View context) { 83 | viewContext = context; 84 | initStyles(context.getContext()); 85 | } 86 | 87 | private void initStyles(Context context) { 88 | TypedValue outValue = new TypedValue(); 89 | context.getTheme().resolveAttribute(R.attr.cropImageStyle, outValue, true); 90 | TypedArray attributes = context.obtainStyledAttributes(outValue.resourceId, R.styleable.CropImageView); 91 | try { 92 | showThirds = attributes.getBoolean(R.styleable.CropImageView_showThirds, false); 93 | showCircle = attributes.getBoolean(R.styleable.CropImageView_showCircle, false); 94 | highlightColor = attributes.getColor(R.styleable.CropImageView_highlightColor, 95 | DEFAULT_HIGHLIGHT_COLOR); 96 | handleMode = HandleMode.values()[attributes.getInt(R.styleable.CropImageView_showHandles, 0)]; 97 | } finally { 98 | attributes.recycle(); 99 | } 100 | } 101 | 102 | //maintainAspectRatio: true imageRect: "Rect(0, 0-1968, 2624)" 103 | //cropRect: "RectF(197.0, 525.0, 1771.0, 2099.0)" 104 | //m: "Matrix{[0.58536583, 0.0, 0.0],[0.0, 0.58536583, 70.5][0.0,0.0,1.0]}" 105 | public void setup(Matrix m, Rect imageRect, RectF cropRect, boolean maintainAspectRatio) { 106 | matrix = new Matrix(m); 107 | 108 | this.cropRect = cropRect; 109 | this.imageRect = new RectF(imageRect); 110 | this.maintainAspectRatio = maintainAspectRatio; 111 | 112 | initialAspectRatio = this.cropRect.width() / this.cropRect.height(); 113 | drawRect = computeLayout(); 114 | 115 | outsidePaint.setARGB(125, 50, 50, 50); 116 | outlinePaint.setStyle(Paint.Style.STROKE); 117 | outlinePaint.setAntiAlias(true); 118 | outlineWidth = dpToPx(OUTLINE_DP); 119 | 120 | handlePaint.setColor(highlightColor); 121 | handlePaint.setStyle(Paint.Style.FILL); 122 | handlePaint.setAntiAlias(true); 123 | handleRadius = dpToPx(HANDLE_RADIUS_DP); 124 | 125 | modifyMode = ModifyMode.None; 126 | } 127 | 128 | private float dpToPx(float dp) { 129 | return dp * viewContext.getResources().getDisplayMetrics().density; 130 | } 131 | 132 | protected void draw(Canvas canvas) { 133 | canvas.save(); 134 | Path path = new Path(); 135 | outlinePaint.setStrokeWidth(outlineWidth); 136 | if (!hasFocus()) { 137 | outlinePaint.setColor(Color.BLACK); 138 | canvas.drawRect(drawRect, outlinePaint); 139 | } else { 140 | Rect viewDrawingRect = new Rect(); 141 | viewContext.getDrawingRect(viewDrawingRect); 142 | 143 | path.addRect(new RectF(drawRect), Path.Direction.CW); 144 | outlinePaint.setColor(highlightColor); 145 | 146 | if (isClipPathSupported(canvas)) { 147 | canvas.clipPath(path, Region.Op.DIFFERENCE); 148 | canvas.drawRect(viewDrawingRect, outsidePaint); 149 | } else { 150 | drawOutsideFallback(canvas); 151 | } 152 | 153 | canvas.restore(); 154 | canvas.drawPath(path, outlinePaint); 155 | 156 | if (showThirds) { 157 | drawThirds(canvas); 158 | } 159 | 160 | if (showCircle) { 161 | drawCircle(canvas); 162 | } 163 | 164 | if (handleMode == HandleMode.Always || 165 | (handleMode == HandleMode.Changing && modifyMode == ModifyMode.Grow)) { 166 | drawHandles(canvas); 167 | } 168 | } 169 | } 170 | 171 | /* 172 | * Fall back to naive method for darkening outside crop area 173 | */ 174 | private void drawOutsideFallback(Canvas canvas) { 175 | canvas.drawRect(0, 0, canvas.getWidth(), drawRect.top, outsidePaint); 176 | canvas.drawRect(0, drawRect.bottom, canvas.getWidth(), canvas.getHeight(), outsidePaint); 177 | canvas.drawRect(0, drawRect.top, drawRect.left, drawRect.bottom, outsidePaint); 178 | canvas.drawRect(drawRect.right, drawRect.top, canvas.getWidth(), drawRect.bottom, outsidePaint); 179 | } 180 | 181 | /* 182 | * Clip path is broken, unreliable or not supported on: 183 | * - JellyBean MR1 184 | * - ICS & ICS MR1 with hardware acceleration turned on 185 | */ 186 | @SuppressLint("NewApi") 187 | private boolean isClipPathSupported(Canvas canvas) { 188 | if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { 189 | return false; 190 | } else if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) 191 | || Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { 192 | return true; 193 | } else { 194 | return !canvas.isHardwareAccelerated(); 195 | } 196 | } 197 | 198 | private void drawHandles(Canvas canvas) { 199 | int xMiddle = drawRect.left + ((drawRect.right - drawRect.left) / 2); 200 | int yMiddle = drawRect.top + ((drawRect.bottom - drawRect.top) / 2); 201 | 202 | canvas.drawCircle(drawRect.left, yMiddle, handleRadius, handlePaint); 203 | canvas.drawCircle(xMiddle, drawRect.top, handleRadius, handlePaint); 204 | canvas.drawCircle(drawRect.right, yMiddle, handleRadius, handlePaint); 205 | canvas.drawCircle(xMiddle, drawRect.bottom, handleRadius, handlePaint); 206 | } 207 | 208 | private void drawThirds(Canvas canvas) { 209 | outlinePaint.setStrokeWidth(1); 210 | float xThird = (drawRect.right - drawRect.left) / 3; 211 | float yThird = (drawRect.bottom - drawRect.top) / 3; 212 | 213 | canvas.drawLine(drawRect.left + xThird, drawRect.top, 214 | drawRect.left + xThird, drawRect.bottom, outlinePaint); 215 | canvas.drawLine(drawRect.left + xThird * 2, drawRect.top, 216 | drawRect.left + xThird * 2, drawRect.bottom, outlinePaint); 217 | canvas.drawLine(drawRect.left, drawRect.top + yThird, 218 | drawRect.right, drawRect.top + yThird, outlinePaint); 219 | canvas.drawLine(drawRect.left, drawRect.top + yThird * 2, 220 | drawRect.right, drawRect.top + yThird * 2, outlinePaint); 221 | } 222 | 223 | private void drawCircle(Canvas canvas) { 224 | outlinePaint.setStrokeWidth(1); 225 | canvas.drawOval(new RectF(drawRect), outlinePaint); 226 | } 227 | 228 | public void setMode(ModifyMode mode) { 229 | if (mode != modifyMode) { 230 | modifyMode = mode; 231 | viewContext.invalidate(); 232 | } 233 | } 234 | 235 | // Determines which edges are hit by touching at (x, y) 236 | public int getHit(float x, float y) { 237 | Rect r = computeLayout(); 238 | final float hysteresis = 20F; 239 | int retval = GROW_NONE; 240 | 241 | // verticalCheck makes sure the position is between the top and 242 | // the bottom edge (with some tolerance). Similar for horizCheck. 243 | boolean verticalCheck = (y >= r.top - hysteresis) 244 | && (y < r.bottom + hysteresis); 245 | boolean horizCheck = (x >= r.left - hysteresis) 246 | && (x < r.right + hysteresis); 247 | 248 | // Check whether the position is near some edge(s) 249 | if ((Math.abs(r.left - x) < hysteresis) && verticalCheck) { 250 | retval |= GROW_LEFT_EDGE; 251 | } 252 | if ((Math.abs(r.right - x) < hysteresis) && verticalCheck) { 253 | retval |= GROW_RIGHT_EDGE; 254 | } 255 | if ((Math.abs(r.top - y) < hysteresis) && horizCheck) { 256 | retval |= GROW_TOP_EDGE; 257 | } 258 | if ((Math.abs(r.bottom - y) < hysteresis) && horizCheck) { 259 | retval |= GROW_BOTTOM_EDGE; 260 | } 261 | 262 | // Not near any edge but inside the rectangle: move 263 | if (retval == GROW_NONE && r.contains((int) x, (int) y)) { 264 | retval = MOVE; 265 | } 266 | return retval; 267 | } 268 | 269 | // Handles motion (dx, dy) in screen space. 270 | // The "edge" parameter specifies which edges the user is dragging. 271 | void handleMotion(int edge, float dx, float dy) { 272 | Rect r = computeLayout(); 273 | if (edge == MOVE) { 274 | // Convert to image space before sending to moveBy() 275 | moveBy(dx * (cropRect.width() / r.width()), 276 | dy * (cropRect.height() / r.height())); 277 | } else { 278 | if (((GROW_LEFT_EDGE | GROW_RIGHT_EDGE) & edge) == 0) { 279 | dx = 0; 280 | } 281 | 282 | if (((GROW_TOP_EDGE | GROW_BOTTOM_EDGE) & edge) == 0) { 283 | dy = 0; 284 | } 285 | 286 | // Convert to image space before sending to growBy() 287 | float xDelta = dx * (cropRect.width() / r.width()); 288 | float yDelta = dy * (cropRect.height() / r.height()); 289 | growBy((((edge & GROW_LEFT_EDGE) != 0) ? -1 : 1) * xDelta, 290 | (((edge & GROW_TOP_EDGE) != 0) ? -1 : 1) * yDelta); 291 | } 292 | } 293 | 294 | // Grows the cropping rectangle by (dx, dy) in image space 295 | void moveBy(float dx, float dy) { 296 | Rect invalRect = new Rect(drawRect); 297 | 298 | cropRect.offset(dx, dy); 299 | 300 | // Put the cropping rectangle inside image rectangle 301 | cropRect.offset( 302 | Math.max(0, imageRect.left - cropRect.left), 303 | Math.max(0, imageRect.top - cropRect.top)); 304 | 305 | cropRect.offset( 306 | Math.min(0, imageRect.right - cropRect.right), 307 | Math.min(0, imageRect.bottom - cropRect.bottom)); 308 | 309 | drawRect = computeLayout(); 310 | invalRect.union(drawRect); 311 | invalRect.inset(-(int) handleRadius, -(int) handleRadius); 312 | viewContext.invalidate(invalRect); 313 | } 314 | 315 | // Grows the cropping rectangle by (dx, dy) in image space. 316 | void growBy(float dx, float dy) { 317 | if (maintainAspectRatio) { 318 | if (dx != 0) { 319 | dy = dx / initialAspectRatio; 320 | } else if (dy != 0) { 321 | dx = dy * initialAspectRatio; 322 | } 323 | } 324 | 325 | // Don't let the cropping rectangle grow too fast. 326 | // Grow at most half of the difference between the image rectangle and 327 | // the cropping rectangle. 328 | RectF r = new RectF(cropRect); 329 | if (dx > 0F && r.width() + 2 * dx > imageRect.width()) { 330 | dx = (imageRect.width() - r.width()) / 2F; 331 | if (maintainAspectRatio) { 332 | dy = dx / initialAspectRatio; 333 | } 334 | } 335 | if (dy > 0F && r.height() + 2 * dy > imageRect.height()) { 336 | dy = (imageRect.height() - r.height()) / 2F; 337 | if (maintainAspectRatio) { 338 | dx = dy * initialAspectRatio; 339 | } 340 | } 341 | 342 | r.inset(-dx, -dy); 343 | 344 | // Don't let the cropping rectangle shrink too fast 345 | final float widthCap = 25F; 346 | if (r.width() < widthCap) { 347 | r.inset(-(widthCap - r.width()) / 2F, 0F); 348 | } 349 | float heightCap = maintainAspectRatio 350 | ? (widthCap / initialAspectRatio) 351 | : widthCap; 352 | if (r.height() < heightCap) { 353 | r.inset(0F, -(heightCap - r.height()) / 2F); 354 | } 355 | 356 | // Put the cropping rectangle inside the image rectangle 357 | if (r.left < imageRect.left) { 358 | r.offset(imageRect.left - r.left, 0F); 359 | } else if (r.right > imageRect.right) { 360 | r.offset(-(r.right - imageRect.right), 0F); 361 | } 362 | if (r.top < imageRect.top) { 363 | r.offset(0F, imageRect.top - r.top); 364 | } else if (r.bottom > imageRect.bottom) { 365 | r.offset(0F, -(r.bottom - imageRect.bottom)); 366 | } 367 | 368 | cropRect.set(r); 369 | drawRect = computeLayout(); 370 | viewContext.invalidate(); 371 | } 372 | 373 | // Returns the cropping rectangle in image space with specified scale 374 | public Rect getScaledCropRect(float scale) { 375 | return new Rect((int) (cropRect.left * scale), (int) (cropRect.top * scale), 376 | (int) (cropRect.right * scale), (int) (cropRect.bottom * scale)); 377 | } 378 | 379 | // Maps the cropping rectangle from image space to screen space 380 | private Rect computeLayout() { 381 | RectF r = new RectF(cropRect.left, cropRect.top, 382 | cropRect.right, cropRect.bottom); 383 | matrix.mapRect(r); 384 | return new Rect(Math.round(r.left), Math.round(r.top), 385 | Math.round(r.right), Math.round(r.bottom)); 386 | } 387 | 388 | public void invalidate() { 389 | drawRect = computeLayout(); 390 | } 391 | 392 | public boolean hasFocus() { 393 | return isFocused; 394 | } 395 | 396 | public void setFocus(boolean isFocused) { 397 | this.isFocused = isFocused; 398 | } 399 | 400 | } 401 | -------------------------------------------------------------------------------- /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 196 | // defined as follows: if the image is scaled down below the 197 | // view's dimensions then center it (literally). If the image 198 | // is scaled larger than the view and is translated out of view 199 | // then translate it back into view (i.e. eliminate black bars). 200 | protected void center(boolean horizontal, boolean vertical) { 201 | final Bitmap bitmap = bitmapDisplayed.getBitmap(); 202 | if (bitmap == null) { 203 | return; 204 | } 205 | Matrix m = getImageViewMatrix(); 206 | 207 | RectF rect = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()); 208 | m.mapRect(rect); 209 | 210 | float height = rect.height(); 211 | float width = rect.width(); 212 | 213 | float deltaX = 0, deltaY = 0; 214 | 215 | if (vertical) { 216 | int viewHeight = getHeight(); 217 | if (height < viewHeight) { 218 | deltaY = (viewHeight - height) / 2 - rect.top; 219 | } else if (rect.top > 0) { 220 | deltaY = -rect.top; 221 | } else if (rect.bottom < viewHeight) { 222 | deltaY = getHeight() - rect.bottom; 223 | } 224 | } 225 | 226 | if (horizontal) { 227 | int viewWidth = getWidth(); 228 | if (width < viewWidth) { 229 | deltaX = (viewWidth - width) / 2 - rect.left; 230 | } else if (rect.left > 0) { 231 | deltaX = -rect.left; 232 | } else if (rect.right < viewWidth) { 233 | deltaX = viewWidth - rect.right; 234 | } 235 | } 236 | 237 | postTranslate(deltaX, deltaY); 238 | setImageMatrix(getImageViewMatrix()); 239 | } 240 | 241 | private void init() { 242 | setScaleType(ImageView.ScaleType.MATRIX); 243 | } 244 | 245 | protected float getValue(Matrix matrix, int whichValue) { 246 | matrix.getValues(matrixValues); 247 | return matrixValues[whichValue]; 248 | } 249 | 250 | // Get the scale factor out of the matrix. 251 | protected float getScale(Matrix matrix) { 252 | return getValue(matrix, Matrix.MSCALE_X); 253 | } 254 | 255 | protected float getScale() { 256 | return getScale(suppMatrix); 257 | } 258 | 259 | // Setup the base matrix so that the image is centered and scaled properly. 260 | private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix, boolean includeRotation) { 261 | float viewWidth = getWidth(); 262 | float viewHeight = getHeight(); 263 | 264 | float w = bitmap.getWidth(); 265 | float h = bitmap.getHeight(); 266 | matrix.reset(); 267 | 268 | // We limit up-scaling to 3x otherwise the result may look bad if it's a small icon 269 | float widthScale = Math.min(viewWidth / w, 3.0f); 270 | float heightScale = Math.min(viewHeight / h, 3.0f); 271 | float scale = Math.min(widthScale, heightScale); 272 | 273 | if (includeRotation) { 274 | matrix.postConcat(bitmap.getRotateMatrix()); 275 | } 276 | matrix.postScale(scale, scale); 277 | matrix.postTranslate((viewWidth - w * scale) / 2F, (viewHeight - h * scale) / 2F); 278 | } 279 | 280 | // Combine the base matrix and the supp matrix to make the final matrix 281 | protected Matrix getImageViewMatrix() { 282 | // The final matrix is computed as the concatentation of the base matrix 283 | // and the supplementary matrix 284 | displayMatrix.set(baseMatrix); 285 | displayMatrix.postConcat(suppMatrix); 286 | return displayMatrix; 287 | } 288 | 289 | public Matrix getUnrotatedMatrix(){ 290 | Matrix unrotated = new Matrix(); 291 | getProperBaseMatrix(bitmapDisplayed, unrotated, false); 292 | unrotated.postConcat(suppMatrix); 293 | return unrotated; 294 | } 295 | 296 | protected float calculateMaxZoom() { 297 | if (bitmapDisplayed.getBitmap() == null) { 298 | return 1F; 299 | } 300 | 301 | float fw = (float) bitmapDisplayed.getWidth() / (float) thisWidth; 302 | float fh = (float) bitmapDisplayed.getHeight() / (float) thisHeight; 303 | return Math.max(fw, fh) * 4; // 400% 304 | } 305 | 306 | protected void zoomTo(float scale, float centerX, float centerY) { 307 | if (scale > maxZoom) { 308 | scale = maxZoom; 309 | } 310 | 311 | float oldScale = getScale(); 312 | float deltaScale = scale / oldScale; 313 | 314 | suppMatrix.postScale(deltaScale, deltaScale, centerX, centerY); 315 | setImageMatrix(getImageViewMatrix()); 316 | center(true, true); 317 | } 318 | 319 | protected void zoomTo(final float scale, final float centerX, 320 | final float centerY, final float durationMs) { 321 | final float incrementPerMs = (scale - getScale()) / durationMs; 322 | final float oldScale = getScale(); 323 | final long startTime = System.currentTimeMillis(); 324 | 325 | handler.post(new Runnable() { 326 | public void run() { 327 | long now = System.currentTimeMillis(); 328 | float currentMs = Math.min(durationMs, now - startTime); 329 | float target = oldScale + (incrementPerMs * currentMs); 330 | zoomTo(target, centerX, centerY); 331 | 332 | if (currentMs < durationMs) { 333 | handler.post(this); 334 | } 335 | } 336 | }); 337 | } 338 | 339 | protected void zoomTo(float scale) { 340 | float cx = getWidth() / 2F; 341 | float cy = getHeight() / 2F; 342 | zoomTo(scale, cx, cy); 343 | } 344 | 345 | protected void zoomIn() { 346 | zoomIn(SCALE_RATE); 347 | } 348 | 349 | protected void zoomOut() { 350 | zoomOut(SCALE_RATE); 351 | } 352 | 353 | protected void zoomIn(float rate) { 354 | if (getScale() >= maxZoom) { 355 | return; // Don't let the user zoom into the molecular level 356 | } 357 | if (bitmapDisplayed.getBitmap() == null) { 358 | return; 359 | } 360 | 361 | float cx = getWidth() / 2F; 362 | float cy = getHeight() / 2F; 363 | 364 | suppMatrix.postScale(rate, rate, cx, cy); 365 | setImageMatrix(getImageViewMatrix()); 366 | } 367 | 368 | protected void zoomOut(float rate) { 369 | if (bitmapDisplayed.getBitmap() == null) { 370 | return; 371 | } 372 | 373 | float cx = getWidth() / 2F; 374 | float cy = getHeight() / 2F; 375 | 376 | // Zoom out to at most 1x 377 | Matrix tmp = new Matrix(suppMatrix); 378 | tmp.postScale(1F / rate, 1F / rate, cx, cy); 379 | 380 | if (getScale(tmp) < 1F) { 381 | suppMatrix.setScale(1F, 1F, cx, cy); 382 | } else { 383 | suppMatrix.postScale(1F / rate, 1F / rate, cx, cy); 384 | } 385 | setImageMatrix(getImageViewMatrix()); 386 | center(true, true); 387 | } 388 | 389 | protected void postTranslate(float dx, float dy) { 390 | suppMatrix.postTranslate(dx, dy); 391 | } 392 | 393 | protected void panBy(float dx, float dy) { 394 | postTranslate(dx, dy); 395 | setImageMatrix(getImageViewMatrix()); 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /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/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/lib/src/main/res/drawable-hdpi/crop__divider.9.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-hdpi/crop__ic_cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/lib/src/main/res/drawable-hdpi/crop__ic_cancel.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-hdpi/crop__ic_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/lib/src/main/res/drawable-hdpi/crop__ic_done.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-mdpi/crop__divider.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/lib/src/main/res/drawable-mdpi/crop__divider.9.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-mdpi/crop__ic_cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/lib/src/main/res/drawable-mdpi/crop__ic_cancel.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-mdpi/crop__ic_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/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/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/lib/src/main/res/drawable-xhdpi/crop__divider.9.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-xhdpi/crop__ic_cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/lib/src/main/res/drawable-xhdpi/crop__ic_cancel.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-xhdpi/crop__ic_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/lib/src/main/res/drawable-xhdpi/crop__ic_done.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-xhdpi/crop__tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/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 | 8 | 9 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /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-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-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-id/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-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-v21/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #aaaaaa 4 | 5 | -------------------------------------------------------------------------------- /lib/src/main/res/values-zh/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/Skykai521/android-crop-master/b617038f4212b420f3904b8def8f7043e0427b9f/screenshot.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':lib', ':example' 2 | --------------------------------------------------------------------------------