Compare commits

...

No commits in common. "master" and "gh-pages" have entirely different histories.

83 changed files with 907 additions and 3178 deletions

7
.gitignore vendored
View File

@ -1,7 +0,0 @@
/local.properties
/.idea
build/
.gradle
.DS_Store
*.iml

View File

@ -1,59 +0,0 @@
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'
}
}
}
}
}
}

View File

@ -1,13 +0,0 @@
language: android
sudo: false
android:
components:
- build-tools-23.0.1
- android-23
- extra-android-support
- extra-android-m2repository
script:
- ./gradlew clean build

View File

@ -1,34 +0,0 @@
## 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

View File

@ -1,84 +0,0 @@
> 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.
[![build status](https://travis-ci.org/jdamcd/android-crop.svg)](https://travis-ci.org/jdamcd/android-crop)
[![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
<activity android:name="com.soundcloud.android.crop.CropImageActivity" />
```
#### 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'
```
#### Users
Apps that use this library include: [SoundCloud](https://play.google.com/store/apps/details?id=com.soundcloud.android), [Depop](https://play.google.com/store/apps/details?id=com.depop), [Polyvore](https://play.google.com/store/apps/details?id=com.polyvore), [TextSecure](https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms)
## 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 2015 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.

View File

@ -1,17 +0,0 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
}
}
allprojects {
group = 'com.soundcloud.android'
version = project.VERSION
repositories {
mavenCentral()
}
}

View File

@ -1,19 +0,0 @@
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')
}

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.soundcloud.android.crop.example" >
<application
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
android:allowBackup="true"
android:theme="@style/CustomTheme">
<activity
android:name="com.soundcloud.android.crop.example.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.soundcloud.android.crop.CropImageActivity" />
</application>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>

View File

@ -1,65 +0,0 @@
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();
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/tile"
android:tileMode="repeat" />

View File

@ -1,14 +0,0 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.soundcloud.android.crop.example.MainActivity">
<ImageView
android:id="@+id/result_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:background="@drawable/texture" />
</FrameLayout>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/action_select"
android:title="@string/action_select"
android:showAsAction="always" />
</menu>

View File

@ -1,5 +0,0 @@
<resources>
<color name="highlight">#f3f3f3</color>
</resources>

View File

@ -1,6 +0,0 @@
<resources>
<string name="app_name">Crop Demo</string>
<string name="action_select">Pick</string>
</resources>

View File

@ -1,14 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style tools:ignore="NewApi" name="CustomTheme" parent="android:Theme.DeviceDefault">
<item name="cropImageStyle">@style/Widget.CropImageView</item>
</style>
<style name="Widget.CropImageView" parent="android:Widget">
<item name="showThirds">true</item>
<item name="showCircle">false</item>
<item name="showHandles">always</item>
<item name="highlightColor">@color/highlight</item>
</style>
</resources>

View File

@ -1,9 +0,0 @@
VERSION=1.0.1
VERSION_CODE=1
signing.keyId=63A46540
signing.secretKeyRingFile=
signing.password=
sonatypeUsername=jdamcd
sonatypePassword=

Binary file not shown.

View File

@ -1,6 +0,0 @@
#Tue Dec 30 00:50:07 CET 2014
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip

164
gradlew vendored
View File

@ -1,164 +0,0 @@
#!/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 "$@"

90
gradlew.bat vendored
View File

@ -1,90 +0,0 @@
@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

121
index.html Normal file
View File

@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="UTF-8">
<title>android-crop by jdamcd</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="stylesheets/normalize.css" media="screen">
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" type="text/css" href="stylesheets/stylesheet.css" media="screen">
<link rel="stylesheet" type="text/css" href="stylesheets/github-light.css" media="screen">
</head>
<body>
<section class="page-header">
<h1 class="project-name">android-crop</h1>
<h2 class="project-tagline">An Android library project for cropping images</h2>
<a href="https://github.com/jdamcd/android-crop" class="btn">View on GitHub</a>
<a href="https://github.com/jdamcd/android-crop/zipball/master" class="btn">Download .zip</a>
<a href="https://github.com/jdamcd/android-crop/tarball/master" class="btn">Download .tar.gz</a>
</section>
<section class="main-content">
<blockquote>
<p>I guess people are just cropping out all the sadness</p>
</blockquote>
<p>An Android library project to provide a simple image cropping <code>Activity</code>, based on code from AOSP.</p>
<p><a href="https://travis-ci.org/jdamcd/android-crop"><img src="https://travis-ci.org/jdamcd/android-crop.png" alt="build status"></a>
<a href="http://search.maven.org/#artifactdetails%7Ccom.soundcloud.android%7Candroid-crop%7C1.0.1%7Caar.asc"><img src="https://img.shields.io/badge/maven%20central-1.0.1-brightgreen.svg" alt="maven central"></a>
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-1.0.1-lightgrey.svg" alt="changelog"></a></p>
<h2>
<a id="features" class="anchor" href="#features" aria-hidden="true"><span class="octicon octicon-link"></span></a>Features</h2>
<ul>
<li>Gradle build &amp; AAR</li>
<li>Modern UI</li>
<li>Backwards compatible to SDK 10</li>
<li>Simple builder for configuration</li>
<li>Example project</li>
</ul>
<h2>
<a id="usage" class="anchor" href="#usage" aria-hidden="true"><span class="octicon octicon-link"></span></a>Usage</h2>
<p>First, declare <code>CropImageActivity</code> in your manifest file:</p>
<p><code>&lt;activity android:name="com.soundcloud.android.crop.CropImageActivity" /&gt;</code></p>
<h4>
<a id="crop" class="anchor" href="#crop" aria-hidden="true"><span class="octicon octicon-link"></span></a>Crop</h4>
<p><code>Crop.of(inputUri, outputUri).asSquare().start(activity)</code></p>
<p>Listen for the result of the crop (see example project if you want to do some error handling):</p>
<pre><code>@Override
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
if (requestCode == Crop.REQUEST_CROP &amp;&amp; resultCode == RESULT_OK) {
doSomethingWithCroppedImage(outputUri);
}
}
</code></pre>
<p>Some options are provided to style the crop screen. See example project theme.</p>
<h4>
<a id="pick" class="anchor" href="#pick" aria-hidden="true"><span class="octicon octicon-link"></span></a>Pick</h4>
<p>The library provides a utility method to start an image picker:</p>
<p><code>Crop.pickImage(activity)</code></p>
<h4>
<a id="dependency" class="anchor" href="#dependency" aria-hidden="true"><span class="octicon octicon-link"></span></a>Dependency</h4>
<p>The AAR is published on Maven Central:</p>
<p><code>compile 'com.soundcloud.android:android-crop:1.0.1@aar'</code></p>
<h4>
<a id="users" class="anchor" href="#users" aria-hidden="true"><span class="octicon octicon-link"></span></a>Users</h4>
<p>Apps that use this library include: <a href="https://play.google.com/store/apps/details?id=com.soundcloud.android">SoundCloud</a>, <a href="https://play.google.com/store/apps/details?id=com.depop">Depop</a>, <a href="https://play.google.com/store/apps/details?id=com.polyvore">Polyvore</a>, <a href="https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms">TextSecure</a></p>
<h2>
<a id="how-does-it-look" class="anchor" href="#how-does-it-look" aria-hidden="true"><span class="octicon octicon-link"></span></a>How does it look?</h2>
<p><img src="https://github.com/jdamcd/android-crop/blob/master/screenshot.png?raw=true" alt="android-crop screenshot"></p>
<h2>
<a id="license" class="anchor" href="#license" aria-hidden="true"><span class="octicon octicon-link"></span></a>License</h2>
<p>This project is based on the <a href="https://source.android.com">AOSP</a> camera image cropper via <a href="https://github.com/lvillani/android-cropimage">android-cropimage</a>.</p>
<pre><code>Copyright 2015 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.
</code></pre>
<footer class="site-footer">
<span class="site-footer-owner"><a href="https://github.com/jdamcd/android-crop">android-crop</a> is maintained by <a href="https://github.com/jdamcd">jdamcd</a>.</span>
<span class="site-footer-credits">This page was generated by <a href="https://pages.github.com">GitHub Pages</a> using the <a href="https://github.com/jasonlong/cayman-theme">Cayman theme</a> by <a href="https://twitter.com/jasonlong">Jason Long</a>.</span>
</footer>
</section>
</body>
</html>

View File

@ -1,30 +0,0 @@
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'
}

