├── .gitignore ├── .project ├── .settings └── org.eclipse.buildship.core.prefs ├── .travis.yml ├── LICENSE.txt ├── README.md ├── build.gradle ├── cutout ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── publish.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── gabrielbb │ │ └── cutout │ │ ├── BitmapUtility.java │ │ ├── CutOut.java │ │ ├── CutOutActivity.java │ │ ├── DrawView.java │ │ ├── IntroActivity.java │ │ └── SaveDrawingTask.java │ └── res │ ├── drawable │ ├── done.png │ ├── intro_magic_wand.png │ ├── intro_magnifier.png │ ├── intro_pencil.png │ ├── magic_active.png │ ├── magic_inactive.png │ ├── magic_selector.xml │ ├── magnifier_active.png │ ├── magnifier_inactive.png │ ├── magnifier_selector.xml │ ├── pencil_active.png │ ├── pencil_inactive.png │ ├── pencil_selector.xml │ ├── redo_active.png │ ├── redo_inactive.png │ ├── redo_selector.xml │ ├── undo_active.png │ ├── undo_inactive.png │ └── undo_selector.xml │ ├── layout │ └── activity_photo_edit.xml │ └── values │ ├── colors.xml │ └── strings.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── Capture.JPG ├── Capture_2.JPG ├── Magic_Wand.JPG ├── Pencil.JPG ├── Zoom.JPG └── before_app.jpg ├── settings.gradle └── test ├── .classpath ├── .gitignore ├── .project ├── .settings └── org.eclipse.buildship.core.prefs ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── github │ └── gabrielbb │ └── cutout │ └── MainActivityTest.java └── main ├── AndroidManifest.xml ├── java └── com │ └── github │ └── gabrielbb │ └── cutout │ └── test │ └── MainActivity.java └── res ├── drawable ├── image_icon.png └── test_image.jpg ├── layout ├── activity_main.xml └── content_main.xml └── values ├── dimens.xml ├── strings.xml └── styles.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .DS_Store 3 | .idea 4 | build/ 5 | local.properties 6 | localhost/ 7 | obj/ 8 | *.iml 9 | Gemfile.lock 10 | _site/ -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Android 4 | Project Android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir= 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: oraclejdk8 3 | sudo: false 4 | 5 | android: 6 | 7 | components: 8 | - tools 9 | - platform-tools 10 | - build-tools-28.0.3 11 | - android-28 12 | - android-22 13 | - extra-google-google_play_services 14 | - extra-google-m2repository 15 | - extra-android-m2repository 16 | - sys-img-armeabi-v7a-android-22 17 | 18 | before_script: 19 | - echo yes | android update sdk --all --filter build-tools-28.0.3 --no-ui --force 20 | - touch local.properties 21 | - echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a -c 100M 22 | - emulator -avd test -no-audio -no-window & 23 | - android-wait-for-emulator 24 | - adb shell input keyevent 82 & 25 | - chmod +x gradlew 26 | 27 | script: 28 | - ./gradlew clean build connectedCheck 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Gabriel Basilio Brito 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to use, 5 | copy, modify, merge, and to permit persons to whom the Software is furnished to do so, 6 | subject to the following conditions: 7 | 8 | 1 - You cannot use this software to make an Android Backgroung Removal App and publish it to the Google Play Store, but you can use it to integrate the functionality in an existing app. 9 | 10 | 2 - The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | ✂️ 5 | Android-CutOut 6 |

