Compare commits

..

No commits in common. "dbo-specific" and "master" have entirely different histories.

30 changed files with 298 additions and 218 deletions

View File

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

View File

@ -1,17 +1,30 @@
apply plugin: 'com.android.library'
apply plugin: 'maven'
apply plugin: 'signing'
//apply from: '../.publishing/sonatype.gradle'
archivesBaseName = 'android-crop'
android {
compileSdkVersion 24
compileSdkVersion 23
buildToolsVersion '23.0.1'
defaultConfig {
minSdkVersion 10
//noinspection OldTargetApi
targetSdkVersion 24
targetSdkVersion 22
testApplicationId 'com.soundcloud.android.crop.test'
testInstrumentationRunner 'android.test.InstrumentationTestRunner'
}
}
dependencies {
compile "androidx.annotation:annotation:$androidx"
compile 'androidx.legacy:legacy-support-v4:1.0.0'
compile "androidx.appcompat:appcompat:$appcompat"
compile 'com.android.support:support-annotations:23.0.1'
compile 'com.android.support:support-v4:23.0.1'
androidTestCompile 'com.squareup:fest-android:1.0.7'
androidTestCompile 'com.android.support:support-v4:23.0.1'
androidTestCompile 'org.mockito:mockito-core:1.9.5'
androidTestCompile 'com.google.dexmaker:dexmaker:1.0'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.0'
}

View File

@ -0,0 +1,15 @@
package com.soundcloud.android.crop;
import android.test.InstrumentationTestCase;
public class BaseTestCase extends InstrumentationTestCase {
@Override
public void setUp() throws Exception {
super.setUp();
// Work around dexmaker issue when running tests on Android 4.3
System.setProperty("dexmaker.dexcache",
getInstrumentation().getTargetContext().getCacheDir().getPath());
}
}

View File

@ -0,0 +1,77 @@
package com.soundcloud.android.crop;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.fest.assertions.api.ANDROID;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.provider.MediaStore;
public class CropBuilderTest extends BaseTestCase {
private Activity activity;
private Crop builder;
@Override
public void setUp() throws Exception {
super.setUp();
activity = mock(Activity.class);
when(activity.getPackageName()).thenReturn("com.example");
builder = Crop.of(Uri.parse("image:input"), Uri.parse("image:output"));
}
public void testInputUriSetAsData() {
ANDROID.assertThat(builder.getIntent(activity)).hasData("image:input");
}
public void testOutputUriSetAsExtra() {
Intent intent = builder.getIntent(activity);
Uri output = intent.getParcelableExtra(MediaStore.EXTRA_OUTPUT);
assertThat(output.toString()).isEqualTo("image:output");
}
public void testAspectRatioSetAsExtras() {
builder.withAspect(16, 10);
Intent intent = builder.getIntent(activity);
assertThat(intent.getIntExtra("aspect_x", 0)).isEqualTo(16);
assertThat(intent.getIntExtra("aspect_y", 0)).isEqualTo(10);
}
public void testFixedAspectRatioSetAsExtras() {
builder.asSquare();
Intent intent = builder.getIntent(activity);
assertThat(intent.getIntExtra("aspect_x", 0)).isEqualTo(1);
assertThat(intent.getIntExtra("aspect_y", 0)).isEqualTo(1);
}
public void testMaxSizeSetAsExtras() {
builder.withMaxSize(400, 300);
Intent intent = builder.getIntent(activity);
assertThat(intent.getIntExtra("max_x", 0)).isEqualTo(400);
assertThat(intent.getIntExtra("max_y", 0)).isEqualTo(300);
}
public void testBuildsIntentWithMultipleOptions() {
builder.asSquare().withMaxSize(200, 200);
Intent intent = builder.getIntent(activity);
assertThat(intent.getIntExtra("aspect_x", 0)).isEqualTo(1);
assertThat(intent.getIntExtra("aspect_y", 0)).isEqualTo(1);
assertThat(intent.getIntExtra("max_x", 0)).isEqualTo(200);
assertThat(intent.getIntExtra("max_y", 0)).isEqualTo(200);
}
}

View File

