diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..82ca65e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# svn +*.svn* + +# built application files +*.apk +*.ap_ + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated GUI files +*/R.java + +# generated folder +bin +gen + +# local +local.properties + +proguard_logs/ + +# log files +log*.txt + +# archives +*.gz +*.tar +*.zip + +# eclipse +*.metadata +*.settings +*.prefs + +#idea +*.idea +*.iml +out/ + +build/ +.gradle/ + +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..48a72dd1 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# uCrop - Image Cropping Library for Android + +#### This project aims to provide an ultimate and flexible image cropping experience. Made in [Yalantis] (https://yalantis.com/?utm_source=github) + +# Usage + +*For a working implementation, please have a look at the Sample Project - sample* + +1. Include the library as local library project. + + ``` compile 'com.yalantis:ucrop:[latest version]' ``` + + +2. The uCrop configuration is created using the builder pattern. + + ```java + UCrop.of(sourceUri, destinationUri) + .withAspectRatio(16, 9) + .withMaxResultSize(maxWidth, maxHeight) + .start(context); + ``` + + +3. Override `onActivityResult` method and handle uCrop result. + + ```java + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK && requestCode == UCrop.REQUEST_CROP) { + final Uri resultUri = UCrop.getOutput(data); + } else if (resultCode == UCrop.RESULT_ERROR) { + final Throwable cropError = UCrop.getError(data); + } + } + ``` + + +# Customization + +uCrop builder class has method `withOptions(UCrop.Options option)` which extends library configurations. + +Currently you can change: + + * image compression format (e.g. PNG, JPEG, WEBP), compression + * image compression quality [0 - 100]. PNG which is lossless, will ignore the quality setting. + * whether all gestures are enabled simultaneously + * maximum size for Bitmap that is decoded from source Uri and used within crop view. If you want to override default behaviour. + * more coming... (e.g. color pallet) + +# Compatibility + + * Library - Android GINGERBREAD 2.3+ + * Sample - Android ICS 4.0+ + +# Changelog + +### Version: 1.0 + + * Initial Build + +### Let us know! + +We’d be really happy if you sent us links to your projects where you use our component. Just send an email to github@yalantis.com And do let us know if you have any questions or suggestion regarding the library. + +## License + + Copyright 2016, Yalantis + + 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. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..eba60cc0 --- /dev/null +++ b/build.gradle @@ -0,0 +1,27 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.0.0-alpha5' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +def isReleaseBuild() { + return version.contains("SNAPSHOT") == false +} + +allprojects { + repositories { + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/captures/com.yalantis.ucrop.sample_2016.01.15_14.06.alloc b/captures/com.yalantis.ucrop.sample_2016.01.15_14.06.alloc new file mode 100644 index 00000000..780825b8 Binary files /dev/null and b/captures/com.yalantis.ucrop.sample_2016.01.15_14.06.alloc differ diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..120529d0 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,33 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +VERSION_NAME=1.0.0 +VERSION_CODE=1 +GROUP=com.yalantis + +POM_DESCRIPTION=Android Library for cropping images +POM_URL=https://github.com/Yalantis/uCrop +POM_SCM_URL=https://github.com/Yalantis/uCrop +POM_SCM_CONNECTION=scm:git@github.com/Yalantis/uCrop.git +POM_SCM_DEV_CONNECTION=scm:git@github.com/Yalantis/uCrop.git +POM_LICENCE_NAME=The Apache Software License, Version 2.0 +POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0 +POM_LICENCE_DIST=repo +POM_DEVELOPER_ID=yalantis +POM_DEVELOPER_NAME=Yalantis \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..8c0fb64a Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..838084e4 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jan 15 00:55:26 EET 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 diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..91a7e269 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/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 "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..8a0b282a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@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 diff --git a/mavenpush.gradle b/mavenpush.gradle new file mode 100644 index 00000000..b741e61b --- /dev/null +++ b/mavenpush.gradle @@ -0,0 +1,92 @@ +apply plugin: 'maven' +apply plugin: 'signing' + +def sonatypeRepositoryUrl +if (isReleaseBuild()) { + println 'RELEASE BUILD' + sonatypeRepositoryUrl = hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL + : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" +} else { + println 'DEBUG BUILD' + sonatypeRepositoryUrl = hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL + : "https://oss.sonatype.org/content/repositories/snapshots/" +} + +def getRepositoryUsername() { + return hasProperty('nexusUsername') ? nexusUsername : "" +} + +def getRepositoryPassword() { + return hasProperty('nexusPassword') ? nexusPassword : "" +} + +afterEvaluate { project -> + uploadArchives { + repositories { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + pom.artifactId = POM_ARTIFACT_ID + + repository(url: sonatypeRepositoryUrl) { + authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) + } + + pom.project { + name POM_NAME + packaging POM_PACKAGING + description POM_DESCRIPTION + url POM_URL + + scm { + url POM_SCM_URL + connection POM_SCM_CONNECTION + developerConnection POM_SCM_DEV_CONNECTION + } + + licenses { + license { + name POM_LICENCE_NAME + url POM_LICENCE_URL + distribution POM_LICENCE_DIST + } + } + + developers { + developer { + id POM_DEVELOPER_ID + name POM_DEVELOPER_NAME + } + } + } + } + } + } + + signing { + required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } + sign configurations.archives + } + + task androidJavadocs(type: Javadoc) { + source = android.sourceSets.main.java.sourceFiles + } + + task androidJavadocsJar(type: Jar) { + classifier = 'javadoc' + //basename = artifact_id + from androidJavadocs.destinationDir + } + + task androidSourcesJar(type: Jar) { + classifier = 'sources' + //basename = artifact_id + from android.sourceSets.main.java.sourceFiles + } + + artifacts { + //archives packageReleaseJar + archives androidSourcesJar + archives androidJavadocsJar + } +} \ No newline at end of file diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 00000000..3651e3e7 --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + defaultConfig { + applicationId "com.yalantis.ucrop.sample" + minSdkVersion 15 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:23.1.1' + + compile project (':ucrop') +} \ No newline at end of file diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro new file mode 100644 index 00000000..0cd55489 --- /dev/null +++ b/sample/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/oleksii/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 00000000..3006bb5a --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/src/main/java/com/yalantis/ucrop/sample/BaseActivity.java b/sample/src/main/java/com/yalantis/ucrop/sample/BaseActivity.java new file mode 100644 index 00000000..e754819e --- /dev/null +++ b/sample/src/main/java/com/yalantis/ucrop/sample/BaseActivity.java @@ -0,0 +1,76 @@ +package com.yalantis.ucrop.sample; + +import android.content.DialogInterface; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.ActivityCompat; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; + +/** + * Created by Oleksii Shliama (https://github.com/shliama). + */ +public class BaseActivity extends AppCompatActivity { + + protected static final int REQUEST_STORAGE_READ_ACCESS_PERMISSION = 101; + protected static final int REQUEST_STORAGE_WRITE_ACCESS_PERMISSION = 102; + + private AlertDialog mAlertDialog; + + /** + * Hide alert dialog if any. + */ + @Override + protected void onStop() { + super.onStop(); + if (mAlertDialog != null && mAlertDialog.isShowing()) { + mAlertDialog.dismiss(); + } + } + + + /** + * Requests given permission. + * If the permission has been denied previously, a Dialog will prompt the user to grant the + * permission, otherwise it is requested directly. + */ + protected void requestPermission(final String permission, String rationale, final int requestCode) { + if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) { + showAlertDialog(getString(R.string.permission_title_rationale), rationale, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ActivityCompat.requestPermissions(BaseActivity.this, + new String[]{permission}, requestCode); + } + }, getString(R.string.label_ok), null, getString(R.string.label_cancel)); + } else { + ActivityCompat.requestPermissions(this, new String[]{permission}, requestCode); + } + } + + /** + * This method shows dialog with given title & message. + * Also there is an option to pass onClickListener for positive & negative button. + * + * @param title - dialog title + * @param message - dialog message + * @param onPositiveButtonClickListener - listener for positive button + * @param positiveText - positive button text + * @param onNegativeButtonClickListener - listener for negative button + * @param negativeText - negative button text + */ + protected void showAlertDialog(@Nullable String title, @Nullable String message, + @Nullable DialogInterface.OnClickListener onPositiveButtonClickListener, + @NonNull String positiveText, + @Nullable DialogInterface.OnClickListener onNegativeButtonClickListener, + @NonNull String negativeText) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(title); + builder.setMessage(message); + builder.setPositiveButton(positiveText, onPositiveButtonClickListener); + builder.setNegativeButton(negativeText, onNegativeButtonClickListener); + mAlertDialog = builder.show(); + } + +} diff --git a/sample/src/main/java/com/yalantis/ucrop/sample/ResultActivity.java b/sample/src/main/java/com/yalantis/ucrop/sample/ResultActivity.java new file mode 100644 index 00000000..f93c4bd3 --- /dev/null +++ b/sample/src/main/java/com/yalantis/ucrop/sample/ResultActivity.java @@ -0,0 +1,155 @@ +package com.yalantis.ucrop.sample; + +import android.Manifest; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.NotificationCompat; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ImageView; +import android.widget.Toast; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.nio.channels.FileChannel; +import java.util.Calendar; + +/** + * Created by Oleksii Shliama (https://github.com/shliama). + */ +public class ResultActivity extends BaseActivity { + + private static final String TAG = "ResultActivity"; + private static final int DOWNLOAD_NOTIFICATION_ID_DONE = 911; + + public static void startWithUri(@NonNull Context context, @NonNull Uri uri) { + Intent intent = new Intent(context, ResultActivity.class); + intent.setData(uri); + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_result); + + ((ImageView) findViewById(R.id.image_view_preview)).setImageURI(getIntent().getData()); + + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(new File(getIntent().getData().getPath()).getAbsolutePath(), options); + + setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); + final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setTitle(getString(R.string.format_crop_result_d_d, options.outWidth, options.outHeight)); + } + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + getMenuInflater().inflate(R.menu.menu_result, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + case R.id.menu_download: + saveCroppedImage(); + break; + } + return super.onOptionsItemSelected(item); + } + + + /** + * Callback received when a permissions request has been completed. + */ + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case REQUEST_STORAGE_WRITE_ACCESS_PERMISSION: + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + saveCroppedImage(); + } + break; + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + private void saveCroppedImage() { + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, + getString(R.string.permission_write_storage_rationale), + REQUEST_STORAGE_WRITE_ACCESS_PERMISSION); + } else { + Uri imageUri = getIntent().getData(); + if (imageUri != null && imageUri.getScheme().equals("file")) { + try { + copyFileToDownloads(getIntent().getData()); + } catch (Exception e) { + Toast.makeText(ResultActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show(); + Log.e(TAG, imageUri.toString(), e); + } + } else { + Toast.makeText(ResultActivity.this, getString(R.string.toast_unexpected_error), Toast.LENGTH_SHORT).show(); + } + } + } + + private void copyFileToDownloads(Uri croppedFileUri) throws Exception { + String downloadsDirectoryPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(); + String filename = String.format("%d_%s", Calendar.getInstance().getTimeInMillis(), croppedFileUri.getLastPathSegment()); + + File saveFile = new File(downloadsDirectoryPath, filename); + + FileInputStream inStream = new FileInputStream(new File(croppedFileUri.getPath())); + FileOutputStream outStream = new FileOutputStream(saveFile); + FileChannel inChannel = inStream.getChannel(); + FileChannel outChannel = outStream.getChannel(); + inChannel.transferTo(0, inChannel.size(), outChannel); + inStream.close(); + outStream.close(); + + showNotification(saveFile); + } + + private void showNotification(@NonNull File file) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setDataAndType(Uri.fromFile(file), "image/*"); + + NotificationCompat.Builder mNotification = new NotificationCompat.Builder(this); + + mNotification + .setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.notification_image_saved_click_to_preview)) + .setTicker(getString(R.string.notification_image_saved)) + .setSmallIcon(R.drawable.ic_done) + .setOngoing(false) + .setContentIntent(PendingIntent.getActivity(this, 0, intent, 0)) + .setAutoCancel(true); + ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).notify(DOWNLOAD_NOTIFICATION_ID_DONE, mNotification.build()); + } + +} diff --git a/sample/src/main/java/com/yalantis/ucrop/sample/SampleActivity.java b/sample/src/main/java/com/yalantis/ucrop/sample/SampleActivity.java new file mode 100644 index 00000000..65f55ddc --- /dev/null +++ b/sample/src/main/java/com/yalantis/ucrop/sample/SampleActivity.java @@ -0,0 +1,254 @@ +package com.yalantis.ucrop.sample; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.util.Log; +import android.view.View; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.RadioGroup; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.yalantis.ucrop.UCrop; +import com.yalantis.ucrop.UCropActivity; + +import java.io.File; + +/** + * Created by Oleksii Shliama (https://github.com/shliama). + */ +public class SampleActivity extends BaseActivity { + + private static final String TAG = "SampleActivity"; + + private static final int REQUEST_SELECT_PICTURE = 0x01; + + private static final int SAMPLE_IMAGE_MAX_SIZE_WIDTH = 200; + private static final int SAMPLE_IMAGE_MAX_SIZE_HEIGHT = 300; + private static final String SAMPLE_CROPPED_IMAGE_NAME = "SampleCropImage.jpeg"; + + private RadioGroup mRadioGroupAspectRatio, mRadioGroupCompressionSettings; + private EditText mEditTextMaxWidth, mEditTextMaxHeight; + private CheckBox mCheckBoxMaxSize; + private SeekBar mSeekBarQuality; + private TextView mTextViewQuality; + + private Uri mDestinationUri; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_sample); + + mDestinationUri = Uri.fromFile(new File(getCacheDir(), SAMPLE_CROPPED_IMAGE_NAME)); + + setupUI(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_SELECT_PICTURE) { + final Uri selectedUri = data.getData(); + if (selectedUri != null) { + startCropActivity(data.getData()); + } else { + Toast.makeText(SampleActivity.this, R.string.toast_cannot_retrieve_selected_image, Toast.LENGTH_SHORT).show(); + } + } else if (requestCode == UCrop.REQUEST_CROP) { + handleCropResult(data); + } + } + if (resultCode == UCrop.RESULT_ERROR) { + handleCropError(data); + } + } + + /** + * Callback received when a permissions request has been completed. + */ + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case REQUEST_STORAGE_READ_ACCESS_PERMISSION: + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + pickFromGallery(); + } + break; + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + private void setupUI() { + findViewById(R.id.button_crop).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + pickFromGallery(); + } + }); + + mRadioGroupAspectRatio = ((RadioGroup) findViewById(R.id.radio_group_aspect_ratio)); + mRadioGroupCompressionSettings = ((RadioGroup) findViewById(R.id.radio_group_compression_settings)); + mCheckBoxMaxSize = ((CheckBox) findViewById(R.id.checkbox_max_size)); + mEditTextMaxWidth = ((EditText) findViewById(R.id.edit_text_max_width)); + mEditTextMaxHeight = ((EditText) findViewById(R.id.edit_text_max_height)); + mSeekBarQuality = ((SeekBar) findViewById(R.id.seekbar_quality)); + mTextViewQuality = ((TextView) findViewById(R.id.text_view_quality)); + + mRadioGroupAspectRatio.check(R.id.radio_dynamic); + mRadioGroupCompressionSettings.check(R.id.radio_jpeg); + mSeekBarQuality.setProgress(UCropActivity.DEFAULT_COMPRESS_QUALITY); + mTextViewQuality.setText(String.format(getString(R.string.format_quality_d), mSeekBarQuality.getProgress())); + mEditTextMaxWidth.setText(String.valueOf(SAMPLE_IMAGE_MAX_SIZE_WIDTH)); + mEditTextMaxHeight.setText(String.valueOf(SAMPLE_IMAGE_MAX_SIZE_HEIGHT)); + mSeekBarQuality.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + mTextViewQuality.setText(String.format(getString(R.string.format_quality_d), progress)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + }); + } + + private void pickFromGallery() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN // Permission was added in API Level 16 + && ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + requestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, + getString(R.string.permission_read_storage_rationale), + REQUEST_STORAGE_READ_ACCESS_PERMISSION); + } else { + Intent intent = new Intent(); + intent.setType("image/*"); + intent.setAction(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + startActivityForResult(Intent.createChooser(intent, getString(R.string.label_select_picture)), REQUEST_SELECT_PICTURE); + } + } + + private void startCropActivity(@NonNull Uri uri) { + UCrop uCrop = UCrop.of(uri, mDestinationUri); + + uCrop = basisConfig(uCrop); + uCrop = advancedConfig(uCrop); + + uCrop.start(SampleActivity.this); + } + + /** + * In most cases you need only to set crop aspect ration and max size for resulting image. + * + * @param uCrop - ucrop builder instance + * @return - ucrop builder instance + */ + private UCrop basisConfig(@NonNull UCrop uCrop) { + switch (mRadioGroupAspectRatio.getCheckedRadioButtonId()) { + case R.id.radio_origin: + uCrop = uCrop.useSourceImageAspectRatio(); + break; + case R.id.radio_square: + uCrop = uCrop.withAspectRatio(1, 1); + break; + case R.id.radio_16_9: + uCrop = uCrop.withAspectRatio(16, 9); + break; + case R.id.radio_dynamic: + default: + // do nothing + break; + } + + if (mCheckBoxMaxSize.isChecked()) { + try { + int maxWidth = Integer.valueOf(mEditTextMaxWidth.getText().toString().trim()); + int maxHeight = Integer.valueOf(mEditTextMaxHeight.getText().toString().trim()); + if (maxWidth > 0 && maxHeight > 0) { + uCrop = uCrop.withMaxResultSize(maxWidth, maxHeight); + } + } catch (NumberFormatException e) { + Log.e(TAG, "Number please", e); + } + } + + return uCrop; + } + + /** + * Sometimes you want to adjust more options, it's done via {@link com.yalantis.ucrop.UCrop.Options} class. + * + * @param uCrop - ucrop builder instance + * @return - ucrop builder instance + */ + private UCrop advancedConfig(@NonNull UCrop uCrop) { + UCrop.Options options = new UCrop.Options(); + + switch (mRadioGroupCompressionSettings.getCheckedRadioButtonId()) { + case R.id.radio_png: + options.setCompressionFormat(Bitmap.CompressFormat.PNG); + break; + case R.id.radio_webp: + options.setCompressionFormat(Bitmap.CompressFormat.WEBP); + break; + case R.id.radio_jpeg: + default: + options.setCompressionFormat(Bitmap.CompressFormat.JPEG); + break; + } + options.setCompressionQuality(mSeekBarQuality.getProgress()); + + /* + If you want to unlock all gestures for all UCropActivity tabs + + options.setGesturesAlwaysEnabled(true); + * */ + + /* + This sets max size for bitmap that will be decoded from source Uri. + More size - more memory allocation, default implementation uses screen diagonal. + + options.setMaxBitmapSize(640); + * */ + + return uCrop.withOptions(options); + } + + private void handleCropResult(@NonNull Intent result) { + final Uri resultUri = UCrop.getOutput(result); + if (resultUri != null) { + ResultActivity.startWithUri(SampleActivity.this, resultUri); + } else { + Toast.makeText(SampleActivity.this, R.string.toast_cannot_retrieve_cropped_image, Toast.LENGTH_SHORT).show(); + } + } + + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + private void handleCropError(@NonNull Intent result) { + final Throwable cropError = UCrop.getError(result); + if (cropError != null) { + Log.e(TAG, "handleCropError: ", cropError); + Toast.makeText(SampleActivity.this, cropError.getMessage(), Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(SampleActivity.this, R.string.toast_unexpected_error, Toast.LENGTH_SHORT).show(); + } + } + +} diff --git a/sample/src/main/res/drawable/bg_rounded_rectangle.xml b/sample/src/main/res/drawable/bg_rounded_rectangle.xml new file mode 100644 index 00000000..abe9809b --- /dev/null +++ b/sample/src/main/res/drawable/bg_rounded_rectangle.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/sample/src/main/res/drawable/ic_done.xml b/sample/src/main/res/drawable/ic_done.xml new file mode 100644 index 00000000..7affe9ba --- /dev/null +++ b/sample/src/main/res/drawable/ic_done.xml @@ -0,0 +1,9 @@ + + + diff --git a/sample/src/main/res/drawable/ic_file_download.xml b/sample/src/main/res/drawable/ic_file_download.xml new file mode 100644 index 00000000..99aa581f --- /dev/null +++ b/sample/src/main/res/drawable/ic_file_download.xml @@ -0,0 +1,9 @@ + + + diff --git a/sample/src/main/res/layout/activity_result.xml b/sample/src/main/res/layout/activity_result.xml new file mode 100644 index 00000000..f645ef02 --- /dev/null +++ b/sample/src/main/res/layout/activity_result.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_sample.xml b/sample/src/main/res/layout/activity_sample.xml new file mode 100644 index 00000000..a57cb9a7 --- /dev/null +++ b/sample/src/main/res/layout/activity_sample.xml @@ -0,0 +1,212 @@ + + + + + + + + + +