├── .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 | [](https://travis-ci.org/jdamcd/android-crop)
6 | [](http://search.maven.org/#artifactdetails%7Ccom.soundcloud.android%7Candroid-crop%7C1.0.0%7Caar.asc)
7 | [](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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------