@ -114,7 +114,7 @@ public class Crop {
* @param context Context
* @param fragment Fragment to receive result
*/
public void start(Context context, androidx.fragment.app.Fragment fragment) {
public void start(Context context, android.support.v4.app.Fragment fragment) {
start(context, fragment, REQUEST_CROP);
}
@ -137,7 +137,7 @@ public class Crop {
* @param fragment Fragment to receive result
* @param requestCode requestCode for result
*/
public void start(Context context, androidx.fragment.app.Fragment fragment, int requestCode) {
public void start(Context context, android.support.v4.app.Fragment fragment, int requestCode) {
fragment.startActivityForResult(getIntent(context), requestCode);
}
@ -196,7 +196,7 @@ public class Crop {
* @param context Context
* @param fragment Fragment to receive result
*/
public static void pickImage(Context context, androidx.fragment.app.Fragment fragment) {
public static void pickImage(Context context, android.support.v4.app.Fragment fragment) {
pickImage(context, fragment, REQUEST_PICK);
}
@ -237,7 +237,7 @@ public class Crop {
* @param fragment Fragment to receive result
* @param requestCode requestCode for result
*/
public static void pickImage(Context context, androidx.fragment.app.Fragment fragment, int requestCode) {
public static void pickImage(Context context, android.support.v4.app.Fragment fragment, int requestCode) {
try {
fragment.startActivityForResult(getImagePicker(), requestCode);
} catch (ActivityNotFoundException e) {

View File

@ -30,7 +30,6 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.MediaStore;
import android.util.Pair;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
@ -57,7 +56,6 @@ public class CropImageActivity extends MonitoredActivity {
private int maxX;
private int maxY;
private int exifRotation;
private int exifScale;
private Uri sourceUri;
private Uri saveUri;
@ -65,7 +63,7 @@ public class CropImageActivity extends MonitoredActivity {
private boolean isSaving;
private int sampleSize;
private Bitmap srcBitmap;
private RotateBitmap rotateBitmap;
private CropImageView imageView;
private HighlightView cropView;
@ -76,7 +74,7 @@ public class CropImageActivity extends MonitoredActivity {
setupViews();
loadInput();
if (srcBitmap == null) {
if (rotateBitmap == null) {
finish();
return;
}
@ -121,7 +119,6 @@ public class CropImageActivity extends MonitoredActivity {
private void loadInput() {
Intent intent = getIntent();
Bundle extras = intent.getExtras();
Bitmap initialBitmap;
if (extras != null) {
aspectX = extras.getInt(Crop.Extra.ASPECT_X);
@ -133,35 +130,15 @@ public class CropImageActivity extends MonitoredActivity {
sourceUri = intent.getData();
if (sourceUri != null) {
Pair<Integer, Integer> exifRotationTrans =
CropUtil.getExifRotationTranslation(
CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri));
exifRotation = exifRotationTrans.first;
exifScale = exifRotationTrans.second;
exifRotation = CropUtil.getExifRotation(CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri));
InputStream is = null;
try {
sampleSize = calculateBitmapSampleSize(sourceUri);
is = getContentResolver().openInputStream(sourceUri);
BitmapFactory.Options option = new BitmapFactory.Options();
option.inSampleSize = sampleSize;
initialBitmap = BitmapFactory.decodeStream(is, null, option);
if (initialBitmap == null) {
throw new IOException();
}
int drawHeight = initialBitmap.getHeight();
int drawWidth = initialBitmap.getWidth();
if ((exifRotation != 0) || (exifScale != 1)) {
Matrix matrix = new Matrix();
if (exifRotation != 0) {
matrix.preRotate(exifRotation);
}
if (exifScale != 1) {
matrix.postScale(exifScale, 1);
}
srcBitmap = Bitmap.createBitmap(initialBitmap, 0, 0, drawWidth, drawHeight, matrix, true);
} else {
srcBitmap = initialBitmap;
}
rotateBitmap = new RotateBitmap(BitmapFactory.decodeStream(is, null, option), exifRotation);
} catch (IOException e) {
Log.e("Error reading image: " + e.getMessage(), e);
setResultException(e);
@ -171,8 +148,6 @@ public class CropImageActivity extends MonitoredActivity {
} finally {
CropUtil.closeSilently(is);
}
} else {
Log.e("Source URI is null");
}
}
@ -215,8 +190,8 @@ public class CropImageActivity extends MonitoredActivity {
if (isFinishing()) {
return;
}
imageView.setImageBitmapResetBase(srcBitmap, true);
CropUtil.startBackgroundJob(this, null, getString(R.string.crop__wait),
imageView.setImageRotateBitmapResetBase(rotateBitmap, true);
CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__wait),
new Runnable() {
public void run() {
final CountDownLatch latch = new CountDownLatch(1);
@ -242,13 +217,13 @@ public class CropImageActivity extends MonitoredActivity {
private class Cropper {
private void makeDefault() {
if (srcBitmap == null) {
if (rotateBitmap == null) {
return;
}
HighlightView hv = new HighlightView(imageView);
final int width = srcBitmap.getWidth();
final int height = srcBitmap.getHeight();
final int width = rotateBitmap.getWidth();
final int height = rotateBitmap.getHeight();
Rect imageRect = new Rect(0, 0, width, height);
@ -300,8 +275,6 @@ public class CropImageActivity extends MonitoredActivity {
int outWidth = width;
int outHeight = height;
Log.e("Crop Width = " + width);
Log.e("Crop Height = " + height);
if (maxX > 0 && maxY > 0 && (width > maxX || height > maxY)) {
float ratio = (float) width / (float) height;
if ((float) maxX / (float) maxY > ratio) {
@ -322,7 +295,7 @@ public class CropImageActivity extends MonitoredActivity {
}
if (croppedImage != null) {
imageView.setImageBitmapResetBase(croppedImage, true);
imageView.setImageRotateBitmapResetBase(new RotateBitmap(croppedImage, exifRotation), true);
imageView.center();
imageView.highlightViews.clear();
}
@ -332,7 +305,7 @@ public class CropImageActivity extends MonitoredActivity {
private void saveImage(Bitmap croppedImage) {
if (croppedImage != null) {
final Bitmap b = croppedImage;
CropUtil.startBackgroundJob(this, null, getString(R.string.crop__saving),
CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__saving),
new Runnable() {
public void run() {
saveOutput(b);
@ -345,20 +318,22 @@ public class CropImageActivity extends MonitoredActivity {
}
private Bitmap decodeRegionCrop(Rect rect, int outWidth, int outHeight) {
Matrix matrix = new Matrix();
// Release memory now
clearImageView();
InputStream is = null;
Bitmap croppedImage = null;
boolean transformed = false;
try {
is = getContentResolver().openInputStream(sourceUri);
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
final int width = decoder.getWidth();
final int height = decoder.getHeight();
if ((exifRotation != 0) || (exifScale != 1)) {
// Adjust crop area to account for EXIF transformation so what you crop is what you get
matrix.preRotate(-exifRotation);
matrix.postScale(exifScale, 1);
if (exifRotation != 0) {
// Adjust crop area to account for image rotation
Matrix matrix = new Matrix();
matrix.setRotate(-exifRotation);
RectF adjusted = new RectF();
matrix.mapRect(adjusted, new RectF(rect));
@ -368,19 +343,10 @@ public class CropImageActivity extends MonitoredActivity {
}
try {
matrix.reset();
croppedImage = decoder.decodeRegion(rect, new BitmapFactory.Options());
if ((exifRotation != 0) || (exifScale != 1)) {
transformed = true;
// Remove the EXIF transformation from the cropped image so we can dispense with it altogether
matrix.preRotate(exifRotation);
matrix.postScale(exifScale, 1);
}
if (rect.width() > outWidth || rect.height() > outHeight) {
transformed = true;
matrix.postScale(((float) outWidth) / ((float) rect.width()), ((float) outHeight) / ((float) rect.height()));
}
if (transformed) {
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) {
@ -397,16 +363,14 @@ public class CropImageActivity extends MonitoredActivity {
setResultException(e);
} finally {
CropUtil.closeSilently(is);
// Release memory now
clearImageView();
}
return croppedImage;
}
private void clearImageView() {
imageView.clear();
if (srcBitmap != null) {
srcBitmap.recycle();
if (rotateBitmap != null) {
rotateBitmap.recycle();
}
System.gc();
}
@ -425,6 +389,12 @@ public class CropImageActivity extends MonitoredActivity {
} finally {
CropUtil.closeSilently(outputStream);
}
CropUtil.copyExifRotation(
CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri),
CropUtil.getFromMediaUri(this, getContentResolver(), saveUri)
);
setResultUri(saveUri);
}
@ -442,8 +412,8 @@ public class CropImageActivity extends MonitoredActivity {
@Override
protected void onDestroy() {
super.onDestroy();
if (srcBitmap != null) {
srcBitmap.recycle();
if (rotateBitmap != null) {
rotateBitmap.recycle();
}
}

View File

@ -3,7 +3,7 @@ package com.soundcloud.android.crop;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import androidx.annotation.NonNull;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.MotionEvent;

View File

@ -25,9 +25,8 @@ import android.net.Uri;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import androidx.annotation.Nullable;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Pair;
import java.io.Closeable;
import java.io.File;
@ -53,43 +52,25 @@ class CropUtil {
}
}
public static Pair<Integer,Integer> getExifRotationTranslation(File imageFile) {
int exifRotation = 0, exifScale = 1;
if (imageFile == null) return new Pair<Integer,Integer>(0,1);
public static int getExifRotation(File imageFile) {
if (imageFile == null) return 0;
try {
ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
// We only recognize a subset of orientation tag values
switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)) {
case ExifInterface.ORIENTATION_UNDEFINED:
case ExifInterface.ORIENTATION_NORMAL:
break ;
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
exifScale=-1;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
exifRotation = 180;
break;
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
exifRotation = 180;
exifScale=-1;
break;
case ExifInterface.ORIENTATION_TRANSPOSE:
exifRotation = 90;
exifScale=-1;
break;
case ExifInterface.ORIENTATION_ROTATE_90:
exifRotation = 90;
break;
case ExifInterface.ORIENTATION_TRANSVERSE:
exifRotation = 270;
exifScale=-1;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
exifRotation = 270;
case ExifInterface.ORIENTATION_ROTATE_90:
return 90;
case ExifInterface.ORIENTATION_ROTATE_180:
return 180;
case ExifInterface.ORIENTATION_ROTATE_270:
return 270;
default:
return ExifInterface.ORIENTATION_UNDEFINED;
}
} catch (IOException e) {
Log.e("Error getting Exif data", e);
return 0;
}
return new Pair<Integer,Integer>(exifRotation,exifScale);
}
public static boolean copyExifRotation(File sourceFile, File destFile) {

View File

@ -24,8 +24,6 @@ import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
@ -43,20 +41,19 @@ import android.view.View;
*/
class HighlightView {
public static final int GROW_NONE = (1 << 0);
public static final int GROW_LEFT_EDGE = (1 << 1);
public static final int GROW_RIGHT_EDGE = (1 << 2);
public static final int GROW_TOP_EDGE = (1 << 3);
public static final int GROW_NONE = (1 << 0);
public static final int GROW_LEFT_EDGE = (1 << 1);
public static final int GROW_RIGHT_EDGE = (1 << 2);
public static final int GROW_TOP_EDGE = (1 << 3);
public static final int GROW_BOTTOM_EDGE = (1 << 4);
public static final int MOVE = (1 << 5);
public static final int MOVE = (1 << 5);
private static final int DEFAULT_HIGHLIGHT_COLOR = 0xFF00985F;
private static final float HANDLE_RADIUS_DP = 8f;
private static final float OUTLINE_DP = 1f;
private static final int DEFAULT_HIGHLIGHT_COLOR = 0xFF33B5E5;
private static final float HANDLE_RADIUS_DP = 12f;
private static final float OUTLINE_DP = 2f;
enum ModifyMode {None, Move, Grow}
enum HandleMode {Changing, Always, Never}
enum ModifyMode { None, Move, Grow }
enum HandleMode { Changing, Always, Never }
RectF cropRect; // Image space
Rect drawRect; // Screen space
@ -72,7 +69,6 @@ class HighlightView {
private boolean showCircle;
private int highlightColor;
private ModifyMode modifyMode = ModifyMode.None;
private HandleMode handleMode = HandleMode.Changing;
private boolean maintainAspectRatio;
@ -80,7 +76,6 @@ class HighlightView {
private float handleRadius;
private float outlineWidth;
private boolean isFocused;
private float hysteresis;
public HighlightView(View context) {
viewContext = context;
@ -90,11 +85,10 @@ class HighlightView {
private void initStyles(Context context) {
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.cropImageStyle, outValue, true);
hysteresis = dpToPx(16);
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, true);
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)];
@ -113,7 +107,7 @@ class HighlightView {
initialAspectRatio = this.cropRect.width() / this.cropRect.height();
drawRect = computeLayout();
outsidePaint.setARGB(153, 0, 0, 0);
outsidePaint.setARGB(125, 50, 50, 50);
outlinePaint.setStyle(Paint.Style.STROKE);
outlinePaint.setAntiAlias(true);
outlineWidth = dpToPx(OUTLINE_DP);
@ -141,11 +135,7 @@ class HighlightView {
Rect viewDrawingRect = new Rect();
viewContext.getDrawingRect(viewDrawingRect);
if (showCircle) {
path.addOval(new RectF(drawRect), Path.Direction.CW);
} else {
path.addRect(new RectF(drawRect), Path.Direction.CW);
}
path.addRect(new RectF(drawRect), Path.Direction.CW);
outlinePaint.setColor(highlightColor);
if (isClipPathSupported(canvas)) {
@ -162,6 +152,10 @@ class HighlightView {
drawThirds(canvas);
}
if (showCircle) {
drawCircle(canvas);
}
if (handleMode == HandleMode.Always ||
(handleMode == HandleMode.Changing && modifyMode == ModifyMode.Grow)) {
drawHandles(canvas);
@ -173,36 +167,31 @@ class HighlightView {
* Fall back to naive method for darkening outside crop area
*/
private void drawOutsideFallback(Canvas canvas) {
int layer = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null,
Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.FULL_COLOR_LAYER_SAVE_FLAG);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setXfermode(null);
paint.setColor(outsidePaint.getColor());
if (showCircle) {
canvas.drawOval(new RectF(drawRect), paint);
} else {
canvas.drawRect(new RectF(drawRect), paint);
}
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
paint.setColor(outsidePaint.getColor());
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), paint);
canvas.restoreToCount(layer);
canvas.drawRect(0, 0, canvas.getWidth(), drawRect.top, outsidePaint);
canvas.drawRect(0, drawRect.bottom, canvas.getWidth(), canvas.getHeight(), outsidePaint);
canvas.drawRect(0, drawRect.top, drawRect.left, drawRect.bottom, outsidePaint);
canvas.drawRect(drawRect.right, drawRect.top, canvas.getWidth(), drawRect.bottom, outsidePaint);
}
/*
* Clip path is broken, unreliable or not supported on:
* - JellyBean MR1
* - ICS & ICS MR1 with hardware acceleration turned on
* NOTE: 4.1.1 HTC One S not working?
*/
@SuppressLint("NewApi")
private boolean isClipPathSupported(Canvas canvas) {
return Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1
|| !canvas.isHardwareAccelerated();
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) {
return false;
} else if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|| Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
return true;
} else {
return !canvas.isHardwareAccelerated();
}
}
private void drawHandles(Canvas canvas) {
int xMiddle = drawRect.left + ((drawRect.right - drawRect.left) / 2);
int xMiddle = drawRect.left + ((drawRect.right - drawRect.left) / 2);
int yMiddle = drawRect.top + ((drawRect.bottom - drawRect.top) / 2);
canvas.drawCircle(drawRect.left, yMiddle, handleRadius, handlePaint);
@ -215,7 +204,7 @@ class HighlightView {
outlinePaint.setStrokeWidth(1);
float xThird = (drawRect.right - drawRect.left) / 3;
float yThird = (drawRect.bottom - drawRect.top) / 3;
canvas.drawLine(drawRect.left + xThird, drawRect.top,
drawRect.left + xThird, drawRect.bottom, outlinePaint);
canvas.drawLine(drawRect.left + xThird * 2, drawRect.top,
@ -226,6 +215,11 @@ 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;
@ -235,43 +229,8 @@ class HighlightView {
// Determines which edges are hit by touching at (x, y)
public int getHit(float x, float y) {
if (showCircle) {
return getCircleHit(x, y);
} else {
return getRectangleHit(x, y);
}
}
private int getCircleHit(float x, float y) {
Rect r = computeLayout();
int retval = GROW_NONE;
double radius = r.height() / (double) 2;
double distanceToCenter = Math.sqrt(Math.pow(r.top + radius - y, 2)
+ Math.pow(r.left + radius - x, 2));
if (Math.abs(distanceToCenter - radius) < hysteresis) {
if (y < r.top + radius / 2) {
retval |= GROW_TOP_EDGE;
}
if (y > r.top + radius * 1.5) {
retval |= GROW_BOTTOM_EDGE;
}
if (x < r.left + radius / 2) {
retval |= GROW_LEFT_EDGE;
}
if (x > r.left + radius * 1.5) {
retval |= GROW_RIGHT_EDGE;
}
}
if (retval == GROW_NONE && r.contains((int) x, (int) y)) {
retval = MOVE;
}
return retval;
}
private int getRectangleHit(float x, float y) {
Rect r = computeLayout();
final float hysteresis = 20F;
int retval = GROW_NONE;
// verticalCheck makes sure the position is between the top and
@ -282,16 +241,16 @@ class HighlightView {
&& (x < r.right + hysteresis);
// Check whether the position is near some edge(s)
if ((Math.abs(r.left - x) < hysteresis) && verticalCheck) {
if ((Math.abs(r.left - x) < hysteresis) && verticalCheck) {
retval |= GROW_LEFT_EDGE;
}
if ((Math.abs(r.right - x) < hysteresis) && verticalCheck) {
if ((Math.abs(r.right - x) < hysteresis) && verticalCheck) {
retval |= GROW_RIGHT_EDGE;
}
if ((Math.abs(r.top - y) < hysteresis) && horizCheck) {
if ((Math.abs(r.top - y) < hysteresis) && horizCheck) {
retval |= GROW_TOP_EDGE;
}
if ((Math.abs(r.bottom - y) < hysteresis) && horizCheck) {
if ((Math.abs(r.bottom - y) < hysteresis) && horizCheck) {
retval |= GROW_BOTTOM_EDGE;
}
@ -309,7 +268,7 @@ class HighlightView {
if (edge == MOVE) {
// Convert to image space before sending to moveBy()
moveBy(dx * (cropRect.width() / r.width()),
dy * (cropRect.height() / r.height()));
dy * (cropRect.height() / r.height()));
} else {
if (((GROW_LEFT_EDGE | GROW_RIGHT_EDGE) & edge) == 0) {
dx = 0;
@ -336,10 +295,10 @@ class HighlightView {
// Put the cropping rectangle inside image rectangle
cropRect.offset(
Math.max(0, imageRect.left - cropRect.left),
Math.max(0, imageRect.top - cropRect.top));
Math.max(0, imageRect.top - cropRect.top));
cropRect.offset(
Math.min(0, imageRect.right - cropRect.right),
Math.min(0, imageRect.right - cropRect.right),
Math.min(0, imageRect.bottom - cropRect.bottom));
drawRect = computeLayout();
@ -415,10 +374,10 @@ class HighlightView {
// Maps the cropping rectangle from image space to screen space
private Rect computeLayout() {
RectF r = new RectF(cropRect.left, cropRect.top,
cropRect.right, cropRect.bottom);
cropRect.right, cropRect.bottom);
matrix.mapRect(r);
return new Rect(Math.round(r.left), Math.round(r.top),
Math.round(r.right), Math.round(r.bottom));
Math.round(r.right), Math.round(r.bottom));
}
public void invalidate() {

View File

@ -22,14 +22,14 @@ import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import androidx.appcompat.widget.AppCompatImageView;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.widget.ImageView;
/*
* Modified from original in AOSP.
*/
abstract class ImageViewTouchBase extends AppCompatImageView {
abstract class ImageViewTouchBase extends ImageView {
private static final float SCALE_RATE = 1.25F;

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 B

View File

@ -9,4 +9,12 @@
</shape>
</item>
<item android:state_focused="true">
<shape>
<solid android:color="@color/crop__selector_focused" />
</shape>
</item>
<item android:drawable="@android:color/transparent" />
</selector>

View File

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

View File

@ -2,10 +2,8 @@
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
android:layout_height="match_parent">
<include
android:id="@+id/done_cancel_bar"
@ -15,6 +13,7 @@
android:id="@+id/crop_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/crop__texture"
android:layout_below="@id/done_cancel_bar" />
</RelativeLayout>

View File

@ -1,18 +1,16 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#00985f">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Crop.DoneCancelBar">
<ImageView
<FrameLayout
android:id="@+id/btn_cancel"
style="@style/Crop.ActionButton"
android:layout_gravity="left"
android:src="@drawable/crop__ic_cancel"/>
style="@style/Crop.ActionButton">
<TextView style="@style/Crop.ActionButtonText.Cancel" />
</FrameLayout>
<ImageView
<FrameLayout
android:id="@+id/btn_done"
style="@style/Crop.ActionButton"
android:layout_gravity="right"
android:src="@drawable/crop__ic_done"/>
style="@style/Crop.ActionButton">
<TextView style="@style/Crop.ActionButtonText.Done" />
</FrameLayout>
</FrameLayout>
</LinearLayout>

View File

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

View File

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

1
settings.gradle Normal file
View File

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