feat:移动crop模块位置
3
libs/lib_crop/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/build
|
||||
*.iml
|
||||
*.DS_Store
|
31
libs/lib_crop/build.gradle
Normal file
@@ -0,0 +1,31 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion COMPILE_SDK_VERSION.toInteger()
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion MIN_SDK_VERSION.toInteger()
|
||||
targetSdkVersion TARGET_SDK_VERSION.toInteger()
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
buildToolsVersion = '30.0.3'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'androidx.annotation:annotation:1.4.0'
|
||||
api 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation "androidx.core:core-ktx:1.7.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
1
libs/lib_crop/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="com.soundcloud.crop" />
|
266
libs/lib_crop/src/main/java/com/soundcloud/crop/Crop.java
Normal file
@@ -0,0 +1,266 @@
|
||||
package com.soundcloud.crop;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.MediaStore;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* Builder for crop Intents and utils for handling result
|
||||
*/
|
||||
public class Crop {
|
||||
|
||||
public static final int REQUEST_CROP = 6709;
|
||||
public static final int REQUEST_PICK = 9162;
|
||||
public static final int RESULT_ERROR = 404;
|
||||
|
||||
interface Extra {
|
||||
String ASPECT_X = "aspect_x";
|
||||
String ASPECT_Y = "aspect_y";
|
||||
String MAX_X = "max_x";
|
||||
String MAX_Y = "max_y";
|
||||
String AS_PNG = "as_png";
|
||||
String ERROR = "error";
|
||||
}
|
||||
|
||||
private Intent cropIntent;
|
||||
|
||||
/**
|
||||
* Create a crop Intent builder with source and destination image Uris
|
||||
*
|
||||
* @param source Uri for image to crop
|
||||
* @param destination Uri for saving the cropped image
|
||||
*/
|
||||
public static Crop of(Uri source, Uri destination) {
|
||||
return new Crop(source, destination);
|
||||
}
|
||||
|
||||
private Crop(Uri source, Uri destination) {
|
||||
cropIntent = new Intent();
|
||||
cropIntent.setData(source);
|
||||
cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, destination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fixed aspect ratio for crop area
|
||||
*
|
||||
* @param x Aspect X
|
||||
* @param y Aspect Y
|
||||
*/
|
||||
public Crop withAspect(int x, int y) {
|
||||
cropIntent.putExtra(Extra.ASPECT_X, x);
|
||||
cropIntent.putExtra(Extra.ASPECT_Y, y);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crop area with fixed 1:1 aspect ratio
|
||||
*/
|
||||
public Crop asSquare() {
|
||||
cropIntent.putExtra(Extra.ASPECT_X, 1);
|
||||
cropIntent.putExtra(Extra.ASPECT_Y, 1);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set maximum crop size
|
||||
*
|
||||
* @param width Max width
|
||||
* @param height Max height
|
||||
*/
|
||||
public Crop withMaxSize(int width, int height) {
|
||||
cropIntent.putExtra(Extra.MAX_X, width);
|
||||
cropIntent.putExtra(Extra.MAX_Y, height);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to save the result as a PNG or not. Helpful to preserve alpha.
|
||||
* @param asPng whether to save the result as a PNG or not
|
||||
*/
|
||||
public Crop asPng(boolean asPng) {
|
||||
cropIntent.putExtra(Extra.AS_PNG, asPng);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the crop Intent from an Activity
|
||||
*
|
||||
* @param activity Activity to receive result
|
||||
*/
|
||||
public void start(Activity activity) {
|
||||
start(activity, REQUEST_CROP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the crop Intent from an Activity with a custom request code
|
||||
*
|
||||
* @param activity Activity to receive result
|
||||
* @param requestCode requestCode for result
|
||||
*/
|
||||
public void start(Activity activity, int requestCode) {
|
||||
activity.startActivityForResult(getIntent(activity), requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the crop Intent from a Fragment
|
||||
*
|
||||
* @param context Context
|
||||
* @param fragment Fragment to receive result
|
||||
*/
|
||||
public void start(Context context, Fragment fragment) {
|
||||
start(context, fragment, REQUEST_CROP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the crop Intent from a support library Fragment
|
||||
*
|
||||
* @param context Context
|
||||
* @param fragment Fragment to receive result
|
||||
*/
|
||||
public void start(Context context, androidx.fragment.app.Fragment fragment) {
|
||||
start(context, fragment, REQUEST_CROP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the crop Intent with a custom request code
|
||||
*
|
||||
* @param context Context
|
||||
* @param fragment Fragment to receive result
|
||||
* @param requestCode requestCode for result
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
public void start(Context context, Fragment fragment, int requestCode) {
|
||||
fragment.startActivityForResult(getIntent(context), requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the crop Intent with a custom request code
|
||||
*
|
||||
* @param context Context
|
||||
* @param fragment Fragment to receive result
|
||||
* @param requestCode requestCode for result
|
||||
*/
|
||||
public void start(Context context, androidx.fragment.app.Fragment fragment, int requestCode) {
|
||||
fragment.startActivityForResult(getIntent(context), requestCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Intent to start crop Activity
|
||||
*
|
||||
* @param context Context
|
||||
* @return Intent for CropImageActivity
|
||||
*/
|
||||
public Intent getIntent(Context context) {
|
||||
cropIntent.setClass(context, CropImageActivity.class);
|
||||
return cropIntent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve URI for cropped image, as set in the Intent builder
|
||||
*
|
||||
* @param result Output Image URI
|
||||
*/
|
||||
public static Uri getOutput(Intent result) {
|
||||
return result.getParcelableExtra(MediaStore.EXTRA_OUTPUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve error that caused crop to fail
|
||||
*
|
||||
* @param result Result Intent
|
||||
* @return Throwable handled in CropImageActivity
|
||||
*/
|
||||
public static Throwable getError(Intent result) {
|
||||
return (Throwable) result.getSerializableExtra(Extra.ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick image from an Activity
|
||||
*
|
||||
* @param activity Activity to receive result
|
||||
*/
|
||||
public static void pickImage(Activity activity) {
|
||||
pickImage(activity, REQUEST_PICK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick image from a Fragment
|
||||
*
|
||||
* @param context Context
|
||||
* @param fragment Fragment to receive result
|
||||
*/
|
||||
public static void pickImage(Context context, Fragment fragment) {
|
||||
pickImage(context, fragment, REQUEST_PICK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick image from a support library Fragment
|
||||
*
|
||||
* @param context Context
|
||||
* @param fragment Fragment to receive result
|
||||
*/
|
||||
public static void pickImage(Context context, androidx.fragment.app.Fragment fragment) {
|
||||
pickImage(context, fragment, REQUEST_PICK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick image from an Activity with a custom request code
|
||||
*
|
||||
* @param activity Activity to receive result
|
||||
* @param requestCode requestCode for result
|
||||
*/
|
||||
public static void pickImage(Activity activity, int requestCode) {
|
||||
try {
|
||||
activity.startActivityForResult(getImagePicker(), requestCode);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
showImagePickerError(activity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick image from a Fragment with a custom request code
|
||||
*
|
||||
* @param context Context
|
||||
* @param fragment Fragment to receive result
|
||||
* @param requestCode requestCode for result
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
public static void pickImage(Context context, Fragment fragment, int requestCode) {
|
||||
try {
|
||||
fragment.startActivityForResult(getImagePicker(), requestCode);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
showImagePickerError(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick image from a support library Fragment with a custom request code
|
||||
*
|
||||
* @param context Context
|
||||
* @param fragment Fragment to receive result
|
||||
* @param requestCode requestCode for result
|
||||
*/
|
||||
public static void pickImage(Context context, androidx.fragment.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.getApplicationContext(), R.string.crop__pick_error, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,441 @@
|
||||
/*
|
||||
* Copyright (C) 2007 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.soundcloud.crop;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.BitmapRegionDecoder;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.net.Uri;
|
||||
import android.opengl.GLES10;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.provider.MediaStore;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
/*
|
||||
* Modified from original in AOSP.
|
||||
*/
|
||||
public class CropImageActivity extends MonitoredActivity {
|
||||
|
||||
private static final int SIZE_DEFAULT = 2048;
|
||||
private static final int SIZE_LIMIT = 4096;
|
||||
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
private int aspectX;
|
||||
private int aspectY;
|
||||
|
||||
// Output image
|
||||
private int maxX;
|
||||
private int maxY;
|
||||
private int exifRotation;
|
||||
private boolean saveAsPng;
|
||||
|
||||
private Uri sourceUri;
|
||||
private Uri saveUri;
|
||||
|
||||
private boolean isSaving;
|
||||
|
||||
private int sampleSize;
|
||||
private RotateBitmap rotateBitmap;
|
||||
private CropImageView imageView;
|
||||
private HighlightView cropView;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
setupWindowFlags();
|
||||
setupViews();
|
||||
|
||||
loadInput();
|
||||
if (rotateBitmap == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
startCrop();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
private void setupWindowFlags() {
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupViews() {
|
||||
setContentView(R.layout.crop__activity_crop);
|
||||
|
||||
imageView = (CropImageView) findViewById(R.id.crop_image);
|
||||
imageView.context = this;
|
||||
imageView.setRecycler(new ImageViewTouchBase.Recycler() {
|
||||
@Override
|
||||
public void recycle(Bitmap b) {
|
||||
b.recycle();
|
||||
System.gc();
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.btn_cancel).setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
onSaveClicked();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadInput() {
|
||||
Intent intent = getIntent();
|
||||
Bundle extras = intent.getExtras();
|
||||
|
||||
if (extras != null) {
|
||||
aspectX = extras.getInt(Crop.Extra.ASPECT_X);
|
||||
aspectY = extras.getInt(Crop.Extra.ASPECT_Y);
|
||||
maxX = extras.getInt(Crop.Extra.MAX_X);
|
||||
maxY = extras.getInt(Crop.Extra.MAX_Y);
|
||||
saveAsPng = extras.getBoolean(Crop.Extra.AS_PNG, false);
|
||||
saveUri = extras.getParcelable(MediaStore.EXTRA_OUTPUT);
|
||||
}
|
||||
|
||||
sourceUri = intent.getData();
|
||||
if (sourceUri != null) {
|
||||
exifRotation = CropUtil.getExifRotation(CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri));
|
||||
|
||||
InputStream is = null;
|
||||
try {
|
||||
sampleSize = calculateBitmapSampleSize(sourceUri);
|
||||
is = getContentResolver().openInputStream(sourceUri);
|
||||
BitmapFactory.Options option = new BitmapFactory.Options();
|
||||
option.inSampleSize = sampleSize;
|
||||
rotateBitmap = new RotateBitmap(BitmapFactory.decodeStream(is, null, option), exifRotation);
|
||||
} catch (IOException e) {
|
||||
Log.e("Error reading image: " + e.getMessage(), e);
|
||||
setResultException(e);
|
||||
} catch (OutOfMemoryError e) {
|
||||
Log.e("OOM reading image: " + e.getMessage(), e);
|
||||
setResultException(e);
|
||||
} finally {
|
||||
CropUtil.closeSilently(is);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateBitmapSampleSize(Uri bitmapUri) throws IOException {
|
||||
InputStream is = null;
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
try {
|
||||
is = getContentResolver().openInputStream(bitmapUri);
|
||||
BitmapFactory.decodeStream(is, null, options); // Just get image size
|
||||
} finally {
|
||||
CropUtil.closeSilently(is);
|
||||
}
|
||||
|
||||
int maxSize = getMaxImageSize();
|
||||
int sampleSize = 1;
|
||||
while (options.outHeight / sampleSize > maxSize || options.outWidth / sampleSize > maxSize) {
|
||||
sampleSize = sampleSize << 1;
|
||||
}
|
||||
return sampleSize;
|
||||
}
|
||||
|
||||
private int getMaxImageSize() {
|
||||
int textureLimit = getMaxTextureSize();
|
||||
if (textureLimit == 0) {
|
||||
return SIZE_DEFAULT;
|
||||
} else {
|
||||
return Math.min(textureLimit, SIZE_LIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
private int getMaxTextureSize() {
|
||||
// The OpenGL texture size is the maximum size that can be drawn in an ImageView
|
||||
int[] maxSize = new int[1];
|
||||
GLES10.glGetIntegerv(GLES10.GL_MAX_TEXTURE_SIZE, maxSize, 0);
|
||||
return maxSize[0];
|
||||
}
|
||||
|
||||
private void startCrop() {
|
||||
if (isFinishing()) {
|
||||
return;
|
||||
}
|
||||
imageView.setImageRotateBitmapResetBase(rotateBitmap, true);
|
||||
CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__wait),
|
||||
new Runnable() {
|
||||
public void run() {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
handler.post(new Runnable() {
|
||||
public void run() {
|
||||
if (imageView.getScale() == 1F) {
|
||||
imageView.center();
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
new Cropper().crop();
|
||||
}
|
||||
}, handler
|
||||
);
|
||||
}
|
||||
|
||||
private class Cropper {
|
||||
|
||||
private void makeDefault() {
|
||||
if (rotateBitmap == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HighlightView hv = new HighlightView(imageView);
|
||||
final int width = rotateBitmap.getWidth();
|
||||
final int height = rotateBitmap.getHeight();
|
||||
|
||||
Rect imageRect = new Rect(0, 0, width, height);
|
||||
|
||||
// Make the default size about 4/5 of the width or height
|
||||
int cropWidth = Math.min(width, height) * 4 / 5;
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
int cropHeight = cropWidth;
|
||||
|
||||
if (aspectX != 0 && aspectY != 0) {
|
||||
if (aspectX > aspectY) {
|
||||
cropHeight = cropWidth * aspectY / aspectX;
|
||||
} else {
|
||||
cropWidth = cropHeight * aspectX / aspectY;
|
||||
}
|
||||
}
|
||||
|
||||
int x = (width - cropWidth) / 2;
|
||||
int y = (height - cropHeight) / 2;
|
||||
|
||||
RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight);
|
||||
hv.setup(imageView.getUnrotatedMatrix(), imageRect, cropRect, aspectX != 0 && aspectY != 0);
|
||||
imageView.add(hv);
|
||||
}
|
||||
|
||||
public void crop() {
|
||||
handler.post(new Runnable() {
|
||||
public void run() {
|
||||
makeDefault();
|
||||
imageView.invalidate();
|
||||
if (imageView.highlightViews.size() == 1) {
|
||||
cropView = imageView.highlightViews.get(0);
|
||||
cropView.setFocus(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void onSaveClicked() {
|
||||
if (cropView == null || isSaving) {
|
||||
return;
|
||||
}
|
||||
isSaving = true;
|
||||
|
||||
Bitmap croppedImage;
|
||||
Rect r = cropView.getScaledCropRect(sampleSize);
|
||||
int width = r.width();
|
||||
int height = r.height();
|
||||
|
||||
int outWidth = width;
|
||||
int outHeight = height;
|
||||
if (maxX > 0 && maxY > 0 && (width > maxX || height > maxY)) {
|
||||
float ratio = (float) width / (float) height;
|
||||
if ((float) maxX / (float) maxY > ratio) {
|
||||
outHeight = maxY;
|
||||
outWidth = (int) ((float) maxY * ratio + .5f);
|
||||
} else {
|
||||
outWidth = maxX;
|
||||
outHeight = (int) ((float) maxX / ratio + .5f);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
croppedImage = decodeRegionCrop(r, outWidth, outHeight);
|
||||
} catch (IllegalArgumentException e) {
|
||||
setResultException(e);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (croppedImage != null) {
|
||||
imageView.setImageRotateBitmapResetBase(new RotateBitmap(croppedImage, exifRotation), true);
|
||||
imageView.center();
|
||||
imageView.highlightViews.clear();
|
||||
}
|
||||
saveImage(croppedImage);
|
||||
}
|
||||
|
||||
private void saveImage(Bitmap croppedImage) {
|
||||
if (croppedImage != null) {
|
||||
final Bitmap b = croppedImage;
|
||||
CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__saving),
|
||||
new Runnable() {
|
||||
public void run() {
|
||||
saveOutput(b);
|
||||
}
|
||||
}, handler
|
||||
);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap decodeRegionCrop(Rect rect, int outWidth, int outHeight) {
|
||||
// Release memory now
|
||||
clearImageView();
|
||||
|
||||
InputStream is = null;
|
||||
Bitmap croppedImage = null;
|
||||
try {
|
||||
is = getContentResolver().openInputStream(sourceUri);
|
||||
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
|
||||
final int width = decoder.getWidth();
|
||||
final int height = decoder.getHeight();
|
||||
|
||||
if (exifRotation != 0) {
|
||||
// Adjust crop area to account for image rotation
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.setRotate(-exifRotation);
|
||||
|
||||
RectF adjusted = new RectF();
|
||||
matrix.mapRect(adjusted, new RectF(rect));
|
||||
|
||||
// Adjust to account for origin at 0,0
|
||||
adjusted.offset(adjusted.left < 0 ? width : 0, adjusted.top < 0 ? height : 0);
|
||||
rect = new Rect((int) adjusted.left, (int) adjusted.top, (int) adjusted.right, (int) adjusted.bottom);
|
||||
}
|
||||
|
||||
try {
|
||||
croppedImage = decoder.decodeRegion(rect, new BitmapFactory.Options());
|
||||
if (croppedImage != null && (rect.width() > outWidth || rect.height() > outHeight)) {
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.postScale((float) outWidth / rect.width(), (float) outHeight / rect.height());
|
||||
croppedImage = Bitmap.createBitmap(croppedImage, 0, 0, croppedImage.getWidth(), croppedImage.getHeight(), matrix, true);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Rethrow with some extra information
|
||||
throw new IllegalArgumentException("Rectangle " + rect + " is outside of the image ("
|
||||
+ width + "," + height + "," + exifRotation + ")", e);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e("Error cropping image: " + e.getMessage(), e);
|
||||
setResultException(e);
|
||||
} catch (OutOfMemoryError e) {
|
||||
Log.e("OOM cropping image: " + e.getMessage(), e);
|
||||
setResultException(e);
|
||||
} finally {
|
||||
CropUtil.closeSilently(is);
|
||||
}
|
||||
return croppedImage;
|
||||
}
|
||||
|
||||
private void clearImageView() {
|
||||
imageView.clear();
|
||||
if (rotateBitmap != null) {
|
||||
rotateBitmap.recycle();
|
||||
}
|
||||
System.gc();
|
||||
}
|
||||
|
||||
private void saveOutput(Bitmap croppedImage) {
|
||||
if (saveUri != null) {
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
outputStream = getContentResolver().openOutputStream(saveUri);
|
||||
if (outputStream != null) {
|
||||
croppedImage.compress(saveAsPng ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG,
|
||||
90, // note: quality is ignored when using PNG
|
||||
outputStream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
setResultException(e);
|
||||
Log.e("Cannot open file: " + saveUri, e);
|
||||
} finally {
|
||||
CropUtil.closeSilently(outputStream);
|
||||
}
|
||||
|
||||
CropUtil.copyExifRotation(
|
||||
CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri),
|
||||
CropUtil.getFromMediaUri(this, getContentResolver(), saveUri)
|
||||
);
|
||||
|
||||
setResultUri(saveUri);
|
||||
}
|
||||
|
||||
final Bitmap b = croppedImage;
|
||||
handler.post(new Runnable() {
|
||||
public void run() {
|
||||
imageView.clear();
|
||||
b.recycle();
|
||||
}
|
||||
});
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (rotateBitmap != null) {
|
||||
rotateBitmap.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSearchRequested() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isSaving() {
|
||||
return isSaving;
|
||||
}
|
||||
|
||||
private void setResultUri(Uri uri) {
|
||||
setResult(RESULT_OK, new Intent().putExtra(MediaStore.EXTRA_OUTPUT, uri));
|
||||
}
|
||||
|
||||
private void setResultException(Throwable throwable) {
|
||||
setResult(Crop.RESULT_ERROR, new Intent().putExtra(Crop.Extra.ERROR, throwable));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,195 @@
|
||||
package com.soundcloud.crop;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class CropImageView extends ImageViewTouchBase {
|
||||
|
||||
ArrayList<HighlightView> highlightViews = new ArrayList<HighlightView>();
|
||||
HighlightView motionHighlightView;
|
||||
Context context;
|
||||
|
||||
private float lastX;
|
||||
private float lastY;
|
||||
private int motionEdge;
|
||||
private int validPointerId;
|
||||
|
||||
public CropImageView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public CropImageView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public CropImageView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
if (bitmapDisplayed.getBitmap() != null) {
|
||||
for (HighlightView hv : highlightViews) {
|
||||
|
||||
hv.matrix.set(getUnrotatedMatrix());
|
||||
hv.invalidate();
|
||||
if (hv.hasFocus()) {
|
||||
centerBasedOnHighlightView(hv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void zoomTo(float scale, float centerX, float centerY) {
|
||||
super.zoomTo(scale, centerX, centerY);
|
||||
for (HighlightView hv : highlightViews) {
|
||||
hv.matrix.set(getUnrotatedMatrix());
|
||||
hv.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void zoomIn() {
|
||||
super.zoomIn();
|
||||
for (HighlightView hv : highlightViews) {
|
||||
hv.matrix.set(getUnrotatedMatrix());
|
||||
hv.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void zoomOut() {
|
||||
super.zoomOut();
|
||||
for (HighlightView hv : highlightViews) {
|
||||
hv.matrix.set(getUnrotatedMatrix());
|
||||
hv.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void postTranslate(float deltaX, float deltaY) {
|
||||
super.postTranslate(deltaX, deltaY);
|
||||
for (HighlightView hv : highlightViews) {
|
||||
hv.matrix.postTranslate(deltaX, deltaY);
|
||||
hv.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(@NonNull MotionEvent event) {
|
||||
CropImageActivity cropImageActivity = (CropImageActivity) context;
|
||||
if (cropImageActivity.isSaving()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
for (HighlightView hv : highlightViews) {
|
||||
int edge = hv.getHit(event.getX(), event.getY());
|
||||
if (edge != HighlightView.GROW_NONE) {
|
||||
motionEdge = edge;
|
||||
motionHighlightView = hv;
|
||||
lastX = event.getX();
|
||||
lastY = event.getY();
|
||||
// Prevent multiple touches from interfering with crop area re-sizing
|
||||
validPointerId = event.getPointerId(event.getActionIndex());
|
||||
motionHighlightView.setMode((edge == HighlightView.MOVE)
|
||||
? HighlightView.ModifyMode.Move
|
||||
: HighlightView.ModifyMode.Grow);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (motionHighlightView != null) {
|
||||
centerBasedOnHighlightView(motionHighlightView);
|
||||
motionHighlightView.setMode(HighlightView.ModifyMode.None);
|
||||
}
|
||||
motionHighlightView = null;
|
||||
center();
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (motionHighlightView != null && event.getPointerId(event.getActionIndex()) == validPointerId) {
|
||||
motionHighlightView.handleMotion(motionEdge, event.getX()
|
||||
- lastX, event.getY() - lastY);
|
||||
lastX = event.getX();
|
||||
lastY = event.getY();
|
||||
}
|
||||
|
||||
// If we're not zoomed then there's no point in even allowing the user to move the image around.
|
||||
// This call to center puts it back to the normalized location.
|
||||
if (getScale() == 1F) {
|
||||
center();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pan the displayed image to make sure the cropping rectangle is visible.
|
||||
private void ensureVisible(HighlightView hv) {
|
||||
Rect r = hv.drawRect;
|
||||
|
||||
int panDeltaX1 = Math.max(0, getLeft() - r.left);
|
||||
int panDeltaX2 = Math.min(0, getRight() - r.right);
|
||||
|
||||
int panDeltaY1 = Math.max(0, getTop() - r.top);
|
||||
int panDeltaY2 = Math.min(0, getBottom() - r.bottom);
|
||||
|
||||
int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2;
|
||||
int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2;
|
||||
|
||||
if (panDeltaX != 0 || panDeltaY != 0) {
|
||||
panBy(panDeltaX, panDeltaY);
|
||||
}
|
||||
}
|
||||
|
||||
// If the cropping rectangle's size changed significantly, change the
|
||||
// view's center and scale according to the cropping rectangle.
|
||||
private void centerBasedOnHighlightView(HighlightView hv) {
|
||||
Rect drawRect = hv.drawRect;
|
||||
|
||||
float width = drawRect.width();
|
||||
float height = drawRect.height();
|
||||
|
||||
float thisWidth = getWidth();
|
||||
float thisHeight = getHeight();
|
||||
|
||||
float z1 = thisWidth / width * .6F;
|
||||
float z2 = thisHeight / height * .6F;
|
||||
|
||||
float zoom = Math.min(z1, z2);
|
||||
zoom = zoom * this.getScale();
|
||||
zoom = Math.max(1F, zoom);
|
||||
|
||||
if ((Math.abs(zoom - getScale()) / zoom) > .1) {
|
||||
float[] coordinates = new float[] { hv.cropRect.centerX(), hv.cropRect.centerY() };
|
||||
getUnrotatedMatrix().mapPoints(coordinates);
|
||||
zoomTo(zoom, coordinates[0], coordinates[1], 300F);
|
||||
}
|
||||
|
||||
ensureVisible(hv);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(@NonNull Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
for (HighlightView highlightView : highlightViews) {
|
||||
highlightView.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(HighlightView hv) {
|
||||
highlightViews.add(hv);
|
||||
invalidate();
|
||||
}
|
||||
}
|
227
libs/lib_crop/src/main/java/com/soundcloud/crop/CropUtil.java
Normal file
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright (C) 2009 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.soundcloud.crop;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.media.ExifInterface;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.MediaStore;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/*
|
||||
* Modified from original in AOSP.
|
||||
*/
|
||||
class CropUtil {
|
||||
|
||||
private static final String SCHEME_FILE = "file";
|
||||
private static final String SCHEME_CONTENT = "content";
|
||||
|
||||
public static void closeSilently(@Nullable Closeable c) {
|
||||
if (c == null) return;
|
||||
try {
|
||||
c.close();
|
||||
} catch (Throwable t) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
public static int getExifRotation(File imageFile) {
|
||||
if (imageFile == null) return 0;
|
||||
try {
|
||||
ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
|
||||
// We only recognize a subset of orientation tag values
|
||||
switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)) {
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
return 90;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
return 180;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
return 270;
|
||||
default:
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e("Error getting Exif data", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean copyExifRotation(File sourceFile, File destFile) {
|
||||
if (sourceFile == null || destFile == null) return false;
|
||||
try {
|
||||
ExifInterface exifSource = new ExifInterface(sourceFile.getAbsolutePath());
|
||||
ExifInterface exifDest = new ExifInterface(destFile.getAbsolutePath());
|
||||
exifDest.setAttribute(ExifInterface.TAG_ORIENTATION, exifSource.getAttribute(ExifInterface.TAG_ORIENTATION));
|
||||
exifDest.saveAttributes();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e("Error copying Exif data", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static File getFromMediaUri(Context context, ContentResolver resolver, Uri uri) {
|
||||
if (uri == null) return null;
|
||||
|
||||
if (SCHEME_FILE.equals(uri.getScheme())) {
|
||||
return new File(uri.getPath());
|
||||
} else if (SCHEME_CONTENT.equals(uri.getScheme())) {
|
||||
final String[] filePathColumn = { MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME };
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = resolver.query(uri, filePathColumn, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
final int columnIndex = (uri.toString().startsWith("content://com.google.android.gallery3d")) ?
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) :
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
|
||||
// Picasa images on API 13+
|
||||
if (columnIndex != -1) {
|
||||
String filePath = cursor.getString(columnIndex);
|
||||
if (!TextUtils.isEmpty(filePath)) {
|
||||
return new File(filePath);
|
||||
}
|
||||
} else if (TextUtils.equals(uri.getAuthority(), UriUtil.getFileProviderName(context))) {
|
||||
//这里修复拍照自定义的uri获取路径失败的问题
|
||||
String path = UriUtil.parseOwnUri(context, uri);
|
||||
if (!TextUtils.isEmpty(path)) {
|
||||
File file = new File(path);
|
||||
if (file.exists()) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Google Drive images
|
||||
return getFromMediaUriPfd(context, resolver, uri);
|
||||
} catch (SecurityException ignored) {
|
||||
// Nothing we can do
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String getTempFilename(Context context) throws IOException {
|
||||
File outputDir = context.getCacheDir();
|
||||
File outputFile = File.createTempFile("image", "tmp", outputDir);
|
||||
return outputFile.getAbsolutePath();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static File getFromMediaUriPfd(Context context, ContentResolver resolver, Uri uri) {
|
||||
if (uri == null) return null;
|
||||
|
||||
FileInputStream input = null;
|
||||
FileOutputStream output = null;
|
||||
try {
|
||||
ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r");
|
||||
FileDescriptor fd = pfd.getFileDescriptor();
|
||||
input = new FileInputStream(fd);
|
||||
|
||||
String tempFilename = getTempFilename(context);
|
||||
output = new FileOutputStream(tempFilename);
|
||||
|
||||
int read;
|
||||
byte[] bytes = new byte[4096];
|
||||
while ((read = input.read(bytes)) != -1) {
|
||||
output.write(bytes, 0, read);
|
||||
}
|
||||
return new File(tempFilename);
|
||||
} catch (IOException ignored) {
|
||||
// Nothing we can do
|
||||
} finally {
|
||||
closeSilently(input);
|
||||
closeSilently(output);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void startBackgroundJob(MonitoredActivity activity,
|
||||
String title, String message, Runnable job, Handler handler) {
|
||||
// Make the progress dialog uncancelable, so that we can guarantee
|
||||
// the thread will be done before the activity getting destroyed
|
||||
ProgressDialog dialog = ProgressDialog.show(
|
||||
activity, title, message, true, false);
|
||||
new Thread(new BackgroundJob(activity, job, dialog, handler)).start();
|
||||
}
|
||||
|
||||
private static class BackgroundJob extends MonitoredActivity.LifeCycleAdapter implements Runnable {
|
||||
|
||||
private final MonitoredActivity activity;
|
||||
private final ProgressDialog dialog;
|
||||
private final Runnable job;
|
||||
private final Handler handler;
|
||||
private final Runnable cleanupRunner = new Runnable() {
|
||||
public void run() {
|
||||
activity.removeLifeCycleListener(BackgroundJob.this);
|
||||
if (dialog.getWindow() != null) dialog.dismiss();
|
||||
}
|
||||
};
|
||||
|
||||
public BackgroundJob(MonitoredActivity activity, Runnable job,
|
||||
ProgressDialog dialog, Handler handler) {
|
||||
this.activity = activity;
|
||||
this.dialog = dialog;
|
||||
this.job = job;
|
||||
this.activity.addLifeCycleListener(this);
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
job.run();
|
||||
} finally {
|
||||
handler.post(cleanupRunner);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(MonitoredActivity activity) {
|
||||
// We get here only when the onDestroyed being called before
|
||||
// the cleanupRunner. So, run it now and remove it from the queue
|
||||
cleanupRunner.run();
|
||||
handler.removeCallbacks(cleanupRunner);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(MonitoredActivity activity) {
|
||||
dialog.hide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(MonitoredActivity activity) {
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,395 @@
|
||||
/*
|
||||
* Copyright (C) 2007 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.soundcloud.crop;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Region;
|
||||
import android.os.Build;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
|
||||
/*
|
||||
* Modified from version in AOSP.
|
||||
*
|
||||
* This class is used to display a highlighted cropping rectangle
|
||||
* overlayed on the image. There are two coordinate spaces in use. One is
|
||||
* image, another is screen. computeLayout() uses matrix to map from image
|
||||
* space to screen space.
|
||||
*/
|
||||
class HighlightView {
|
||||
|
||||
public static final int GROW_NONE = (1 << 0);
|
||||
public static final int GROW_LEFT_EDGE = (1 << 1);
|
||||
public static final int GROW_RIGHT_EDGE = (1 << 2);
|
||||
public static final int GROW_TOP_EDGE = (1 << 3);
|
||||
public static final int GROW_BOTTOM_EDGE = (1 << 4);
|
||||
public static final int MOVE = (1 << 5);
|
||||
|
||||
private static final int DEFAULT_HIGHLIGHT_COLOR = 0xFF33B5E5;
|
||||
private static final float HANDLE_RADIUS_DP = 12f;
|
||||
private static final float OUTLINE_DP = 2f;
|
||||
|
||||
enum ModifyMode { None, Move, Grow }
|
||||
enum HandleMode { Changing, Always, Never }
|
||||
|
||||
RectF cropRect; // Image space
|
||||
Rect drawRect; // Screen space
|
||||
Matrix matrix;
|
||||
private RectF imageRect; // Image space
|
||||
|
||||
private final Paint outsidePaint = new Paint();
|
||||
private final Paint outlinePaint = new Paint();
|
||||
private final Paint handlePaint = new Paint();
|
||||
|
||||
private View viewContext; // View displaying image
|
||||
private boolean showThirds;
|
||||
private boolean showCircle;
|
||||
private int highlightColor;
|
||||
|
||||
private ModifyMode modifyMode = ModifyMode.None;
|
||||
private HandleMode handleMode = HandleMode.Changing;
|
||||
private boolean maintainAspectRatio;
|
||||
private float initialAspectRatio;
|
||||
private float handleRadius;
|
||||
private float outlineWidth;
|
||||
private boolean isFocused;
|
||||
|
||||
public HighlightView(View context) {
|
||||
viewContext = context;
|
||||
initStyles(context.getContext());
|
||||
}
|
||||
|
||||
private void initStyles(Context context) {
|
||||
TypedValue outValue = new TypedValue();
|
||||
context.getTheme().resolveAttribute(R.attr.cropImageStyle, outValue, true);
|
||||
TypedArray attributes = context.obtainStyledAttributes(outValue.resourceId, R.styleable.CropImageView);
|
||||
try {
|
||||
showThirds = attributes.getBoolean(R.styleable.CropImageView_showThirds, false);
|
||||
showCircle = attributes.getBoolean(R.styleable.CropImageView_showCircle, false);
|
||||
highlightColor = attributes.getColor(R.styleable.CropImageView_highlightColor,
|
||||
DEFAULT_HIGHLIGHT_COLOR);
|
||||
handleMode = HandleMode.values()[attributes.getInt(R.styleable.CropImageView_showHandles, 0)];
|
||||
} finally {
|
||||
attributes.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public void setup(Matrix m, Rect imageRect, RectF cropRect, boolean maintainAspectRatio) {
|
||||
matrix = new Matrix(m);
|
||||
|
||||
this.cropRect = cropRect;
|
||||
this.imageRect = new RectF(imageRect);
|
||||
this.maintainAspectRatio = maintainAspectRatio;
|
||||
|
||||
initialAspectRatio = this.cropRect.width() / this.cropRect.height();
|
||||
drawRect = computeLayout();
|
||||
|
||||
outsidePaint.setARGB(125, 50, 50, 50);
|
||||
outlinePaint.setStyle(Paint.Style.STROKE);
|
||||
outlinePaint.setAntiAlias(true);
|
||||
outlineWidth = dpToPx(OUTLINE_DP);
|
||||
|
||||
handlePaint.setColor(highlightColor);
|
||||
handlePaint.setStyle(Paint.Style.FILL);
|
||||
handlePaint.setAntiAlias(true);
|
||||
handleRadius = dpToPx(HANDLE_RADIUS_DP);
|
||||
|
||||
modifyMode = ModifyMode.None;
|
||||
}
|
||||
|
||||
private float dpToPx(float dp) {
|
||||
return dp * viewContext.getResources().getDisplayMetrics().density;
|
||||
}
|
||||
|
||||
protected void draw(Canvas canvas) {
|
||||
canvas.save();
|
||||
Path path = new Path();
|
||||
outlinePaint.setStrokeWidth(outlineWidth);
|
||||
if (!hasFocus()) {
|
||||
outlinePaint.setColor(Color.BLACK);
|
||||
canvas.drawRect(drawRect, outlinePaint);
|
||||
} else {
|
||||
Rect viewDrawingRect = new Rect();
|
||||
viewContext.getDrawingRect(viewDrawingRect);
|
||||
|
||||
path.addRect(new RectF(drawRect), Path.Direction.CW);
|
||||
outlinePaint.setColor(highlightColor);
|
||||
|
||||
if (isClipPathSupported(canvas)) {
|
||||
canvas.clipPath(path, Region.Op.DIFFERENCE);
|
||||
canvas.drawRect(viewDrawingRect, outsidePaint);
|
||||
} else {
|
||||
drawOutsideFallback(canvas);
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
canvas.drawPath(path, outlinePaint);
|
||||
|
||||
if (showThirds) {
|
||||
drawThirds(canvas);
|
||||
}
|
||||
|
||||
if (showCircle) {
|
||||
drawCircle(canvas);
|
||||
}
|
||||
|
||||
if (handleMode == HandleMode.Always ||
|
||||
(handleMode == HandleMode.Changing && modifyMode == ModifyMode.Grow)) {
|
||||
drawHandles(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Fall back to naive method for darkening outside crop area
|
||||
*/
|
||||
private void drawOutsideFallback(Canvas canvas) {
|
||||
canvas.drawRect(0, 0, canvas.getWidth(), drawRect.top, outsidePaint);
|
||||
canvas.drawRect(0, drawRect.bottom, canvas.getWidth(), canvas.getHeight(), outsidePaint);
|
||||
canvas.drawRect(0, drawRect.top, drawRect.left, drawRect.bottom, outsidePaint);
|
||||
canvas.drawRect(drawRect.right, drawRect.top, canvas.getWidth(), drawRect.bottom, outsidePaint);
|
||||
}
|
||||
|
||||
/*
|
||||
* Clip path is broken, unreliable or not supported on:
|
||||
* - JellyBean MR1
|
||||
* - ICS & ICS MR1 with hardware acceleration turned on
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
private boolean isClipPathSupported(Canvas canvas) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
return false;
|
||||
} else if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||
|| Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
|
||||
return true;
|
||||
} else {
|
||||
return !canvas.isHardwareAccelerated();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawHandles(Canvas canvas) {
|
||||
int xMiddle = drawRect.left + ((drawRect.right - drawRect.left) / 2);
|
||||
int yMiddle = drawRect.top + ((drawRect.bottom - drawRect.top) / 2);
|
||||
|
||||
canvas.drawCircle(drawRect.left, yMiddle, handleRadius, handlePaint);
|
||||
canvas.drawCircle(xMiddle, drawRect.top, handleRadius, handlePaint);
|
||||
canvas.drawCircle(drawRect.right, yMiddle, handleRadius, handlePaint);
|
||||
canvas.drawCircle(xMiddle, drawRect.bottom, handleRadius, handlePaint);
|
||||
}
|
||||
|
||||
private void drawThirds(Canvas canvas) {
|
||||
outlinePaint.setStrokeWidth(1);
|
||||
float xThird = (drawRect.right - drawRect.left) / 3;
|
||||
float yThird = (drawRect.bottom - drawRect.top) / 3;
|
||||
|
||||
canvas.drawLine(drawRect.left + xThird, drawRect.top,
|
||||
drawRect.left + xThird, drawRect.bottom, outlinePaint);
|
||||
canvas.drawLine(drawRect.left + xThird * 2, drawRect.top,
|
||||
drawRect.left + xThird * 2, drawRect.bottom, outlinePaint);
|
||||
canvas.drawLine(drawRect.left, drawRect.top + yThird,
|
||||
drawRect.right, drawRect.top + yThird, outlinePaint);
|
||||
canvas.drawLine(drawRect.left, drawRect.top + yThird * 2,
|
||||
drawRect.right, drawRect.top + yThird * 2, outlinePaint);
|
||||
}
|
||||
|
||||
private void drawCircle(Canvas canvas) {
|
||||
outlinePaint.setStrokeWidth(1);
|
||||
canvas.drawOval(new RectF(drawRect), outlinePaint);
|
||||
}
|
||||
|
||||
public void setMode(ModifyMode mode) {
|
||||
if (mode != modifyMode) {
|
||||
modifyMode = mode;
|
||||
viewContext.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
// Determines which edges are hit by touching at (x, y)
|
||||
public int getHit(float x, float y) {
|
||||
Rect r = computeLayout();
|
||||
final float hysteresis = 20F;
|
||||
int retval = GROW_NONE;
|
||||
|
||||
// verticalCheck makes sure the position is between the top and
|
||||
// the bottom edge (with some tolerance). Similar for horizCheck.
|
||||
boolean verticalCheck = (y >= r.top - hysteresis)
|
||||
&& (y < r.bottom + hysteresis);
|
||||
boolean horizCheck = (x >= r.left - hysteresis)
|
||||
&& (x < r.right + hysteresis);
|
||||
|
||||
// Check whether the position is near some edge(s)
|
||||
if ((Math.abs(r.left - x) < hysteresis) && verticalCheck) {
|
||||
retval |= GROW_LEFT_EDGE;
|
||||
}
|
||||
if ((Math.abs(r.right - x) < hysteresis) && verticalCheck) {
|
||||
retval |= GROW_RIGHT_EDGE;
|
||||
}
|
||||
if ((Math.abs(r.top - y) < hysteresis) && horizCheck) {
|
||||
retval |= GROW_TOP_EDGE;
|
||||
}
|
||||
if ((Math.abs(r.bottom - y) < hysteresis) && horizCheck) {
|
||||
retval |= GROW_BOTTOM_EDGE;
|
||||
}
|
||||
|
||||
// Not near any edge but inside the rectangle: move
|
||||
if (retval == GROW_NONE && r.contains((int) x, (int) y)) {
|
||||
retval = MOVE;
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
// Handles motion (dx, dy) in screen space.
|
||||
// The "edge" parameter specifies which edges the user is dragging.
|
||||
void handleMotion(int edge, float dx, float dy) {
|
||||
Rect r = computeLayout();
|
||||
if (edge == MOVE) {
|
||||
// Convert to image space before sending to moveBy()
|
||||
moveBy(dx * (cropRect.width() / r.width()),
|
||||
dy * (cropRect.height() / r.height()));
|
||||
} else {
|
||||
if (((GROW_LEFT_EDGE | GROW_RIGHT_EDGE) & edge) == 0) {
|
||||
dx = 0;
|
||||
}
|
||||
|
||||
if (((GROW_TOP_EDGE | GROW_BOTTOM_EDGE) & edge) == 0) {
|
||||
dy = 0;
|
||||
}
|
||||
|
||||
// Convert to image space before sending to growBy()
|
||||
float xDelta = dx * (cropRect.width() / r.width());
|
||||
float yDelta = dy * (cropRect.height() / r.height());
|
||||
growBy((((edge & GROW_LEFT_EDGE) != 0) ? -1 : 1) * xDelta,
|
||||
(((edge & GROW_TOP_EDGE) != 0) ? -1 : 1) * yDelta);
|
||||
}
|
||||
}
|
||||
|
||||
// Grows the cropping rectangle by (dx, dy) in image space
|
||||
void moveBy(float dx, float dy) {
|
||||
Rect invalRect = new Rect(drawRect);
|
||||
|
||||
cropRect.offset(dx, dy);
|
||||
|
||||
// Put the cropping rectangle inside image rectangle
|
||||
cropRect.offset(
|
||||
Math.max(0, imageRect.left - cropRect.left),
|
||||
Math.max(0, imageRect.top - cropRect.top));
|
||||
|
||||
cropRect.offset(
|
||||
Math.min(0, imageRect.right - cropRect.right),
|
||||
Math.min(0, imageRect.bottom - cropRect.bottom));
|
||||
|
||||
drawRect = computeLayout();
|
||||
invalRect.union(drawRect);
|
||||
invalRect.inset(-(int) handleRadius, -(int) handleRadius);
|
||||
viewContext.invalidate(invalRect);
|
||||
}
|
||||
|
||||
// Grows the cropping rectangle by (dx, dy) in image space.
|
||||
void growBy(float dx, float dy) {
|
||||
if (maintainAspectRatio) {
|
||||
if (dx != 0) {
|
||||
dy = dx / initialAspectRatio;
|
||||
} else if (dy != 0) {
|
||||
dx = dy * initialAspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't let the cropping rectangle grow too fast.
|
||||
// Grow at most half of the difference between the image rectangle and
|
||||
// the cropping rectangle.
|
||||
RectF r = new RectF(cropRect);
|
||||
if (dx > 0F && r.width() + 2 * dx > imageRect.width()) {
|
||||
dx = (imageRect.width() - r.width()) / 2F;
|
||||
if (maintainAspectRatio) {
|
||||
dy = dx / initialAspectRatio;
|
||||
}
|
||||
}
|
||||
if (dy > 0F && r.height() + 2 * dy > imageRect.height()) {
|
||||
dy = (imageRect.height() - r.height()) / 2F;
|
||||
if (maintainAspectRatio) {
|
||||
dx = dy * initialAspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
r.inset(-dx, -dy);
|
||||
|
||||
// Don't let the cropping rectangle shrink too fast
|
||||
final float widthCap = 25F;
|
||||
if (r.width() < widthCap) {
|
||||
r.inset(-(widthCap - r.width()) / 2F, 0F);
|
||||
}
|
||||
float heightCap = maintainAspectRatio
|
||||
? (widthCap / initialAspectRatio)
|
||||
: widthCap;
|
||||
if (r.height() < heightCap) {
|
||||
r.inset(0F, -(heightCap - r.height()) / 2F);
|
||||
}
|
||||
|
||||
// Put the cropping rectangle inside the image rectangle
|
||||
if (r.left < imageRect.left) {
|
||||
r.offset(imageRect.left - r.left, 0F);
|
||||
} else if (r.right > imageRect.right) {
|
||||
r.offset(-(r.right - imageRect.right), 0F);
|
||||
}
|
||||
if (r.top < imageRect.top) {
|
||||
r.offset(0F, imageRect.top - r.top);
|
||||
} else if (r.bottom > imageRect.bottom) {
|
||||
r.offset(0F, -(r.bottom - imageRect.bottom));
|
||||
}
|
||||
|
||||
cropRect.set(r);
|
||||
drawRect = computeLayout();
|
||||
viewContext.invalidate();
|
||||
}
|
||||
|
||||
// Returns the cropping rectangle in image space with specified scale
|
||||
public Rect getScaledCropRect(float scale) {
|
||||
return new Rect((int) (cropRect.left * scale), (int) (cropRect.top * scale),
|
||||
(int) (cropRect.right * scale), (int) (cropRect.bottom * scale));
|
||||
}
|
||||
|
||||
// Maps the cropping rectangle from image space to screen space
|
||||
private Rect computeLayout() {
|
||||
RectF r = new RectF(cropRect.left, cropRect.top,
|
||||
cropRect.right, cropRect.bottom);
|
||||
matrix.mapRect(r);
|
||||
return new Rect(Math.round(r.left), Math.round(r.top),
|
||||
Math.round(r.right), Math.round(r.bottom));
|
||||
}
|
||||
|
||||
public void invalidate() {
|
||||
drawRect = computeLayout();
|
||||
}
|
||||
|
||||
public boolean hasFocus() {
|
||||
return isFocused;
|
||||
}
|
||||
|
||||
public void setFocus(boolean isFocused) {
|
||||
this.isFocused = isFocused;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,400 @@
|
||||
/*
|
||||
* Copyright (C) 2009 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.soundcloud.crop;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.widget.ImageView;
|
||||
|
||||
/*
|
||||
* Modified from original in AOSP.
|
||||
*/
|
||||
abstract class ImageViewTouchBase extends ImageView {
|
||||
|
||||
private static final float SCALE_RATE = 1.25F;
|
||||
|
||||
// This is the base transformation which is used to show the image
|
||||
// initially. The current computation for this shows the image in
|
||||
// it's entirety, letterboxing as needed. One could choose to
|
||||
// show the image as cropped instead.
|
||||
//
|
||||
// This matrix is recomputed when we go from the thumbnail image to
|
||||
// the full size image.
|
||||
protected Matrix baseMatrix = new Matrix();
|
||||
|
||||
// This is the supplementary transformation which reflects what
|
||||
// the user has done in terms of zooming and panning.
|
||||
//
|
||||
// This matrix remains the same when we go from the thumbnail image
|
||||
// to the full size image.
|
||||
protected Matrix suppMatrix = new Matrix();
|
||||
|
||||
// This is the final matrix which is computed as the concatentation
|
||||
// of the base matrix and the supplementary matrix.
|
||||
private final Matrix displayMatrix = new Matrix();
|
||||
|
||||
// Temporary buffer used for getting the values out of a matrix.
|
||||
private final float[] matrixValues = new float[9];
|
||||
|
||||
// The current bitmap being displayed.
|
||||
protected final RotateBitmap bitmapDisplayed = new RotateBitmap(null, 0);
|
||||
|
||||
int thisWidth = -1;
|
||||
int thisHeight = -1;
|
||||
|
||||
float maxZoom;
|
||||
|
||||
private Runnable onLayoutRunnable;
|
||||
|
||||
protected Handler handler = new Handler();
|
||||
|
||||
// ImageViewTouchBase will pass a Bitmap to the Recycler if it has finished
|
||||
// its use of that Bitmap
|
||||
public interface Recycler {
|
||||
public void recycle(Bitmap b);
|
||||
}
|
||||
|
||||
private Recycler recycler;
|
||||
|
||||
public ImageViewTouchBase(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public ImageViewTouchBase(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public ImageViewTouchBase(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init();
|
||||
}
|
||||
|
||||
public void setRecycler(Recycler recycler) {
|
||||
this.recycler = recycler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
thisWidth = right - left;
|
||||
thisHeight = bottom - top;
|
||||
Runnable r = onLayoutRunnable;
|
||||
if (r != null) {
|
||||
onLayoutRunnable = null;
|
||||
r.run();
|
||||
}
|
||||
if (bitmapDisplayed.getBitmap() != null) {
|
||||
getProperBaseMatrix(bitmapDisplayed, baseMatrix, true);
|
||||
setImageMatrix(getImageViewMatrix());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
|
||||
event.startTracking();
|
||||
return true;
|
||||
}
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) {
|
||||
if (getScale() > 1.0f) {
|
||||
// If we're zoomed in, pressing Back jumps out to show the
|
||||
// entire image, otherwise Back returns the user to the gallery
|
||||
zoomTo(1.0f);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageBitmap(Bitmap bitmap) {
|
||||
setImageBitmap(bitmap, 0);
|
||||
}
|
||||
|
||||
private void setImageBitmap(Bitmap bitmap, int rotation) {
|
||||
super.setImageBitmap(bitmap);
|
||||
Drawable d = getDrawable();
|
||||
if (d != null) {
|
||||
d.setDither(true);
|
||||
}
|
||||
|
||||
Bitmap old = bitmapDisplayed.getBitmap();
|
||||
bitmapDisplayed.setBitmap(bitmap);
|
||||
bitmapDisplayed.setRotation(rotation);
|
||||
|
||||
if (old != null && old != bitmap && recycler != null) {
|
||||
recycler.recycle(old);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
setImageBitmapResetBase(null, true);
|
||||
}
|
||||
|
||||
|
||||
// This function changes bitmap, reset base matrix according to the size
|
||||
// of the bitmap, and optionally reset the supplementary matrix
|
||||
public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp) {
|
||||
setImageRotateBitmapResetBase(new RotateBitmap(bitmap, 0), resetSupp);
|
||||
}
|
||||
|
||||
public void setImageRotateBitmapResetBase(final RotateBitmap bitmap, final boolean resetSupp) {
|
||||
final int viewWidth = getWidth();
|
||||
|
||||
if (viewWidth <= 0) {
|
||||
onLayoutRunnable = new Runnable() {
|
||||
public void run() {
|
||||
setImageRotateBitmapResetBase(bitmap, resetSupp);
|
||||
}
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (bitmap.getBitmap() != null) {
|
||||
getProperBaseMatrix(bitmap, baseMatrix, true);
|
||||
setImageBitmap(bitmap.getBitmap(), bitmap.getRotation());
|
||||
} else {
|
||||
baseMatrix.reset();
|
||||
setImageBitmap(null);
|
||||
}
|
||||
|
||||
if (resetSupp) {
|
||||
suppMatrix.reset();
|
||||
}
|
||||
setImageMatrix(getImageViewMatrix());
|
||||
maxZoom = calculateMaxZoom();
|
||||
}
|
||||
|
||||
// Center as much as possible in one or both axis. Centering is defined as follows:
|
||||
// * If the image is scaled down below the view's dimensions then center it.
|
||||
// * If the image is scaled larger than the view and is translated out of view then translate it back into view.
|
||||
protected void center() {
|
||||
final Bitmap bitmap = bitmapDisplayed.getBitmap();
|
||||
if (bitmap == null) {
|
||||
return;
|
||||
}
|
||||
Matrix m = getImageViewMatrix();
|
||||
|
||||
RectF rect = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
m.mapRect(rect);
|
||||
|
||||
float height = rect.height();
|
||||
float width = rect.width();
|
||||
|
||||
float deltaX = 0, deltaY = 0;
|
||||
|
||||
deltaY = centerVertical(rect, height, deltaY);
|
||||
deltaX = centerHorizontal(rect, width, deltaX);
|
||||
|
||||
postTranslate(deltaX, deltaY);
|
||||
setImageMatrix(getImageViewMatrix());
|
||||
}
|
||||
|
||||
private float centerVertical(RectF rect, float height, float deltaY) {
|
||||
int viewHeight = getHeight();
|
||||
if (height < viewHeight) {
|
||||
deltaY = (viewHeight - height) / 2 - rect.top;
|
||||
} else if (rect.top > 0) {
|
||||
deltaY = -rect.top;
|
||||
} else if (rect.bottom < viewHeight) {
|
||||
deltaY = getHeight() - rect.bottom;
|
||||
}
|
||||
return deltaY;
|
||||
}
|
||||
|
||||
private float centerHorizontal(RectF rect, float width, float deltaX) {
|
||||
int viewWidth = getWidth();
|
||||
if (width < viewWidth) {
|
||||
deltaX = (viewWidth - width) / 2 - rect.left;
|
||||
} else if (rect.left > 0) {
|
||||
deltaX = -rect.left;
|
||||
} else if (rect.right < viewWidth) {
|
||||
deltaX = viewWidth - rect.right;
|
||||
}
|
||||
return deltaX;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setScaleType(ImageView.ScaleType.MATRIX);
|
||||
}
|
||||
|
||||
protected float getValue(Matrix matrix, int whichValue) {
|
||||
matrix.getValues(matrixValues);
|
||||
return matrixValues[whichValue];
|
||||
}
|
||||
|
||||
// Get the scale factor out of the matrix.
|
||||
protected float getScale(Matrix matrix) {
|
||||
return getValue(matrix, Matrix.MSCALE_X);
|
||||
}
|
||||
|
||||
protected float getScale() {
|
||||
return getScale(suppMatrix);
|
||||
}
|
||||
|
||||
// Setup the base matrix so that the image is centered and scaled properly.
|
||||
private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix, boolean includeRotation) {
|
||||
float viewWidth = getWidth();
|
||||
float viewHeight = getHeight();
|
||||
|
||||
float w = bitmap.getWidth();
|
||||
float h = bitmap.getHeight();
|
||||
matrix.reset();
|
||||
|
||||
// We limit up-scaling to 3x otherwise the result may look bad if it's a small icon
|
||||
float widthScale = Math.min(viewWidth / w, 3.0f);
|
||||
float heightScale = Math.min(viewHeight / h, 3.0f);
|
||||
float scale = Math.min(widthScale, heightScale);
|
||||
|
||||
if (includeRotation) {
|
||||
matrix.postConcat(bitmap.getRotateMatrix());
|
||||
}
|
||||
matrix.postScale(scale, scale);
|
||||
matrix.postTranslate((viewWidth - w * scale) / 2F, (viewHeight - h * scale) / 2F);
|
||||
}
|
||||
|
||||
// Combine the base matrix and the supp matrix to make the final matrix
|
||||
protected Matrix getImageViewMatrix() {
|
||||
// The final matrix is computed as the concatentation of the base matrix
|
||||
// and the supplementary matrix
|
||||
displayMatrix.set(baseMatrix);
|
||||
displayMatrix.postConcat(suppMatrix);
|
||||
return displayMatrix;
|
||||
}
|
||||
|
||||
public Matrix getUnrotatedMatrix(){
|
||||
Matrix unrotated = new Matrix();
|
||||
getProperBaseMatrix(bitmapDisplayed, unrotated, false);
|
||||
unrotated.postConcat(suppMatrix);
|
||||
return unrotated;
|
||||
}
|
||||
|
||||
protected float calculateMaxZoom() {
|
||||
if (bitmapDisplayed.getBitmap() == null) {
|
||||
return 1F;
|
||||
}
|
||||
|
||||
float fw = (float) bitmapDisplayed.getWidth() / (float) thisWidth;
|
||||
float fh = (float) bitmapDisplayed.getHeight() / (float) thisHeight;
|
||||
return Math.max(fw, fh) * 4; // 400%
|
||||
}
|
||||
|
||||
protected void zoomTo(float scale, float centerX, float centerY) {
|
||||
if (scale > maxZoom) {
|
||||
scale = maxZoom;
|
||||
}
|
||||
|
||||
float oldScale = getScale();
|
||||
float deltaScale = scale / oldScale;
|
||||
|
||||
suppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
|
||||
setImageMatrix(getImageViewMatrix());
|
||||
center();
|
||||
}
|
||||
|
||||
protected void zoomTo(final float scale, final float centerX,
|
||||
final float centerY, final float durationMs) {
|
||||
final float incrementPerMs = (scale - getScale()) / durationMs;
|
||||
final float oldScale = getScale();
|
||||
final long startTime = System.currentTimeMillis();
|
||||
|
||||
handler.post(new Runnable() {
|
||||
public void run() {
|
||||
long now = System.currentTimeMillis();
|
||||
float currentMs = Math.min(durationMs, now - startTime);
|
||||
float target = oldScale + (incrementPerMs * currentMs);
|
||||
zoomTo(target, centerX, centerY);
|
||||
|
||||
if (currentMs < durationMs) {
|
||||
handler.post(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void zoomTo(float scale) {
|
||||
float cx = getWidth() / 2F;
|
||||
float cy = getHeight() / 2F;
|
||||
zoomTo(scale, cx, cy);
|
||||
}
|
||||
|
||||
protected void zoomIn() {
|
||||
zoomIn(SCALE_RATE);
|
||||
}
|
||||
|
||||
protected void zoomOut() {
|
||||
zoomOut(SCALE_RATE);
|
||||
}
|
||||
|
||||
protected void zoomIn(float rate) {
|
||||
if (getScale() >= maxZoom) {
|
||||
return; // Don't let the user zoom into the molecular level
|
||||
}
|
||||
if (bitmapDisplayed.getBitmap() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
float cx = getWidth() / 2F;
|
||||
float cy = getHeight() / 2F;
|
||||
|
||||
suppMatrix.postScale(rate, rate, cx, cy);
|
||||
setImageMatrix(getImageViewMatrix());
|
||||
}
|
||||
|
||||
protected void zoomOut(float rate) {
|
||||
if (bitmapDisplayed.getBitmap() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
float cx = getWidth() / 2F;
|
||||
float cy = getHeight() / 2F;
|
||||
|
||||
// Zoom out to at most 1x
|
||||
Matrix tmp = new Matrix(suppMatrix);
|
||||
tmp.postScale(1F / rate, 1F / rate, cx, cy);
|
||||
|
||||
if (getScale(tmp) < 1F) {
|
||||
suppMatrix.setScale(1F, 1F, cx, cy);
|
||||
} else {
|
||||
suppMatrix.postScale(1F / rate, 1F / rate, cx, cy);
|
||||
}
|
||||
setImageMatrix(getImageViewMatrix());
|
||||
center();
|
||||
}
|
||||
|
||||
protected void postTranslate(float dx, float dy) {
|
||||
suppMatrix.postTranslate(dx, dy);
|
||||
}
|
||||
|
||||
protected void panBy(float dx, float dy) {
|
||||
postTranslate(dx, dy);
|
||||
setImageMatrix(getImageViewMatrix());
|
||||
}
|
||||
}
|
15
libs/lib_crop/src/main/java/com/soundcloud/crop/Log.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.soundcloud.crop;
|
||||
|
||||
class Log {
|
||||
|
||||
private static final String TAG = "android-crop";
|
||||
|
||||
public static void e(String msg) {
|
||||
android.util.Log.e(TAG, msg);
|
||||
}
|
||||
|
||||
public static void e(String msg, Throwable e) {
|
||||
android.util.Log.e(TAG, msg, e);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2009 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.soundcloud.crop;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/*
|
||||
* Modified from original in AOSP.
|
||||
*/
|
||||
abstract class MonitoredActivity extends Activity {
|
||||
|
||||
private final ArrayList<LifeCycleListener> listeners = new ArrayList<LifeCycleListener>();
|
||||
|
||||
public static interface LifeCycleListener {
|
||||
public void onActivityCreated(MonitoredActivity activity);
|
||||
public void onActivityDestroyed(MonitoredActivity activity);
|
||||
public void onActivityStarted(MonitoredActivity activity);
|
||||
public void onActivityStopped(MonitoredActivity activity);
|
||||
}
|
||||
|
||||
public static class LifeCycleAdapter implements LifeCycleListener {
|
||||
public void onActivityCreated(MonitoredActivity activity) {}
|
||||
public void onActivityDestroyed(MonitoredActivity activity) {}
|
||||
public void onActivityStarted(MonitoredActivity activity) {}
|
||||
public void onActivityStopped(MonitoredActivity activity) {}
|
||||
}
|
||||
|
||||
public void addLifeCycleListener(LifeCycleListener listener) {
|
||||
if (listeners.contains(listener)) return;
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeLifeCycleListener(LifeCycleListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
for (LifeCycleListener listener : listeners) {
|
||||
listener.onActivityCreated(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
for (LifeCycleListener listener : listeners) {
|
||||
listener.onActivityDestroyed(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
for (LifeCycleListener listener : listeners) {
|
||||
listener.onActivityStarted(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
for (LifeCycleListener listener : listeners) {
|
||||
listener.onActivityStopped(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2009 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.soundcloud.crop;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
|
||||
/*
|
||||
* Modified from original in AOSP.
|
||||
*/
|
||||
class RotateBitmap {
|
||||
|
||||
private Bitmap bitmap;
|
||||
private int rotation;
|
||||
|
||||
public RotateBitmap(Bitmap bitmap, int rotation) {
|
||||
this.bitmap = bitmap;
|
||||
this.rotation = rotation % 360;
|
||||
}
|
||||
|
||||
public void setRotation(int rotation) {
|
||||
this.rotation = rotation;
|
||||
}
|
||||
|
||||
public int getRotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public Bitmap getBitmap() {
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public void setBitmap(Bitmap bitmap) {
|
||||
this.bitmap = bitmap;
|
||||
}
|
||||
|
||||
public Matrix getRotateMatrix() {
|
||||
// By default this is an identity matrix
|
||||
Matrix matrix = new Matrix();
|
||||
if (bitmap != null && rotation != 0) {
|
||||
// We want to do the rotation at origin, but since the bounding
|
||||
// rectangle will be changed after rotation, so the delta values
|
||||
// are based on old & new width/height respectively.
|
||||
int cx = bitmap.getWidth() / 2;
|
||||
int cy = bitmap.getHeight() / 2;
|
||||
matrix.preTranslate(-cx, -cy);
|
||||
matrix.postRotate(rotation);
|
||||
matrix.postTranslate(getWidth() / 2, getHeight() / 2);
|
||||
}
|
||||
return matrix;
|
||||
}
|
||||
|
||||
public boolean isOrientationChanged() {
|
||||
return (rotation / 90) % 2 != 0;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
if (bitmap == null) return 0;
|
||||
if (isOrientationChanged()) {
|
||||
return bitmap.getWidth();
|
||||
} else {
|
||||
return bitmap.getHeight();
|
||||
}
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
if (bitmap == null) return 0;
|
||||
if (isOrientationChanged()) {
|
||||
return bitmap.getHeight();
|
||||
} else {
|
||||
return bitmap.getWidth();
|
||||
}
|
||||
}
|
||||
|
||||
public void recycle() {
|
||||
if (bitmap != null) {
|
||||
bitmap.recycle();
|
||||
bitmap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
48
libs/lib_crop/src/main/java/com/soundcloud/crop/UriUtil.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.soundcloud.crop;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class UriUtil {
|
||||
|
||||
public final static String getFileProviderName(Context context){
|
||||
return context.getPackageName()+".fileprovider";
|
||||
}
|
||||
|
||||
/**
|
||||
* 将TakePhoto 提供的Uri 解析出文件绝对路径
|
||||
*
|
||||
* @param uri
|
||||
* @return
|
||||
*/
|
||||
public static String parseOwnUri(Context context, Uri uri) {
|
||||
if (uri == null) return null;
|
||||
String path;
|
||||
if (TextUtils.equals(uri.getAuthority(), getFileProviderName(context))) {
|
||||
String target_text_camera_photos = "camera_photos/";
|
||||
if (uri.getPath() != null && uri.getPath().contains(target_text_camera_photos)) {
|
||||
path = new File(uri.getPath().replace(target_text_camera_photos, ""))
|
||||
.getAbsolutePath();
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
path = new File(Environment.getExternalStorageDirectory(),
|
||||
uri.getPath())
|
||||
.getAbsolutePath();
|
||||
} else {
|
||||
path = uri.getPath();
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
path = uri.getPath();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
}
|
BIN
libs/lib_crop/src/main/res/drawable-hdpi/crop__divider.9.png
Normal file
After Width: | Height: | Size: 76 B |
BIN
libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_cancel.webp
Normal file
After Width: | Height: | Size: 204 B |
BIN
libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_done.webp
Normal file
After Width: | Height: | Size: 194 B |
BIN
libs/lib_crop/src/main/res/drawable-mdpi/crop__divider.9.png
Normal file
After Width: | Height: | Size: 76 B |
BIN
libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_cancel.webp
Normal file
After Width: | Height: | Size: 160 B |
BIN
libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_done.webp
Normal file
After Width: | Height: | Size: 168 B |
@@ -0,0 +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">
|
||||
<item android:drawable="@color/crop__button_bar" />
|
||||
</ripple>
|
BIN
libs/lib_crop/src/main/res/drawable-xhdpi/crop__divider.9.png
Normal file
After Width: | Height: | Size: 83 B |
BIN
libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_cancel.webp
Normal file
After Width: | Height: | Size: 234 B |
BIN
libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_done.webp
Normal file
After Width: | Height: | Size: 268 B |
BIN
libs/lib_crop/src/main/res/drawable-xhdpi/crop__tile.webp
Normal file
After Width: | Height: | Size: 52 B |
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
|
||||
|
||||
<item android:state_pressed="true">
|
||||
<shape>
|
||||
<solid android:color="@color/crop__selector_pressed" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item android:state_focused="true">
|
||||
<shape>
|
||||
<solid android:color="@color/crop__selector_focused" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item android:drawable="@android:color/transparent" />
|
||||
|
||||
</selector>
|
5
libs/lib_crop/src/main/res/drawable/crop__texture.xml
Normal 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" />
|
19
libs/lib_crop/src/main/res/layout/crop__activity_crop.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include
|
||||
android:id="@+id/done_cancel_bar"
|
||||
layout="@layout/crop__layout_done_cancel" />
|
||||
|
||||
<com.soundcloud.crop.CropImageView
|
||||
android:id="@+id/crop_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/crop__texture"
|
||||
android:layout_below="@id/done_cancel_bar" />
|
||||
|
||||
</RelativeLayout>
|
@@ -0,0 +1,16 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/Crop.DoneCancelBar">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/btn_cancel"
|
||||
style="@style/Crop.ActionButton">
|
||||
<TextView style="@style/Crop.ActionButtonText.Cancel" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/btn_done"
|
||||
style="@style/Crop.ActionButton">
|
||||
<TextView style="@style/Crop.ActionButtonText.Done" />
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
5
libs/lib_crop/src/main/res/values-v21/colors.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<resources>
|
||||
|
||||
<color name="crop__selector_pressed">#aaaaaa</color>
|
||||
|
||||
</resources>
|
16
libs/lib_crop/src/main/res/values/attrs.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<resources>
|
||||
|
||||
<attr name="cropImageStyle" format="reference" />
|
||||
|
||||
<declare-styleable name="CropImageView">
|
||||
<attr name="highlightColor" format="reference|color" />
|
||||
<attr name="showThirds" format="boolean" />
|
||||
<attr name="showCircle" format="boolean" />
|
||||
<attr name="showHandles">
|
||||
<enum name="changing" value="0" />
|
||||
<enum name="always" value="1" />
|
||||
<enum name="never" value="2" />
|
||||
</attr>
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
8
libs/lib_crop/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<resources>
|
||||
|
||||
<color name="crop__button_bar">#f3f3f3</color>
|
||||
<color name="crop__button_text">#666666</color>
|
||||
<color name="crop__selector_pressed">#1a000000</color>
|
||||
<color name="crop__selector_focused">#77000000</color>
|
||||
|
||||
</resources>
|
5
libs/lib_crop/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<resources>
|
||||
|
||||
<dimen name="crop__bar_height">56dp</dimen>
|
||||
|
||||
</resources>
|
10
libs/lib_crop/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<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>
|
44
libs/lib_crop/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="Crop"></style>
|
||||
|
||||
<style name="Crop.DoneCancelBar">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">@dimen/crop__bar_height</item>
|
||||
<item name="android:orientation">horizontal</item>
|
||||
<item name="android:divider">@drawable/crop__divider</item>
|
||||
<item name="android:showDividers" tools:ignore="NewApi">middle</item>
|
||||
<item name="android:dividerPadding" tools:ignore="NewApi">12dp</item>
|
||||
<item name="android:background">@color/crop__button_bar</item>
|
||||
</style>
|
||||
|
||||
<style name="Crop.ActionButton">
|
||||
<item name="android:layout_width">0dp</item>
|
||||
<item name="android:layout_height">match_parent</item>
|
||||
<item name="android:layout_weight">1</item>
|
||||
<item name="android:background">@drawable/crop__selectable_background</item>
|
||||
</style>
|
||||
|
||||
<style name="Crop.ActionButtonText">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_gravity">center</item>
|
||||
<item name="android:gravity">center_vertical</item>
|
||||
<item name="android:paddingRight">20dp</item> <!-- Offsets left drawable -->
|
||||
<item name="android:drawablePadding">8dp</item>
|
||||
<item name="android:textColor">@color/crop__button_text</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:textSize">13sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Crop.ActionButtonText.Done">
|
||||
<item name="android:drawableLeft">@drawable/crop__ic_done</item>
|
||||
<item name="android:text">@string/crop__done</item>
|
||||
</style>
|
||||
|
||||
<style name="Crop.ActionButtonText.Cancel">
|
||||
<item name="android:drawableLeft">@drawable/crop__ic_cancel</item>
|
||||
<item name="android:text">@string/crop__cancel</item>
|
||||
</style>
|
||||
|
||||
</resources>
|