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