View File

@ -1,15 +0,0 @@
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());
}
}

View File

@ -1,77 +0,0 @@
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);
}
}

View File

@ -1 +0,0 @@
<manifest package="com.soundcloud.android.crop" />

View File

@ -1,256 +0,0 @@
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 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;
}
/**
* 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, R.string.crop__pick_error, Toast.LENGTH_SHORT).show();
}
}

View File

@ -1,437 +0,0 @@
/*
* 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 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);
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(Bitmap.CompressFormat.JPEG, 90, 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));
}
}

View File

@ -1,195 +0,0 @@
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<HighlightView> highlightViews = new ArrayList<HighlightView>();
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();
}
}

View File

@ -1,218 +0,0 @@
/*
* 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();
}
}
}

View File

@ -1,395 +0,0 @@
/*
* 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;
}
}

View File

@ -1,400 +0,0 @@
/*
* 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());
}
}

View File

@ -1,15 +0,0 @@
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);
}
}

View File

@ -1,86 +0,0 @@
/*
* 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<LifeCycleListener> listeners = new ArrayList<LifeCycleListener>();
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);
}
}
}

View File

@ -1,96 +0,0 @@
/*
* 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;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/crop__selector_pressed">
<item android:drawable="@color/crop__button_bar" />
</ripple>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 B

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:state_pressed="true">
<shape>
<solid android:color="@color/crop__selector_pressed" />
</shape>
</item>
<item android:state_focused="true">
<shape>
<solid android:color="@color/crop__selector_focused" />
</shape>
</item>
<item android:drawable="@android:color/transparent" />
</selector>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/crop__tile"
android:tileMode="repeat" />

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/done_cancel_bar"
layout="@layout/crop__layout_done_cancel" />
<com.soundcloud.android.crop.CropImageView
android:id="@+id/crop_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/crop__texture"
android:layout_below="@id/done_cancel_bar" />
</RelativeLayout>

View File

@ -1,16 +0,0 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Crop.DoneCancelBar">
<FrameLayout
android:id="@+id/btn_cancel"
style="@style/Crop.ActionButton">
<TextView style="@style/Crop.ActionButtonText.Cancel" />
</FrameLayout>
<FrameLayout
android:id="@+id/btn_done"
style="@style/Crop.ActionButton">
<TextView style="@style/Crop.ActionButtonText.Done" />
</FrameLayout>
</LinearLayout>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">جارى حفظ الصورة …</string>
<string name="crop__wait">رجاء الأنتظار …</string>
<string name="crop__pick_error">الصورة غير متاحة</string>
<string name="crop__done">تم</string>
<string name="crop__cancel" tools:ignore="ButtonCase">الغاء</string>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">Guardant imatge…</string>
<string name="crop__wait">Si us plau esperi…</string>
<string name="crop__pick_error">No hi ha imatges disponibles</string>
<string name="crop__done">ACCEPTAR</string>
<string name="crop__cancel" tools:ignore="ButtonCase">CANCEL·LAR</string>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">Bild speichern…</string>
<string name="crop__wait">Bitte warten…</string>
<string name="crop__pick_error">Keine Bildquellen verfügbar</string>
<string name="crop__done">übernehmen</string>
<string name="crop__cancel" tools:ignore="ButtonCase">abbrechen</string>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">Guardando imagen…</string>
<string name="crop__wait">Por favor espere…</string>
<string name="crop__pick_error">No hay imágenes disponibles</string>
<string name="crop__done">ACEPTAR</string>
<string name="crop__cancel" tools:ignore="ButtonCase">CANCELAR</string>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">Enregistrement de l\'image…</string>
<string name="crop__wait">Veuillez patienter…</string>
<string name="crop__pick_error">Aucune image disponible</string>
<string name="crop__done">ACCEPTER</string>
<string name="crop__cancel" tools:ignore="ButtonCase">ANNULER</string>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">Menyimpan gambar…</string>
<string name="crop__wait">Silakan tunggu…</string>
<string name="crop__pick_error">Tidak ada sumber gambar yang tersedia</string>
<string name="crop__done">SELESAI</string>
<string name="crop__cancel" tools:ignore="ButtonCase">BATAL</string>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">Salvataggio immagine…</string>
<string name="crop__wait">Attendere prego…</string>
<string name="crop__pick_error">Nessuna immagine disponibile</string>
<string name="crop__done">ACCETTA</string>
<string name="crop__cancel" tools:ignore="ButtonCase">ANNULLA</string>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">保存中…</string>
<string name="crop__wait">お待ちください…</string>
<string name="crop__pick_error">画像が見つかりません</string>
<string name="crop__done">決定</string>
<string name="crop__cancel" tools:ignore="ButtonCase">キャンセル</string>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">사진을 저장중입니다…</string>
<string name="crop__wait">잠시만 기다려주세요…</string>
<string name="crop__pick_error">이미지가 존재하지 않습니다.</string>
<string name="crop__done">확인</string>
<string name="crop__cancel" tools:ignore="ButtonCase">취소</string>
</resources>

View File

@ -1,5 +0,0 @@
<resources>
<dimen name="crop__bar_height">48dp</dimen>
</resources>

View File

@ -1,5 +0,0 @@
<resources>
<dimen name="crop__bar_height">64dp</dimen>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">Salvando imagem…</string>
<string name="crop__wait">Por favor, aguarde…</string>
<string name="crop__pick_error">Sem fontes de imagem disponíveis</string>
<string name="crop__done">FINALIZADO</string>
<string name="crop__cancel" tools:ignore="ButtonCase">CANCELAR</string>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">Изображение сохраняется…</string>
<string name="crop__wait">Пожалуйста, подождите…</string>
<string name="crop__pick_error">Нет доступных изображений</string>
<string name="crop__done">ГОТОВО</string>
<string name="crop__cancel" tools:ignore="ButtonCase">ОТМЕНА</string>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">Sparar bild…</string>
<string name="crop__wait">Var god vänta…</string>
<string name="crop__pick_error">Inga bildkällor tillgängliga</string>
<string name="crop__done">KLAR</string>
<string name="crop__cancel" tools:ignore="ButtonCase">AVBRYT</string>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">Fotoğraf kaydediliyor…</string>
<string name="crop__wait">Lütfen bekleyin…</string>
<string name="crop__pick_error">Fotoğraf bulunamadı</string>
<string name="crop__done">TAMAM</string>
<string name="crop__cancel" tools:ignore="ButtonCase">ÇIKIŞ</string>
</resources>

View File

@ -1,5 +0,0 @@
<resources>
<color name="crop__selector_pressed">#aaaaaa</color>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">正在保存照片…</string>
<string name="crop__wait">请等待…</string>
<string name="crop__pick_error">无效的图片</string>
<string name="crop__done">完成</string>
<string name="crop__cancel" tools:ignore="ButtonCase">取消</string>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">正在儲存相片…</string>
<string name="crop__wait">請稍候…</string>
<string name="crop__pick_error">沒有可用的圖片來源</string>
<string name="crop__done">完成</string>
<string name="crop__cancel" tools:ignore="ButtonCase">取消</string>
</resources>

View File

@ -1,16 +0,0 @@
<resources>
<attr name="cropImageStyle" format="reference" />
<declare-styleable name="CropImageView">
<attr name="highlightColor" format="reference|color" />
<attr name="showThirds" format="boolean" />
<attr name="showCircle" format="boolean" />
<attr name="showHandles">
<enum name="changing" value="0" />
<enum name="always" value="1" />
<enum name="never" value="2" />
</attr>
</declare-styleable>
</resources>

View File

@ -1,8 +0,0 @@
<resources>
<color name="crop__button_bar">#f3f3f3</color>
<color name="crop__button_text">#666666</color>
<color name="crop__selector_pressed">#1a000000</color>
<color name="crop__selector_focused">#77000000</color>
</resources>

View File

@ -1,5 +0,0 @@
<resources>
<dimen name="crop__bar_height">56dp</dimen>
</resources>

View File

@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="crop__saving">Saving picture…</string>
<string name="crop__wait">Please wait…</string>
<string name="crop__pick_error">No image sources available</string>
<string name="crop__done">DONE</string>
<string name="crop__cancel" tools:ignore="ButtonCase">CANCEL</string>
</resources>

View File

@ -1,44 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Crop"></style>
<style name="Crop.DoneCancelBar">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">@dimen/crop__bar_height</item>
<item name="android:orientation">horizontal</item>
<item name="android:divider">@drawable/crop__divider</item>
<item name="android:showDividers" tools:ignore="NewApi">middle</item>
<item name="android:dividerPadding" tools:ignore="NewApi">12dp</item>
<item name="android:background">@color/crop__button_bar</item>
</style>
<style name="Crop.ActionButton">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">match_parent</item>
<item name="android:layout_weight">1</item>
<item name="android:background">@drawable/crop__selectable_background</item>
</style>
<style name="Crop.ActionButtonText">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_gravity">center</item>
<item name="android:gravity">center_vertical</item>
<item name="android:paddingRight">20dp</item> <!-- Offsets left drawable -->
<item name="android:drawablePadding">8dp</item>
<item name="android:textColor">@color/crop__button_text</item>
<item name="android:textStyle">bold</item>
<item name="android:textSize">13sp</item>
</style>
<style name="Crop.ActionButtonText.Done">
<item name="android:drawableLeft">@drawable/crop__ic_done</item>
<item name="android:text">@string/crop__done</item>
</style>
<style name="Crop.ActionButtonText.Cancel">
<item name="android:drawableLeft">@drawable/crop__ic_cancel</item>
<item name="android:text">@string/crop__cancel</item>
</style>
</resources>

1
params.json Normal file
View File

@ -0,0 +1 @@
{"name":"android-crop","tagline":"An Android library project for cropping images","body":"> I guess people are just cropping out all the sadness\r\n\r\nAn Android library project to provide a simple image cropping `Activity`, based on code from AOSP.\r\n\r\n[![build status](https://travis-ci.org/jdamcd/android-crop.png)](https://travis-ci.org/jdamcd/android-crop) \r\n[![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)\r\n[![changelog](https://img.shields.io/badge/changelog-1.0.1-lightgrey.svg)](CHANGELOG.md)\r\n\r\n## Features\r\n\r\n* Gradle build & AAR\r\n* Modern UI\r\n* Backwards compatible to SDK 10\r\n* Simple builder for configuration\r\n* Example project\r\n\r\n## Usage\r\n\r\nFirst, declare `CropImageActivity` in your manifest file:\r\n\r\n`<activity android:name=\"com.soundcloud.android.crop.CropImageActivity\" />`\r\n\r\n#### Crop\r\n\r\n`Crop.of(inputUri, outputUri).asSquare().start(activity)`\r\n\r\nListen for the result of the crop (see example project if you want to do some error handling):\r\n\r\n @Override\r\n protected void onActivityResult(int requestCode, int resultCode, Intent result) {\r\n if (requestCode == Crop.REQUEST_CROP && resultCode == RESULT_OK) {\r\n doSomethingWithCroppedImage(outputUri);\r\n }\r\n }\r\n \r\nSome options are provided to style the crop screen. See example project theme.\r\n\r\n#### Pick\r\n\r\nThe library provides a utility method to start an image picker:\r\n\r\n`Crop.pickImage(activity)`\r\n\r\n#### Dependency\r\n\r\nThe AAR is published on Maven Central:\r\n\r\n`compile 'com.soundcloud.android:android-crop:1.0.1@aar'`\r\n\r\n#### Users\r\n\r\nApps that use this library include: [SoundCloud](https://play.google.com/store/apps/details?id=com.soundcloud.android), [Depop](https://play.google.com/store/apps/details?id=com.depop), [Polyvore](https://play.google.com/store/apps/details?id=com.polyvore), [TextSecure](https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms)\r\n\r\n## How does it look?\r\n\r\n![android-crop screenshot](https://github.com/jdamcd/android-crop/blob/master/screenshot.png?raw=true)\r\n\r\n## License\r\n\r\nThis project is based on the [AOSP](https://source.android.com) camera image cropper via [android-cropimage](https://github.com/lvillani/android-cropimage).\r\n\r\n Copyright 2015 SoundCloud\r\n\r\n Licensed under the Apache License, Version 2.0 (the \"License\");\r\n you may not use this file except in compliance with the License.\r\n You may obtain a copy of the License at\r\n\r\n http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n Unless required by applicable law or agreed to in writing, software\r\n distributed under the License is distributed on an \"AS IS\" BASIS,\r\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n See the License for the specific language governing permissions and\r\n limitations under the License.\r\n","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 KiB

View File

@ -1 +0,0 @@
include ':lib', ':example'

View File

@ -0,0 +1,116 @@
/*
Copyright 2014 GitHub Inc.
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.
*/
.pl-c /* comment */ {
color: #969896;
}
.pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */,
.pl-s .pl-v /* string variable */ {
color: #0086b3;
}
.pl-e /* entity */,
.pl-en /* entity.name */ {
color: #795da3;
}
.pl-s .pl-s1 /* string source */,
.pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ {
color: #333;
}
.pl-ent /* entity.name.tag */ {
color: #63a35c;
}
.pl-k /* keyword, storage, storage.type */ {
color: #a71d5d;
}
.pl-pds /* punctuation.definition.string, string.regexp.character-class */,
.pl-s /* string */,
.pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */,
.pl-sr /* string.regexp */,
.pl-sr .pl-cce /* string.regexp constant.character.escape */,
.pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */,
.pl-sr .pl-sre /* string.regexp source.ruby.embedded */ {
color: #183691;
}
.pl-v /* variable */ {
color: #ed6a43;
}
.pl-id /* invalid.deprecated */ {
color: #b52a1d;
}
.pl-ii /* invalid.illegal */ {
background-color: #b52a1d;
color: #f8f8f8;
}
.pl-sr .pl-cce /* string.regexp constant.character.escape */ {
color: #63a35c;
font-weight: bold;
}
.pl-ml /* markup.list */ {
color: #693a17;
}
.pl-mh /* markup.heading */,
.pl-mh .pl-en /* markup.heading entity.name */,
.pl-ms /* meta.separator */ {
color: #1d3e81;
font-weight: bold;
}
.pl-mq /* markup.quote */ {
color: #008080;
}
.pl-mi /* markup.italic */ {
color: #333;
font-style: italic;
}
.pl-mb /* markup.bold */ {
color: #333;
font-weight: bold;
}
.pl-md /* markup.deleted, meta.diff.header.from-file */ {
background-color: #ffecec;
color: #bd2c00;
}
.pl-mi1 /* markup.inserted, meta.diff.header.to-file */ {
background-color: #eaffea;
color: #55a532;
}
.pl-mdr /* meta.diff.range */ {
color: #795da3;
font-weight: bold;
}
.pl-mo /* meta.output */ {
color: #1d3e81;
}