7 | Android image background cutting library 8 |
9 | 10 | [ ![Version](https://api.bintray.com/packages/gabrielbb/Android-CutOut/Android-CutOut/images/download.svg) ](https://bintray.com/gabrielbb/Android-CutOut/Android-CutOut/_latestVersion) 11 | [ ![Build](https://api.travis-ci.org/GabrielBB/Android-CutOut.svg?branch=master) ](https://api.travis-ci.org/GabrielBB/Android-CutOut.svg?branch=master) 12 | 13 | ## Usage 14 | 15 | Add Gradle dependency: 16 | ```groovy 17 | implementation 'com.github.gabrielbb:cutout:0.1.2' 18 | ``` 19 | 20 | Start the CutOut screen with this single line: 21 | 22 | ```java 23 | CutOut.activity().start(this); 24 | ``` 25 | 26 |   27 | 28 | ### Getting the result 29 | 30 | ```java 31 | @Override 32 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 33 | if (requestCode == CutOut.CUTOUT_ACTIVITY_REQUEST_CODE) { 34 | 35 | switch (resultCode) { 36 | case Activity.RESULT_OK: 37 | Uri imageUri = CutOut.getUri(data); 38 | // Save the image using the returned Uri here 39 | break; 40 | case CutOut.CUTOUT_ACTIVITY_RESULT_ERROR_CODE: 41 | Exception ex = CutOut.getError(data); 42 | break; 43 | default: 44 | System.out.print("User cancelled the CutOut screen"); 45 | } 46 | } 47 | } 48 | ``` 49 | 50 | ## Features 51 | 52 |     53 | 54 | 55 | ## Options 56 | 57 | You can use one or more options from these: 58 | 59 | ```java 60 | CutOut.activity() 61 | .src(uri) 62 | .bordered() 63 | .noCrop() 64 | .intro() 65 | .start(this); 66 | ``` 67 | 68 | - #### src 69 | 70 | By default the user can select images from camera or gallery but you can also pass an `android.net.Uri` of an image that is already saved: 71 | 72 | ```java 73 | Uri uri = Uri.parse("/images/cat.jpg"); 74 | 75 | CutOut.activity().src(uri).start(this); 76 | ``` 77 | 78 | 79 | - #### bordered 80 | 81 | ```java 82 | CutOut.activity().bordered().start(this); 83 | ``` 84 | 85 | This option makes the final PNG have a border around it. The default border color is White. You can also pass the `android.graphics.Color` of your choice. 86 | 87 | 88 | - #### noCrop 89 | 90 | ```java 91 | CutOut.activity().noCrop().start(this); 92 | ``` 93 | 94 | By default and thanks to this library: [Android-Image-Cropper](https://github.com/ArthurHub/Android-Image-Cropper), the user can crop or rotate the image. This option disables that cropping screen. 95 | 96 | 97 | 98 | - #### intro 99 | 100 | ```java 101 | CutOut.activity().intro().start(this); 102 | ``` 103 | 104 | Display an intro explaining every button usage. The user can skip the intro and it is only shown once. The images displayed in the intro are the same you saw in the "Features" section of this document. 105 | 106 | ## Change log 107 | *0.1.2* 108 | - Removed Admob Ads automatic integration. I will probably add it later. For now, it was causing problems. 109 | - Images are now saved as temporary files in the cache directory. This guarantees that these images will be deleted when users uninstall your app or when the disk memory is low. If you want the images to live forever on the Gallery, you should take the returned Uri and save the image there by yourself. 110 | 111 | ## License 112 | Copyright (c) 2018 Gabriel Basilio Brito 113 | 114 | Permission is hereby granted, free of charge, to any person obtaining a copy 115 | of this software and associated documentation files (the "Software"), to use, 116 | copy, modify, merge, and to permit persons to whom the Software is furnished to do so, 117 | subject to the following conditions: 118 | 119 | 1 - You cannot use this software to make an Android app that its main goal is to remove background from images and publish it to the Google Play Store, but you can use it to integrate the functionality in an existing app or a new app that does more than just this. 120 | 121 | 2 - The above copyright notice and this permission notice shall be included in all 122 | copies or substantial portions of the Software. 123 | 124 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 125 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 126 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 127 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 128 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 129 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 130 | SOFTWARE. 131 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.2.1' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | maven { 22 | url 'https://jitpack.io' 23 | } 24 | maven { url 'https://dl.bintray.com/gabrielbb/Android-CutOut' } 25 | } 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | 32 | ext { 33 | compileSdkVersion = 28 34 | minSdkVersion = 15 35 | buildToolsVersion = '28.0.3' 36 | versionCode = 2 37 | javaVersion = JavaVersion.VERSION_1_8 38 | PUBLISH_GROUP_ID = 'com.github.gabrielbb' 39 | PUBLISH_ARTIFACT_ID = 'cutout' 40 | PUBLISH_VERSION = '0.1.2' 41 | } -------------------------------------------------------------------------------- /cutout/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /cutout/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply from: 'publish.gradle' 3 | 4 | android { 5 | compileSdkVersion rootProject.compileSdkVersion 6 | defaultConfig { 7 | minSdkVersion rootProject.minSdkVersion 8 | targetSdkVersion rootProject.compileSdkVersion 9 | versionCode rootProject.versionCode 10 | versionName rootProject.PUBLISH_VERSION 11 | 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | 14 | } 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | 23 | compileOptions { 24 | sourceCompatibility rootProject.javaVersion 25 | targetCompatibility rootProject.javaVersion 26 | } 27 | 28 | lintOptions { 29 | abortOnError false 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation fileTree(dir: 'libs', include: ['*.jar']) 35 | 36 | implementation 'com.android.support:appcompat-v7:28.0.0' 37 | implementation 'com.github.duanhong169:checkerboarddrawable:1.0.2' 38 | implementation 'com.theartofdev.edmodo:android-image-cropper:2.7.0' 39 | implementation 'com.alexvasilkov:gesture-views:2.5.2' 40 | implementation 'com.github.apl-devs:appintro:v4.2.3' 41 | implementation 'com.github.jkwiecien:EasyImage:1.3.1' 42 | } 43 | 44 | buildscript { 45 | repositories { 46 | jcenter() 47 | } 48 | dependencies { 49 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' 50 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cutout/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /cutout/publish.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.github.dcendents.android-maven' 2 | apply plugin: 'com.jfrog.bintray' 3 | 4 | android { 5 | compileSdkVersion rootProject.compileSdkVersion 6 | } 7 | 8 | 9 | ext { 10 | libraryName = 'Android-CutOut' 11 | bintrayRepo = libraryName 12 | bintrayName = bintrayRepo 13 | libraryDescription = 'Android image background removing library' 14 | 15 | gitUrl = 'https://github.com/GabrielBB/Android-CutOut.git' 16 | siteUrl = gitUrl 17 | 18 | developerId = 'gabrielbb' 19 | developerName = 'Gabriel Basilio Brito' 20 | developerEmail = 'gabrielbb0306@gmail.com' 21 | 22 | licenseName = 'The Apache Software License, Version 2.0' 23 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 24 | allLicenses = ["Apache-2.0"] 25 | } 26 | 27 | group = rootProject.PUBLISH_GROUP_ID 28 | version = rootProject.PUBLISH_VERSION 29 | 30 | install { 31 | repositories.mavenInstaller { 32 | pom.project { 33 | packaging 'aar' 34 | groupId rootProject.PUBLISH_GROUP_ID 35 | artifactId rootProject.PUBLISH_ARTIFACT_ID 36 | 37 | name libraryName 38 | description libraryDescription 39 | url siteUrl 40 | 41 | licenses { 42 | license { 43 | name licenseName 44 | url licenseUrl 45 | } 46 | } 47 | developers { 48 | developer { 49 | id developerId 50 | name developerName 51 | email developerEmail 52 | } 53 | } 54 | scm { 55 | connection gitUrl 56 | developerConnection gitUrl 57 | url siteUrl 58 | } 59 | } 60 | } 61 | } 62 | 63 | task sourcesJar(type: Jar) { 64 | classifier = 'sources' 65 | from android.sourceSets.main.java.srcDirs 66 | } 67 | 68 | task javadoc(type: Javadoc) { 69 | source = android.sourceSets.main.java.srcDirs 70 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 71 | } 72 | 73 | task javadocJar(type: Jar, dependsOn: javadoc) { 74 | classifier = 'javadoc' 75 | from javadoc.destinationDir 76 | } 77 | 78 | artifacts { 79 | archives javadocJar 80 | archives sourcesJar 81 | } 82 | 83 | Properties properties = new Properties() 84 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 85 | 86 | bintray { 87 | user = properties.getProperty("bintray.user") 88 | key = properties.getProperty("bintray.apikey") 89 | 90 | configurations = ['archives'] 91 | pkg { 92 | repo = bintrayRepo 93 | name = bintrayName 94 | desc = libraryDescription 95 | websiteUrl = siteUrl 96 | vcsUrl = gitUrl 97 | licenses = allLicenses 98 | dryRun = false 99 | publish = true 100 | override = false 101 | publicDownloadNumbers = true 102 | version { 103 | desc = libraryDescription 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /cutout/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 14 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /cutout/src/main/java/com/github/gabrielbb/cutout/BitmapUtility.java: -------------------------------------------------------------------------------- 1 | package com.github.gabrielbb.cutout; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Canvas; 5 | import android.graphics.Matrix; 6 | import android.graphics.Paint; 7 | import android.graphics.PorterDuff; 8 | import android.graphics.PorterDuffColorFilter; 9 | 10 | class BitmapUtility { 11 | 12 | static Bitmap getResizedBitmap(Bitmap bitmap, int width, int height) { 13 | Bitmap background = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 14 | 15 | float originalWidth = bitmap.getWidth(); 16 | float originalHeight = bitmap.getHeight(); 17 | 18 | Canvas canvas = new Canvas(background); 19 | 20 | float scale = width / originalWidth; 21 | 22 | float xTranslation = 0.0f; 23 | float yTranslation = (height - originalHeight * scale) / 2.0f; 24 | 25 | Matrix transformation = new Matrix(); 26 | transformation.postTranslate(xTranslation, yTranslation); 27 | transformation.preScale(scale, scale); 28 | 29 | Paint paint = new Paint(); 30 | paint.setFilterBitmap(true); 31 | 32 | canvas.drawBitmap(bitmap, transformation, paint); 33 | 34 | return background; 35 | } 36 | 37 | static Bitmap getBorderedBitmap(Bitmap image, int borderColor, int borderSize) { 38 | 39 | // Creating a canvas with an empty bitmap, this is the bitmap that gonna store the final canvas changes 40 | Bitmap finalImage = Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888); 41 | Canvas canvas = new Canvas(finalImage); 42 | 43 | // Make a smaller copy of the image to draw on top of original 44 | Bitmap imageCopy = Bitmap.createScaledBitmap(image, image.getWidth() - borderSize, image.getHeight() - borderSize, true); 45 | 46 | // Let's draw the bigger image using a white paint brush 47 | Paint paint = new Paint(); 48 | paint.setColorFilter(new PorterDuffColorFilter(borderColor, PorterDuff.Mode.SRC_ATOP)); 49 | canvas.drawBitmap(image, 0, 0, paint); 50 | 51 | int width = image.getWidth(); 52 | int height = image.getHeight(); 53 | float centerX = (width - imageCopy.getWidth()) * 0.5f; 54 | float centerY = (height - imageCopy.getHeight()) * 0.5f; 55 | // Now let's draw the original image on top of the white image, passing a null paint because we want to keep it original 56 | canvas.drawBitmap(imageCopy, centerX, centerY, null); 57 | 58 | // Returning the image with the final results 59 | return finalImage; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /cutout/src/main/java/com/github/gabrielbb/cutout/CutOut.java: -------------------------------------------------------------------------------- 1 | package com.github.gabrielbb.cutout; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.graphics.Color; 7 | import android.net.Uri; 8 | import android.support.annotation.NonNull; 9 | import android.support.annotation.Nullable; 10 | 11 | public class CutOut { 12 | 13 | public static final short CUTOUT_ACTIVITY_REQUEST_CODE = 368; 14 | public static final short CUTOUT_ACTIVITY_RESULT_ERROR_CODE = 3680; 15 | 16 | static final String CUTOUT_EXTRA_SOURCE = "CUTOUT_EXTRA_SOURCE"; 17 | static final String CUTOUT_EXTRA_RESULT = "CUTOUT_EXTRA_RESULT"; 18 | static final String CUTOUT_EXTRA_BORDER_COLOR = "CUTOUT_EXTRA_BORDER_COLOR"; 19 | static final String CUTOUT_EXTRA_CROP = "CUTOUT_EXTRA_CROP"; 20 | static final String CUTOUT_EXTRA_INTRO = "CUTOUT_EXTRA_INTRO"; 21 | 22 | public static ActivityBuilder activity() { 23 | return new ActivityBuilder(); 24 | } 25 | 26 | /** 27 | * Builder used for creating CutOut Activity by user request. 28 | */ 29 | public static final class ActivityBuilder { 30 | 31 | @Nullable 32 | private Uri source; // The image to crop source Android uri 33 | private boolean bordered; 34 | private boolean crop = true; // By default the cropping activity is started 35 | private boolean intro; 36 | private int borderColor = Color.WHITE; // Default border color is no border color is passed 37 | 38 | private ActivityBuilder() { 39 | 40 | } 41 | 42 | /** 43 | * Get {@link CutOutActivity} intent to start the activity. 44 | */ 45 | private Intent getIntent(@NonNull Context context) { 46 | Intent intent = new Intent(); 47 | intent.setClass(context, CutOutActivity.class); 48 | 49 | if (source != null) { 50 | intent.putExtra(CUTOUT_EXTRA_SOURCE, source); 51 | } 52 | 53 | if (bordered) { 54 | intent.putExtra(CUTOUT_EXTRA_BORDER_COLOR, borderColor); 55 | } 56 | 57 | if (crop) { 58 | intent.putExtra(CUTOUT_EXTRA_CROP, true); 59 | } 60 | 61 | if (intro) { 62 | intent.putExtra(CUTOUT_EXTRA_INTRO, true); 63 | } 64 | 65 | return intent; 66 | } 67 | 68 | /** 69 | * By default the user can select images from camera or gallery but you can also call this method to load a pre-saved image 70 | * 71 | * @param source {@link android.net.Uri} instance of the image to be loaded 72 | */ 73 | public ActivityBuilder src(Uri source) { 74 | this.source = source; 75 | return this; 76 | } 77 | 78 | /** 79 | * This method adds a white border around the final PNG image 80 | */ 81 | public ActivityBuilder bordered() { 82 | this.bordered = true; 83 | return this; 84 | } 85 | 86 | /** 87 | * This method adds a border around the final PNG image 88 | * 89 | * @param borderColor The border color. You can pass any {@link android.graphics.Color} 90 | */ 91 | public ActivityBuilder bordered(int borderColor) { 92 | this.borderColor = borderColor; 93 | return bordered(); 94 | } 95 | 96 | /** 97 | * Disables the cropping screen shown before the background removal screen 98 | */ 99 | public ActivityBuilder noCrop() { 100 | this.crop = false; 101 | return this; 102 | } 103 | 104 | /** 105 | * Shows an introduction to the activity, explaining every button usage. The intro is show only once. 106 | */ 107 | public ActivityBuilder intro() { 108 | this.intro = true; 109 | return this; 110 | } 111 | 112 | /** 113 | * Start {@link CutOutActivity}. 114 | * 115 | * @param activity activity to receive result 116 | */ 117 | public void start(@NonNull Activity activity) { 118 | activity.startActivityForResult(getIntent(activity), CUTOUT_ACTIVITY_REQUEST_CODE); 119 | } 120 | } 121 | 122 | /** 123 | * Reads the {@link android.net.Uri} from the result data. This Uri is the path to the saved PNG 124 | * 125 | * @param data Result data to get the Uri from 126 | */ 127 | public static Uri getUri(@Nullable Intent data) { 128 | return data != null ? data.getParcelableExtra(CUTOUT_EXTRA_RESULT) : null; 129 | } 130 | 131 | /** 132 | * Gets an Exception from the result data if the {@link CutOutActivity} failed at some point 133 | * 134 | * @param data Result data to get the Exception from 135 | */ 136 | public static Exception getError(@Nullable Intent data) { 137 | return data != null ? (Exception) data.getSerializableExtra(CUTOUT_EXTRA_RESULT) : null; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /cutout/src/main/java/com/github/gabrielbb/cutout/CutOutActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.gabrielbb.cutout; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.SharedPreferences; 8 | import android.content.pm.PackageManager; 9 | import android.graphics.Bitmap; 10 | import android.graphics.Color; 11 | import android.graphics.PorterDuff; 12 | import android.net.Uri; 13 | import android.os.Bundle; 14 | import android.provider.MediaStore; 15 | import android.support.annotation.NonNull; 16 | import android.support.v4.app.ActivityCompat; 17 | import android.support.v4.content.ContextCompat; 18 | import android.support.v7.app.AppCompatActivity; 19 | import android.support.v7.widget.Toolbar; 20 | import android.view.MenuItem; 21 | import android.view.View; 22 | import android.widget.Button; 23 | import android.widget.FrameLayout; 24 | import android.widget.LinearLayout; 25 | import android.widget.SeekBar; 26 | 27 | import com.alexvasilkov.gestures.views.interfaces.GestureView; 28 | import com.theartofdev.edmodo.cropper.CropImage; 29 | import com.theartofdev.edmodo.cropper.CropImageView; 30 | 31 | import java.io.File; 32 | import java.io.IOException; 33 | 34 | import pl.aprilapps.easyphotopicker.DefaultCallback; 35 | import pl.aprilapps.easyphotopicker.EasyImage; 36 | import top.defaults.checkerboarddrawable.CheckerboardDrawable; 37 | 38 | import static android.view.View.INVISIBLE; 39 | import static android.view.View.VISIBLE; 40 | import static com.github.gabrielbb.cutout.CutOut.CUTOUT_EXTRA_INTRO; 41 | 42 | public class CutOutActivity extends AppCompatActivity { 43 | 44 | private static final int INTRO_REQUEST_CODE = 4; 45 | private static final int WRITE_EXTERNAL_STORAGE_CODE = 1; 46 | private static final int IMAGE_CHOOSER_REQUEST_CODE = 2; 47 | private static final int CAMERA_REQUEST_CODE = 3; 48 | 49 | private static final String INTRO_SHOWN = "INTRO_SHOWN"; 50 | FrameLayout loadingModal; 51 | private GestureView gestureView; 52 | private DrawView drawView; 53 | private LinearLayout manualClearSettingsLayout; 54 | 55 | private static final short MAX_ERASER_SIZE = 150; 56 | private static final short BORDER_SIZE = 45; 57 | private static final float MAX_ZOOM = 4F; 58 | 59 | @Override 60 | protected void onCreate(Bundle savedInstanceState) { 61 | super.onCreate(savedInstanceState); 62 | 63 | setContentView(R.layout.activity_photo_edit); 64 | 65 | Toolbar toolbar = findViewById(R.id.photo_edit_toolbar); 66 | toolbar.setBackgroundColor(Color.BLACK); 67 | toolbar.setTitleTextColor(Color.WHITE); 68 | setSupportActionBar(toolbar); 69 | 70 | FrameLayout drawViewLayout = findViewById(R.id.drawViewLayout); 71 | 72 | int sdk = android.os.Build.VERSION.SDK_INT; 73 | 74 | if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) { 75 | drawViewLayout.setBackgroundDrawable(CheckerboardDrawable.create()); 76 | } else { 77 | drawViewLayout.setBackground(CheckerboardDrawable.create()); 78 | } 79 | 80 | SeekBar strokeBar = findViewById(R.id.strokeBar); 81 | strokeBar.setMax(MAX_ERASER_SIZE); 82 | strokeBar.setProgress(50); 83 | 84 | gestureView = findViewById(R.id.gestureView); 85 | 86 | drawView = findViewById(R.id.drawView); 87 | drawView.setDrawingCacheEnabled(true); 88 | drawView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 89 | //drawView.setDrawingCacheEnabled(true); 90 | drawView.setStrokeWidth(strokeBar.getProgress()); 91 | 92 | strokeBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 93 | @Override 94 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 95 | 96 | } 97 | 98 | @Override 99 | public void onStartTrackingTouch(SeekBar seekBar) { 100 | 101 | } 102 | 103 | @Override 104 | public void onStopTrackingTouch(SeekBar seekBar) { 105 | drawView.setStrokeWidth(seekBar.getProgress()); 106 | } 107 | }); 108 | 109 | loadingModal = findViewById(R.id.loadingModal); 110 | loadingModal.setVisibility(INVISIBLE); 111 | 112 | drawView.setLoadingModal(loadingModal); 113 | 114 | manualClearSettingsLayout = findViewById(R.id.manual_clear_settings_layout); 115 | 116 | setUndoRedo(); 117 | initializeActionButtons(); 118 | 119 | if (getSupportActionBar() != null) { 120 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 121 | getSupportActionBar().setDisplayShowHomeEnabled(true); 122 | getSupportActionBar().setDisplayShowTitleEnabled(false); 123 | 124 | if (toolbar.getNavigationIcon() != null) { 125 | toolbar.getNavigationIcon().setColorFilter(getResources().getColor(R.color.white), PorterDuff.Mode.SRC_ATOP); 126 | } 127 | 128 | } 129 | 130 | Button doneButton = findViewById(R.id.done); 131 | 132 | doneButton.setOnClickListener(v -> startSaveDrawingTask()); 133 | 134 | if (getIntent().getBooleanExtra(CUTOUT_EXTRA_INTRO, false) && !getPreferences(Context.MODE_PRIVATE).getBoolean(INTRO_SHOWN, false)) { 135 | Intent intent = new Intent(this, IntroActivity.class); 136 | startActivityForResult(intent, INTRO_REQUEST_CODE); 137 | } else { 138 | start(); 139 | } 140 | } 141 | 142 | @Override 143 | public boolean onOptionsItemSelected(MenuItem item) { 144 | switch (item.getItemId()) { 145 | // Respond to the action bar's Up/Home button 146 | case android.R.id.home: 147 | setResult(RESULT_CANCELED); 148 | finish(); 149 | return true; 150 | } 151 | return super.onOptionsItemSelected(item); 152 | } 153 | 154 | private Uri getExtraSource() { 155 | return getIntent().hasExtra(CutOut.CUTOUT_EXTRA_SOURCE) ? (Uri) getIntent().getParcelableExtra(CutOut.CUTOUT_EXTRA_SOURCE) : null; 156 | } 157 | 158 | private void start() { 159 | if (ContextCompat.checkSelfPermission(this, 160 | Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { 161 | 162 | Uri uri = getExtraSource(); 163 | 164 | if (getIntent().getBooleanExtra(CutOut.CUTOUT_EXTRA_CROP, false)) { 165 | 166 | CropImage.ActivityBuilder cropImageBuilder; 167 | if (uri != null) { 168 | cropImageBuilder = CropImage.activity(uri); 169 | } else { 170 | if (ContextCompat.checkSelfPermission(this, 171 | Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { 172 | 173 | cropImageBuilder = CropImage.activity(); 174 | } else { 175 | ActivityCompat.requestPermissions(this, 176 | new String[]{Manifest.permission.CAMERA}, 177 | CAMERA_REQUEST_CODE); 178 | return; 179 | } 180 | } 181 | 182 | cropImageBuilder = cropImageBuilder.setGuidelines(CropImageView.Guidelines.ON); 183 | cropImageBuilder.start(this); 184 | } else { 185 | if (uri != null) { 186 | setDrawViewBitmap(uri); 187 | } else { 188 | if (ContextCompat.checkSelfPermission(this, 189 | Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { 190 | 191 | EasyImage.openChooserWithGallery(this, getString(R.string.image_chooser_message), IMAGE_CHOOSER_REQUEST_CODE); 192 | } else { 193 | ActivityCompat.requestPermissions(this, 194 | new String[]{Manifest.permission.CAMERA}, 195 | CAMERA_REQUEST_CODE); 196 | } 197 | } 198 | } 199 | 200 | } else { 201 | ActivityCompat.requestPermissions(this, 202 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 203 | WRITE_EXTERNAL_STORAGE_CODE); 204 | } 205 | } 206 | 207 | private void startSaveDrawingTask() { 208 | SaveDrawingTask task = new SaveDrawingTask(this); 209 | 210 | int borderColor; 211 | if ((borderColor = getIntent().getIntExtra(CutOut.CUTOUT_EXTRA_BORDER_COLOR, -1)) != -1) { 212 | Bitmap image = BitmapUtility.getBorderedBitmap(this.drawView.getDrawingCache(), borderColor, BORDER_SIZE); 213 | task.execute(image); 214 | } else { 215 | task.execute(this.drawView.getDrawingCache()); 216 | } 217 | } 218 | 219 | @Override 220 | public void onRequestPermissionsResult(int requestCode, 221 | @NonNull String permissions[], @NonNull int[] grantResults) { 222 | if (grantResults.length > 0 223 | && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 224 | start(); 225 | } else { 226 | setResult(Activity.RESULT_CANCELED); 227 | finish(); 228 | } 229 | } 230 | 231 | private void activateGestureView() { 232 | gestureView.getController().getSettings() 233 | .setMaxZoom(MAX_ZOOM) 234 | .setDoubleTapZoom(-1f) // Falls back to max zoom level 235 | .setPanEnabled(true) 236 | .setZoomEnabled(true) 237 | .setDoubleTapEnabled(true) 238 | .setOverscrollDistance(0f, 0f) 239 | .setOverzoomFactor(2f); 240 | } 241 | 242 | private void deactivateGestureView() { 243 | gestureView.getController().getSettings() 244 | .setPanEnabled(false) 245 | .setZoomEnabled(false) 246 | .setDoubleTapEnabled(false); 247 | } 248 | 249 | private void initializeActionButtons() { 250 | Button autoClearButton = findViewById(R.id.auto_clear_button); 251 | Button manualClearButton = findViewById(R.id.manual_clear_button); 252 | Button zoomButton = findViewById(R.id.zoom_button); 253 | 254 | autoClearButton.setActivated(false); 255 | autoClearButton.setOnClickListener((buttonView) -> { 256 | if (!autoClearButton.isActivated()) { 257 | drawView.setAction(DrawView.DrawViewAction.AUTO_CLEAR); 258 | manualClearSettingsLayout.setVisibility(INVISIBLE); 259 | autoClearButton.setActivated(true); 260 | manualClearButton.setActivated(false); 261 | zoomButton.setActivated(false); 262 | deactivateGestureView(); 263 | } 264 | }); 265 | 266 | manualClearButton.setActivated(true); 267 | drawView.setAction(DrawView.DrawViewAction.MANUAL_CLEAR); 268 | manualClearButton.setOnClickListener((buttonView) -> { 269 | if (!manualClearButton.isActivated()) { 270 | drawView.setAction(DrawView.DrawViewAction.MANUAL_CLEAR); 271 | manualClearSettingsLayout.setVisibility(VISIBLE); 272 | manualClearButton.setActivated(true); 273 | autoClearButton.setActivated(false); 274 | zoomButton.setActivated(false); 275 | deactivateGestureView(); 276 | } 277 | 278 | }); 279 | 280 | zoomButton.setActivated(false); 281 | deactivateGestureView(); 282 | zoomButton.setOnClickListener((buttonView) -> { 283 | if (!zoomButton.isActivated()) { 284 | drawView.setAction(DrawView.DrawViewAction.ZOOM); 285 | manualClearSettingsLayout.setVisibility(INVISIBLE); 286 | zoomButton.setActivated(true); 287 | manualClearButton.setActivated(false); 288 | autoClearButton.setActivated(false); 289 | activateGestureView(); 290 | } 291 | 292 | }); 293 | } 294 | 295 | private void setUndoRedo() { 296 | Button undoButton = findViewById(R.id.undo); 297 | undoButton.setEnabled(false); 298 | undoButton.setOnClickListener(v -> undo()); 299 | Button redoButton = findViewById(R.id.redo); 300 | redoButton.setEnabled(false); 301 | redoButton.setOnClickListener(v -> redo()); 302 | 303 | drawView.setButtons(undoButton, redoButton); 304 | } 305 | 306 | void exitWithError(Exception e) { 307 | Intent intent = new Intent(); 308 | intent.putExtra(CutOut.CUTOUT_EXTRA_RESULT, e); 309 | setResult(CutOut.CUTOUT_ACTIVITY_RESULT_ERROR_CODE, intent); 310 | finish(); 311 | } 312 | 313 | private void setDrawViewBitmap(Uri uri) { 314 | try { 315 | Bitmap bitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), uri); 316 | drawView.setBitmap(bitmap); 317 | } catch (IOException e) { 318 | exitWithError(e); 319 | } 320 | } 321 | 322 | @Override 323 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 324 | 325 | if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) { 326 | 327 | CropImage.ActivityResult result = CropImage.getActivityResult(data); 328 | 329 | if (resultCode == Activity.RESULT_OK) { 330 | 331 | setDrawViewBitmap(result.getUri()); 332 | 333 | } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) { 334 | exitWithError(result.getError()); 335 | } else { 336 | setResult(Activity.RESULT_CANCELED); 337 | finish(); 338 | } 339 | } else if (requestCode == INTRO_REQUEST_CODE) { 340 | SharedPreferences.Editor editor = getPreferences(Context.MODE_PRIVATE).edit(); 341 | editor.putBoolean(INTRO_SHOWN, true); 342 | editor.apply(); 343 | start(); 344 | } else { 345 | EasyImage.handleActivityResult(requestCode, resultCode, data, this, new DefaultCallback() { 346 | @Override 347 | public void onImagePickerError(Exception e, EasyImage.ImageSource source, int type) { 348 | exitWithError(e); 349 | } 350 | 351 | @Override 352 | public void onImagePicked(File imageFile, EasyImage.ImageSource source, int type) { 353 | setDrawViewBitmap(Uri.parse(imageFile.toURI().toString())); 354 | } 355 | 356 | @Override 357 | public void onCanceled(EasyImage.ImageSource source, int type) { 358 | // Cancel handling, removing taken photo if it was canceled 359 | if (source == EasyImage.ImageSource.CAMERA) { 360 | File photoFile = EasyImage.lastlyTakenButCanceledPhoto(CutOutActivity.this); 361 | if (photoFile != null) photoFile.delete(); 362 | } 363 | 364 | setResult(RESULT_CANCELED); 365 | finish(); 366 | } 367 | }); 368 | } 369 | } 370 | 371 | private void undo() { 372 | drawView.undo(); 373 | } 374 | 375 | private void redo() { 376 | drawView.redo(); 377 | } 378 | 379 | } -------------------------------------------------------------------------------- /cutout/src/main/java/com/github/gabrielbb/cutout/DrawView.java: -------------------------------------------------------------------------------- 1 | package com.github.gabrielbb.cutout; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.Path; 9 | import android.graphics.PorterDuff; 10 | import android.graphics.PorterDuffXfermode; 11 | import android.os.AsyncTask; 12 | import android.util.AttributeSet; 13 | import android.util.Pair; 14 | import android.view.MotionEvent; 15 | import android.view.View; 16 | import android.widget.Button; 17 | 18 | import java.lang.ref.WeakReference; 19 | import java.util.Stack; 20 | 21 | import static com.github.gabrielbb.cutout.DrawView.DrawViewAction.AUTO_CLEAR; 22 | import static com.github.gabrielbb.cutout.DrawView.DrawViewAction.MANUAL_CLEAR; 23 | import static com.github.gabrielbb.cutout.DrawView.DrawViewAction.ZOOM; 24 | 25 | class DrawView extends View { 26 | 27 | private Path livePath; 28 | private Paint pathPaint; 29 | 30 | private Bitmap imageBitmap; 31 | private final Stack, Bitmap>> cuts = new Stack<>(); 32 | private final Stack, Bitmap>> undoneCuts = new Stack<>(); 33 | 34 | private float pathX, pathY; 35 | 36 | private static final float TOUCH_TOLERANCE = 4; 37 | private static final float COLOR_TOLERANCE = 20; 38 | 39 | private Button undoButton; 40 | private Button redoButton; 41 | 42 | private View loadingModal; 43 | 44 | private DrawViewAction currentAction; 45 | 46 | public enum DrawViewAction { 47 | AUTO_CLEAR, 48 | MANUAL_CLEAR, 49 | ZOOM 50 | } 51 | 52 | public DrawView(Context c, AttributeSet attrs) { 53 | 54 | super(c, attrs); 55 | 56 | livePath = new Path(); 57 | 58 | pathPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 59 | pathPaint.setDither(true); 60 | pathPaint.setColor(Color.TRANSPARENT); 61 | pathPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 62 | pathPaint.setStyle(Paint.Style.STROKE); 63 | pathPaint.setStrokeJoin(Paint.Join.ROUND); 64 | pathPaint.setStrokeCap(Paint.Cap.ROUND); 65 | } 66 | 67 | public void setButtons(Button undoButton, Button redoButton) { 68 | this.undoButton = undoButton; 69 | this.redoButton = redoButton; 70 | } 71 | 72 | 73 | @Override 74 | protected void onSizeChanged(int newWidth, int newHeight, int oldWidth, int oldHeight) { 75 | super.onSizeChanged(newWidth, newHeight, oldWidth, oldHeight); 76 | 77 | resizeBitmap(newWidth, newHeight); 78 | } 79 | 80 | @Override 81 | protected void onDraw(Canvas canvas) { 82 | super.onDraw(canvas); 83 | canvas.save(); 84 | 85 | if (imageBitmap != null) { 86 | 87 | canvas.drawBitmap(this.imageBitmap, 0, 0, null); 88 | 89 | for (Pair, Bitmap> action : cuts) { 90 | if (action.first != null) { 91 | canvas.drawPath(action.first.first, action.first.second); 92 | } 93 | } 94 | 95 | if (currentAction == MANUAL_CLEAR) { 96 | canvas.drawPath(livePath, pathPaint); 97 | } 98 | } 99 | 100 | canvas.restore(); 101 | } 102 | 103 | private void touchStart(float x, float y) { 104 | pathX = x; 105 | pathY = y; 106 | 107 | undoneCuts.clear(); 108 | redoButton.setEnabled(false); 109 | 110 | if (currentAction == AUTO_CLEAR) { 111 | new AutomaticPixelClearingTask(this).execute((int) x, (int) y); 112 | } else { 113 | livePath.moveTo(x, y); 114 | } 115 | 116 | invalidate(); 117 | } 118 | 119 | private void touchMove(float x, float y) { 120 | if (currentAction == MANUAL_CLEAR) { 121 | float dx = Math.abs(x - pathX); 122 | float dy = Math.abs(y - pathY); 123 | if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) { 124 | livePath.quadTo(pathX, pathY, (x + pathX) / 2, (y + pathY) / 2); 125 | pathX = x; 126 | pathY = y; 127 | } 128 | } 129 | } 130 | 131 | 132 | private void touchUp() { 133 | if (currentAction == MANUAL_CLEAR) { 134 | livePath.lineTo(pathX, pathY); 135 | cuts.push(new Pair<>(new Pair<>(livePath, pathPaint), null)); 136 | livePath = new Path(); 137 | undoButton.setEnabled(true); 138 | } 139 | } 140 | 141 | public void undo() { 142 | if (cuts.size() > 0) { 143 | 144 | Pair, Bitmap> cut = cuts.pop(); 145 | 146 | if (cut.second != null) { 147 | undoneCuts.push(new Pair<>(null, imageBitmap)); 148 | this.imageBitmap = cut.second; 149 | } else { 150 | undoneCuts.push(cut); 151 | } 152 | 153 | if (cuts.isEmpty()) { 154 | undoButton.setEnabled(false); 155 | } 156 | 157 | redoButton.setEnabled(true); 158 | 159 | invalidate(); 160 | } 161 | //toast the user 162 | } 163 | 164 | public void redo() { 165 | if (undoneCuts.size() > 0) { 166 | 167 | Pair, Bitmap> cut = undoneCuts.pop(); 168 | 169 | if (cut.second != null) { 170 | cuts.push(new Pair<>(null, imageBitmap)); 171 | this.imageBitmap = cut.second; 172 | } else { 173 | cuts.push(cut); 174 | } 175 | 176 | if (undoneCuts.isEmpty()) { 177 | redoButton.setEnabled(false); 178 | } 179 | 180 | undoButton.setEnabled(true); 181 | 182 | invalidate(); 183 | } 184 | //toast the user 185 | } 186 | 187 | @Override 188 | public boolean onTouchEvent(MotionEvent ev) { 189 | 190 | if (imageBitmap != null && currentAction != ZOOM) { 191 | switch (ev.getAction()) { 192 | case MotionEvent.ACTION_DOWN: 193 | touchStart(ev.getX(), ev.getY()); 194 | return true; 195 | case MotionEvent.ACTION_MOVE: 196 | touchMove(ev.getX(), ev.getY()); 197 | invalidate(); 198 | return true; 199 | case MotionEvent.ACTION_UP: 200 | touchUp(); 201 | invalidate(); 202 | return true; 203 | } 204 | } 205 | 206 | return super.onTouchEvent(ev); 207 | } 208 | 209 | private void resizeBitmap(int width, int height) { 210 | if (width > 0 && height > 0 && imageBitmap != null) { 211 | imageBitmap = BitmapUtility.getResizedBitmap(this.imageBitmap, width, height); 212 | imageBitmap.setHasAlpha(true); 213 | invalidate(); 214 | } 215 | } 216 | 217 | public void setBitmap(Bitmap bitmap) { 218 | this.imageBitmap = bitmap; 219 | resizeBitmap(getWidth(), getHeight()); 220 | } 221 | 222 | public Bitmap getCurrentBitmap() { 223 | return this.imageBitmap; 224 | } 225 | 226 | public void setAction(DrawViewAction newAction) { 227 | this.currentAction = newAction; 228 | } 229 | 230 | public void setStrokeWidth(int strokeWidth) { 231 | pathPaint = new Paint(pathPaint); 232 | pathPaint.setStrokeWidth(strokeWidth); 233 | } 234 | 235 | public void setLoadingModal(View loadingModal) { 236 | this.loadingModal = loadingModal; 237 | } 238 | 239 | private static class AutomaticPixelClearingTask extends AsyncTask { 240 | 241 | private WeakReference drawViewWeakReference; 242 | 243 | AutomaticPixelClearingTask(DrawView drawView) { 244 | this.drawViewWeakReference = new WeakReference<>(drawView); 245 | } 246 | 247 | @Override 248 | protected void onPreExecute() { 249 | super.onPreExecute(); 250 | drawViewWeakReference.get().loadingModal.setVisibility(VISIBLE); 251 | drawViewWeakReference.get().cuts.push(new Pair<>(null, drawViewWeakReference.get().imageBitmap)); 252 | } 253 | 254 | @Override 255 | protected Bitmap doInBackground(Integer... points) { 256 | Bitmap oldBitmap = drawViewWeakReference.get().imageBitmap; 257 | 258 | int colorToReplace = oldBitmap.getPixel(points[0], points[1]); 259 | 260 | int width = oldBitmap.getWidth(); 261 | int height = oldBitmap.getHeight(); 262 | int[] pixels = new int[width * height]; 263 | oldBitmap.getPixels(pixels, 0, width, 0, 0, width, height); 264 | 265 | int rA = Color.alpha(colorToReplace); 266 | int rR = Color.red(colorToReplace); 267 | int rG = Color.green(colorToReplace); 268 | int rB = Color.blue(colorToReplace); 269 | 270 | int pixel; 271 | 272 | // iteration through pixels 273 | for (int y = 0; y < height; ++y) { 274 | for (int x = 0; x < width; ++x) { 275 | // get current index in 2D-matrix 276 | int index = y * width + x; 277 | pixel = pixels[index]; 278 | int rrA = Color.alpha(pixel); 279 | int rrR = Color.red(pixel); 280 | int rrG = Color.green(pixel); 281 | int rrB = Color.blue(pixel); 282 | 283 | if (rA - COLOR_TOLERANCE < rrA && rrA < rA + COLOR_TOLERANCE && rR - COLOR_TOLERANCE < rrR && rrR < rR + COLOR_TOLERANCE && 284 | rG - COLOR_TOLERANCE < rrG && rrG < rG + COLOR_TOLERANCE && rB - COLOR_TOLERANCE < rrB && rrB < rB + COLOR_TOLERANCE) { 285 | pixels[index] = Color.TRANSPARENT; 286 | } 287 | } 288 | } 289 | 290 | Bitmap newBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 291 | newBitmap.setPixels(pixels, 0, width, 0, 0, width, height); 292 | 293 | return newBitmap; 294 | } 295 | 296 | protected void onPostExecute(Bitmap result) { 297 | super.onPostExecute(result); 298 | drawViewWeakReference.get().imageBitmap = result; 299 | drawViewWeakReference.get().undoButton.setEnabled(true); 300 | drawViewWeakReference.get().loadingModal.setVisibility(INVISIBLE); 301 | drawViewWeakReference.get().invalidate(); 302 | } 303 | } 304 | } -------------------------------------------------------------------------------- /cutout/src/main/java/com/github/gabrielbb/cutout/IntroActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.gabrielbb.cutout; 2 | 3 | import android.graphics.Color; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.app.Fragment; 7 | 8 | import com.github.paolorotolo.appintro.AppIntro; 9 | import com.github.paolorotolo.appintro.AppIntroFragment; 10 | 11 | public class IntroActivity extends AppIntro { 12 | @Override 13 | protected void onCreate(@Nullable Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | 16 | // Note here that we DO NOT use setContentView(); 17 | 18 | // Add your slide fragments here. 19 | // AppIntro will automatically generate the dots indicator and buttons. 20 | addSlide(AppIntroFragment.newInstance(getString(R.string.intro_magic_title), getString(R.string.intro_magic_description), R.drawable.intro_magic_wand, getResources().getColor(R.color.intro_magic_background_color))); 21 | addSlide(AppIntroFragment.newInstance(getString(R.string.intro_manual_title), getString(R.string.intro_manual_description), R.drawable.intro_pencil, getResources().getColor(R.color.intro_manual_background_color))); 22 | addSlide(AppIntroFragment.newInstance(getString(R.string.intro_zoom_title), getString(R.string.intro_zoom_description), R.drawable.intro_magnifier, getResources().getColor(R.color.intro_zoom_background_color))); 23 | 24 | // OPTIONAL METHODS 25 | // Override bar/separator color. 26 | setBarColor(Color.parseColor("#3F51B5")); 27 | setSeparatorColor(Color.parseColor("#2196F3")); 28 | 29 | // Hide Skip/Done button. 30 | showSkipButton(false); 31 | setProgressButtonEnabled(true); 32 | 33 | // Turn vibration on and set intensity. 34 | // NOTE: you will probably need to ask VIBRATE permission in Manifest. 35 | setVibrate(false); 36 | 37 | setFadeAnimation(); 38 | } 39 | 40 | @Override 41 | public void onSkipPressed(Fragment currentFragment) { 42 | super.onSkipPressed(currentFragment); 43 | // Do something when users tap on Skip button. 44 | finish(); 45 | } 46 | 47 | @Override 48 | public void onDonePressed(Fragment currentFragment) { 49 | super.onDonePressed(currentFragment); 50 | // Do something when users tap on Done button. 51 | finish(); 52 | } 53 | 54 | @Override 55 | public void onSlideChanged(@Nullable Fragment oldFragment, @Nullable Fragment newFragment) { 56 | super.onSlideChanged(oldFragment, newFragment); 57 | // Do something when the slide changes. 58 | } 59 | } -------------------------------------------------------------------------------- /cutout/src/main/java/com/github/gabrielbb/cutout/SaveDrawingTask.java: -------------------------------------------------------------------------------- 1 | package com.github.gabrielbb.cutout; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.graphics.Bitmap; 6 | import android.net.Uri; 7 | import android.os.AsyncTask; 8 | import android.os.Environment; 9 | import android.util.Pair; 10 | 11 | import java.io.File; 12 | import java.io.FileOutputStream; 13 | import java.io.IOException; 14 | import java.lang.ref.WeakReference; 15 | import java.util.UUID; 16 | 17 | import static android.view.View.VISIBLE; 18 | 19 | class SaveDrawingTask extends AsyncTask> { 20 | 21 | private static final String SAVED_IMAGE_FORMAT = "png"; 22 | private static final String SAVED_IMAGE_NAME = "cutout_tmp"; 23 | 24 | private final WeakReference activityWeakReference; 25 | 26 | SaveDrawingTask(CutOutActivity activity) { 27 | this.activityWeakReference = new WeakReference<>(activity); 28 | } 29 | 30 | @Override 31 | protected void onPreExecute() { 32 | super.onPreExecute(); 33 | activityWeakReference.get().loadingModal.setVisibility(VISIBLE); 34 | } 35 | 36 | @Override 37 | protected Pair doInBackground(Bitmap... bitmaps) { 38 | 39 | try { 40 | File file = File.createTempFile(SAVED_IMAGE_NAME, SAVED_IMAGE_FORMAT, activityWeakReference.get().getApplicationContext().getCacheDir()); 41 | 42 | try (FileOutputStream out = new FileOutputStream(file)) { 43 | bitmaps[0].compress(Bitmap.CompressFormat.PNG, 95, out); 44 | return new Pair<>(file, null); 45 | } 46 | } catch (IOException e) { 47 | return new Pair<>(null, e); 48 | } 49 | } 50 | 51 | protected void onPostExecute(Pair result) { 52 | super.onPostExecute(result); 53 | 54 | Intent resultIntent = new Intent(); 55 | 56 | if (result.first != null) { 57 | Uri uri = Uri.fromFile(result.first); 58 | 59 | resultIntent.putExtra(CutOut.CUTOUT_EXTRA_RESULT, uri); 60 | activityWeakReference.get().setResult(Activity.RESULT_OK, resultIntent); 61 | activityWeakReference.get().finish(); 62 | 63 | } else { 64 | activityWeakReference.get().exitWithError(result.second); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/done.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/intro_magic_wand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/intro_magic_wand.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/intro_magnifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/intro_magnifier.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/intro_pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/intro_pencil.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/magic_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/magic_active.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/magic_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/magic_inactive.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/magic_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/magnifier_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/magnifier_active.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/magnifier_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/magnifier_inactive.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/magnifier_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/pencil_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/pencil_active.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/pencil_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/pencil_inactive.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/pencil_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/redo_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/redo_active.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/redo_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/redo_inactive.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/redo_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/undo_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/undo_active.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/undo_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GabrielBB/Android-CutOut/b2c81188e00d212d945a59e29ad2b2dce9b28e72/cutout/src/main/res/drawable/undo_inactive.png -------------------------------------------------------------------------------- /cutout/src/main/res/drawable/undo_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /cutout/src/main/res/layout/activity_photo_edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 20 | 21 |