diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9928918
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,111 @@
+##### ANDROID #####
+
+# built application files
+out/
+*.apk
+*.ap_
+
+# files for the dex VM
+*.dex
+
+# Java class files
+*.class
+
+# generated files
+bin/
+gen/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Secure keystore settings
+
+secure.properties
+
+assets/
+
+##### ANDROID #####
+
+##### Gradle ######
+.gradle
+build
+
+##### JAVA #####
+
+*.class
+
+# Package Files #
+*.war
+*.ear
+
+##### JAVA #####
+
+##### IntelliJ #####
+
+*.iml
+*.ipr
+*.iws
+.idea/
+
+##### IntelliJ #####
+
+##### Eclipse #####
+
+*.pydevproject
+.project
+.metadata
+bin/**
+tmp/**
+tmp/**/*
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.classpath
+.settings/
+.loadpath
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# CDT-specific
+.cproject
+
+# PDT-specific
+.buildpath
+
+##### Eclipse #####
+
+##### Maven #####
+
+target/
+gen-external-apklibs/
+
+##### Maven #####
+
+## OSX ##
+
+.DS_Store
+
+# Thumbnails
+._*
+
+# Files that might appear on external disk
+.Spotlight-V100
+.Trashes
+
+#Crashlytics
+
+com_crashlytics_export_strings.xml
+
+# Automation
+*.pyc
+venv
+
+junit.xml
+executeAppium.bash
+automation/**/*.jpeg
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..d2ce381
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,23 @@
+language: android
+
+android:
+ components:
+ - build-tools-22.0.1
+ - android-22
+ - extra-android-m2repository
+
+after_success:
+ - gradle/deploy_snapshot.sh
+
+branches:
+ except:
+ - gh-pages
+
+notifications:
+ email: false
+
+sudo: false
+
+cache:
+ directories:
+ - $HOME/.gradle
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..8c2c0d3
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+Change Log
+==========
+
+Version 1.0.0 *(2015-TBD-TBD)*
+----------------------------
+
+Initial release.
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..400ef66
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2015 Lyft, 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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b31d5be
--- /dev/null
+++ b/README.md
@@ -0,0 +1,80 @@
+
+
+Scissors
+=========================
+
+Fixed viewport image cropping library for Android with built-in support for [Picasso][picasso] or [Glide][glide].
+
+Usage
+-----
+
+See `scissors-sample`.
+
+
+
+
+- Include it on your layout:
+```xml
+
+```
+- Set a Bitmap to be cropped. In example by calling `cropView.setImageBitmap(someBitmap);`
+- Call `Bitmap croppedBitmap = cropView.crop();` to obtain a cropped Bitmap to match viewport dimensions
+
+Extensions
+----------
+Scissors comes with handy extensions which help with common tasks like:
+
+#### Loading a Bitmap
+To load a Bitmap automatically with [Picasso][picasso] or [Glide][glide] into `CropView` use as follows:
+
+```java
+cropView.extensions()
+ .load(galleryUri);
+```
+#### Cropping into a File
+To save a cropped Bitmap into a `File` use as follows:
+
+```java
+cropView.extensions()
+ .crop()
+ .quality(87)
+ .format(PNG)
+ .into(croppedFile))
+```
+
+Download
+--------
+
+```groovy
+compile 'com.lyft:scissors:1.0.0'
+```
+
+Snapshots of development version are available in [Sonatype's `snapshots` repository][snap].
+
+License
+-------
+
+ Copyright (C) 2015 Lyft, 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.
+
+
+
+ [snap]: https://oss.sonatype.org/content/repositories/snapshots/
+ [picasso]: https://github.com/square/picasso
+ [glide]: https://github.com/bumptech/glide
\ No newline at end of file
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 0000000..e0bc969
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,13 @@
+Releasing
+========
+
+ 1. Change the version in `gradle.properties` to a non-SNAPSHOT version.
+ 2. Update the `CHANGELOG.md` for the impending release.
+ 3. Update the `README.md` with the new version.
+ 4. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version)
+ 5. `git tag -a X.Y.X -m "Version X.Y.Z"` (where X.Y.Z is the new version)
+ 6. `./gradlew clean uploadArchives`
+ 7. Update the `gradle.properties` to the next SNAPSHOT version.
+ 8. `git commit -am "Prepare next development version."`
+ 9. `git push && git push --tags`
+ 10. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact.
diff --git a/art/demo.gif b/art/demo.gif
new file mode 100644
index 0000000..cef2680
Binary files /dev/null and b/art/demo.gif differ
diff --git a/art/ic_launcher/res/mipmap-hdpi/ic_launcher.png b/art/ic_launcher/res/mipmap-hdpi/ic_launcher.png
new file mode 100755
index 0000000..a68693f
Binary files /dev/null and b/art/ic_launcher/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/art/ic_launcher/res/mipmap-mdpi/ic_launcher.png b/art/ic_launcher/res/mipmap-mdpi/ic_launcher.png
new file mode 100755
index 0000000..cbf6201
Binary files /dev/null and b/art/ic_launcher/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/art/ic_launcher/res/mipmap-xhdpi/ic_launcher.png b/art/ic_launcher/res/mipmap-xhdpi/ic_launcher.png
new file mode 100755
index 0000000..79676ae
Binary files /dev/null and b/art/ic_launcher/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/art/ic_launcher/res/mipmap-xxhdpi/ic_launcher.png b/art/ic_launcher/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100755
index 0000000..05ddf02
Binary files /dev/null and b/art/ic_launcher/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/art/ic_launcher/res/mipmap-xxxhdpi/ic_launcher.png b/art/ic_launcher/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100755
index 0000000..c8e7d22
Binary files /dev/null and b/art/ic_launcher/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/art/ic_launcher/web_hi_res_512.png b/art/ic_launcher/web_hi_res_512.png
new file mode 100755
index 0000000..f933a87
Binary files /dev/null and b/art/ic_launcher/web_hi_res_512.png differ
diff --git a/art/scissors.png b/art/scissors.png
new file mode 100644
index 0000000..5a68185
Binary files /dev/null and b/art/scissors.png differ
diff --git a/art/scissors.sketch b/art/scissors.sketch
new file mode 100644
index 0000000..64b393f
Binary files /dev/null and b/art/scissors.sketch differ
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..8d7fa24
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,30 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:1.3.1'
+ }
+}
+
+allprojects {
+
+ repositories {
+ mavenCentral()
+ }
+}
+
+ext {
+ minSdkVersion = 14
+ compileSdkVersion = 22
+ buildToolsVersion = '22.0.1'
+
+ junitVersion = '4.12'
+ mockitoVersion = '1.10.19'
+ robolectricVersion = '3.0'
+ assertjVersion = '1.7.1'
+ supportVersion = '22.2.1'
+
+ ci = 'true'.equals(System.getenv('CI'))
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..9351111
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,16 @@
+GROUP=com.lyft
+VERSION_NAME=1.0.0-SNAPSHOT
+
+POM_DESCRIPTION=Android image cropping library.
+
+POM_URL=https://github.com/lyft/scissors/
+POM_SCM_URL=https://github.com/lyft/scissors/
+POM_SCM_CONNECTION=scm:git:git://github.com/lyft/scissors.git
+POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/lyft/scissors.git
+
+POM_LICENCE_NAME=The Apache Software License, Version 2.0
+POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
+POM_LICENCE_DIST=repo
+
+POM_DEVELOPER_ID=lyft
+POM_DEVELOPER_NAME=Lyft Open Source
\ No newline at end of file
diff --git a/gradle/deploy_snapshot.sh b/gradle/deploy_snapshot.sh
new file mode 100755
index 0000000..e177d9a
--- /dev/null
+++ b/gradle/deploy_snapshot.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+#
+# Deploy artifacts to Sonatype's snapshot repo.
+#
+# Adapted from https://coderwall.com/p/9b_lfq and
+# http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
+
+SLUG="lyft/scissors"
+BRANCH="master"
+
+set -e
+
+if [ "$TRAVIS_REPO_SLUG" != "$SLUG" ]; then
+ echo "Skipping snapshot deployment: wrong repository. Expected '$SLUG' but was '$TRAVIS_REPO_SLUG'."
+elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
+ echo "Skipping snapshot deployment: was pull request."
+elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then
+ echo "Skipping snapshot deployment: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'."
+else
+ echo "Deploying snapshot..."
+ ./gradlew clean uploadArchives
+ echo "Snapshot deployed!"
+fi
diff --git a/gradle/gradle-mvn-push.gradle b/gradle/gradle-mvn-push.gradle
new file mode 100644
index 0000000..eb8ef8f
--- /dev/null
+++ b/gradle/gradle-mvn-push.gradle
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2013 Chris Banes
+ *
+ * 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.
+ */
+
+apply plugin: 'maven'
+apply plugin: 'signing'
+
+def isReleaseBuild() {
+ return VERSION_NAME.contains("SNAPSHOT") == false
+}
+
+def getRepositoryUsername() {
+ return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : "$System.env.SONATYPE_NEXUS_USERNAME"
+}
+
+def getRepositoryPassword() {
+ return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : "$System.env.SONATYPE_NEXUS_PASSWORD"
+}
+
+afterEvaluate { project ->
+ uploadArchives {
+ repositories {
+ mavenDeployer {
+ beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
+
+ pom.groupId = GROUP
+ pom.artifactId = POM_ARTIFACT_ID
+ pom.version = VERSION_NAME
+
+ repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
+ authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
+ }
+ snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
+ authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
+ }
+
+ pom.project {
+ name POM_NAME
+ packaging POM_PACKAGING
+ description POM_DESCRIPTION
+ url POM_URL
+
+ scm {
+ url POM_SCM_URL
+ connection POM_SCM_CONNECTION
+ developerConnection POM_SCM_DEV_CONNECTION
+ }
+
+ licenses {
+ license {
+ name POM_LICENCE_NAME
+ url POM_LICENCE_URL
+ distribution POM_LICENCE_DIST
+ }
+ }
+
+ developers {
+ developer {
+ id POM_DEVELOPER_ID
+ name POM_DEVELOPER_NAME
+ }
+ }
+ }
+ }
+ }
+ }
+
+ signing {
+ required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
+ sign configurations.archives
+ }
+
+ task androidJavadocs(type: Javadoc) {
+ source = android.sourceSets.main.java.srcDirs
+ classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+
+ if (JavaVersion.current().isJava8Compatible()) {
+ allprojects {
+ tasks.withType(Javadoc) {
+ options.addStringOption('Xdoclint:none', '-quiet')
+ }
+ }
+ }
+ }
+
+ task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
+ classifier = 'javadoc'
+ from androidJavadocs.destinationDir
+ }
+
+ task androidSourcesJar(type: Jar) {
+ classifier = 'sources'
+ from android.sourceSets.main.java.sourceFiles
+ }
+
+ artifacts {
+ archives androidSourcesJar
+ archives androidJavadocsJar
+ }
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8c0fb64
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..af3f50e
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sun Dec 14 08:38:12 YEKT 2014
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..91a7e26
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/scissors-sample/build.gradle b/scissors-sample/build.gradle
new file mode 100644
index 0000000..70ba53d
--- /dev/null
+++ b/scissors-sample/build.gradle
@@ -0,0 +1,38 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+ buildToolsVersion rootProject.ext.buildToolsVersion
+ defaultConfig {
+ applicationId 'com.lyft.scissorssample'
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.compileSdkVersion
+ versionCode 1
+ versionName "$VERSION_NAME"
+ }
+
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+
+ dexOptions {
+ preDexLibraries = !rootProject.ext.ci
+ }
+}
+
+dependencies {
+ compile project(':scissors')
+ compile 'com.android.support:appcompat-v7:' + rootProject.ext.supportVersion
+ compile 'com.android.support:design:' + rootProject.ext.supportVersion
+ compile 'com.jakewharton:butterknife:7.0.1'
+ compile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
+ compile 'com.squareup.picasso:picasso:2.5.2'
+ // Or Glide
+ // compile 'com.github.bumptech.glide:glide:3.6.1'
+ compile 'io.reactivex:rxjava:1.0.15'
+ compile 'io.reactivex:rxandroid:1.0.1'
+}
\ No newline at end of file
diff --git a/scissors-sample/src/main/AndroidManifest.xml b/scissors-sample/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a977558
--- /dev/null
+++ b/scissors-sample/src/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scissors-sample/src/main/java/com/lyft/android/scissorssample/App.java b/scissors-sample/src/main/java/com/lyft/android/scissorssample/App.java
new file mode 100644
index 0000000..502c3e0
--- /dev/null
+++ b/scissors-sample/src/main/java/com/lyft/android/scissorssample/App.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissorssample;
+
+import android.app.Application;
+import android.content.Context;
+import android.os.StrictMode;
+import com.squareup.leakcanary.LeakCanary;
+import com.squareup.leakcanary.RefWatcher;
+
+public class App extends Application {
+
+ public static RefWatcher getRefWatcher(Context context) {
+ App application = (App) context.getApplicationContext();
+ return application.refWatcher;
+ }
+
+ private RefWatcher refWatcher;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ refWatcher = LeakCanary.install(this);
+
+ StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
+ .detectDiskReads()
+ .detectDiskWrites()
+ .detectNetwork() // or .detectAll() for all detectable problems
+ .penaltyLog()
+ .build());
+ StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
+ .detectLeakedSqlLiteObjects()
+ .detectLeakedClosableObjects()
+ .penaltyLog()
+ .penaltyDeath()
+ .build());
+ }
+}
\ No newline at end of file
diff --git a/scissors-sample/src/main/java/com/lyft/android/scissorssample/CropResultActivity.java b/scissors-sample/src/main/java/com/lyft/android/scissorssample/CropResultActivity.java
new file mode 100644
index 0000000..039ad43
--- /dev/null
+++ b/scissors-sample/src/main/java/com/lyft/android/scissorssample/CropResultActivity.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissorssample;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.ImageView;
+import butterknife.Bind;
+import butterknife.ButterKnife;
+import com.squareup.picasso.MemoryPolicy;
+import com.squareup.picasso.Picasso;
+import java.io.File;
+
+public class CropResultActivity extends Activity {
+
+ private static final String EXTRA_FILE_PATH = "EXTRA_FILE_PATH";
+
+ @Bind(R.id.result_image)
+ ImageView resultView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_crop_result);
+ ButterKnife.bind(this);
+
+ String filePath = getIntent().getStringExtra(EXTRA_FILE_PATH);
+ File imageFile = new File(filePath);
+
+ Picasso.with(this)
+ .load(imageFile)
+ .memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE)
+ .into(resultView);
+
+ // Or Glide
+ //Glide.with(this)
+ // .load(imageFile)
+ // .diskCacheStrategy(DiskCacheStrategy.NONE)
+ // .skipMemoryCache(true)
+ // .into(resultView);
+ }
+
+ static void startUsing(File croppedPath, Activity activity) {
+ Intent intent = new Intent(activity, CropResultActivity.class);
+ intent.putExtra(EXTRA_FILE_PATH, croppedPath.getPath());
+ activity.startActivity(intent);
+ }
+}
diff --git a/scissors-sample/src/main/java/com/lyft/android/scissorssample/MainActivity.java b/scissors-sample/src/main/java/com/lyft/android/scissorssample/MainActivity.java
new file mode 100644
index 0000000..dff66c4
--- /dev/null
+++ b/scissors-sample/src/main/java/com/lyft/android/scissorssample/MainActivity.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissorssample;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.MotionEvent;
+import android.view.View;
+import butterknife.Bind;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
+import butterknife.OnTouch;
+import com.lyft.android.scissors.CropView;
+import com.squareup.leakcanary.RefWatcher;
+import java.io.File;
+import java.util.List;
+import rx.Observable;
+import rx.functions.Action1;
+import rx.subscriptions.CompositeSubscription;
+
+import static android.graphics.Bitmap.CompressFormat.JPEG;
+import static rx.android.schedulers.AndroidSchedulers.mainThread;
+import static rx.schedulers.Schedulers.io;
+
+public class MainActivity extends Activity {
+
+ @Bind(R.id.crop_view)
+ CropView cropView;
+
+ @Bind({ R.id.crop_fab, R.id.pick_mini_fab })
+ List buttons;
+
+ @Bind(R.id.pick_fab)
+ View pickButton;
+
+ CompositeSubscription subscriptions = new CompositeSubscription();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ setContentView(R.layout.activity_main);
+
+ ButterKnife.bind(this);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == RequestCodes.PICK_IMAGE_FROM_GALLERY
+ && resultCode == Activity.RESULT_OK) {
+ Uri galleryPictureUri = data.getData();
+
+ cropView.extensions()
+ .load(galleryPictureUri);
+
+ updateButtons();
+ }
+ }
+
+ @OnClick(R.id.crop_fab)
+ public void onCropClicked() {
+ final File croppedFile = new File(getCacheDir(), "cropped.jpg");
+
+ Observable onSave = Observable.from(cropView.extensions()
+ .crop()
+ .quality(100)
+ .format(JPEG)
+ .into(croppedFile))
+ .subscribeOn(io())
+ .observeOn(mainThread());
+
+ subscriptions.add(onSave
+ .subscribe(new Action1() {
+ @Override
+ public void call(Void nothing) {
+ CropResultActivity.startUsing(croppedFile, MainActivity.this);
+ }
+ }));
+ }
+
+ @OnClick({ R.id.pick_fab, R.id.pick_mini_fab })
+ public void onPickClicked() {
+ cropView.extensions()
+ .pickUsing(this, RequestCodes.PICK_IMAGE_FROM_GALLERY);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ subscriptions.unsubscribe();
+
+ RefWatcher refWatcher = App.getRefWatcher(this);
+ refWatcher.watch(this, "MainActivity");
+ refWatcher.watch(cropView, "cropView");
+ }
+
+ @OnTouch(R.id.crop_view)
+ public boolean onTouchCropView(MotionEvent event) { // GitHub issue #4
+ if (event.getPointerCount() > 1 || cropView.getImageBitmap() == null) {
+ return true;
+ }
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ ButterKnife.apply(buttons, VISIBILITY, View.INVISIBLE);
+ break;
+ default:
+ ButterKnife.apply(buttons, VISIBILITY, View.VISIBLE);
+ break;
+ }
+ return true;
+ }
+
+ private void updateButtons() {
+ ButterKnife.apply(buttons, VISIBILITY, View.VISIBLE);
+ pickButton.setVisibility(View.GONE);
+ }
+
+ static final ButterKnife.Setter VISIBILITY = new ButterKnife.Setter() {
+ @Override
+ public void set(final View view, final Integer visibility, int index) {
+ view.setVisibility(visibility);
+ }
+ };
+}
diff --git a/scissors-sample/src/main/java/com/lyft/android/scissorssample/RequestCodes.java b/scissors-sample/src/main/java/com/lyft/android/scissorssample/RequestCodes.java
new file mode 100644
index 0000000..c78cb57
--- /dev/null
+++ b/scissors-sample/src/main/java/com/lyft/android/scissorssample/RequestCodes.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissorssample;
+
+public interface RequestCodes {
+
+ int PICK_IMAGE_FROM_GALLERY = 10001;
+}
diff --git a/scissors-sample/src/main/res/layout/activity_crop_result.xml b/scissors-sample/src/main/res/layout/activity_crop_result.xml
new file mode 100644
index 0000000..5eaa212
--- /dev/null
+++ b/scissors-sample/src/main/res/layout/activity_crop_result.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/scissors-sample/src/main/res/layout/activity_main.xml b/scissors-sample/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..d50d322
--- /dev/null
+++ b/scissors-sample/src/main/res/layout/activity_main.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scissors-sample/src/main/res/mipmap-hdpi/ic_launcher.png b/scissors-sample/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100755
index 0000000..a68693f
Binary files /dev/null and b/scissors-sample/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/scissors-sample/src/main/res/mipmap-mdpi/ic_launcher.png b/scissors-sample/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100755
index 0000000..cbf6201
Binary files /dev/null and b/scissors-sample/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/scissors-sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/scissors-sample/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100755
index 0000000..79676ae
Binary files /dev/null and b/scissors-sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/scissors-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/scissors-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100755
index 0000000..05ddf02
Binary files /dev/null and b/scissors-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/scissors-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/scissors-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100755
index 0000000..c8e7d22
Binary files /dev/null and b/scissors-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/scissors-sample/src/main/res/values-v21/dimens.xml b/scissors-sample/src/main/res/values-v21/dimens.xml
new file mode 100644
index 0000000..d0650d3
--- /dev/null
+++ b/scissors-sample/src/main/res/values-v21/dimens.xml
@@ -0,0 +1,5 @@
+
+
+ 24dp
+ 32dp
+
\ No newline at end of file
diff --git a/scissors-sample/src/main/res/values-v23/dimens.xml b/scissors-sample/src/main/res/values-v23/dimens.xml
new file mode 100644
index 0000000..fc158ab
--- /dev/null
+++ b/scissors-sample/src/main/res/values-v23/dimens.xml
@@ -0,0 +1,5 @@
+
+
+ 32dp
+ 40dp
+
\ No newline at end of file
diff --git a/scissors-sample/src/main/res/values/colors.xml b/scissors-sample/src/main/res/values/colors.xml
new file mode 100644
index 0000000..5780d83
--- /dev/null
+++ b/scissors-sample/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #ffe91e63
+ #ffc2185b
+
\ No newline at end of file
diff --git a/scissors-sample/src/main/res/values/dimens.xml b/scissors-sample/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..73998c5
--- /dev/null
+++ b/scissors-sample/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+
+
+ 0dp
+ 8dp
+
\ No newline at end of file
diff --git a/scissors-sample/src/main/res/values/strings.xml b/scissors-sample/src/main/res/values/strings.xml
new file mode 100644
index 0000000..eadfe9d
--- /dev/null
+++ b/scissors-sample/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Scissors Sample
+
\ No newline at end of file
diff --git a/scissors-sample/src/main/res/values/styles.xml b/scissors-sample/src/main/res/values/styles.xml
new file mode 100644
index 0000000..0420913
--- /dev/null
+++ b/scissors-sample/src/main/res/values/styles.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/scissors-sample/src/main/res/values/themes.xml b/scissors-sample/src/main/res/values/themes.xml
new file mode 100644
index 0000000..d418020
--- /dev/null
+++ b/scissors-sample/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/scissors/build.gradle b/scissors/build.gradle
new file mode 100644
index 0000000..fe6e773
--- /dev/null
+++ b/scissors/build.gradle
@@ -0,0 +1,37 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+ buildToolsVersion rootProject.ext.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+
+ dexOptions {
+ preDexLibraries = !rootProject.ext.ci
+ }
+}
+
+dependencies {
+ // Test
+ testCompile 'junit:junit:' + rootProject.ext.junitVersion
+ testCompile 'org.mockito:mockito-core:' + rootProject.ext.mockitoVersion
+ testCompile 'org.robolectric:robolectric:' + rootProject.ext.robolectricVersion
+ testCompile 'org.assertj:assertj-core:' + rootProject.ext.assertjVersion
+ // Support Annotations
+ compile 'com.android.support:support-annotations:' + rootProject.ext.supportVersion
+ // Optional dependencies for extensions
+ // Picasso
+ provided 'com.squareup.picasso:picasso:[2.4.0, 2.5.2)'
+ // Glide
+ provided 'com.github.bumptech.glide:glide:[3.5.0, 3.6.1)'
+ provided 'com.android.support:support-v4:' + rootProject.ext.supportVersion
+}
+
+apply from: rootProject.file('gradle/gradle-mvn-push.gradle')
\ No newline at end of file
diff --git a/scissors/gradle.properties b/scissors/gradle.properties
new file mode 100644
index 0000000..4888f76
--- /dev/null
+++ b/scissors/gradle.properties
@@ -0,0 +1,3 @@
+POM_ARTIFACT_ID=scissors
+POM_NAME=Scissors Library
+POM_PACKAGING=aar
\ No newline at end of file
diff --git a/scissors/src/main/AndroidManifest.xml b/scissors/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..967a645
--- /dev/null
+++ b/scissors/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/scissors/src/main/java/com/lyft/android/scissors/BitmapLoader.java b/scissors/src/main/java/com/lyft/android/scissors/BitmapLoader.java
new file mode 100644
index 0000000..1b591e3
--- /dev/null
+++ b/scissors/src/main/java/com/lyft/android/scissors/BitmapLoader.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissors;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.widget.ImageView;
+
+/**
+ * Load extension delegates actual Bitmap loading to a BitmapLoader allowing it to use different implementations.
+ *
+ * @see PicassoBitmapLoader
+ * @see GlideBitmapLoader
+ */
+public interface BitmapLoader {
+
+ void load(@Nullable Object model, @NonNull ImageView view);
+}
diff --git a/scissors/src/main/java/com/lyft/android/scissors/CropView.java b/scissors/src/main/java/com/lyft/android/scissors/CropView.java
new file mode 100644
index 0000000..9af0729
--- /dev/null
+++ b/scissors/src/main/java/com/lyft/android/scissors/CropView.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissors;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.ImageView;
+import com.lyft.android.scissors.CropViewExtensions.CropRequest;
+import com.lyft.android.scissors.CropViewExtensions.LoadRequest;
+import java.io.File;
+import java.io.OutputStream;
+
+/**
+ * An {@link ImageView} with a fixed viewport and cropping capabilities.
+ */
+public class CropView extends ImageView {
+
+ private static final int MAX_TOUCH_POINTS = 2;
+ private TouchManager touchManager;
+
+ private Paint viewportPaint = new Paint();
+ private Paint bitmapPaint = new Paint();
+
+ private Bitmap bitmap;
+ private Matrix transform = new Matrix();
+ private Extensions extensions;
+
+ public CropView(Context context) {
+ super(context);
+ initCropView(context, null);
+ }
+
+ public CropView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ initCropView(context, attrs);
+ }
+
+ void initCropView(Context context, AttributeSet attrs) {
+ CropViewConfig config = CropViewConfig.from(context, attrs);
+
+ touchManager = new TouchManager(MAX_TOUCH_POINTS, config);
+
+ bitmapPaint.setFilterBitmap(true);
+ viewportPaint.setColor(config.getViewportHeaderFooterColor());
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (bitmap == null) {
+ return;
+ }
+
+ drawBitmap(canvas);
+
+ final int bottom = getBottom();
+ final int viewportWidth = touchManager.getViewportWidth();
+ final int viewportHeight = touchManager.getViewportHeight();
+ final int remainingHalf = (bottom - viewportHeight) / 2;
+ canvas.drawRect(0, 0, viewportWidth, remainingHalf, viewportPaint);
+ canvas.drawRect(0, bottom - remainingHalf, viewportWidth, bottom, viewportPaint);
+ }
+
+ private void drawBitmap(Canvas canvas) {
+ transform.reset();
+ touchManager.applyPositioningAndScale(transform);
+
+ canvas.drawBitmap(bitmap, transform, bitmapPaint);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ resetTouchManager();
+ }
+
+ @Override
+ public void setImageResource(@DrawableRes int resId) {
+ final Bitmap bitmap = resId > 0
+ ? BitmapFactory.decodeResource(getResources(), resId)
+ : null;
+ setImageBitmap(bitmap);
+ }
+
+ @Override
+ public void setImageDrawable(@Nullable Drawable drawable) {
+ final Bitmap bitmap;
+ if (drawable instanceof BitmapDrawable) {
+ BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+ bitmap = bitmapDrawable.getBitmap();
+ } else if (drawable != null) {
+ bitmap = Utils.asBitmap(drawable, getWidth(), getHeight());
+ } else {
+ bitmap = null;
+ }
+
+ setImageBitmap(bitmap);
+ }
+
+ @Override
+ public void setImageURI(@Nullable Uri uri) {
+ extensions().load(uri);
+ }
+
+ @Override
+ public void setImageBitmap(@Nullable Bitmap bitmap) {
+ this.bitmap = bitmap;
+ resetTouchManager();
+ invalidate();
+ }
+
+ /**
+ * @return Current working Bitmap or null if none has been set yet.
+ */
+ @Nullable
+ public Bitmap getImageBitmap() {
+ return bitmap;
+ }
+
+ private void resetTouchManager() {
+ final boolean invalidBitmap = bitmap == null;
+ final int bitmapWidth = invalidBitmap ? 0 : bitmap.getWidth();
+ final int bitmapHeight = invalidBitmap ? 0 : bitmap.getHeight();
+ touchManager.resetFor(bitmapWidth, bitmapHeight, getWidth(), getHeight());
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ super.dispatchTouchEvent(event);
+
+ touchManager.onEvent(event);
+ invalidate();
+ return true;
+ }
+
+ /**
+ * Performs synchronous image cropping based on configuration.
+ *
+ * @return A {@link Bitmap} cropped based on viewport and user panning and zooming or null if no {@link Bitmap} has been
+ * provided.
+ */
+ @Nullable
+ public Bitmap crop() {
+ if (bitmap == null) {
+ return null;
+ }
+
+ final Bitmap src = bitmap;
+ final Bitmap.Config srcConfig = src.getConfig();
+ final Bitmap.Config config = srcConfig == null ? Bitmap.Config.ARGB_8888 : srcConfig;
+ final int viewportHeight = touchManager.getViewportHeight();
+ final int viewportWidth = touchManager.getViewportWidth();
+
+ final Bitmap dst = Bitmap.createBitmap(viewportWidth, viewportHeight, config);
+
+ Canvas canvas = new Canvas(dst);
+ final int remainingHalf = (getBottom() - viewportHeight) / 2;
+ canvas.translate(0, -remainingHalf);
+
+ drawBitmap(canvas);
+
+ return dst;
+ }
+
+ /**
+ * Obtain current viewport width.
+ *
+ * @return Current viewport width.
+ *
Note: It might be 0 if layout pass has not been completed.
+ */
+ public int getViewportWidth() {
+ return touchManager.getViewportWidth();
+ }
+
+ /**
+ * Obtain current viewport height.
+ *
+ * @return Current viewport height.
+ *
Note: It might be 0 if layout pass has not been completed.
+ */
+ public int getViewportHeight() {
+ return touchManager.getViewportHeight();
+ }
+
+ /**
+ * Offers common utility extensions.
+ *
+ * @return Extensions object used to perform chained calls.
+ */
+ public Extensions extensions() {
+ if (extensions == null) {
+ extensions = new Extensions(this);
+ }
+ return extensions;
+ }
+
+ /**
+ * Optional extensions to perform common actions involving a {@link CropView}
+ */
+ public static class Extensions {
+
+ private final CropView cropView;
+
+ Extensions(CropView cropView) {
+ this.cropView = cropView;
+ }
+
+ /**
+ * Load a {@link Bitmap} using an automatically resolved {@link BitmapLoader} which will attempt to scale image to fill view.
+ *
+ * @param model Model used by {@link BitmapLoader} to load desired {@link Bitmap}
+ * @see PicassoBitmapLoader
+ * @see GlideBitmapLoader
+ */
+ public void load(@Nullable Object model) {
+ new LoadRequest(cropView)
+ .load(model);
+ }
+
+ /**
+ * Load a {@link Bitmap} using given {@link BitmapLoader}, you must call {@link LoadRequest#load(Object)} afterwards.
+ *
+ * @param bitmapLoader {@link BitmapLoader} used to load desired {@link Bitmap}
+ * @see PicassoBitmapLoader
+ * @see GlideBitmapLoader
+ */
+ public LoadRequest using(@Nullable BitmapLoader bitmapLoader) {
+ return new LoadRequest(cropView).using(bitmapLoader);
+ }
+
+ /**
+ * Perform an asynchronous crop request.
+ *
+ * @return {@link CropRequest} used to chain a configure cropping request, you must call either one of:
+ *
+ *
{@link CropRequest#into(File)}
+ *
{@link CropRequest#into(OutputStream, boolean)}
+ *
+ */
+ public CropRequest crop() {
+ return new CropRequest(cropView);
+ }
+
+ /**
+ * Perform a pick image request using {@link Activity#startActivityForResult(Intent, int)}.
+ */
+ public void pickUsing(@NonNull Activity activity, int requestCode) {
+ CropViewExtensions.pickUsing(activity, requestCode);
+ }
+ }
+}
diff --git a/scissors/src/main/java/com/lyft/android/scissors/CropViewConfig.java b/scissors/src/main/java/com/lyft/android/scissors/CropViewConfig.java
new file mode 100644
index 0000000..6171e00
--- /dev/null
+++ b/scissors/src/main/java/com/lyft/android/scissors/CropViewConfig.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissors;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+class CropViewConfig {
+
+ public static final float DEFAULT_VIEWPORT_HEIGHT_RATIO = 1f;
+ public static final float DEFAULT_MAXIMUM_SCALE = 10f;
+ public static final float DEFAULT_MINIMUM_SCALE = 0f;
+ public static final int DEFAULT_IMAGE_QUALITY = 100;
+ public static final int DEFAULT_VIEWPORT_HEADER_FOOTER_COLOR = 0xC8000000; // Black with 200 alpha
+
+ private float viewportHeightRatio = DEFAULT_VIEWPORT_HEIGHT_RATIO;
+ private float maxScale = DEFAULT_MAXIMUM_SCALE;
+ private float minScale = DEFAULT_MINIMUM_SCALE;
+ private int viewportHeaderFooterColor = DEFAULT_VIEWPORT_HEADER_FOOTER_COLOR;
+
+ public int getViewportHeaderFooterColor() {
+ return viewportHeaderFooterColor;
+ }
+
+ void setViewportHeaderFooterColor(int viewportHeaderFooterColor) {
+ this.viewportHeaderFooterColor =
+ viewportHeaderFooterColor <= 0
+ ? DEFAULT_VIEWPORT_HEADER_FOOTER_COLOR
+ : viewportHeaderFooterColor;
+ }
+
+ public float getViewportHeightRatio() {
+ return viewportHeightRatio;
+ }
+
+ void setViewportHeightRatio(float viewportHeightRatio) {
+ this.viewportHeightRatio =
+ viewportHeightRatio <= 0 ? DEFAULT_VIEWPORT_HEIGHT_RATIO : viewportHeightRatio;
+ }
+
+ public float getMaxScale() {
+ return maxScale;
+ }
+
+ void setMaxScale(float maxScale) {
+ this.maxScale = maxScale <= 0 ? DEFAULT_MAXIMUM_SCALE : maxScale;
+ }
+
+ public float getMinScale() {
+ return minScale;
+ }
+
+ void setMinScale(float minScale) {
+ this.minScale = minScale <= 0 ? DEFAULT_MINIMUM_SCALE : minScale;
+ }
+
+ public static CropViewConfig from(Context context, AttributeSet attrs) {
+ final CropViewConfig cropViewConfig = new CropViewConfig();
+
+ if (attrs == null) {
+ return cropViewConfig;
+ }
+
+ TypedArray attributes = context.obtainStyledAttributes(
+ attrs,
+ R.styleable.CropView);
+
+ cropViewConfig.setViewportHeightRatio(
+ attributes.getFloat(R.styleable.CropView_cropviewViewportHeightRatio,
+ CropViewConfig.DEFAULT_VIEWPORT_HEIGHT_RATIO));
+
+ cropViewConfig.setMaxScale(
+ attributes.getFloat(R.styleable.CropView_cropviewMaxScale,
+ CropViewConfig.DEFAULT_MAXIMUM_SCALE));
+
+ cropViewConfig.setMinScale(
+ attributes.getFloat(R.styleable.CropView_cropviewMinScale,
+ CropViewConfig.DEFAULT_MINIMUM_SCALE));
+
+ cropViewConfig.setViewportHeaderFooterColor(
+ attributes.getColor(R.styleable.CropView_cropviewViewportHeaderFooterColor,
+ CropViewConfig.DEFAULT_VIEWPORT_HEADER_FOOTER_COLOR));
+ attributes.recycle();
+
+ return cropViewConfig;
+ }
+}
diff --git a/scissors/src/main/java/com/lyft/android/scissors/CropViewExtensions.java b/scissors/src/main/java/com/lyft/android/scissors/CropViewExtensions.java
new file mode 100644
index 0000000..e3f1b85
--- /dev/null
+++ b/scissors/src/main/java/com/lyft/android/scissors/CropViewExtensions.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissors;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import java.io.File;
+import java.io.OutputStream;
+import java.util.concurrent.Future;
+
+class CropViewExtensions {
+
+ static void pickUsing(Activity activity, int requestCode) {
+ Intent intent = new Intent();
+ intent.setType("image/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+
+ Intent chooserIntent = Intent.createChooser(intent, null);
+
+ activity.startActivityForResult(
+ chooserIntent,
+ requestCode);
+ }
+
+ public static class LoadRequest {
+
+ private final CropView cropView;
+ private BitmapLoader bitmapLoader;
+
+ LoadRequest(CropView cropView) {
+ Utils.checkNotNull(cropView, "cropView == null");
+ this.cropView = cropView;
+ }
+
+ /**
+ * Load a {@link Bitmap} using given {@link BitmapLoader}, you must call {@link LoadRequest#load(Object)} afterwards.
+ *
+ * @param bitmapLoader {@link BitmapLoader} to use
+ * @return current request for chaining, you should call {@link #load(Object)} afterwards.
+ */
+ public LoadRequest using(@Nullable BitmapLoader bitmapLoader) {
+ this.bitmapLoader = bitmapLoader;
+ return this;
+ }
+
+ /**
+ * Load a {@link Bitmap} using a {@link BitmapLoader} into {@link CropView}
+ *
+ * @param model Model used by {@link BitmapLoader} to load desired {@link Bitmap}
+ */
+ public void load(@Nullable Object model) {
+ if (bitmapLoader == null) {
+ bitmapLoader = resolveBitmapLoader(cropView);
+ }
+
+ bitmapLoader.load(model, cropView);
+ }
+ }
+
+ public static class CropRequest {
+
+ private final CropView cropView;
+ private Bitmap.CompressFormat format = Bitmap.CompressFormat.JPEG;
+ private int quality = CropViewConfig.DEFAULT_IMAGE_QUALITY;
+
+ CropRequest(@NonNull CropView cropView) {
+ Utils.checkNotNull(cropView, "cropView == null");
+ this.cropView = cropView;
+ }
+
+ /**
+ * Compression format to use, defaults to {@link Bitmap.CompressFormat#JPEG}.
+ *
+ * @return current request for chaining.
+ */
+ public CropRequest format(@NonNull Bitmap.CompressFormat format) {
+ Utils.checkNotNull(format, "format == null");
+ this.format = format;
+ return this;
+ }
+
+ /**
+ * Compression quality to use (must be 0..100), defaults to {@value CropViewConfig#DEFAULT_IMAGE_QUALITY}.
+ *
+ * @return current request for chaining.
+ */
+ public CropRequest quality(int quality) {
+ Utils.checkArg(quality >= 0 && quality <= 100, "quality must be 0..100");
+ this.quality = quality;
+ return this;
+ }
+
+ /**
+ * Asynchronously flush cropped bitmap into provided file, creating parent directory if required. This is performed in another
+ * thread.
+ *
+ * @param file Must have permissions to write, will be created if doesn't exist or overwrite if it does.
+ * @return {@link Future} used to cancel or wait for this request.
+ */
+ public Future into(@NonNull File file) {
+ final Bitmap croppedBitmap = cropView.crop();
+ return Utils.flushToFile(croppedBitmap, format, quality, file);
+ }
+
+ /**
+ * Asynchronously flush cropped bitmap into provided stream.
+ *
+ * @param outputStream Stream to write to
+ * @param closeWhenDone wetter or not to close provided stream once flushing is done
+ * @return {@link Future} used to cancel or wait for this request.
+ */
+ public Future into(@NonNull OutputStream outputStream, boolean closeWhenDone) {
+ final Bitmap croppedBitmap = cropView.crop();
+ return Utils.flushToStream(croppedBitmap, format, quality, outputStream, closeWhenDone);
+ }
+ }
+
+ final static boolean HAS_PICASSO = canHasClass("com.squareup.picasso.Picasso");
+ final static boolean HAS_GLIDE = canHasClass("com.bumptech.glide.Glide");
+
+ static BitmapLoader resolveBitmapLoader(CropView cropView) {
+ if (HAS_PICASSO) {
+ return PicassoBitmapLoader.createUsing(cropView);
+ }
+ if (HAS_GLIDE) {
+ return GlideBitmapLoader.createUsing(cropView);
+ }
+ throw new IllegalStateException("You must provide a BitmapLoader.");
+ }
+
+ static boolean canHasClass(String className) {
+ try {
+ Class.forName(className);
+ return true;
+ } catch (ClassNotFoundException e) {
+ }
+ return false;
+ }
+
+ static Rect computeTargetSize(int sourceWidth, int sourceHeight, int viewportWidth, int viewportHeight) {
+
+ if (sourceWidth == viewportWidth && sourceHeight == viewportHeight) {
+ return new Rect(0, 0, viewportWidth, viewportHeight); // Fail fast for when source matches exactly on viewport
+ }
+
+ float scale;
+ if (sourceWidth * viewportHeight > viewportWidth * sourceHeight) {
+ scale = (float) viewportHeight / (float) sourceHeight;
+ } else {
+ scale = (float) viewportWidth / (float) sourceWidth;
+ }
+ final int recommendedWidth = (int) ((sourceWidth * scale) + 0.5f);
+ final int recommendedHeight = (int) ((sourceHeight * scale) + 0.5f);
+ return new Rect(0, 0, recommendedWidth, recommendedHeight);
+ }
+}
diff --git a/scissors/src/main/java/com/lyft/android/scissors/GlideBitmapLoader.java b/scissors/src/main/java/com/lyft/android/scissors/GlideBitmapLoader.java
new file mode 100644
index 0000000..a569e2d
--- /dev/null
+++ b/scissors/src/main/java/com/lyft/android/scissors/GlideBitmapLoader.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissors;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.widget.ImageView;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestManager;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
+
+/**
+ * A {@link BitmapLoader} with transformation for {@link Glide} image library.
+ *
+ * @see GlideBitmapLoader#createUsing(CropView)
+ * @see GlideBitmapLoader#createUsing(CropView, RequestManager, BitmapPool)
+ */
+public class GlideBitmapLoader implements BitmapLoader {
+
+ private final RequestManager requestManager;
+ private final BitmapTransformation transformation;
+
+ public GlideBitmapLoader(@NonNull RequestManager requestManager, @NonNull BitmapTransformation transformation) {
+ this.requestManager = requestManager;
+ this.transformation = transformation;
+ }
+
+ @Override
+ public void load(@Nullable Object model, @NonNull ImageView imageView) {
+ requestManager.load(model)
+ .asBitmap()
+ .skipMemoryCache(true)
+ .diskCacheStrategy(DiskCacheStrategy.SOURCE)
+ .transform(transformation)
+ .into(imageView);
+ }
+
+ public static BitmapLoader createUsing(@NonNull CropView cropView) {
+ return createUsing(cropView, Glide.with(cropView.getContext()), Glide.get(cropView.getContext()).getBitmapPool());
+ }
+
+ public static BitmapLoader createUsing(@NonNull CropView cropView, @NonNull RequestManager requestManager,
+ @NonNull BitmapPool bitmapPool) {
+ return new GlideBitmapLoader(requestManager,
+ GlideFillViewportTransformation.createUsing(bitmapPool, cropView.getViewportWidth(), cropView.getViewportHeight()));
+ }
+}
diff --git a/scissors/src/main/java/com/lyft/android/scissors/GlideFillViewportTransformation.java b/scissors/src/main/java/com/lyft/android/scissors/GlideFillViewportTransformation.java
new file mode 100644
index 0000000..558d082
--- /dev/null
+++ b/scissors/src/main/java/com/lyft/android/scissors/GlideFillViewportTransformation.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissors;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
+
+class GlideFillViewportTransformation extends BitmapTransformation {
+
+ private final int viewportWidth;
+ private final int viewportHeight;
+
+ public GlideFillViewportTransformation(BitmapPool bitmapPool, int viewportWidth, int viewportHeight) {
+ super(bitmapPool);
+ this.viewportWidth = viewportWidth;
+ this.viewportHeight = viewportHeight;
+ }
+
+ @Override
+ protected Bitmap transform(BitmapPool bitmapPool, Bitmap source, int outWidth, int outHeight) {
+ int sourceWidth = source.getWidth();
+ int sourceHeight = source.getHeight();
+
+ Rect target = CropViewExtensions.computeTargetSize(sourceWidth, sourceHeight, viewportWidth, viewportHeight);
+
+ int targetWidth = target.width();
+ int targetHeight = target.height();
+
+ return Bitmap.createScaledBitmap(
+ source,
+ targetWidth,
+ targetHeight,
+ true);
+ }
+
+ @Override
+ public String getId() {
+ return getClass().getName();
+ }
+
+ public static BitmapTransformation createUsing(BitmapPool bitmapPool, int viewportWidth, int viewportHeight) {
+ return new GlideFillViewportTransformation(bitmapPool, viewportWidth, viewportHeight);
+ }
+}
diff --git a/scissors/src/main/java/com/lyft/android/scissors/PicassoBitmapLoader.java b/scissors/src/main/java/com/lyft/android/scissors/PicassoBitmapLoader.java
new file mode 100644
index 0000000..2edcdf6
--- /dev/null
+++ b/scissors/src/main/java/com/lyft/android/scissors/PicassoBitmapLoader.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissors;
+
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.widget.ImageView;
+import com.squareup.picasso.Picasso;
+import com.squareup.picasso.RequestCreator;
+import com.squareup.picasso.Transformation;
+import java.io.File;
+
+/**
+ * A {@link BitmapLoader} with transformation for {@link Picasso} image library.
+ *
+ * @see PicassoBitmapLoader#createUsing(CropView)
+ * @see PicassoBitmapLoader#createUsing(CropView, Picasso)
+ */
+public class PicassoBitmapLoader implements BitmapLoader {
+
+ private final Picasso picasso;
+ private final Transformation transformation;
+
+ public PicassoBitmapLoader(Picasso picasso, Transformation transformation) {
+ this.picasso = picasso;
+ this.transformation = transformation;
+ }
+
+ @Override
+ public void load(@Nullable Object model, @NonNull ImageView imageView) {
+ final RequestCreator requestCreator;
+
+ if (model instanceof Uri || model == null) {
+ requestCreator = picasso.load((Uri) model);
+ } else if (model instanceof String) {
+ requestCreator = picasso.load((String) model);
+ } else if (model instanceof File) {
+ requestCreator = picasso.load((File) model);
+ } else if (model instanceof Integer) {
+ requestCreator = picasso.load((Integer) model);
+ } else {
+ throw new IllegalArgumentException("Unsupported model " + model);
+ }
+
+ requestCreator
+ .skipMemoryCache()
+ .transform(transformation)
+ .into(imageView);
+ }
+
+ public static BitmapLoader createUsing(CropView cropView) {
+ return createUsing(cropView, Picasso.with(cropView.getContext()));
+ }
+
+ public static BitmapLoader createUsing(CropView cropView, Picasso picasso) {
+ return new PicassoBitmapLoader(picasso,
+ PicassoFillViewportTransformation.createUsing(cropView.getViewportWidth(), cropView.getViewportHeight()));
+ }
+}
diff --git a/scissors/src/main/java/com/lyft/android/scissors/PicassoFillViewportTransformation.java b/scissors/src/main/java/com/lyft/android/scissors/PicassoFillViewportTransformation.java
new file mode 100644
index 0000000..8174c12
--- /dev/null
+++ b/scissors/src/main/java/com/lyft/android/scissors/PicassoFillViewportTransformation.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissors;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import com.squareup.picasso.Transformation;
+
+class PicassoFillViewportTransformation implements Transformation {
+
+ private final int viewportWidth;
+ private final int viewportHeight;
+
+ public PicassoFillViewportTransformation(int viewportWidth, int viewportHeight) {
+ this.viewportWidth = viewportWidth;
+ this.viewportHeight = viewportHeight;
+ }
+
+ @Override
+ public Bitmap transform(Bitmap source) {
+ int sourceWidth = source.getWidth();
+ int sourceHeight = source.getHeight();
+
+ Rect target = CropViewExtensions.computeTargetSize(sourceWidth, sourceHeight, viewportWidth, viewportHeight);
+ final Bitmap result = Bitmap.createScaledBitmap(
+ source,
+ target.width(),
+ target.height(),
+ true);
+
+ if (result != source) {
+ source.recycle();
+ }
+
+ return result;
+ }
+
+ @Override
+ public String key() {
+ return viewportWidth + "x" + viewportHeight;
+ }
+
+ public static Transformation createUsing(int viewportWidth, int viewportHeight) {
+ return new PicassoFillViewportTransformation(viewportWidth, viewportHeight);
+ }
+}
diff --git a/scissors/src/main/java/com/lyft/android/scissors/TouchManager.java b/scissors/src/main/java/com/lyft/android/scissors/TouchManager.java
new file mode 100644
index 0000000..2feecd8
--- /dev/null
+++ b/scissors/src/main/java/com/lyft/android/scissors/TouchManager.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissors;
+
+import android.annotation.TargetApi;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.os.Build;
+import android.view.MotionEvent;
+
+class TouchManager {
+
+ private final int maxNumberOfTouchPoints;
+ private final CropViewConfig cropViewConfig;
+
+ private final TouchPoint[] points;
+ private final TouchPoint[] previousPoints;
+
+ private float minimumScale;
+ private float maximumScale;
+ private Rect imageBounds;
+ private int viewportWidth;
+ private int viewportHeight;
+ private int bitmapWidth;
+ private int bitmapHeight;
+
+ private int verticalLimit;
+ private int horizontalLimit;
+
+ private float scale = 1.0f;
+ private TouchPoint position = new TouchPoint();
+
+ public TouchManager(final int maxNumberOfTouchPoints, final CropViewConfig cropViewConfig) {
+ this.maxNumberOfTouchPoints = maxNumberOfTouchPoints;
+ this.cropViewConfig = cropViewConfig;
+
+ points = new TouchPoint[maxNumberOfTouchPoints];
+ previousPoints = new TouchPoint[maxNumberOfTouchPoints];
+ minimumScale = cropViewConfig.getMinScale();
+ maximumScale = cropViewConfig.getMaxScale();
+ }
+
+ @TargetApi(Build.VERSION_CODES.FROYO)
+ public void onEvent(MotionEvent event) {
+ int index = event.getActionIndex();
+ if (index >= maxNumberOfTouchPoints) {
+ return; // We don't care about this pointer, ignore it.
+ }
+
+ if (isUpAction(event.getActionMasked())) {
+ previousPoints[index] = null;
+ points[index] = null;
+ } else {
+ updateCurrentAndPreviousPoints(event);
+ }
+
+ handleDragGesture();
+ handlePinchGesture();
+ handleDragOutsideViewport(event);
+ }
+
+ public void applyPositioningAndScale(Matrix matrix) {
+ matrix.postTranslate(-bitmapWidth / 2.0f, -bitmapHeight / 2.0f);
+ matrix.postScale(scale, scale);
+ matrix.postTranslate(position.getX(), position.getY());
+ }
+
+ public void resetFor(int bitmapWidth, int bitmapHeight, int availableWidth, int availableHeight) {
+ position.set(availableWidth / 2, availableHeight / 2);
+
+ imageBounds = new Rect(0, 0, availableWidth / 2, availableHeight / 2);
+ setViewport(availableWidth);
+ setMinimumScale();
+
+ this.bitmapWidth = bitmapWidth;
+ this.bitmapHeight = bitmapHeight;
+
+ horizontalLimit = computeLimit(bitmapWidth, viewportWidth);
+ verticalLimit = computeLimit(bitmapHeight, viewportHeight);
+ }
+
+ public int getViewportWidth() {
+ return viewportWidth;
+ }
+
+ public int getViewportHeight() {
+ return viewportHeight;
+ }
+
+ private void handleDragGesture() {
+ if (getDownCount() != 1) {
+ return;
+ }
+ position.add(moveDelta(0));
+ }
+
+ private void handlePinchGesture() {
+ if (getDownCount() != 2) {
+ return;
+ }
+ updateScale();
+ horizontalLimit = computeLimit((int) (bitmapWidth * scale), viewportWidth);
+ verticalLimit = computeLimit((int) (bitmapHeight * scale), viewportHeight);
+ }
+
+ @TargetApi(Build.VERSION_CODES.FROYO)
+ private void handleDragOutsideViewport(MotionEvent event) {
+ if (imageBounds == null || !isUpAction(event.getActionMasked())) {
+ return;
+ }
+
+ float newY = position.getY();
+ int bottom = imageBounds.bottom;
+
+ if (bottom - newY >= verticalLimit) {
+ newY = bottom - verticalLimit;
+ } else if (newY - bottom >= verticalLimit) {
+ newY = bottom + verticalLimit;
+ }
+
+ float newX = position.getX();
+ int right = imageBounds.right;
+ if (newX <= right - horizontalLimit) {
+ newX = right - horizontalLimit;
+ } else if (newX > right + horizontalLimit) {
+ newX = right + horizontalLimit;
+ }
+
+ position.set(newX, newY);
+ }
+
+ private void updateCurrentAndPreviousPoints(MotionEvent event) {
+ for (int i = 0; i < maxNumberOfTouchPoints; i++) {
+ if (i < event.getPointerCount()) {
+ final float eventX = event.getX(i);
+ final float eventY = event.getY(i);
+
+ if (points[i] == null) {
+ points[i] = new TouchPoint(eventX, eventY);
+ previousPoints[i] = null;
+ } else {
+ if (previousPoints[i] == null) {
+ previousPoints[i] = new TouchPoint();
+ }
+ previousPoints[i].copy(points[i]);
+ points[i].set(eventX, eventY);
+ }
+ } else {
+ previousPoints[i] = null;
+ points[i] = null;
+ }
+ }
+ }
+
+ private void setViewport(int w) {
+ viewportWidth = w;
+ viewportHeight = (int) (w * cropViewConfig.getViewportHeightRatio());
+ }
+
+ private void setMinimumScale() {
+ float imageAspect = (float) bitmapWidth / bitmapHeight;
+ float viewportAspect = (float) viewportWidth / viewportHeight;
+
+ if (bitmapWidth < viewportWidth || bitmapHeight < viewportHeight) {
+ minimumScale = 1f;
+ } else if (imageAspect > viewportAspect) {
+ minimumScale = (float) viewportHeight / bitmapHeight;
+ } else {
+ minimumScale = (float) viewportWidth / bitmapWidth;
+ }
+ scale = minimumScale;
+ }
+
+ private void updateScale() {
+ TouchPoint current = vector(points[0], points[1]);
+ TouchPoint previous = previousVector(0, 1);
+ float currentDistance = current.getLength();
+ float previousDistance = previous.getLength();
+
+ float newScale = scale;
+ if (previousDistance != 0) {
+ newScale *= currentDistance / previousDistance;
+ }
+ newScale = newScale < minimumScale ? minimumScale : newScale;
+ newScale = newScale > maximumScale ? maximumScale : newScale;
+
+ scale = newScale;
+ }
+
+ private boolean isPressed(int index) {
+ return points[index] != null;
+ }
+
+ private int getDownCount() {
+ int count = 0;
+ for (TouchPoint point : points) {
+ if (point != null) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private TouchPoint moveDelta(int index) {
+ if (isPressed(index)) {
+ TouchPoint previous =
+ previousPoints[index] != null ? previousPoints[index] : points[index];
+ return TouchPoint.subtract(points[index], previous);
+ } else {
+ return new TouchPoint();
+ }
+ }
+
+ private TouchPoint previousVector(int indexA, int indexB) {
+ return previousPoints[indexA] == null || previousPoints[indexB] == null
+ ? vector(points[indexA], points[indexB])
+ : vector(previousPoints[indexA], previousPoints[indexB]);
+ }
+
+ private static int computeLimit
+ (int bitmapSize, int viewportSize) {
+ return (bitmapSize - viewportSize) / 2;
+ }
+
+ private static TouchPoint vector(TouchPoint a, TouchPoint b) {
+ return TouchPoint.subtract(b, a);
+ }
+
+ private static boolean isUpAction(int actionMasked) {
+ return actionMasked == MotionEvent.ACTION_POINTER_UP || actionMasked == MotionEvent.ACTION_UP;
+ }
+}
diff --git a/scissors/src/main/java/com/lyft/android/scissors/TouchPoint.java b/scissors/src/main/java/com/lyft/android/scissors/TouchPoint.java
new file mode 100644
index 0000000..8dc089a
--- /dev/null
+++ b/scissors/src/main/java/com/lyft/android/scissors/TouchPoint.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissors;
+
+class TouchPoint {
+
+ private float x;
+ private float y;
+
+ public TouchPoint() {
+ }
+
+ public TouchPoint(float x, float y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ public float getX() {
+ return x;
+ }
+
+ public float getY() {
+ return y;
+ }
+
+ public float getLength() {
+ return (float) Math.sqrt(x * x + y * y);
+ }
+
+ public TouchPoint copy(TouchPoint other) {
+ x = other.getX();
+ y = other.getY();
+ return this;
+ }
+
+ public TouchPoint set(float x, float y) {
+ this.x = x;
+ this.y = y;
+ return this;
+ }
+
+ public TouchPoint add(TouchPoint value) {
+ this.x += value.getX();
+ this.y += value.getY();
+ return this;
+ }
+
+ public static TouchPoint subtract(TouchPoint lhs, TouchPoint rhs) {
+ return new TouchPoint(lhs.x - rhs.x, lhs.y - rhs.y);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("(%.4f, %.4f)", x, y);
+ }
+}
diff --git a/scissors/src/main/java/com/lyft/android/scissors/Utils.java b/scissors/src/main/java/com/lyft/android/scissors/Utils.java
new file mode 100644
index 0000000..78a623f
--- /dev/null
+++ b/scissors/src/main/java/com/lyft/android/scissors/Utils.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2015 Lyft, 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.
+ */
+package com.lyft.android.scissors;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+class Utils {
+
+ public static void checkArg(boolean expression, String msg) {
+ if (!expression) {
+ throw new IllegalArgumentException(msg);
+ }
+ }
+
+ public static void checkNotNull(Object object, String msg) {
+ if (object == null) {
+ throw new NullPointerException(msg);
+ }
+ }
+
+ public static Bitmap asBitmap(Drawable drawable, int minWidth, int minHeight) {
+ final Rect tmpRect = new Rect();
+ drawable.copyBounds(tmpRect);
+ if (tmpRect.isEmpty()) {
+ tmpRect.set(0, 0, Math.max(minWidth, drawable.getIntrinsicWidth()), Math.max(minHeight, drawable.getIntrinsicHeight()));
+ drawable.setBounds(tmpRect);
+ }
+ Bitmap bitmap = Bitmap.createBitmap(tmpRect.width(), tmpRect.height(), Bitmap.Config.ARGB_8888);
+ drawable.draw(new Canvas(bitmap));
+ return bitmap;
+ }
+
+ private final static ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();
+ private static final String TAG = "scissors.Utils";
+
+ public static Future flushToFile(final Bitmap bitmap,
+ final Bitmap.CompressFormat format,
+ final int quality,
+ final File file) {
+
+ return EXECUTOR_SERVICE.submit(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ file.getParentFile().mkdirs();
+
+ OutputStream outputStream = new FileOutputStream(file);
+ bitmap.compress(format, quality, outputStream);
+ outputStream.flush();
+ outputStream.close();
+ } catch (final Throwable throwable) {
+ if (BuildConfig.DEBUG) {
+ Log.e(TAG, "Error attempting to save bitmap.", throwable);
+ }
+ }
+ }
+ }, null);
+ }
+
+ public static Future flushToStream(final Bitmap bitmap,
+ final Bitmap.CompressFormat format,
+ final int quality,
+ final OutputStream outputStream,
+ final boolean closeWhenDone) {
+
+ return EXECUTOR_SERVICE.submit(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ bitmap.compress(format, quality, outputStream);
+ outputStream.flush();
+ if (closeWhenDone) {
+ outputStream.close();
+ }
+ } catch (final Throwable throwable) {
+ if (BuildConfig.DEBUG) {
+ Log.e(TAG, "Error attempting to save bitmap.", throwable);
+ }
+ }
+ }
+ }, null);
+ }
+}
diff --git a/scissors/src/main/res/drawable-hdpi/ic_content_add.png b/scissors/src/main/res/drawable-hdpi/ic_content_add.png
new file mode 100755
index 0000000..3855989
Binary files /dev/null and b/scissors/src/main/res/drawable-hdpi/ic_content_add.png differ
diff --git a/scissors/src/main/res/drawable-hdpi/ic_content_content_cut.png b/scissors/src/main/res/drawable-hdpi/ic_content_content_cut.png
new file mode 100755
index 0000000..4ab8515
Binary files /dev/null and b/scissors/src/main/res/drawable-hdpi/ic_content_content_cut.png differ
diff --git a/scissors/src/main/res/drawable-mdpi/ic_content_add.png b/scissors/src/main/res/drawable-mdpi/ic_content_add.png
new file mode 100755
index 0000000..2fd396f
Binary files /dev/null and b/scissors/src/main/res/drawable-mdpi/ic_content_add.png differ
diff --git a/scissors/src/main/res/drawable-mdpi/ic_content_content_cut.png b/scissors/src/main/res/drawable-mdpi/ic_content_content_cut.png
new file mode 100755
index 0000000..7acc8f2
Binary files /dev/null and b/scissors/src/main/res/drawable-mdpi/ic_content_content_cut.png differ
diff --git a/scissors/src/main/res/drawable-xhdpi/ic_content_add.png b/scissors/src/main/res/drawable-xhdpi/ic_content_add.png
new file mode 100755
index 0000000..478cb6e
Binary files /dev/null and b/scissors/src/main/res/drawable-xhdpi/ic_content_add.png differ
diff --git a/scissors/src/main/res/drawable-xhdpi/ic_content_content_cut.png b/scissors/src/main/res/drawable-xhdpi/ic_content_content_cut.png
new file mode 100755
index 0000000..0cc6ded
Binary files /dev/null and b/scissors/src/main/res/drawable-xhdpi/ic_content_content_cut.png differ
diff --git a/scissors/src/main/res/drawable-xxhdpi/ic_content_add.png b/scissors/src/main/res/drawable-xxhdpi/ic_content_add.png
new file mode 100755
index 0000000..f976d7e
Binary files /dev/null and b/scissors/src/main/res/drawable-xxhdpi/ic_content_add.png differ
diff --git a/scissors/src/main/res/drawable-xxhdpi/ic_content_content_cut.png b/scissors/src/main/res/drawable-xxhdpi/ic_content_content_cut.png
new file mode 100755
index 0000000..8da18d5
Binary files /dev/null and b/scissors/src/main/res/drawable-xxhdpi/ic_content_content_cut.png differ
diff --git a/scissors/src/main/res/drawable-xxxhdpi/ic_content_add.png b/scissors/src/main/res/drawable-xxxhdpi/ic_content_add.png
new file mode 100755
index 0000000..512f8f3
Binary files /dev/null and b/scissors/src/main/res/drawable-xxxhdpi/ic_content_add.png differ
diff --git a/scissors/src/main/res/drawable-xxxhdpi/ic_content_content_cut.png b/scissors/src/main/res/drawable-xxxhdpi/ic_content_content_cut.png
new file mode 100755
index 0000000..c23e9ef
Binary files /dev/null and b/scissors/src/main/res/drawable-xxxhdpi/ic_content_content_cut.png differ
diff --git a/scissors/src/main/res/values/attrs.xml b/scissors/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..82024e2
--- /dev/null
+++ b/scissors/src/main/res/values/attrs.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scissors/src/test/java/com/lyft/android/scissors/TargetSizeTest.java b/scissors/src/test/java/com/lyft/android/scissors/TargetSizeTest.java
new file mode 100644
index 0000000..5e128b8
--- /dev/null
+++ b/scissors/src/test/java/com/lyft/android/scissors/TargetSizeTest.java
@@ -0,0 +1,99 @@
+package com.lyft.android.scissors;
+
+import android.graphics.Rect;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+
+@RunWith(ParameterizedRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class TargetSizeTest {
+
+ @ParameterizedRobolectricTestRunner.Parameters(name = "{2} viewport = [{0}x{1}]")
+ public static Collection