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