Compare commits

..

1 Commits

Author SHA1 Message Date
Jamie McDonald bd544eb7d2 Save Exif data when you use the same source and destination URIs 2015-02-13 15:04:46 +01:00
34 changed files with 230 additions and 437 deletions

View File

@ -1,13 +1,21 @@
language: android
sudo: false
android:
components:
- build-tools-23.0.1
- android-23
- build-tools-21.1.1
- android-21
- extra-android-support
- extra-android-m2repository
- sys-img-armeabi-v7a-android-21
install:
- ./gradlew :lib:build
before_script:
- echo no | android create avd --force -n test -t android-21 --abi armeabi-v7a
- emulator -avd test -no-skin -no-audio -no-window &
- android-wait-for-emulator
- adb shell input keyevent 82 &
script:
- ./gradlew clean build
- ./gradlew :lib:connectedAndroidTest

View File

@ -1,28 +1,3 @@
## Next
* Fix max size crash when input cannot be decoded
* Translations: German, Chinese (simplified & traditional)
## 1.0.1
* Support image picker helper from Fragments
* Restore support for SDK level 10
* Fix translucent status bar set via app theme
* Fix wrong result code when crop results in IOException
* Fix image "twitching" on zoom out to max bounds
* Translations: Italian, Turkish, Catalan, Swedish
## 1.0.0
* Improved builder interface: `Crop.of(in, out).start(activity)`
* Material styling
* Drop support for SDK level 9
* Start crop from support Fragment
* Fix max size
* Fix issue cropping images from Google Drive
* Optional circle crop guide
* Optional custom request code
* Translations: French, Korean, Chinese, Spanish, Japanese, Arabic, Portuguese, Indonesian, Russian
## 0.9.10
* Fix bug on some devices where image was displayed with 0 size
@ -30,5 +5,5 @@
## 0.9.9
* Downscale source images that are too big to load
* Optional always show crop handles
* Fix shading outside crop area on some API levels
* Add option to always show crop handles

View File