424
stylesheets/normalize.css vendored Normal file
View File

@ -0,0 +1,424 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */ /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

245
stylesheets/stylesheet.css Normal file
View File

@ -0,0 +1,245 @@
* {
box-sizing: border-box; }
body {
padding: 0;
margin: 0;
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.5;
color: #606c71; }
a {
color: #1e6bb8;
text-decoration: none; }
a:hover {
text-decoration: underline; }
.btn {
display: inline-block;
margin-bottom: 1rem;
color: rgba(255, 255, 255, 0.7);
background-color: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.2);
border-style: solid;
border-width: 1px;
border-radius: 0.3rem;
transition: color 0.2s, background-color 0.2s, border-color 0.2s; }
.btn + .btn {
margin-left: 1rem; }
.btn:hover {
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3); }
@media screen and (min-width: 64em) {
.btn {
padding: 0.75rem 1rem; } }
@media screen and (min-width: 42em) and (max-width: 64em) {
.btn {
padding: 0.6rem 0.9rem;
font-size: 0.9rem; } }
@media screen and (max-width: 42em) {
.btn {
display: block;
width: 100%;
padding: 0.75rem;
font-size: 0.9rem; }
.btn + .btn {
margin-top: 1rem;
margin-left: 0; } }
.page-header {
color: #fff;
text-align: center;
background-color: #159957;
background-image: linear-gradient(120deg, #155799, #159957); }
@media screen and (min-width: 64em) {
.page-header {
padding: 5rem 6rem; } }
@media screen and (min-width: 42em) and (max-width: 64em) {
.page-header {
padding: 3rem 4rem; } }
@media screen and (max-width: 42em) {
.page-header {
padding: 2rem 1rem; } }
.project-name {
margin-top: 0;
margin-bottom: 0.1rem; }
@media screen and (min-width: 64em) {
.project-name {
font-size: 3.25rem; } }
@media screen and (min-width: 42em) and (max-width: 64em) {
.project-name {
font-size: 2.25rem; } }
@media screen and (max-width: 42em) {
.project-name {
font-size: 1.75rem; } }
.project-tagline {
margin-bottom: 2rem;
font-weight: normal;
opacity: 0.7; }
@media screen and (min-width: 64em) {
.project-tagline {
font-size: 1.25rem; } }
@media screen and (min-width: 42em) and (max-width: 64em) {
.project-tagline {
font-size: 1.15rem; } }
@media screen and (max-width: 42em) {
.project-tagline {
font-size: 1rem; } }
.main-content :first-child {
margin-top: 0; }
.main-content img {
max-width: 100%; }
.main-content h1, .main-content h2, .main-content h3, .main-content h4, .main-content h5, .main-content h6 {
margin-top: 2rem;
margin-bottom: 1rem;
font-weight: normal;
color: #159957; }
.main-content p {
margin-bottom: 1em; }
.main-content code {
padding: 2px 4px;
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 0.9rem;
color: #383e41;
background-color: #f3f6fa;
border-radius: 0.3rem; }
.main-content pre {
padding: 0.8rem;
margin-top: 0;
margin-bottom: 1rem;
font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace;
color: #567482;
word-wrap: normal;
background-color: #f3f6fa;
border: solid 1px #dce6f0;
border-radius: 0.3rem; }
.main-content pre > code {
padding: 0;
margin: 0;
font-size: 0.9rem;
color: #567482;
word-break: normal;
white-space: pre;
background: transparent;
border: 0; }
.main-content .highlight {
margin-bottom: 1rem; }
.main-content .highlight pre {
margin-bottom: 0;
word-break: normal; }
.main-content .highlight pre, .main-content pre {
padding: 0.8rem;
overflow: auto;
font-size: 0.9rem;
line-height: 1.45;
border-radius: 0.3rem; }
.main-content pre code, .main-content pre tt {
display: inline;
max-width: initial;
padding: 0;
margin: 0;
overflow: initial;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0; }
.main-content pre code:before, .main-content pre code:after, .main-content pre tt:before, .main-content pre tt:after {
content: normal; }
.main-content ul, .main-content ol {
margin-top: 0; }
.main-content blockquote {
padding: 0 1rem;
margin-left: 0;
color: #819198;
border-left: 0.3rem solid #dce6f0; }
.main-content blockquote > :first-child {
margin-top: 0; }
.main-content blockquote > :last-child {
margin-bottom: 0; }
.main-content table {
display: block;
width: 100%;
overflow: auto;
word-break: normal;
word-break: keep-all; }
.main-content table th {
font-weight: bold; }
.main-content table th, .main-content table td {
padding: 0.5rem 1rem;
border: 1px solid #e9ebec; }
.main-content dl {
padding: 0; }
.main-content dl dt {
padding: 0;
margin-top: 1rem;
font-size: 1rem;
font-weight: bold; }
.main-content dl dd {
padding: 0;
margin-bottom: 1rem; }
.main-content hr {
height: 2px;
padding: 0;
margin: 1rem 0;
background-color: #eff0f1;
border: 0; }
@media screen and (min-width: 64em) {
.main-content {
max-width: 64rem;
padding: 2rem 6rem;
margin: 0 auto;
font-size: 1.1rem; } }
@media screen and (min-width: 42em) and (max-width: 64em) {
.main-content {
padding: 2rem 4rem;
font-size: 1.1rem; } }
@media screen and (max-width: 42em) {
.main-content {
padding: 2rem 1rem;
font-size: 1rem; } }
.site-footer {
padding-top: 2rem;
margin-top: 2rem;
border-top: solid 1px #eff0f1; }
.site-footer-owner {
display: block;
font-weight: bold; }
.site-footer-credits {
color: #819198; }
@media screen and (min-width: 64em) {
.site-footer {
font-size: 1rem; } }
@media screen and (min-width: 42em) and (max-width: 64em) {
.site-footer {
font-size: 1rem; } }
@media screen and (max-width: 42em) {
.site-footer {
font-size: 0.9rem; } }