@ -1,65 +1,52 @@
> I guess people are just cropping out all the sadness
An Android library project that provides a simple image cropping `Activity`, based on code from AOSP.
An Android library project to provide a simple image cropping `Activity`, based on code from AOSP.
[![build status](https://travis-ci.org/jdamcd/android-crop.svg)](https://travis-ci.org/jdamcd/android-crop)
[![maven central](https://img.shields.io/badge/maven%20central-1.0.1-brightgreen.svg)](http://search.maven.org/#artifactdetails%7Ccom.soundcloud.android%7Candroid-crop%7C1.0.1%7Caar.asc)
[![changelog](https://img.shields.io/badge/changelog-1.0.1-lightgrey.svg)](CHANGELOG.md)
[![Build Status](https://travis-ci.org/jdamcd/android-crop.png)](https://travis-ci.org/jdamcd/android-crop)
## Features
## Goals
* Gradle build & AAR
* Gradle build with AAR
* Modern UI
* Backwards compatible to SDK 10
* Backwards compatible to Gingerbread
* Simple builder for configuration
* Example project
* More tests, less unused complexity
## Usage
First, declare `CropImageActivity` in your manifest file:
```xml
<activity android:name="com.soundcloud.android.crop.CropImageActivity" />
```
`<activity android:name="com.soundcloud.android.crop.CropImageActivity" />`
#### Crop
```java
Crop.of(inputUri, outputUri).asSquare().start(activity)
```
`new Crop(inputUri).output(outputUri).asSquare().start(activity)`
Listen for the result of the crop (see example project if you want to do some error handling):
```java
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
if (requestCode == Crop.REQUEST_CROP && resultCode == RESULT_OK) {
doSomethingWithCroppedImage(outputUri);
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
if (requestCode == Crop.REQUEST_CROP && resultCode == RESULT_OK) {
doSomethingWithCroppedImage(outputUri);
}
}
}
```
Some attributes are provided to customise the crop screen. See the example project [theme](https://github.com/jdamcd/android-crop/blob/master/example/src/main/res/values/theme.xml).
#### Pick
The library provides a utility method to start an image picker:
```java
Crop.pickImage(activity)
```
`Crop.pickImage(activity)`
#### Dependency
The AAR is published on Maven Central:
```groovy
compile 'com.soundcloud.android:android-crop:1.0.1@aar'
```
`compile 'com.soundcloud.android:android-crop:0.9.10@aar'`
#### Users
#### Apps
Apps that use this library include: [SoundCloud](https://play.google.com/store/apps/details?id=com.soundcloud.android), [Depop](https://play.google.com/store/apps/details?id=com.depop), [Polyvore](https://play.google.com/store/apps/details?id=com.polyvore), [TextSecure](https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms)
Apps that use this library include: [SoundCloud](https://play.google.com/store/apps/details?id=com.soundcloud.android), [Depop](https://play.google.com/store/apps/details?id=com.depop)
## How does it look?
@ -69,16 +56,5 @@ Apps that use this library include: [SoundCloud](https://play.google.com/store/a
This project is based on the [AOSP](https://source.android.com) camera image cropper via [android-cropimage](https://github.com/lvillani/android-cropimage).
Copyright 2015 SoundCloud
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Copyright 2014 [SoundCloud](https://soundcloud.com)
Apache License, Version 2.0

View File

@ -3,7 +3,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
classpath 'com.android.tools.build:gradle:1.0.0'
}
}

View File

@ -3,12 +3,12 @@ apply plugin: 'com.android.application'
archivesBaseName = 'android-crop-example'
android {
compileSdkVersion 23
buildToolsVersion '23.0.1'
compileSdkVersion 21
buildToolsVersion '21.1.2'
defaultConfig {
minSdkVersion 10
targetSdkVersion 22
minSdkVersion 9
targetSdkVersion 21
versionCode Integer.parseInt(project.VERSION_CODE)
versionName project.VERSION
}

View File

@ -1,16 +1,17 @@
package com.soundcloud.android.crop.example;
import com.soundcloud.android.crop.Crop;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ImageView;
import android.widget.Toast;
import com.soundcloud.android.crop.Crop;
import java.io.File;
public class MainActivity extends Activity {
@ -50,8 +51,8 @@ public class MainActivity extends Activity {
}
private void beginCrop(Uri source) {
Uri destination = Uri.fromFile(new File(getCacheDir(), "cropped"));
Crop.of(source, destination).asSquare().start(this);
Uri outputUri = Uri.fromFile(new File(getCacheDir(), "cropped"));
new Crop(source).output(outputUri).asSquare().start(this);
}
private void handleCrop(int resultCode, Intent result) {

View File

@ -6,7 +6,6 @@
<style name="Widget.CropImageView" parent="android:Widget">
<item name="showThirds">true</item>
<item name="showCircle">false</item>
<item name="showHandles">always</item>
<item name="highlightColor">@color/highlight</item>
</style>

View File

@ -1,4 +1,4 @@
VERSION=1.0.1
VERSION=0.9.10
VERSION_CODE=1
signing.keyId=63A46540
@ -6,4 +6,4 @@ signing.secretKeyRingFile=
signing.password=
sonatypeUsername=jdamcd
sonatypePassword=
sonatypePassword=

View File

@ -6,12 +6,12 @@ apply plugin: 'signing'
archivesBaseName = 'android-crop'
android {
compileSdkVersion 23
buildToolsVersion '23.0.1'
compileSdkVersion 21
buildToolsVersion '21.1.1'
defaultConfig {
minSdkVersion 10
targetSdkVersion 22
minSdkVersion 9
targetSdkVersion 21
testApplicationId 'com.soundcloud.android.crop.test'
testInstrumentationRunner 'android.test.InstrumentationTestRunner'
@ -19,10 +19,9 @@ android {
}
dependencies {
compile 'com.android.support:support-annotations:23.0.1'
compile 'com.android.support:support-v4:23.0.1'
compile 'com.android.support:support-annotations:21.0.0'
androidTestCompile 'com.squareup:fest-android:1.0.7'
androidTestCompile 'com.android.support:support-v4:23.0.1'
androidTestCompile 'com.android.support:support-v4:21.0.0'
androidTestCompile 'org.mockito:mockito-core:1.9.5'
androidTestCompile 'com.google.dexmaker:dexmaker:1.0'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.0'

View File

@ -1,16 +1,16 @@
package com.soundcloud.android.crop;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.fest.assertions.api.ANDROID;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.provider.MediaStore;
import org.fest.assertions.api.ANDROID;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class CropBuilderTest extends BaseTestCase {
private Activity activity;
@ -22,7 +22,7 @@ public class CropBuilderTest extends BaseTestCase {
activity = mock(Activity.class);
when(activity.getPackageName()).thenReturn("com.example");
builder = Crop.of(Uri.parse("image:input"), Uri.parse("image:output"));
builder = new Crop(Uri.parse("image:input"));
}
public void testInputUriSetAsData() {
@ -30,6 +30,8 @@ public class CropBuilderTest extends BaseTestCase {
}
public void testOutputUriSetAsExtra() {
builder.output(Uri.parse("image:output"));
Intent intent = builder.getIntent(activity);
Uri output = intent.getParcelableExtra(MediaStore.EXTRA_OUTPUT);

View File

@ -1 +1,7 @@
<manifest package="com.soundcloud.android.crop" />
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.soundcloud.android.crop">
<application
android:allowBackup="true" />
</manifest>

View File

@ -20,7 +20,7 @@ public class Crop {
public static final int REQUEST_PICK = 9162;
public static final int RESULT_ERROR = 404;
interface Extra {
static interface Extra {
String ASPECT_X = "aspect_x";
String ASPECT_Y = "aspect_y";
String MAX_X = "max_x";
@ -31,19 +31,23 @@ public class Crop {
private Intent cropIntent;
/**
* Create a crop Intent builder with source and destination image Uris
* Create a crop Intent builder with source image
*
* @param source Uri for image to crop
* @param destination Uri for saving the cropped image
* @param source Source image URI
*/
public static Crop of(Uri source, Uri destination) {
return new Crop(source, destination);
}
private Crop(Uri source, Uri destination) {
public Crop(Uri source) {
cropIntent = new Intent();
cropIntent.setData(source);
cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, destination);
}
/**
* Set output URI where the cropped image will be saved
*
* @param output Output image URI
*/
public Crop output(Uri output) {
cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, output);
return this;
}
/**
@ -70,7 +74,7 @@ public class Crop {
/**
* Set maximum crop size
*
* @param width Max width
* @param width Max width
* @param height Max height
*/
public Crop withMaxSize(int width, int height) {
@ -80,7 +84,7 @@ public class Crop {
}
/**
* Send the crop Intent from an Activity
* Send the crop Intent!
*
* @param activity Activity to receive result
*/
@ -89,9 +93,9 @@ public class Crop {
}
/**
* Send the crop Intent from an Activity with a custom request code
* Send the crop Intent with a custom requestCode
*
* @param activity Activity to receive result
* @param activity Activity to receive result
* @param requestCode requestCode for result
*/
public void start(Activity activity, int requestCode) {
@ -99,30 +103,21 @@ public class Crop {
}
/**
* Send the crop Intent from a Fragment
* Send the crop Intent!
*
* @param context Context
* @param context Context
* @param fragment Fragment to receive result
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void start(Context context, Fragment fragment) {
start(context, fragment, REQUEST_CROP);
}
/**
* Send the crop Intent from a support library Fragment
* Send the crop Intent with a custom requestCode
*
* @param context Context
* @param context Context
* @param fragment Fragment to receive result
*/
public void start(Context context, android.support.v4.app.Fragment fragment) {
start(context, fragment, REQUEST_CROP);
}
/**
* Send the crop Intent with a custom request code
*
* @param context Context
* @param fragment Fragment to receive result
* @param requestCode requestCode for result
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@ -130,17 +125,6 @@ public class Crop {
fragment.startActivityForResult(getIntent(context), requestCode);
}
/**
* Send the crop Intent with a custom request code
*
* @param context Context
* @param fragment Fragment to receive result
* @param requestCode requestCode for result
*/
public void start(Context context, android.support.v4.app.Fragment fragment, int requestCode) {
fragment.startActivityForResult(getIntent(context), requestCode);
}
/**
* Get Intent to start crop Activity
*
@ -172,85 +156,27 @@ public class Crop {
}
/**
* Pick image from an Activity
* Utility to start an image picker
*
* @param activity Activity to receive result
* @param activity Activity that will receive result
*/
public static void pickImage(Activity activity) {
pickImage(activity, REQUEST_PICK);
}
/**
* Pick image from a Fragment
* Utility to start an image picker with request code
*
* @param context Context
* @param fragment Fragment to receive result
*/
public static void pickImage(Context context, Fragment fragment) {
pickImage(context, fragment, REQUEST_PICK);
}
/**
* Pick image from a support library Fragment
*
* @param context Context
* @param fragment Fragment to receive result
*/
public static void pickImage(Context context, android.support.v4.app.Fragment fragment) {
pickImage(context, fragment, REQUEST_PICK);
}
/**
* Pick image from an Activity with a custom request code
*
* @param activity Activity to receive result
* @param activity Activity that will receive result
* @param requestCode requestCode for result
*/
public static void pickImage(Activity activity, int requestCode) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT).setType("image/*");
try {
activity.startActivityForResult(getImagePicker(), requestCode);
activity.startActivityForResult(intent, requestCode);
} catch (ActivityNotFoundException e) {
showImagePickerError(activity);
Toast.makeText(activity, R.string.crop__pick_error, Toast.LENGTH_SHORT).show();
}
}
/**
* Pick image from a Fragment with a custom request code
*
* @param context Context
* @param fragment Fragment to receive result
* @param requestCode requestCode for result
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static void pickImage(Context context, Fragment fragment, int requestCode) {
try {
fragment.startActivityForResult(getImagePicker(), requestCode);
} catch (ActivityNotFoundException e) {
showImagePickerError(context);
}
}
/**
* Pick image from a support library Fragment with a custom request code
*
* @param context Context
* @param fragment Fragment to receive result
* @param requestCode requestCode for result
*/
public static void pickImage(Context context, android.support.v4.app.Fragment fragment, int requestCode) {
try {
fragment.startActivityForResult(getImagePicker(), requestCode);
} catch (ActivityNotFoundException e) {
showImagePickerError(context);
}
}
private static Intent getImagePicker() {
return new Intent(Intent.ACTION_GET_CONTENT).setType("image/*");
}
private static void showImagePickerError(Context context) {
Toast.makeText(context, R.string.crop__pick_error, Toast.LENGTH_SHORT).show();
}
}

View File

@ -21,6 +21,7 @@ import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
@ -32,8 +33,8 @@ import android.os.Handler;
import android.provider.MediaStore;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -44,6 +45,7 @@ import java.util.concurrent.CountDownLatch;
*/
public class CropImageActivity extends MonitoredActivity {
private static final boolean IN_MEMORY_CROP = Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD_MR1;
private static final int SIZE_DEFAULT = 2048;
private static final int SIZE_LIMIT = 4096;
@ -70,10 +72,11 @@ public class CropImageActivity extends MonitoredActivity {
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setupWindowFlags();
setupViews();
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.crop__activity_crop);
initViews();
loadInput();
setupFromIntent();
if (rotateBitmap == null) {
finish();
return;
@ -81,17 +84,7 @@ public class CropImageActivity extends MonitoredActivity {
startCrop();
}
@TargetApi(Build.VERSION_CODES.KITKAT)
private void setupWindowFlags() {
requestWindowFeature(Window.FEATURE_NO_TITLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
}
private void setupViews() {
setContentView(R.layout.crop__activity_crop);
private void initViews() {
imageView = (CropImageView) findViewById(R.id.crop_image);
imageView.context = this;
imageView.setRecycler(new ImageViewTouchBase.Recycler() {
@ -116,7 +109,7 @@ public class CropImageActivity extends MonitoredActivity {
});
}
private void loadInput() {
private void setupFromIntent() {
Intent intent = getIntent();
Bundle extras = intent.getExtras();
@ -198,7 +191,7 @@ public class CropImageActivity extends MonitoredActivity {
handler.post(new Runnable() {
public void run() {
if (imageView.getScale() == 1F) {
imageView.center();
imageView.center(true, true);
}
latch.countDown();
}
@ -262,19 +255,23 @@ public class CropImageActivity extends MonitoredActivity {
}
}
/*
* TODO
* This should use the decode/crop/encode single step API so that the whole
* (possibly large) Bitmap doesn't need to be read into memory
*/
private void onSaveClicked() {
if (cropView == null || isSaving) {
return;
}
isSaving = true;
Bitmap croppedImage;
Bitmap croppedImage = null;
Rect r = cropView.getScaledCropRect(sampleSize);
int width = r.width();
int height = r.height();
int outWidth = width;
int outHeight = height;
int outWidth = width, outHeight = height;
if (maxX > 0 && maxY > 0 && (width > maxX || height > maxY)) {
float ratio = (float) width / (float) height;
if ((float) maxX / (float) maxY > ratio) {
@ -286,18 +283,27 @@ public class CropImageActivity extends MonitoredActivity {
}
}
try {
croppedImage = decodeRegionCrop(r, outWidth, outHeight);
} catch (IllegalArgumentException e) {
setResultException(e);
finish();
return;
}
if (IN_MEMORY_CROP && rotateBitmap != null) {
croppedImage = inMemoryCrop(rotateBitmap, r, width, height, outWidth, outHeight);
if (croppedImage != null) {
imageView.setImageBitmapResetBase(croppedImage, true);
imageView.center(true, true);
imageView.highlightViews.clear();
}
} else {
try {
croppedImage = decodeRegionCrop(r);
} catch (IllegalArgumentException e) {
setResultException(e);
finish();
return;
}
if (croppedImage != null) {
imageView.setImageRotateBitmapResetBase(new RotateBitmap(croppedImage, exifRotation), true);
imageView.center();
imageView.highlightViews.clear();
if (croppedImage != null) {
imageView.setImageRotateBitmapResetBase(new RotateBitmap(croppedImage, exifRotation), true);
imageView.center(true, true);
imageView.highlightViews.clear();
}
}
saveImage(croppedImage);
}
@ -317,7 +323,8 @@ public class CropImageActivity extends MonitoredActivity {
}
}
private Bitmap decodeRegionCrop(Rect rect, int outWidth, int outHeight) {
@TargetApi(10)
private Bitmap decodeRegionCrop(Rect rect) {
// Release memory now
clearImageView();
@ -344,11 +351,6 @@ public class CropImageActivity extends MonitoredActivity {
try {
croppedImage = decoder.decodeRegion(rect, new BitmapFactory.Options());
if (croppedImage != null && (rect.width() > outWidth || rect.height() > outHeight)) {
Matrix matrix = new Matrix();
matrix.postScale((float) outWidth / rect.width(), (float) outHeight / rect.height());
croppedImage = Bitmap.createBitmap(croppedImage, 0, 0, croppedImage.getWidth(), croppedImage.getHeight(), matrix, true);
}
} catch (IllegalArgumentException e) {
// Rethrow with some extra information
throw new IllegalArgumentException("Rectangle " + rect + " is outside of the image ("
@ -357,7 +359,7 @@ public class CropImageActivity extends MonitoredActivity {
} catch (IOException e) {
Log.e("Error cropping image: " + e.getMessage(), e);
setResultException(e);
finish();
} catch (OutOfMemoryError e) {
Log.e("OOM cropping image: " + e.getMessage(), e);
setResultException(e);
@ -367,6 +369,33 @@ public class CropImageActivity extends MonitoredActivity {
return croppedImage;
}
private Bitmap inMemoryCrop(RotateBitmap rotateBitmap, Rect r, int width, int height, int outWidth, int outHeight) {
// In-memory crop means potential OOM errors,
// but we have no choice as we can't selectively decode a bitmap with this API level
System.gc();
Bitmap croppedImage = null;
try {
croppedImage = Bitmap.createBitmap(outWidth, outHeight, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(croppedImage);
RectF dstRect = new RectF(0, 0, width, height);
Matrix m = new Matrix();
m.setRectToRect(new RectF(r), dstRect, Matrix.ScaleToFit.FILL);
m.preConcat(rotateBitmap.getRotateMatrix());
canvas.drawBitmap(rotateBitmap.getBitmap(), m, null);
} catch (OutOfMemoryError e) {
Log.e("OOM cropping image: " + e.getMessage(), e);
setResultException(e);
System.gc();
}
// Release Bitmap memory as soon as possible
clearImageView();
return croppedImage;
}
private void clearImageView() {
imageView.clear();
if (rotateBitmap != null) {
@ -390,10 +419,11 @@ public class CropImageActivity extends MonitoredActivity {
CropUtil.closeSilently(outputStream);
}
CropUtil.copyExifRotation(
CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri),
CropUtil.getFromMediaUri(this, getContentResolver(), saveUri)
);
if (!IN_MEMORY_CROP) {
// In-memory crop negates the rotation
File saveFile = CropUtil.getFromMediaUri(this, getContentResolver(), saveUri);
CropUtil.saveExifRotation(saveFile, exifRotation);
}
setResultUri(saveUri);
}

View File

@ -3,7 +3,6 @@ package com.soundcloud.android.crop;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.MotionEvent;
@ -18,16 +17,18 @@ public class CropImageView extends ImageViewTouchBase {
private float lastX;
private float lastY;
private int motionEdge;
private int validPointerId;
@SuppressWarnings("UnusedDeclaration")
public CropImageView(Context context) {
super(context);
}
@SuppressWarnings("UnusedDeclaration")
public CropImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@SuppressWarnings("UnusedDeclaration")
public CropImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@ -84,7 +85,7 @@ public class CropImageView extends ImageViewTouchBase {
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
public boolean onTouchEvent(MotionEvent event) {
CropImageActivity cropImageActivity = (CropImageActivity) context;
if (cropImageActivity.isSaving()) {
return false;
@ -99,8 +100,6 @@ public class CropImageView extends ImageViewTouchBase {
motionHighlightView = hv;
lastX = event.getX();
lastY = event.getY();
// Prevent multiple touches from interfering with crop area re-sizing
validPointerId = event.getPointerId(event.getActionIndex());
motionHighlightView.setMode((edge == HighlightView.MOVE)
? HighlightView.ModifyMode.Move
: HighlightView.ModifyMode.Grow);
@ -114,20 +113,29 @@ public class CropImageView extends ImageViewTouchBase {
motionHighlightView.setMode(HighlightView.ModifyMode.None);
}
motionHighlightView = null;
center();
break;
case MotionEvent.ACTION_MOVE:
if (motionHighlightView != null && event.getPointerId(event.getActionIndex()) == validPointerId) {
if (motionHighlightView != null) {
motionHighlightView.handleMotion(motionEdge, event.getX()
- lastX, event.getY() - lastY);
lastX = event.getX();
lastY = event.getY();
ensureVisible(motionHighlightView);
}
break;
}
// If we're not zoomed then there's no point in even allowing the user to move the image around.
// This call to center puts it back to the normalized location.
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
center(true, true);
break;
case MotionEvent.ACTION_MOVE:
// if we're not zoomed then there's no point in even allowing
// the user to move the image around. This call to center puts
// it back to the normalized location (with false meaning don't
// animate).
if (getScale() == 1F) {
center();
center(true, true);
}
break;
}
@ -181,10 +189,10 @@ public class CropImageView extends ImageViewTouchBase {
}
@Override
protected void onDraw(@NonNull Canvas canvas) {
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (HighlightView highlightView : highlightViews) {
highlightView.draw(canvas);
for (HighlightView mHighlightView : highlightViews) {
mHighlightView.draw(canvas);
}
}

View File

@ -68,21 +68,20 @@ class CropUtil {
return ExifInterface.ORIENTATION_UNDEFINED;
}
} catch (IOException e) {
Log.e("Error getting Exif data", e);
Log.e("Error reading Exif rotation data", e);
return 0;
}
}
public static boolean copyExifRotation(File sourceFile, File destFile) {
if (sourceFile == null || destFile == null) return false;
public static boolean saveExifRotation(File file, int exifRotation) {
if (file == null) return false;
try {
ExifInterface exifSource = new ExifInterface(sourceFile.getAbsolutePath());
ExifInterface exifDest = new ExifInterface(destFile.getAbsolutePath());
exifDest.setAttribute(ExifInterface.TAG_ORIENTATION, exifSource.getAttribute(ExifInterface.TAG_ORIENTATION));
ExifInterface exifDest = new ExifInterface(file.getAbsolutePath());
exifDest.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(exifRotation));
exifDest.saveAttributes();
return true;
} catch (IOException e) {
Log.e("Error copying Exif data", e);
Log.e("Error saving Exif rotation data", e);
return false;
}
}
@ -159,7 +158,7 @@ class CropUtil {
public static void startBackgroundJob(MonitoredActivity activity,
String title, String message, Runnable job, Handler handler) {
// Make the progress dialog uncancelable, so that we can guarantee
// Make the progress dialog uncancelable, so that we can gurantee
// the thread will be done before the activity getting destroyed
ProgressDialog dialog = ProgressDialog.show(
activity, title, message, true, false);

View File

@ -66,7 +66,6 @@ class HighlightView {
private View viewContext; // View displaying image
private boolean showThirds;
private boolean showCircle;
private int highlightColor;
private ModifyMode modifyMode = ModifyMode.None;
@ -88,7 +87,6 @@ class HighlightView {
TypedArray attributes = context.obtainStyledAttributes(outValue.resourceId, R.styleable.CropImageView);
try {
showThirds = attributes.getBoolean(R.styleable.CropImageView_showThirds, false);
showCircle = attributes.getBoolean(R.styleable.CropImageView_showCircle, false);
highlightColor = attributes.getColor(R.styleable.CropImageView_highlightColor,
DEFAULT_HIGHLIGHT_COLOR);
handleMode = HandleMode.values()[attributes.getInt(R.styleable.CropImageView_showHandles, 0)];
@ -152,10 +150,6 @@ class HighlightView {
drawThirds(canvas);
}
if (showCircle) {
drawCircle(canvas);
}
if (handleMode == HandleMode.Always ||
(handleMode == HandleMode.Changing && modifyMode == ModifyMode.Grow)) {
drawHandles(canvas);
@ -215,11 +209,6 @@ class HighlightView {
drawRect.right, drawRect.top + yThird * 2, outlinePaint);
}
private void drawCircle(Canvas canvas) {
outlinePaint.setStrokeWidth(1);
canvas.drawOval(new RectF(drawRect), outlinePaint);
}
public void setMode(ModifyMode mode) {
if (mode != modifyMode) {
modifyMode = mode;

View File

@ -192,10 +192,12 @@ abstract class ImageViewTouchBase extends ImageView {
maxZoom = calculateMaxZoom();
}
// Center as much as possible in one or both axis. Centering is defined as follows:
// * If the image is scaled down below the view's dimensions then center it.
// * If the image is scaled larger than the view and is translated out of view then translate it back into view.
protected void center() {
// Center as much as possible in one or both axis. Centering is
// defined as follows: if the image is scaled down below the
// view's dimensions then center it (literally). If the image
// is scaled larger than the view and is translated out of view
// then translate it back into view (i.e. eliminate black bars).
protected void center(boolean horizontal, boolean vertical) {
final Bitmap bitmap = bitmapDisplayed.getBitmap();
if (bitmap == null) {
return;
@ -210,37 +212,32 @@ abstract class ImageViewTouchBase extends ImageView {
float deltaX = 0, deltaY = 0;
deltaY = centerVertical(rect, height, deltaY);
deltaX = centerHorizontal(rect, width, deltaX);
if (vertical) {
int viewHeight = getHeight();
if (height < viewHeight) {
deltaY = (viewHeight - height) / 2 - rect.top;
} else if (rect.top > 0) {
deltaY = -rect.top;
} else if (rect.bottom < viewHeight) {
deltaY = getHeight() - rect.bottom;
}
}
if (horizontal) {
int viewWidth = getWidth();
if (width < viewWidth) {
deltaX = (viewWidth - width) / 2 - rect.left;
} else if (rect.left > 0) {
deltaX = -rect.left;
} else if (rect.right < viewWidth) {
deltaX = viewWidth - rect.right;
}
}
postTranslate(deltaX, deltaY);
setImageMatrix(getImageViewMatrix());
}
private float centerVertical(RectF rect, float height, float deltaY) {
int viewHeight = getHeight();
if (height < viewHeight) {
deltaY = (viewHeight - height) / 2 - rect.top;
} else if (rect.top > 0) {
deltaY = -rect.top;
} else if (rect.bottom < viewHeight) {
deltaY = getHeight() - rect.bottom;
}
return deltaY;
}
private float centerHorizontal(RectF rect, float width, float deltaX) {
int viewWidth = getWidth();
if (width < viewWidth) {
deltaX = (viewWidth - width) / 2 - rect.left;
} else if (rect.left > 0) {
deltaX = -rect.left;
} else if (rect.right < viewWidth) {
deltaX = viewWidth - rect.right;
}
return deltaX;
}
private void init() {
setScaleType(ImageView.ScaleType.MATRIX);
}
@ -316,7 +313,7 @@ abstract class ImageViewTouchBase extends ImageView {
suppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
setImageMatrix(getImageViewMatrix());
center();
center(true, true);
}
protected void zoomTo(final float scale, final float centerX,
@ -386,7 +383,7 @@ abstract class ImageViewTouchBase extends ImageView {
suppMatrix.postScale(1F / rate, 1F / rate, cx, cy);
}
setImageMatrix(getImageViewMatrix());
center();
center(true, true);
}
protected void postTranslate(float dx, float dy) {

View File

@ -4,11 +4,11 @@ class Log {
private static final String TAG = "android-crop";
public static void e(String msg) {
public static final void e(String msg) {
android.util.Log.e(TAG, msg);
}
public static void e(String msg, Throwable e) {
public static final void e(String msg, Throwable e) {
android.util.Log.e(TAG, msg, e);
}

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
<string name="crop__saving">Guardando imagen…</string>
<string name="crop__wait">Por favor espere…</string>
<string name="crop__pick_error">No hay imágenes disponibles</string>
<string name="crop__pick_error">No hay imagenes disponibles</string>
<string name="crop__done">ACEPTAR</string>
<string name="crop__cancel" tools:ignore="ButtonCase">CANCELAR</string>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,7 @@
<string name="crop__saving">正在保存照片…</string>
<string name="crop__wait">请等待…</string>
<string name="crop__pick_error">无效的图片</string>
<string name="crop__done">完成</string>
<string name="crop__cancel" tools:ignore="ButtonCase">取消</string>
</resources>
</resources>

View File

@ -5,7 +5,6 @@
<declare-styleable name="CropImageView">
<attr name="highlightColor" format="reference|color" />
<attr name="showThirds" format="boolean" />
<attr name="showCircle" format="boolean" />
<attr name="showHandles">
<enum name="changing" value="0" />
<enum name="always" value="1" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 KiB

After

Width:  |  Height:  |  Size: 179 KiB