switch lib
|
@ -8,11 +8,11 @@ plugins {
|
||||||
}
|
}
|
||||||
def flavor
|
def flavor
|
||||||
android {
|
android {
|
||||||
compileSdk 32
|
compileSdk 33
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 32
|
targetSdk 33
|
||||||
versionCode 451
|
versionCode 451
|
||||||
versionName "3.12.1"
|
versionName "3.12.1"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
@ -106,7 +106,7 @@ dependencies {
|
||||||
|
|
||||||
|
|
||||||
implementation 'com.burhanrashid52:photoeditor:1.5.1'
|
implementation 'com.burhanrashid52:photoeditor:1.5.1'
|
||||||
implementation project(path: ':cropper')
|
implementation("com.vanniktech:android-image-cropper:4.3.3")
|
||||||
annotationProcessor "com.github.bumptech.glide:compiler:4.12.0"
|
annotationProcessor "com.github.bumptech.glide:compiler:4.12.0"
|
||||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||||
implementation 'com.github.penfeizhou.android.animation:glide-plugin:2.24.0'
|
implementation 'com.github.penfeizhou.android.animation:glide-plugin:2.24.0'
|
||||||
|
|
|
@ -213,7 +213,7 @@
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
android:label="@string/scheduled" />
|
android:label="@string/scheduled" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
android:name="com.canhub.cropper.CropImageActivity"
|
||||||
android:theme="@style/Base.Theme.AppCompat" />
|
android:theme="@style/Base.Theme.AppCompat" />
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -126,6 +126,7 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana
|
||||||
if (focusX != -2) {
|
if (focusX != -2) {
|
||||||
attachment.focus = focusX + "," + focusY;
|
attachment.focus = focusX + "," + focusY;
|
||||||
}
|
}
|
||||||
|
|
||||||
composeAdapter.notifyItemChanged(position);
|
composeAdapter.notifyItemChanged(position);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -251,7 +251,7 @@ public interface MastodonStatusesService {
|
||||||
@Part MultipartBody.Part file,
|
@Part MultipartBody.Part file,
|
||||||
@Part MultipartBody.Part thumbnail,
|
@Part MultipartBody.Part thumbnail,
|
||||||
@Part("description") RequestBody description,
|
@Part("description") RequestBody description,
|
||||||
@Part("focus") String focus
|
@Part("focus") RequestBody focus
|
||||||
);
|
);
|
||||||
|
|
||||||
//Edit a Media
|
//Edit a Media
|
||||||
|
|
|
@ -1243,6 +1243,20 @@ public class Helper {
|
||||||
counter++;
|
counter++;
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
attachment.filename = formatter.format(now) + "." + extension;
|
attachment.filename = formatter.format(now) + "." + extension;
|
||||||
|
if (attachment.mimeType.startsWith("image")) {
|
||||||
|
try {
|
||||||
|
final File certCacheDir = new File(context.getCacheDir(), TEMP_MEDIA_DIRECTORY);
|
||||||
|
boolean isCertCacheDirExists = certCacheDir.exists();
|
||||||
|
if (!isCertCacheDirExists) {
|
||||||
|
certCacheDir.mkdirs();
|
||||||
|
}
|
||||||
|
String filePath = certCacheDir.getAbsolutePath() + "/" + attachment.filename;
|
||||||
|
MediaHelper.ResizedImageRequestBody(context, uri, filePath);
|
||||||
|
attachment.local_path = filePath;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
InputStream selectedFileInputStream;
|
InputStream selectedFileInputStream;
|
||||||
try {
|
try {
|
||||||
selectedFileInputStream = context.getContentResolver().openInputStream(uri);
|
selectedFileInputStream = context.getContentResolver().openInputStream(uri);
|
||||||
|
@ -1269,6 +1283,8 @@ public class Helper {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
Runnable myRunnable = () -> callBack.onAttachmentCopied(attachment);
|
Runnable myRunnable = () -> callBack.onAttachmentCopied(attachment);
|
||||||
mainHandler.post(myRunnable);
|
mainHandler.post(myRunnable);
|
||||||
|
|
|
@ -24,14 +24,23 @@ import android.app.DownloadManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.ImageDecoder;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.media.ExifInterface;
|
||||||
import android.media.MediaRecorder;
|
import android.media.MediaRecorder;
|
||||||
import android.media.MediaScannerConnection;
|
import android.media.MediaScannerConnection;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.text.format.DateFormat;
|
import android.text.format.DateFormat;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
@ -55,6 +64,7 @@ import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
@ -69,10 +79,12 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import app.fedilab.android.BuildConfig;
|
import app.fedilab.android.BuildConfig;
|
||||||
import app.fedilab.android.R;
|
import app.fedilab.android.R;
|
||||||
import app.fedilab.android.activities.ComposeActivity;
|
import app.fedilab.android.activities.ComposeActivity;
|
||||||
|
import app.fedilab.android.activities.MainActivity;
|
||||||
import app.fedilab.android.client.entities.api.Attachment;
|
import app.fedilab.android.client.entities.api.Attachment;
|
||||||
import app.fedilab.android.databinding.DatetimePickerBinding;
|
import app.fedilab.android.databinding.DatetimePickerBinding;
|
||||||
import app.fedilab.android.databinding.PopupRecordBinding;
|
import app.fedilab.android.databinding.PopupRecordBinding;
|
||||||
import es.dmoral.toasty.Toasty;
|
import es.dmoral.toasty.Toasty;
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
|
||||||
public class MediaHelper {
|
public class MediaHelper {
|
||||||
|
|
||||||
|
@ -420,4 +432,146 @@ public class MediaHelper {
|
||||||
public interface OnSchedule {
|
public interface OnSchedule {
|
||||||
void scheduledAt(String scheduledDate);
|
void scheduledAt(String scheduledDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void ResizedImageRequestBody(Context context, Uri uri, String fullpatch) throws IOException {
|
||||||
|
|
||||||
|
BitmapFactory.Options opts = new BitmapFactory.Options();
|
||||||
|
opts.inJustDecodeBounds = true;
|
||||||
|
String contentType;
|
||||||
|
if ("file".equals(uri.getScheme())) {
|
||||||
|
BitmapFactory.decodeFile(uri.getPath(), opts);
|
||||||
|
contentType = MediaHelper.getFileMediaType(new File(uri.getPath())).type();
|
||||||
|
} else {
|
||||||
|
try (InputStream in = context.getContentResolver().openInputStream(uri)) {
|
||||||
|
BitmapFactory.decodeStream(in, null, opts);
|
||||||
|
}
|
||||||
|
contentType = context.getContentResolver().getType(uri);
|
||||||
|
}
|
||||||
|
if (TextUtils.isEmpty(contentType))
|
||||||
|
contentType = "image/jpeg";
|
||||||
|
Bitmap bitmap;
|
||||||
|
if (Build.VERSION.SDK_INT >= 28) {
|
||||||
|
ImageDecoder.Source source;
|
||||||
|
if ("file".equals(uri.getScheme())) {
|
||||||
|
source = ImageDecoder.createSource(new File(uri.getPath()));
|
||||||
|
} else {
|
||||||
|
source = ImageDecoder.createSource(context.getContentResolver(), uri);
|
||||||
|
}
|
||||||
|
BitmapFactory.Options finalOpts = opts;
|
||||||
|
bitmap = ImageDecoder.decodeBitmap(source, (decoder, info, _source) -> {
|
||||||
|
int[] size = getTargetSize(info.getSize().getWidth(), info.getSize().getHeight());
|
||||||
|
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
|
||||||
|
if (needResize(finalOpts.outWidth, finalOpts.outHeight)) {
|
||||||
|
decoder.setTargetSize(size[0], size[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
int[] size = getTargetSize(opts.outWidth, opts.outHeight);
|
||||||
|
int targetWidth;
|
||||||
|
int targetHeight;
|
||||||
|
if (needResize(opts.outWidth, opts.outHeight)) {
|
||||||
|
targetWidth = size[0];
|
||||||
|
targetHeight = size[1];
|
||||||
|
} else {
|
||||||
|
targetWidth = opts.outWidth;
|
||||||
|
targetHeight = opts.outHeight;
|
||||||
|
}
|
||||||
|
float factor = opts.outWidth / (float) targetWidth;
|
||||||
|
opts = new BitmapFactory.Options();
|
||||||
|
opts.inSampleSize = (int) factor;
|
||||||
|
int orientation = 0;
|
||||||
|
String[] projection = {MediaStore.Images.ImageColumns.ORIENTATION};
|
||||||
|
try {
|
||||||
|
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
int photoRotation = cursor.getInt(0);
|
||||||
|
}
|
||||||
|
cursor.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("file".equals(uri.getScheme())) {
|
||||||
|
ExifInterface exif = new ExifInterface(uri.getPath());
|
||||||
|
orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
try (InputStream in = context.getContentResolver().openInputStream(uri)) {
|
||||||
|
ExifInterface exif = new ExifInterface(in);
|
||||||
|
orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("file".equals(uri.getScheme())) {
|
||||||
|
bitmap = BitmapFactory.decodeFile(uri.getPath(), opts);
|
||||||
|
} else {
|
||||||
|
try (InputStream in = context.getContentResolver().openInputStream(uri)) {
|
||||||
|
bitmap = BitmapFactory.decodeStream(in, null, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (factor % 1f != 0f) {
|
||||||
|
Rect srcBounds = null;
|
||||||
|
Rect dstBounds;
|
||||||
|
dstBounds = new Rect(0, 0, targetWidth, targetHeight);
|
||||||
|
Bitmap scaled = Bitmap.createBitmap(dstBounds.width(), dstBounds.height(), Bitmap.Config.ARGB_8888);
|
||||||
|
new Canvas(scaled).drawBitmap(bitmap, srcBounds, dstBounds, new Paint(Paint.FILTER_BITMAP_FLAG));
|
||||||
|
bitmap = scaled;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int rotation = 0;
|
||||||
|
switch (orientation) {
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||||
|
rotation = 90;
|
||||||
|
break;
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||||
|
rotation = 180;
|
||||||
|
break;
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||||
|
rotation = 270;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (rotation != 0) {
|
||||||
|
Matrix matrix = new Matrix();
|
||||||
|
matrix.setRotate(rotation);
|
||||||
|
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isPNG = "image/png".equals(contentType);
|
||||||
|
File tempFile = new File(fullpatch);
|
||||||
|
try (FileOutputStream out = new FileOutputStream(tempFile)) {
|
||||||
|
if (isPNG) {
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
|
||||||
|
} else {
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static int[] getTargetSize(int srcWidth, int srcHeight) {
|
||||||
|
int maxSize = 1;
|
||||||
|
if (MainActivity.instanceInfo != null && MainActivity.instanceInfo.configuration != null && MainActivity.instanceInfo.configuration.media_attachments != null) {
|
||||||
|
maxSize = MainActivity.instanceInfo.configuration.media_attachments.image_size_limit;
|
||||||
|
}
|
||||||
|
int targetWidth = Math.round((float) Math.sqrt((float) maxSize * ((float) srcWidth / srcHeight)));
|
||||||
|
int targetHeight = Math.round((float) Math.sqrt((float) maxSize * ((float) srcHeight / srcWidth)));
|
||||||
|
return new int[]{targetWidth, targetHeight};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean needResize(int srcWidth, int srcHeight) {
|
||||||
|
int maxSize;
|
||||||
|
if (MainActivity.instanceInfo != null && MainActivity.instanceInfo.configuration != null && MainActivity.instanceInfo.configuration.media_attachments != null) {
|
||||||
|
maxSize = MainActivity.instanceInfo.configuration.media_attachments.image_size_limit;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return srcWidth * srcHeight > maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static MediaType getFileMediaType(File file) {
|
||||||
|
String name = file.getName();
|
||||||
|
return MediaType.parse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.substring(name.lastIndexOf('.') + 1)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,12 @@ import android.graphics.Typeface;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.animation.AnticipateOvershootInterpolator;
|
import android.view.animation.AnticipateOvershootInterpolator;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.constraintlayout.widget.ConstraintSet;
|
import androidx.constraintlayout.widget.ConstraintSet;
|
||||||
|
@ -25,8 +27,12 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.transition.ChangeBounds;
|
import androidx.transition.ChangeBounds;
|
||||||
import androidx.transition.TransitionManager;
|
import androidx.transition.TransitionManager;
|
||||||
|
|
||||||
|
import com.canhub.cropper.CropImage;
|
||||||
|
import com.canhub.cropper.CropImageContract;
|
||||||
|
import com.canhub.cropper.CropImageContractOptions;
|
||||||
|
import com.canhub.cropper.CropImageOptions;
|
||||||
|
import com.canhub.cropper.CropImageView;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
import com.theartofdev.edmodo.cropper.CropImage;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -73,6 +79,8 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
|
||||||
private Uri uri;
|
private Uri uri;
|
||||||
private boolean exit;
|
private boolean exit;
|
||||||
private ActivityEditImageBinding binding;
|
private ActivityEditImageBinding binding;
|
||||||
|
CropImageContractOptions cropImageContractOptions;
|
||||||
|
ActivityResultLauncher<CropImageContractOptions> cropImageContractOptionsActivityResultLauncher;
|
||||||
|
|
||||||
private static int exifToDegrees(int exifOrientation) {
|
private static int exifToDegrees(int exifOrientation) {
|
||||||
if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) {
|
if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) {
|
||||||
|
@ -146,6 +154,35 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cropImageContractOptions = new CropImageContractOptions(uri, new CropImageOptions())
|
||||||
|
.setGuidelines(CropImageView.Guidelines.ON)
|
||||||
|
.setCropShape(CropImageView.CropShape.RECTANGLE)
|
||||||
|
.setAllowRotation(true)
|
||||||
|
.setAllowFlipping(true)
|
||||||
|
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||||
|
.setAllowCounterRotation(true)
|
||||||
|
.setImageSource(true, false)
|
||||||
|
.setScaleType(CropImageView.ScaleType.CENTER);
|
||||||
|
cropImageContractOptionsActivityResultLauncher = registerForActivityResult(
|
||||||
|
new CropImageContract(),
|
||||||
|
result -> {
|
||||||
|
if (result.isSuccessful()) {
|
||||||
|
Uri resultUri = result.getUriContent();
|
||||||
|
if (resultUri != null) {
|
||||||
|
binding.photoEditorView.getSource().setImageURI(resultUri);
|
||||||
|
if (uri != null && uri.getPath() != null) {
|
||||||
|
File fdelete = new File(uri.getPath());
|
||||||
|
if (fdelete.exists()) {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
fdelete.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uri = resultUri;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(Helper.TAG, "onActivityResult...Error CropImage: " + result.getError());
|
||||||
|
}
|
||||||
|
});
|
||||||
mPhotoEditor.setFilterEffect(PhotoFilter.NONE);
|
mPhotoEditor.setFilterEffect(PhotoFilter.NONE);
|
||||||
binding.send.setOnClickListener(v -> {
|
binding.send.setOnClickListener(v -> {
|
||||||
exit = true;
|
exit = true;
|
||||||
|
@ -286,6 +323,7 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
|
||||||
}
|
}
|
||||||
intentImage.putExtra("focusX", focusX);
|
intentImage.putExtra("focusX", focusX);
|
||||||
intentImage.putExtra("focusY", focusY);
|
intentImage.putExtra("focusY", focusY);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalBroadcastManager.getInstance(EditImageActivity.this).sendBroadcast(intentImage);
|
LocalBroadcastManager.getInstance(EditImageActivity.this).sendBroadcast(intentImage);
|
||||||
|
@ -351,13 +389,23 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE:
|
case CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE:
|
||||||
|
if (data != null && data.getData() != null) {
|
||||||
CropImage.ActivityResult result = CropImage.getActivityResult(data);
|
CropImageContractOptions cropImageContractOptions = new CropImageContractOptions(data.getData(), new CropImageOptions())
|
||||||
if (result != null) {
|
.setGuidelines(CropImageView.Guidelines.ON)
|
||||||
Uri resultUri = result.getUri();
|
.setCropShape(CropImageView.CropShape.RECTANGLE)
|
||||||
|
.setAllowRotation(true)
|
||||||
|
.setAllowFlipping(true)
|
||||||
|
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||||
|
.setAllowCounterRotation(true)
|
||||||
|
.setImageSource(true, false)
|
||||||
|
.setScaleType(CropImageView.ScaleType.CENTER);
|
||||||
|
ActivityResultLauncher<CropImageContractOptions> cropImageContractOptionsActivityResultLauncher = registerForActivityResult(
|
||||||
|
new CropImageContract(),
|
||||||
|
result -> {
|
||||||
|
if (result.isSuccessful()) {
|
||||||
|
Uri resultUri = result.getUriContent();
|
||||||
if (resultUri != null) {
|
if (resultUri != null) {
|
||||||
binding.photoEditorView.getSource().setImageURI(resultUri);
|
binding.photoEditorView.getSource().setImageURI(resultUri);
|
||||||
binding.photoEditorView.getSource().setRotation(rotationInDegrees);
|
|
||||||
if (uri != null && uri.getPath() != null) {
|
if (uri != null && uri.getPath() != null) {
|
||||||
File fdelete = new File(uri.getPath());
|
File fdelete = new File(uri.getPath());
|
||||||
if (fdelete.exists()) {
|
if (fdelete.exists()) {
|
||||||
|
@ -367,6 +415,11 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
|
||||||
}
|
}
|
||||||
uri = resultUri;
|
uri = resultUri;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(Helper.TAG, "onActivityResult...Error CropImage: " + result.getError());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cropImageContractOptionsActivityResultLauncher.launch(cropImageContractOptions);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -454,8 +507,8 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
|
||||||
mPropertiesBSFragment.show(getSupportFragmentManager(), mPropertiesBSFragment.getTag());
|
mPropertiesBSFragment.show(getSupportFragmentManager(), mPropertiesBSFragment.getTag());
|
||||||
break;
|
break;
|
||||||
case CROP:
|
case CROP:
|
||||||
CropImage.activity(uri)
|
|
||||||
.start(this);
|
cropImageContractOptionsActivityResultLauncher.launch(cropImageContractOptions);
|
||||||
break;
|
break;
|
||||||
case FOCUS:
|
case FOCUS:
|
||||||
binding.focusCircle.setVisibility(View.VISIBLE);
|
binding.focusCircle.setVisibility(View.VISIBLE);
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package app.fedilab.android.interfaces;
|
||||||
|
|
||||||
|
public interface ProgressListener {
|
||||||
|
void onProgress(long transferred, long total);
|
||||||
|
}
|
|
@ -341,10 +341,14 @@ public class ComposeWorker extends Worker {
|
||||||
private static String postAttachment(MastodonStatusesService mastodonStatusesService, DataPost dataPost, MultipartBody.Part fileMultipartBody, Attachment attachment) {
|
private static String postAttachment(MastodonStatusesService mastodonStatusesService, DataPost dataPost, MultipartBody.Part fileMultipartBody, Attachment attachment) {
|
||||||
|
|
||||||
RequestBody descriptionBody = null;
|
RequestBody descriptionBody = null;
|
||||||
|
RequestBody focusBody = null;
|
||||||
if (attachment.description != null && attachment.description.trim().length() > 0) {
|
if (attachment.description != null && attachment.description.trim().length() > 0) {
|
||||||
descriptionBody = RequestBody.create(MediaType.parse("text/plain"), attachment.description);
|
descriptionBody = RequestBody.create(MediaType.parse("text/plain"), attachment.description);
|
||||||
}
|
}
|
||||||
Call<Attachment> attachmentCall = mastodonStatusesService.postMedia(dataPost.token, fileMultipartBody, null, descriptionBody, attachment.focus);
|
if (attachment.focus != null && attachment.focus.trim().length() > 0) {
|
||||||
|
focusBody = RequestBody.create(MediaType.parse("text/plain"), attachment.focus);
|
||||||
|
}
|
||||||
|
Call<Attachment> attachmentCall = mastodonStatusesService.postMedia(dataPost.token, fileMultipartBody, null, descriptionBody, focusBody);
|
||||||
|
|
||||||
if (attachmentCall != null) {
|
if (attachmentCall != null) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -127,7 +127,11 @@ public class StatusesVM extends AndroidViewModel {
|
||||||
if (description != null && description.trim().length() > 0) {
|
if (description != null && description.trim().length() > 0) {
|
||||||
descriptionBody = RequestBody.create(MediaType.parse("text/plain"), description);
|
descriptionBody = RequestBody.create(MediaType.parse("text/plain"), description);
|
||||||
}
|
}
|
||||||
Call<Attachment> attachmentCall = mastodonStatusesService.postMedia(token, fileMultipartBody, thumbnailMultipartBody, descriptionBody, focus);
|
RequestBody focusBody = null;
|
||||||
|
if (focus != null && focus.trim().length() > 0) {
|
||||||
|
focusBody = RequestBody.create(MediaType.parse("text/plain"), focus);
|
||||||
|
}
|
||||||
|
Call<Attachment> attachmentCall = mastodonStatusesService.postMedia(token, fileMultipartBody, thumbnailMultipartBody, descriptionBody, focusBody);
|
||||||
Attachment attachment = null;
|
Attachment attachment = null;
|
||||||
if (attachmentCall != null) {
|
if (attachmentCall != null) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
apply plugin: 'com.android.library'
|
|
||||||
|
|
||||||
android {
|
|
||||||
|
|
||||||
compileSdk 31
|
|
||||||
defaultConfig {
|
|
||||||
minSdkVersion 14
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
lintOptions {
|
|
||||||
abortOnError false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api 'androidx.appcompat:appcompat:1.4.2'
|
|
||||||
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<manifest package="com.theartofdev.edmodo.cropper">
|
|
||||||
|
|
||||||
</manifest>
|
|
|
@ -1,354 +0,0 @@
|
||||||
// "Therefore those skilled at the unorthodox
|
|
||||||
// are infinite as heaven and earth,
|
|
||||||
// inexhaustible as the great rivers.
|
|
||||||
// When they come to an end,
|
|
||||||
// they begin again,
|
|
||||||
// like the days and months;
|
|
||||||
// they die and are reborn,
|
|
||||||
// like the four seasons."
|
|
||||||
//
|
|
||||||
// - Sun Tsu,
|
|
||||||
// "The Art of War"
|
|
||||||
|
|
||||||
package com.theartofdev.edmodo.cropper;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task to crop bitmap asynchronously from the UI thread.
|
|
||||||
*/
|
|
||||||
final class BitmapCroppingWorkerTask
|
|
||||||
extends AsyncTask<Void, Void, BitmapCroppingWorkerTask.Result> {
|
|
||||||
|
|
||||||
// region: Fields and Consts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use a WeakReference to ensure the ImageView can be garbage collected
|
|
||||||
*/
|
|
||||||
private final WeakReference<CropImageView> mCropImageViewReference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the bitmap to crop
|
|
||||||
*/
|
|
||||||
private final Bitmap mBitmap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Android URI of the image to load
|
|
||||||
*/
|
|
||||||
private final Uri mUri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The context of the crop image view widget used for loading of bitmap by Android URI
|
|
||||||
*/
|
|
||||||
private final Context mContext;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Required cropping 4 points (x0,y0,x1,y1,x2,y2,x3,y3)
|
|
||||||
*/
|
|
||||||
private final float[] mCropPoints;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Degrees the image was rotated after loading
|
|
||||||
*/
|
|
||||||
private final int mDegreesRotated;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the original width of the image to be cropped (for image loaded from URI)
|
|
||||||
*/
|
|
||||||
private final int mOrgWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the original height of the image to be cropped (for image loaded from URI)
|
|
||||||
*/
|
|
||||||
private final int mOrgHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* is there is fixed aspect ratio for the crop rectangle
|
|
||||||
*/
|
|
||||||
private final boolean mFixAspectRatio;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the X aspect ration of the crop rectangle
|
|
||||||
*/
|
|
||||||
private final int mAspectRatioX;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the Y aspect ration of the crop rectangle
|
|
||||||
*/
|
|
||||||
private final int mAspectRatioY;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* required width of the cropping image
|
|
||||||
*/
|
|
||||||
private final int mReqWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* required height of the cropping image
|
|
||||||
*/
|
|
||||||
private final int mReqHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* is the image flipped horizontally
|
|
||||||
*/
|
|
||||||
private final boolean mFlipHorizontally;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* is the image flipped vertically
|
|
||||||
*/
|
|
||||||
private final boolean mFlipVertically;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The option to handle requested width/height
|
|
||||||
*/
|
|
||||||
private final CropImageView.RequestSizeOptions mReqSizeOptions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the Android Uri to save the cropped image to
|
|
||||||
*/
|
|
||||||
private final Uri mSaveUri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the compression format to use when writing the image
|
|
||||||
*/
|
|
||||||
private final Bitmap.CompressFormat mSaveCompressFormat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the quality (if applicable) to use when writing the image (0 - 100)
|
|
||||||
*/
|
|
||||||
private final int mSaveCompressQuality;
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
BitmapCroppingWorkerTask(
|
|
||||||
CropImageView cropImageView,
|
|
||||||
Bitmap bitmap,
|
|
||||||
float[] cropPoints,
|
|
||||||
int degreesRotated,
|
|
||||||
boolean fixAspectRatio,
|
|
||||||
int aspectRatioX,
|
|
||||||
int aspectRatioY,
|
|
||||||
int reqWidth,
|
|
||||||
int reqHeight,
|
|
||||||
boolean flipHorizontally,
|
|
||||||
boolean flipVertically,
|
|
||||||
CropImageView.RequestSizeOptions options,
|
|
||||||
Uri saveUri,
|
|
||||||
Bitmap.CompressFormat saveCompressFormat,
|
|
||||||
int saveCompressQuality) {
|
|
||||||
|
|
||||||
mCropImageViewReference = new WeakReference<>(cropImageView);
|
|
||||||
mContext = cropImageView.getContext();
|
|
||||||
mBitmap = bitmap;
|
|
||||||
mCropPoints = cropPoints;
|
|
||||||
mUri = null;
|
|
||||||
mDegreesRotated = degreesRotated;
|
|
||||||
mFixAspectRatio = fixAspectRatio;
|
|
||||||
mAspectRatioX = aspectRatioX;
|
|
||||||
mAspectRatioY = aspectRatioY;
|
|
||||||
mReqWidth = reqWidth;
|
|
||||||
mReqHeight = reqHeight;
|
|
||||||
mFlipHorizontally = flipHorizontally;
|
|
||||||
mFlipVertically = flipVertically;
|
|
||||||
mReqSizeOptions = options;
|
|
||||||
mSaveUri = saveUri;
|
|
||||||
mSaveCompressFormat = saveCompressFormat;
|
|
||||||
mSaveCompressQuality = saveCompressQuality;
|
|
||||||
mOrgWidth = 0;
|
|
||||||
mOrgHeight = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
BitmapCroppingWorkerTask(
|
|
||||||
CropImageView cropImageView,
|
|
||||||
Uri uri,
|
|
||||||
float[] cropPoints,
|
|
||||||
int degreesRotated,
|
|
||||||
int orgWidth,
|
|
||||||
int orgHeight,
|
|
||||||
boolean fixAspectRatio,
|
|
||||||
int aspectRatioX,
|
|
||||||
int aspectRatioY,
|
|
||||||
int reqWidth,
|
|
||||||
int reqHeight,
|
|
||||||
boolean flipHorizontally,
|
|
||||||
boolean flipVertically,
|
|
||||||
CropImageView.RequestSizeOptions options,
|
|
||||||
Uri saveUri,
|
|
||||||
Bitmap.CompressFormat saveCompressFormat,
|
|
||||||
int saveCompressQuality) {
|
|
||||||
|
|
||||||
mCropImageViewReference = new WeakReference<>(cropImageView);
|
|
||||||
mContext = cropImageView.getContext();
|
|
||||||
mUri = uri;
|
|
||||||
mCropPoints = cropPoints;
|
|
||||||
mDegreesRotated = degreesRotated;
|
|
||||||
mFixAspectRatio = fixAspectRatio;
|
|
||||||
mAspectRatioX = aspectRatioX;
|
|
||||||
mAspectRatioY = aspectRatioY;
|
|
||||||
mOrgWidth = orgWidth;
|
|
||||||
mOrgHeight = orgHeight;
|
|
||||||
mReqWidth = reqWidth;
|
|
||||||
mReqHeight = reqHeight;
|
|
||||||
mFlipHorizontally = flipHorizontally;
|
|
||||||
mFlipVertically = flipVertically;
|
|
||||||
mReqSizeOptions = options;
|
|
||||||
mSaveUri = saveUri;
|
|
||||||
mSaveCompressFormat = saveCompressFormat;
|
|
||||||
mSaveCompressQuality = saveCompressQuality;
|
|
||||||
mBitmap = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Android URI that this task is currently loading.
|
|
||||||
*/
|
|
||||||
public Uri getUri() {
|
|
||||||
return mUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Crop image in background.
|
|
||||||
*
|
|
||||||
* @param params ignored
|
|
||||||
* @return the decoded bitmap data
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected BitmapCroppingWorkerTask.Result doInBackground(Void... params) {
|
|
||||||
try {
|
|
||||||
if (!isCancelled()) {
|
|
||||||
|
|
||||||
BitmapUtils.BitmapSampled bitmapSampled;
|
|
||||||
if (mUri != null) {
|
|
||||||
bitmapSampled =
|
|
||||||
BitmapUtils.cropBitmap(
|
|
||||||
mContext,
|
|
||||||
mUri,
|
|
||||||
mCropPoints,
|
|
||||||
mDegreesRotated,
|
|
||||||
mOrgWidth,
|
|
||||||
mOrgHeight,
|
|
||||||
mFixAspectRatio,
|
|
||||||
mAspectRatioX,
|
|
||||||
mAspectRatioY,
|
|
||||||
mReqWidth,
|
|
||||||
mReqHeight,
|
|
||||||
mFlipHorizontally,
|
|
||||||
mFlipVertically);
|
|
||||||
} else if (mBitmap != null) {
|
|
||||||
bitmapSampled =
|
|
||||||
BitmapUtils.cropBitmapObjectHandleOOM(
|
|
||||||
mBitmap,
|
|
||||||
mCropPoints,
|
|
||||||
mDegreesRotated,
|
|
||||||
mFixAspectRatio,
|
|
||||||
mAspectRatioX,
|
|
||||||
mAspectRatioY,
|
|
||||||
mFlipHorizontally,
|
|
||||||
mFlipVertically);
|
|
||||||
} else {
|
|
||||||
return new Result((Bitmap) null, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Bitmap bitmap =
|
|
||||||
BitmapUtils.resizeBitmap(bitmapSampled.bitmap, mReqWidth, mReqHeight, mReqSizeOptions);
|
|
||||||
|
|
||||||
if (mSaveUri == null) {
|
|
||||||
return new Result(bitmap, bitmapSampled.sampleSize);
|
|
||||||
} else {
|
|
||||||
BitmapUtils.writeBitmapToUri(
|
|
||||||
mContext, bitmap, mSaveUri, mSaveCompressFormat, mSaveCompressQuality);
|
|
||||||
if (bitmap != null) {
|
|
||||||
bitmap.recycle();
|
|
||||||
}
|
|
||||||
return new Result(mSaveUri, bitmapSampled.sampleSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return new Result(e, mSaveUri != null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Once complete, see if ImageView is still around and set bitmap.
|
|
||||||
*
|
|
||||||
* @param result the result of bitmap cropping
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Result result) {
|
|
||||||
if (result != null) {
|
|
||||||
boolean completeCalled = false;
|
|
||||||
if (!isCancelled()) {
|
|
||||||
CropImageView cropImageView = mCropImageViewReference.get();
|
|
||||||
if (cropImageView != null) {
|
|
||||||
completeCalled = true;
|
|
||||||
cropImageView.onImageCroppingAsyncComplete(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!completeCalled && result.bitmap != null) {
|
|
||||||
// fast release of unused bitmap
|
|
||||||
result.bitmap.recycle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// region: Inner class: Result
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The result of BitmapCroppingWorkerTask async loading.
|
|
||||||
*/
|
|
||||||
static final class Result {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The cropped bitmap
|
|
||||||
*/
|
|
||||||
public final Bitmap bitmap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The saved cropped bitmap uri
|
|
||||||
*/
|
|
||||||
public final Uri uri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The error that occurred during async bitmap cropping.
|
|
||||||
*/
|
|
||||||
final Exception error;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* is the cropping request was to get a bitmap or to save it to uri
|
|
||||||
*/
|
|
||||||
final boolean isSave;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* sample size used creating the crop bitmap to lower its size
|
|
||||||
*/
|
|
||||||
final int sampleSize;
|
|
||||||
|
|
||||||
Result(Bitmap bitmap, int sampleSize) {
|
|
||||||
this.bitmap = bitmap;
|
|
||||||
this.uri = null;
|
|
||||||
this.error = null;
|
|
||||||
this.isSave = false;
|
|
||||||
this.sampleSize = sampleSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result(Uri uri, int sampleSize) {
|
|
||||||
this.bitmap = null;
|
|
||||||
this.uri = uri;
|
|
||||||
this.error = null;
|
|
||||||
this.isSave = true;
|
|
||||||
this.sampleSize = sampleSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result(Exception error, boolean isSave) {
|
|
||||||
this.bitmap = null;
|
|
||||||
this.uri = null;
|
|
||||||
this.error = error;
|
|
||||||
this.isSave = isSave;
|
|
||||||
this.sampleSize = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
|
@ -1,176 +0,0 @@
|
||||||
// "Therefore those skilled at the unorthodox
|
|
||||||
// are infinite as heaven and earth,
|
|
||||||
// inexhaustible as the great rivers.
|
|
||||||
// When they come to an end,
|
|
||||||
// they begin again,
|
|
||||||
// like the days and months;
|
|
||||||
// they die and are reborn,
|
|
||||||
// like the four seasons."
|
|
||||||
//
|
|
||||||
// - Sun Tsu,
|
|
||||||
// "The Art of War"
|
|
||||||
|
|
||||||
package com.theartofdev.edmodo.cropper;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task to load bitmap asynchronously from the UI thread.
|
|
||||||
*/
|
|
||||||
final class BitmapLoadingWorkerTask extends AsyncTask<Void, Void, BitmapLoadingWorkerTask.Result> {
|
|
||||||
|
|
||||||
// region: Fields and Consts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use a WeakReference to ensure the ImageView can be garbage collected
|
|
||||||
*/
|
|
||||||
private final WeakReference<CropImageView> mCropImageViewReference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Android URI of the image to load
|
|
||||||
*/
|
|
||||||
private final Uri mUri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The context of the crop image view widget used for loading of bitmap by Android URI
|
|
||||||
*/
|
|
||||||
private final Context mContext;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* required width of the cropping image after density adjustment
|
|
||||||
*/
|
|
||||||
private final int mWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* required height of the cropping image after density adjustment
|
|
||||||
*/
|
|
||||||
private final int mHeight;
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
public BitmapLoadingWorkerTask(CropImageView cropImageView, Uri uri) {
|
|
||||||
mUri = uri;
|
|
||||||
mCropImageViewReference = new WeakReference<>(cropImageView);
|
|
||||||
|
|
||||||
mContext = cropImageView.getContext();
|
|
||||||
|
|
||||||
DisplayMetrics metrics = cropImageView.getResources().getDisplayMetrics();
|
|
||||||
double densityAdj = metrics.density > 1 ? 1 / metrics.density : 1;
|
|
||||||
mWidth = (int) (metrics.widthPixels * densityAdj);
|
|
||||||
mHeight = (int) (metrics.heightPixels * densityAdj);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Android URI that this task is currently loading.
|
|
||||||
*/
|
|
||||||
public Uri getUri() {
|
|
||||||
return mUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode image in background.
|
|
||||||
*
|
|
||||||
* @param params ignored
|
|
||||||
* @return the decoded bitmap data
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected Result doInBackground(Void... params) {
|
|
||||||
try {
|
|
||||||
if (!isCancelled()) {
|
|
||||||
|
|
||||||
BitmapUtils.BitmapSampled decodeResult =
|
|
||||||
BitmapUtils.decodeSampledBitmap(mContext, mUri, mWidth, mHeight);
|
|
||||||
|
|
||||||
if (!isCancelled()) {
|
|
||||||
|
|
||||||
BitmapUtils.RotateBitmapResult rotateResult =
|
|
||||||
BitmapUtils.rotateBitmapByExif(decodeResult.bitmap, mContext, mUri);
|
|
||||||
|
|
||||||
return new Result(
|
|
||||||
mUri, rotateResult.bitmap, decodeResult.sampleSize, rotateResult.degrees);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return new Result(mUri, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Once complete, see if ImageView is still around and set bitmap.
|
|
||||||
*
|
|
||||||
* @param result the result of bitmap loading
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Result result) {
|
|
||||||
if (result != null) {
|
|
||||||
boolean completeCalled = false;
|
|
||||||
if (!isCancelled()) {
|
|
||||||
CropImageView cropImageView = mCropImageViewReference.get();
|
|
||||||
if (cropImageView != null) {
|
|
||||||
completeCalled = true;
|
|
||||||
cropImageView.onSetImageUriAsyncComplete(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!completeCalled && result.bitmap != null) {
|
|
||||||
// fast release of unused bitmap
|
|
||||||
result.bitmap.recycle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// region: Inner class: Result
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The result of BitmapLoadingWorkerTask async loading.
|
|
||||||
*/
|
|
||||||
public static final class Result {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Android URI of the image to load
|
|
||||||
*/
|
|
||||||
public final Uri uri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The loaded bitmap
|
|
||||||
*/
|
|
||||||
public final Bitmap bitmap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The sample size used to load the given bitmap
|
|
||||||
*/
|
|
||||||
public final int loadSampleSize;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The degrees the image was rotated
|
|
||||||
*/
|
|
||||||
public final int degreesRotated;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The error that occurred during async bitmap loading.
|
|
||||||
*/
|
|
||||||
public final Exception error;
|
|
||||||
|
|
||||||
Result(Uri uri, Bitmap bitmap, int loadSampleSize, int degreesRotated) {
|
|
||||||
this.uri = uri;
|
|
||||||
this.bitmap = bitmap;
|
|
||||||
this.loadSampleSize = loadSampleSize;
|
|
||||||
this.degreesRotated = degreesRotated;
|
|
||||||
this.error = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result(Uri uri, Exception error) {
|
|
||||||
this.uri = uri;
|
|
||||||
this.bitmap = null;
|
|
||||||
this.loadSampleSize = 0;
|
|
||||||
this.degreesRotated = 0;
|
|
||||||
this.error = error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
|
@ -1,923 +0,0 @@
|
||||||
// "Therefore those skilled at the unorthodox
|
|
||||||
// are infinite as heaven and earth,
|
|
||||||
// inexhaustible as the great rivers.
|
|
||||||
// When they come to an end,
|
|
||||||
// they begin again,
|
|
||||||
// like the days and months;
|
|
||||||
// they die and are reborn,
|
|
||||||
// like the four seasons."
|
|
||||||
//
|
|
||||||
// - Sun Tsu,
|
|
||||||
// "The Art of War"
|
|
||||||
|
|
||||||
package com.theartofdev.edmodo.cropper;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
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.util.Log;
|
|
||||||
import android.util.Pair;
|
|
||||||
|
|
||||||
import androidx.exifinterface.media.ExifInterface;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
import javax.microedition.khronos.egl.EGL10;
|
|
||||||
import javax.microedition.khronos.egl.EGLConfig;
|
|
||||||
import javax.microedition.khronos.egl.EGLContext;
|
|
||||||
import javax.microedition.khronos.egl.EGLDisplay;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class that deals with operations with an ImageView.
|
|
||||||
*/
|
|
||||||
final class BitmapUtils {
|
|
||||||
|
|
||||||
static final Rect EMPTY_RECT = new Rect();
|
|
||||||
|
|
||||||
static final RectF EMPTY_RECT_F = new RectF();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reusable rectangle for general internal usage
|
|
||||||
*/
|
|
||||||
static final RectF RECT = new RectF();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reusable point for general internal usage
|
|
||||||
*/
|
|
||||||
static final float[] POINTS = new float[6];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reusable point for general internal usage
|
|
||||||
*/
|
|
||||||
static final float[] POINTS2 = new float[6];
|
|
||||||
/**
|
|
||||||
* used to save bitmaps during state save and restore so not to reload them.
|
|
||||||
*/
|
|
||||||
static Pair<String, WeakReference<Bitmap>> mStateBitmap;
|
|
||||||
/**
|
|
||||||
* Used to know the max texture size allowed to be rendered
|
|
||||||
*/
|
|
||||||
private static int mMaxTextureSize;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rotate the given image by reading the Exif value of the image (uri).<br>
|
|
||||||
* If no rotation is required the image will not be rotated.<br>
|
|
||||||
* New bitmap is created and the old one is recycled.
|
|
||||||
*/
|
|
||||||
static RotateBitmapResult rotateBitmapByExif(Bitmap bitmap, Context context, Uri uri) {
|
|
||||||
ExifInterface ei = null;
|
|
||||||
try {
|
|
||||||
InputStream is = context.getContentResolver().openInputStream(uri);
|
|
||||||
if (is != null) {
|
|
||||||
ei = new ExifInterface(is);
|
|
||||||
is.close();
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
return ei != null ? rotateBitmapByExif(bitmap, ei) : new RotateBitmapResult(bitmap, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rotate the given image by given Exif value.<br>
|
|
||||||
* If no rotation is required the image will not be rotated.<br>
|
|
||||||
* New bitmap is created and the old one is recycled.
|
|
||||||
*/
|
|
||||||
static RotateBitmapResult rotateBitmapByExif(Bitmap bitmap, ExifInterface exif) {
|
|
||||||
int degrees;
|
|
||||||
int orientation =
|
|
||||||
exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
|
||||||
switch (orientation) {
|
|
||||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
|
||||||
degrees = 90;
|
|
||||||
break;
|
|
||||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
|
||||||
degrees = 180;
|
|
||||||
break;
|
|
||||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
|
||||||
degrees = 270;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
degrees = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return new RotateBitmapResult(bitmap, degrees);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode bitmap from stream using sampling to get bitmap with the requested limit.
|
|
||||||
*/
|
|
||||||
static BitmapSampled decodeSampledBitmap(Context context, Uri uri, int reqWidth, int reqHeight) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
ContentResolver resolver = context.getContentResolver();
|
|
||||||
|
|
||||||
// First decode with inJustDecodeBounds=true to check dimensions
|
|
||||||
BitmapFactory.Options options = decodeImageForOption(resolver, uri);
|
|
||||||
|
|
||||||
if (options.outWidth == -1 && options.outHeight == -1)
|
|
||||||
throw new RuntimeException("File is not a picture");
|
|
||||||
|
|
||||||
// Calculate inSampleSize
|
|
||||||
options.inSampleSize =
|
|
||||||
Math.max(
|
|
||||||
calculateInSampleSizeByReqestedSize(
|
|
||||||
options.outWidth, options.outHeight, reqWidth, reqHeight),
|
|
||||||
calculateInSampleSizeByMaxTextureSize(options.outWidth, options.outHeight));
|
|
||||||
|
|
||||||
// Decode bitmap with inSampleSize set
|
|
||||||
Bitmap bitmap = decodeImage(resolver, uri, options);
|
|
||||||
|
|
||||||
return new BitmapSampled(bitmap, options.inSampleSize);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Failed to load sampled bitmap: " + uri + "\r\n" + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Crop image bitmap from given bitmap using the given points in the original bitmap and the given
|
|
||||||
* rotation.<br>
|
|
||||||
* if the rotation is not 0,90,180 or 270 degrees then we must first crop a larger area of the
|
|
||||||
* image that contains the requires rectangle, rotate and then crop again a sub rectangle.<br>
|
|
||||||
* If crop fails due to OOM we scale the cropping image by 0.5 every time it fails until it is
|
|
||||||
* small enough.
|
|
||||||
*/
|
|
||||||
static BitmapSampled cropBitmapObjectHandleOOM(
|
|
||||||
Bitmap bitmap,
|
|
||||||
float[] points,
|
|
||||||
int degreesRotated,
|
|
||||||
boolean fixAspectRatio,
|
|
||||||
int aspectRatioX,
|
|
||||||
int aspectRatioY,
|
|
||||||
boolean flipHorizontally,
|
|
||||||
boolean flipVertically) {
|
|
||||||
int scale = 1;
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
Bitmap cropBitmap =
|
|
||||||
cropBitmapObjectWithScale(
|
|
||||||
bitmap,
|
|
||||||
points,
|
|
||||||
degreesRotated,
|
|
||||||
fixAspectRatio,
|
|
||||||
aspectRatioX,
|
|
||||||
aspectRatioY,
|
|
||||||
1 / (float) scale,
|
|
||||||
flipHorizontally,
|
|
||||||
flipVertically);
|
|
||||||
return new BitmapSampled(cropBitmap, scale);
|
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
scale *= 2;
|
|
||||||
if (scale > 8) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Crop image bitmap from given bitmap using the given points in the original bitmap and the given
|
|
||||||
* rotation.<br>
|
|
||||||
* if the rotation is not 0,90,180 or 270 degrees then we must first crop a larger area of the
|
|
||||||
* image that contains the requires rectangle, rotate and then crop again a sub rectangle.
|
|
||||||
*
|
|
||||||
* @param scale how much to scale the cropped image part, use 0.5 to lower the image by half (OOM
|
|
||||||
* handling)
|
|
||||||
*/
|
|
||||||
private static Bitmap cropBitmapObjectWithScale(
|
|
||||||
Bitmap bitmap,
|
|
||||||
float[] points,
|
|
||||||
int degreesRotated,
|
|
||||||
boolean fixAspectRatio,
|
|
||||||
int aspectRatioX,
|
|
||||||
int aspectRatioY,
|
|
||||||
float scale,
|
|
||||||
boolean flipHorizontally,
|
|
||||||
boolean flipVertically) {
|
|
||||||
|
|
||||||
// get the rectangle in original image that contains the required cropped area (larger for non
|
|
||||||
// rectangular crop)
|
|
||||||
Rect rect =
|
|
||||||
getRectFromPoints(
|
|
||||||
points,
|
|
||||||
bitmap.getWidth(),
|
|
||||||
bitmap.getHeight(),
|
|
||||||
fixAspectRatio,
|
|
||||||
aspectRatioX,
|
|
||||||
aspectRatioY);
|
|
||||||
|
|
||||||
// crop and rotate the cropped image in one operation
|
|
||||||
Matrix matrix = new Matrix();
|
|
||||||
matrix.setRotate(degreesRotated, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
|
|
||||||
matrix.postScale(flipHorizontally ? -scale : scale, flipVertically ? -scale : scale);
|
|
||||||
Bitmap result =
|
|
||||||
Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height(), matrix, true);
|
|
||||||
|
|
||||||
if (result == bitmap) {
|
|
||||||
// corner case when all bitmap is selected, no worth optimizing for it
|
|
||||||
result = bitmap.copy(bitmap.getConfig(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping
|
|
||||||
if (degreesRotated % 90 != 0) {
|
|
||||||
|
|
||||||
// extra crop because non rectangular crop cannot be done directly on the image without
|
|
||||||
// rotating first
|
|
||||||
result =
|
|
||||||
cropForRotatedImage(
|
|
||||||
result, points, rect, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Crop image bitmap from URI by decoding it with specific width and height to down-sample if
|
|
||||||
* required.<br>
|
|
||||||
* Additionally if OOM is thrown try to increase the sampling (2,4,8).
|
|
||||||
*/
|
|
||||||
static BitmapSampled cropBitmap(
|
|
||||||
Context context,
|
|
||||||
Uri loadedImageUri,
|
|
||||||
float[] points,
|
|
||||||
int degreesRotated,
|
|
||||||
int orgWidth,
|
|
||||||
int orgHeight,
|
|
||||||
boolean fixAspectRatio,
|
|
||||||
int aspectRatioX,
|
|
||||||
int aspectRatioY,
|
|
||||||
int reqWidth,
|
|
||||||
int reqHeight,
|
|
||||||
boolean flipHorizontally,
|
|
||||||
boolean flipVertically) {
|
|
||||||
int sampleMulti = 1;
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
// if successful, just return the resulting bitmap
|
|
||||||
return cropBitmap(
|
|
||||||
context,
|
|
||||||
loadedImageUri,
|
|
||||||
points,
|
|
||||||
degreesRotated,
|
|
||||||
orgWidth,
|
|
||||||
orgHeight,
|
|
||||||
fixAspectRatio,
|
|
||||||
aspectRatioX,
|
|
||||||
aspectRatioY,
|
|
||||||
reqWidth,
|
|
||||||
reqHeight,
|
|
||||||
flipHorizontally,
|
|
||||||
flipVertically,
|
|
||||||
sampleMulti);
|
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
// if OOM try to increase the sampling to lower the memory usage
|
|
||||||
sampleMulti *= 2;
|
|
||||||
if (sampleMulti > 16) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Failed to handle OOM by sampling ("
|
|
||||||
+ sampleMulti
|
|
||||||
+ "): "
|
|
||||||
+ loadedImageUri
|
|
||||||
+ "\r\n"
|
|
||||||
+ e.getMessage(),
|
|
||||||
e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get left value of the bounding rectangle of the given points.
|
|
||||||
*/
|
|
||||||
static float getRectLeft(float[] points) {
|
|
||||||
return Math.min(Math.min(Math.min(points[0], points[2]), points[4]), points[6]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get top value of the bounding rectangle of the given points.
|
|
||||||
*/
|
|
||||||
static float getRectTop(float[] points) {
|
|
||||||
return Math.min(Math.min(Math.min(points[1], points[3]), points[5]), points[7]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get right value of the bounding rectangle of the given points.
|
|
||||||
*/
|
|
||||||
static float getRectRight(float[] points) {
|
|
||||||
return Math.max(Math.max(Math.max(points[0], points[2]), points[4]), points[6]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get bottom value of the bounding rectangle of the given points.
|
|
||||||
*/
|
|
||||||
static float getRectBottom(float[] points) {
|
|
||||||
return Math.max(Math.max(Math.max(points[1], points[3]), points[5]), points[7]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get width of the bounding rectangle of the given points.
|
|
||||||
*/
|
|
||||||
static float getRectWidth(float[] points) {
|
|
||||||
return getRectRight(points) - getRectLeft(points);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get height of the bounding rectangle of the given points.
|
|
||||||
*/
|
|
||||||
static float getRectHeight(float[] points) {
|
|
||||||
return getRectBottom(points) - getRectTop(points);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get horizontal center value of the bounding rectangle of the given points.
|
|
||||||
*/
|
|
||||||
static float getRectCenterX(float[] points) {
|
|
||||||
return (getRectRight(points) + getRectLeft(points)) / 2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get vertical center value of the bounding rectangle of the given points.
|
|
||||||
*/
|
|
||||||
static float getRectCenterY(float[] points) {
|
|
||||||
return (getRectBottom(points) + getRectTop(points)) / 2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a rectangle for the given 4 points (x0,y0,x1,y1,x2,y2,x3,y3) by finding the min/max 2
|
|
||||||
* points that contains the given 4 points and is a straight rectangle.
|
|
||||||
*/
|
|
||||||
static Rect getRectFromPoints(
|
|
||||||
float[] points,
|
|
||||||
int imageWidth,
|
|
||||||
int imageHeight,
|
|
||||||
boolean fixAspectRatio,
|
|
||||||
int aspectRatioX,
|
|
||||||
int aspectRatioY) {
|
|
||||||
int left = Math.round(Math.max(0, getRectLeft(points)));
|
|
||||||
int top = Math.round(Math.max(0, getRectTop(points)));
|
|
||||||
int right = Math.round(Math.min(imageWidth, getRectRight(points)));
|
|
||||||
int bottom = Math.round(Math.min(imageHeight, getRectBottom(points)));
|
|
||||||
|
|
||||||
Rect rect = new Rect(left, top, right, bottom);
|
|
||||||
if (fixAspectRatio) {
|
|
||||||
fixRectForAspectRatio(rect, aspectRatioX, aspectRatioY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fix the given rectangle if it doesn't confirm to aspect ration rule.<br>
|
|
||||||
* Make sure that width and height are equal if 1:1 fixed aspect ratio is requested.
|
|
||||||
*/
|
|
||||||
private static void fixRectForAspectRatio(Rect rect, int aspectRatioX, int aspectRatioY) {
|
|
||||||
if (aspectRatioX == aspectRatioY && rect.width() != rect.height()) {
|
|
||||||
if (rect.height() > rect.width()) {
|
|
||||||
rect.bottom -= rect.height() - rect.width();
|
|
||||||
} else {
|
|
||||||
rect.right -= rect.width() - rect.height();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write given bitmap to a temp file. If file already exists no-op as we already saved the file in
|
|
||||||
* this session. Uses JPEG 95% compression.
|
|
||||||
*
|
|
||||||
* @param uri the uri to write the bitmap to, if null
|
|
||||||
* @return the uri where the image was saved in, either the given uri or new pointing to temp
|
|
||||||
* file.
|
|
||||||
*/
|
|
||||||
static Uri writeTempStateStoreBitmap(Context context, Bitmap bitmap, Uri uri) {
|
|
||||||
try {
|
|
||||||
boolean needSave = true;
|
|
||||||
if (uri == null) {
|
|
||||||
uri =
|
|
||||||
Uri.fromFile(
|
|
||||||
File.createTempFile("aic_state_store_temp", ".jpg", context.getCacheDir()));
|
|
||||||
} else if (new File(uri.getPath()).exists()) {
|
|
||||||
needSave = false;
|
|
||||||
}
|
|
||||||
if (needSave) {
|
|
||||||
writeBitmapToUri(context, bitmap, uri, Bitmap.CompressFormat.JPEG, 95);
|
|
||||||
}
|
|
||||||
return uri;
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.w("AIC", "Failed to write bitmap to temp file for image-cropper save instance state", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write the given bitmap to the given uri using the given compression.
|
|
||||||
*/
|
|
||||||
static void writeBitmapToUri(
|
|
||||||
Context context,
|
|
||||||
Bitmap bitmap,
|
|
||||||
Uri uri,
|
|
||||||
Bitmap.CompressFormat compressFormat,
|
|
||||||
int compressQuality)
|
|
||||||
throws FileNotFoundException {
|
|
||||||
OutputStream outputStream = null;
|
|
||||||
try {
|
|
||||||
outputStream = context.getContentResolver().openOutputStream(uri);
|
|
||||||
bitmap.compress(compressFormat, compressQuality, outputStream);
|
|
||||||
} finally {
|
|
||||||
closeSafe(outputStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resize the given bitmap to the given width/height by the given option.<br>
|
|
||||||
*/
|
|
||||||
static Bitmap resizeBitmap(
|
|
||||||
Bitmap bitmap, int reqWidth, int reqHeight, CropImageView.RequestSizeOptions options) {
|
|
||||||
try {
|
|
||||||
if (reqWidth > 0
|
|
||||||
&& reqHeight > 0
|
|
||||||
&& (options == CropImageView.RequestSizeOptions.RESIZE_FIT
|
|
||||||
|| options == CropImageView.RequestSizeOptions.RESIZE_INSIDE
|
|
||||||
|| options == CropImageView.RequestSizeOptions.RESIZE_EXACT)) {
|
|
||||||
|
|
||||||
Bitmap resized = null;
|
|
||||||
if (options == CropImageView.RequestSizeOptions.RESIZE_EXACT) {
|
|
||||||
resized = Bitmap.createScaledBitmap(bitmap, reqWidth, reqHeight, false);
|
|
||||||
} else {
|
|
||||||
int width = bitmap.getWidth();
|
|
||||||
int height = bitmap.getHeight();
|
|
||||||
float scale = Math.max(width / (float) reqWidth, height / (float) reqHeight);
|
|
||||||
if (scale > 1 || options == CropImageView.RequestSizeOptions.RESIZE_FIT) {
|
|
||||||
resized =
|
|
||||||
Bitmap.createScaledBitmap(
|
|
||||||
bitmap, (int) (width / scale), (int) (height / scale), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (resized != null) {
|
|
||||||
if (resized != bitmap) {
|
|
||||||
bitmap.recycle();
|
|
||||||
}
|
|
||||||
return resized;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.w("AIC", "Failed to resize cropped image, return bitmap before resize", e);
|
|
||||||
}
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// region: Private methods
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Crop image bitmap from URI by decoding it with specific width and height to down-sample if
|
|
||||||
* required.
|
|
||||||
*
|
|
||||||
* @param orgWidth used to get rectangle from points (handle edge cases to limit rectangle)
|
|
||||||
* @param orgHeight used to get rectangle from points (handle edge cases to limit rectangle)
|
|
||||||
* @param sampleMulti used to increase the sampling of the image to handle memory issues.
|
|
||||||
*/
|
|
||||||
private static BitmapSampled cropBitmap(
|
|
||||||
Context context,
|
|
||||||
Uri loadedImageUri,
|
|
||||||
float[] points,
|
|
||||||
int degreesRotated,
|
|
||||||
int orgWidth,
|
|
||||||
int orgHeight,
|
|
||||||
boolean fixAspectRatio,
|
|
||||||
int aspectRatioX,
|
|
||||||
int aspectRatioY,
|
|
||||||
int reqWidth,
|
|
||||||
int reqHeight,
|
|
||||||
boolean flipHorizontally,
|
|
||||||
boolean flipVertically,
|
|
||||||
int sampleMulti) {
|
|
||||||
|
|
||||||
// get the rectangle in original image that contains the required cropped area (larger for non
|
|
||||||
// rectangular crop)
|
|
||||||
Rect rect =
|
|
||||||
getRectFromPoints(points, orgWidth, orgHeight, fixAspectRatio, aspectRatioX, aspectRatioY);
|
|
||||||
|
|
||||||
int width = reqWidth > 0 ? reqWidth : rect.width();
|
|
||||||
int height = reqHeight > 0 ? reqHeight : rect.height();
|
|
||||||
|
|
||||||
Bitmap result = null;
|
|
||||||
int sampleSize = 1;
|
|
||||||
try {
|
|
||||||
// decode only the required image from URI, optionally sub-sampling if reqWidth/reqHeight is
|
|
||||||
// given.
|
|
||||||
BitmapSampled bitmapSampled =
|
|
||||||
decodeSampledBitmapRegion(context, loadedImageUri, rect, width, height, sampleMulti);
|
|
||||||
result = bitmapSampled.bitmap;
|
|
||||||
sampleSize = bitmapSampled.sampleSize;
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
try {
|
|
||||||
// rotate the decoded region by the required amount
|
|
||||||
result = rotateAndFlipBitmapInt(result, degreesRotated, flipHorizontally, flipVertically);
|
|
||||||
|
|
||||||
// rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping
|
|
||||||
if (degreesRotated % 90 != 0) {
|
|
||||||
|
|
||||||
// extra crop because non rectangular crop cannot be done directly on the image without
|
|
||||||
// rotating first
|
|
||||||
result =
|
|
||||||
cropForRotatedImage(
|
|
||||||
result, points, rect, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY);
|
|
||||||
}
|
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
if (result != null) {
|
|
||||||
result.recycle();
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
return new BitmapSampled(result, sampleSize);
|
|
||||||
} else {
|
|
||||||
// failed to decode region, may be skia issue, try full decode and then crop
|
|
||||||
return cropBitmap(
|
|
||||||
context,
|
|
||||||
loadedImageUri,
|
|
||||||
points,
|
|
||||||
degreesRotated,
|
|
||||||
fixAspectRatio,
|
|
||||||
aspectRatioX,
|
|
||||||
aspectRatioY,
|
|
||||||
sampleMulti,
|
|
||||||
rect,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
flipHorizontally,
|
|
||||||
flipVertically);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Crop bitmap by fully loading the original and then cropping it, fallback in case cropping
|
|
||||||
* region failed.
|
|
||||||
*/
|
|
||||||
private static BitmapSampled cropBitmap(
|
|
||||||
Context context,
|
|
||||||
Uri loadedImageUri,
|
|
||||||
float[] points,
|
|
||||||
int degreesRotated,
|
|
||||||
boolean fixAspectRatio,
|
|
||||||
int aspectRatioX,
|
|
||||||
int aspectRatioY,
|
|
||||||
int sampleMulti,
|
|
||||||
Rect rect,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
boolean flipHorizontally,
|
|
||||||
boolean flipVertically) {
|
|
||||||
Bitmap result = null;
|
|
||||||
int sampleSize;
|
|
||||||
try {
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inSampleSize =
|
|
||||||
sampleSize =
|
|
||||||
sampleMulti
|
|
||||||
* calculateInSampleSizeByReqestedSize(rect.width(), rect.height(), width, height);
|
|
||||||
|
|
||||||
Bitmap fullBitmap = decodeImage(context.getContentResolver(), loadedImageUri, options);
|
|
||||||
if (fullBitmap != null) {
|
|
||||||
try {
|
|
||||||
// adjust crop points by the sampling because the image is smaller
|
|
||||||
float[] points2 = new float[points.length];
|
|
||||||
System.arraycopy(points, 0, points2, 0, points.length);
|
|
||||||
for (int i = 0; i < points2.length; i++) {
|
|
||||||
points2[i] = points2[i] / options.inSampleSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
result =
|
|
||||||
cropBitmapObjectWithScale(
|
|
||||||
fullBitmap,
|
|
||||||
points2,
|
|
||||||
degreesRotated,
|
|
||||||
fixAspectRatio,
|
|
||||||
aspectRatioX,
|
|
||||||
aspectRatioY,
|
|
||||||
1,
|
|
||||||
flipHorizontally,
|
|
||||||
flipVertically);
|
|
||||||
} finally {
|
|
||||||
if (result != fullBitmap) {
|
|
||||||
fullBitmap.recycle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
if (result != null) {
|
|
||||||
result.recycle();
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Failed to load sampled bitmap: " + loadedImageUri + "\r\n" + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
return new BitmapSampled(result, sampleSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode image from uri using "inJustDecodeBounds" to get the image dimensions.
|
|
||||||
*/
|
|
||||||
private static BitmapFactory.Options decodeImageForOption(ContentResolver resolver, Uri uri)
|
|
||||||
throws FileNotFoundException {
|
|
||||||
InputStream stream = null;
|
|
||||||
try {
|
|
||||||
stream = resolver.openInputStream(uri);
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inJustDecodeBounds = true;
|
|
||||||
BitmapFactory.decodeStream(stream, EMPTY_RECT, options);
|
|
||||||
options.inJustDecodeBounds = false;
|
|
||||||
return options;
|
|
||||||
} finally {
|
|
||||||
closeSafe(stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode image from uri using given "inSampleSize", but if failed due to out-of-memory then raise
|
|
||||||
* the inSampleSize until success.
|
|
||||||
*/
|
|
||||||
private static Bitmap decodeImage(
|
|
||||||
ContentResolver resolver, Uri uri, BitmapFactory.Options options)
|
|
||||||
throws FileNotFoundException {
|
|
||||||
do {
|
|
||||||
InputStream stream = null;
|
|
||||||
try {
|
|
||||||
stream = resolver.openInputStream(uri);
|
|
||||||
return BitmapFactory.decodeStream(stream, EMPTY_RECT, options);
|
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
options.inSampleSize *= 2;
|
|
||||||
} finally {
|
|
||||||
closeSafe(stream);
|
|
||||||
}
|
|
||||||
} while (options.inSampleSize <= 512);
|
|
||||||
throw new RuntimeException("Failed to decode image: " + uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode specific rectangle bitmap from stream using sampling to get bitmap with the requested
|
|
||||||
* limit.
|
|
||||||
*
|
|
||||||
* @param sampleMulti used to increase the sampling of the image to handle memory issues.
|
|
||||||
*/
|
|
||||||
private static BitmapSampled decodeSampledBitmapRegion(
|
|
||||||
Context context, Uri uri, Rect rect, int reqWidth, int reqHeight, int sampleMulti) {
|
|
||||||
InputStream stream = null;
|
|
||||||
BitmapRegionDecoder decoder = null;
|
|
||||||
try {
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inSampleSize =
|
|
||||||
sampleMulti
|
|
||||||
* calculateInSampleSizeByReqestedSize(
|
|
||||||
rect.width(), rect.height(), reqWidth, reqHeight);
|
|
||||||
|
|
||||||
stream = context.getContentResolver().openInputStream(uri);
|
|
||||||
decoder = BitmapRegionDecoder.newInstance(stream, false);
|
|
||||||
do {
|
|
||||||
try {
|
|
||||||
return new BitmapSampled(decoder.decodeRegion(rect, options), options.inSampleSize);
|
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
options.inSampleSize *= 2;
|
|
||||||
}
|
|
||||||
} while (options.inSampleSize <= 512);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Failed to load sampled bitmap: " + uri + "\r\n" + e.getMessage(), e);
|
|
||||||
} finally {
|
|
||||||
closeSafe(stream);
|
|
||||||
if (decoder != null) {
|
|
||||||
decoder.recycle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new BitmapSampled(null, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Special crop of bitmap rotated by not stright angle, in this case the original crop bitmap
|
|
||||||
* contains parts beyond the required crop area, this method crops the already cropped and rotated
|
|
||||||
* bitmap to the final rectangle.<br>
|
|
||||||
* Note: rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping.
|
|
||||||
*/
|
|
||||||
private static Bitmap cropForRotatedImage(
|
|
||||||
Bitmap bitmap,
|
|
||||||
float[] points,
|
|
||||||
Rect rect,
|
|
||||||
int degreesRotated,
|
|
||||||
boolean fixAspectRatio,
|
|
||||||
int aspectRatioX,
|
|
||||||
int aspectRatioY) {
|
|
||||||
if (degreesRotated % 90 != 0) {
|
|
||||||
|
|
||||||
int adjLeft = 0, adjTop = 0, width = 0, height = 0;
|
|
||||||
double rads = Math.toRadians(degreesRotated);
|
|
||||||
int compareTo =
|
|
||||||
degreesRotated < 90 || (degreesRotated > 180 && degreesRotated < 270)
|
|
||||||
? rect.left
|
|
||||||
: rect.right;
|
|
||||||
for (int i = 0; i < points.length; i += 2) {
|
|
||||||
if (points[i] >= compareTo - 1 && points[i] <= compareTo + 1) {
|
|
||||||
adjLeft = (int) Math.abs(Math.sin(rads) * (rect.bottom - points[i + 1]));
|
|
||||||
adjTop = (int) Math.abs(Math.cos(rads) * (points[i + 1] - rect.top));
|
|
||||||
width = (int) Math.abs((points[i + 1] - rect.top) / Math.sin(rads));
|
|
||||||
height = (int) Math.abs((rect.bottom - points[i + 1]) / Math.cos(rads));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rect.set(adjLeft, adjTop, adjLeft + width, adjTop + height);
|
|
||||||
if (fixAspectRatio) {
|
|
||||||
fixRectForAspectRatio(rect, aspectRatioX, aspectRatioY);
|
|
||||||
}
|
|
||||||
|
|
||||||
Bitmap bitmapTmp = bitmap;
|
|
||||||
bitmap = Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height());
|
|
||||||
if (bitmapTmp != bitmap) {
|
|
||||||
bitmapTmp.recycle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width
|
|
||||||
* larger than the requested height and width.
|
|
||||||
*/
|
|
||||||
private static int calculateInSampleSizeByReqestedSize(
|
|
||||||
int width, int height, int reqWidth, int reqHeight) {
|
|
||||||
int inSampleSize = 1;
|
|
||||||
if (height > reqHeight || width > reqWidth) {
|
|
||||||
while ((height / 2 / inSampleSize) > reqHeight && (width / 2 / inSampleSize) > reqWidth) {
|
|
||||||
inSampleSize *= 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return inSampleSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width
|
|
||||||
* smaller than max texture size allowed for the device.
|
|
||||||
*/
|
|
||||||
private static int calculateInSampleSizeByMaxTextureSize(int width, int height) {
|
|
||||||
int inSampleSize = 1;
|
|
||||||
if (mMaxTextureSize == 0) {
|
|
||||||
mMaxTextureSize = getMaxTextureSize();
|
|
||||||
}
|
|
||||||
if (mMaxTextureSize > 0) {
|
|
||||||
while ((height / inSampleSize) > mMaxTextureSize
|
|
||||||
|| (width / inSampleSize) > mMaxTextureSize) {
|
|
||||||
inSampleSize *= 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return inSampleSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rotate the given bitmap by the given degrees.<br>
|
|
||||||
* New bitmap is created and the old one is recycled.
|
|
||||||
*/
|
|
||||||
private static Bitmap rotateAndFlipBitmapInt(
|
|
||||||
Bitmap bitmap, int degrees, boolean flipHorizontally, boolean flipVertically) {
|
|
||||||
if (degrees > 0 || flipHorizontally || flipVertically) {
|
|
||||||
Matrix matrix = new Matrix();
|
|
||||||
matrix.setRotate(degrees);
|
|
||||||
matrix.postScale(flipHorizontally ? -1 : 1, flipVertically ? -1 : 1);
|
|
||||||
Bitmap newBitmap =
|
|
||||||
Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
|
|
||||||
if (newBitmap != bitmap) {
|
|
||||||
bitmap.recycle();
|
|
||||||
}
|
|
||||||
return newBitmap;
|
|
||||||
} else {
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the max size of bitmap allowed to be rendered on the device.<br>
|
|
||||||
* http://stackoverflow.com/questions/7428996/hw-accelerated-activity-how-to-get-opengl-texture-size-limit.
|
|
||||||
*/
|
|
||||||
private static int getMaxTextureSize() {
|
|
||||||
// Safe minimum default size
|
|
||||||
final int IMAGE_MAX_BITMAP_DIMENSION = 2048;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get EGL Display
|
|
||||||
EGL10 egl = (EGL10) EGLContext.getEGL();
|
|
||||||
EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
|
|
||||||
|
|
||||||
// Initialise
|
|
||||||
int[] version = new int[2];
|
|
||||||
egl.eglInitialize(display, version);
|
|
||||||
|
|
||||||
// Query total number of configurations
|
|
||||||
int[] totalConfigurations = new int[1];
|
|
||||||
egl.eglGetConfigs(display, null, 0, totalConfigurations);
|
|
||||||
|
|
||||||
// Query actual list configurations
|
|
||||||
EGLConfig[] configurationsList = new EGLConfig[totalConfigurations[0]];
|
|
||||||
egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations);
|
|
||||||
|
|
||||||
int[] textureSize = new int[1];
|
|
||||||
int maximumTextureSize = 0;
|
|
||||||
|
|
||||||
// Iterate through all the configurations to located the maximum texture size
|
|
||||||
for (int i = 0; i < totalConfigurations[0]; i++) {
|
|
||||||
// Only need to check for width since opengl textures are always squared
|
|
||||||
egl.eglGetConfigAttrib(
|
|
||||||
display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize);
|
|
||||||
|
|
||||||
// Keep track of the maximum texture size
|
|
||||||
if (maximumTextureSize < textureSize[0]) {
|
|
||||||
maximumTextureSize = textureSize[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release
|
|
||||||
egl.eglTerminate(display);
|
|
||||||
|
|
||||||
// Return largest texture size found, or default
|
|
||||||
return Math.max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return IMAGE_MAX_BITMAP_DIMENSION;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the given closeable object (Stream) in a safe way: check if it is null and catch-log
|
|
||||||
* exception thrown.
|
|
||||||
*
|
|
||||||
* @param closeable the closable object to close
|
|
||||||
*/
|
|
||||||
private static void closeSafe(Closeable closeable) {
|
|
||||||
if (closeable != null) {
|
|
||||||
try {
|
|
||||||
closeable.close();
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Inner class: BitmapSampled
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds bitmap instance and the sample size that the bitmap was loaded/cropped with.
|
|
||||||
*/
|
|
||||||
static final class BitmapSampled {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The bitmap instance
|
|
||||||
*/
|
|
||||||
public final Bitmap bitmap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The sample size used to lower the size of the bitmap (1,2,4,8,...)
|
|
||||||
*/
|
|
||||||
final int sampleSize;
|
|
||||||
|
|
||||||
BitmapSampled(Bitmap bitmap, int sampleSize) {
|
|
||||||
this.bitmap = bitmap;
|
|
||||||
this.sampleSize = sampleSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Inner class: RotateBitmapResult
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The result of {@link #rotateBitmapByExif(android.graphics.Bitmap, ExifInterface)}.
|
|
||||||
*/
|
|
||||||
static final class RotateBitmapResult {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The loaded bitmap
|
|
||||||
*/
|
|
||||||
public final Bitmap bitmap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The degrees the image was rotated
|
|
||||||
*/
|
|
||||||
final int degrees;
|
|
||||||
|
|
||||||
RotateBitmapResult(Bitmap bitmap, int degrees) {
|
|
||||||
this.bitmap = bitmap;
|
|
||||||
this.degrees = degrees;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
|
@ -1,367 +0,0 @@
|
||||||
// "Therefore those skilled at the unorthodox
|
|
||||||
// are infinite as heaven and earth,
|
|
||||||
// inexhaustible as the great rivers.
|
|
||||||
// When they come to an end,
|
|
||||||
// they begin again,
|
|
||||||
// like the days and months;
|
|
||||||
// they die and are reborn,
|
|
||||||
// like the four seasons."
|
|
||||||
//
|
|
||||||
// - Sun Tsu,
|
|
||||||
// "The Art of War"
|
|
||||||
|
|
||||||
package com.theartofdev.edmodo.cropper;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Built-in activity for image cropping.<br>
|
|
||||||
* Use {@link CropImage#activity(Uri)} to create a builder to start this activity.
|
|
||||||
*/
|
|
||||||
public class CropImageActivity extends AppCompatActivity
|
|
||||||
implements CropImageView.OnSetImageUriCompleteListener,
|
|
||||||
CropImageView.OnCropImageCompleteListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The crop image view library widget used in the activity
|
|
||||||
*/
|
|
||||||
private CropImageView mCropImageView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Persist URI image to crop URI if specific permissions are required
|
|
||||||
*/
|
|
||||||
private Uri mCropImageUri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the options that were set for the crop image
|
|
||||||
*/
|
|
||||||
private CropImageOptions mOptions;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.crop_image_activity);
|
|
||||||
|
|
||||||
mCropImageView = findViewById(R.id.cropImageView);
|
|
||||||
|
|
||||||
Bundle bundle = getIntent().getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE);
|
|
||||||
mCropImageUri = bundle.getParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE);
|
|
||||||
mOptions = bundle.getParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS);
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
if (mCropImageUri == null || mCropImageUri.equals(Uri.EMPTY)) {
|
|
||||||
if (CropImage.isExplicitCameraPermissionRequired(this)) {
|
|
||||||
// request permissions and handle the result in onRequestPermissionsResult()
|
|
||||||
requestPermissions(
|
|
||||||
new String[]{Manifest.permission.CAMERA},
|
|
||||||
CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE);
|
|
||||||
} else {
|
|
||||||
CropImage.startPickImageActivity(this);
|
|
||||||
}
|
|
||||||
} else if (CropImage.isReadExternalStoragePermissionsRequired(this, mCropImageUri)) {
|
|
||||||
// request permissions and handle the result in onRequestPermissionsResult()
|
|
||||||
requestPermissions(
|
|
||||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
|
|
||||||
CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE);
|
|
||||||
} else {
|
|
||||||
// no permissions required or already grunted, can start crop image activity
|
|
||||||
mCropImageView.setImageUriAsync(mCropImageUri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ActionBar actionBar = getSupportActionBar();
|
|
||||||
if (actionBar != null) {
|
|
||||||
CharSequence title = mOptions != null &&
|
|
||||||
mOptions.activityTitle != null && mOptions.activityTitle.length() > 0
|
|
||||||
? mOptions.activityTitle
|
|
||||||
: getResources().getString(R.string.crop_image_activity_title);
|
|
||||||
actionBar.setTitle(title);
|
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
mCropImageView.setOnSetImageUriCompleteListener(this);
|
|
||||||
mCropImageView.setOnCropImageCompleteListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
mCropImageView.setOnSetImageUriCompleteListener(null);
|
|
||||||
mCropImageView.setOnCropImageCompleteListener(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
getMenuInflater().inflate(R.menu.crop_image_menu, menu);
|
|
||||||
|
|
||||||
if (!mOptions.allowRotation) {
|
|
||||||
menu.removeItem(R.id.crop_image_menu_rotate_left);
|
|
||||||
menu.removeItem(R.id.crop_image_menu_rotate_right);
|
|
||||||
} else if (mOptions.allowCounterRotation) {
|
|
||||||
menu.findItem(R.id.crop_image_menu_rotate_left).setVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mOptions.allowFlipping) {
|
|
||||||
menu.removeItem(R.id.crop_image_menu_flip);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mOptions.cropMenuCropButtonTitle != null) {
|
|
||||||
menu.findItem(R.id.crop_image_menu_crop).setTitle(mOptions.cropMenuCropButtonTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
Drawable cropIcon = null;
|
|
||||||
try {
|
|
||||||
if (mOptions.cropMenuCropButtonIcon != 0) {
|
|
||||||
cropIcon = ContextCompat.getDrawable(this, mOptions.cropMenuCropButtonIcon);
|
|
||||||
menu.findItem(R.id.crop_image_menu_crop).setIcon(cropIcon);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mOptions.activityMenuIconColor != 0) {
|
|
||||||
updateMenuItemIconColor(
|
|
||||||
menu, R.id.crop_image_menu_rotate_left, mOptions.activityMenuIconColor);
|
|
||||||
updateMenuItemIconColor(
|
|
||||||
menu, R.id.crop_image_menu_rotate_right, mOptions.activityMenuIconColor);
|
|
||||||
updateMenuItemIconColor(menu, R.id.crop_image_menu_flip, mOptions.activityMenuIconColor);
|
|
||||||
if (cropIcon != null) {
|
|
||||||
updateMenuItemIconColor(menu, R.id.crop_image_menu_crop, mOptions.activityMenuIconColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
if (item.getItemId() == R.id.crop_image_menu_crop) {
|
|
||||||
cropImage();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (item.getItemId() == R.id.crop_image_menu_rotate_left) {
|
|
||||||
rotateImage(-mOptions.rotationDegrees);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (item.getItemId() == R.id.crop_image_menu_rotate_right) {
|
|
||||||
rotateImage(mOptions.rotationDegrees);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (item.getItemId() == R.id.crop_image_menu_flip_horizontally) {
|
|
||||||
mCropImageView.flipImageHorizontally();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (item.getItemId() == R.id.crop_image_menu_flip_vertically) {
|
|
||||||
mCropImageView.flipImageVertically();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (item.getItemId() == android.R.id.home) {
|
|
||||||
setResultCancel();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
super.onBackPressed();
|
|
||||||
setResultCancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
|
|
||||||
// handle result of pick image chooser
|
|
||||||
if (requestCode == CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE) {
|
|
||||||
if (resultCode == Activity.RESULT_CANCELED) {
|
|
||||||
// User cancelled the picker. We don't have anything to crop
|
|
||||||
setResultCancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
mCropImageUri = CropImage.getPickImageResultUri(this, data);
|
|
||||||
|
|
||||||
// For API >= 23 we need to check specifically that we have permissions to read external
|
|
||||||
// storage.
|
|
||||||
if (CropImage.isReadExternalStoragePermissionsRequired(this, mCropImageUri)) {
|
|
||||||
// request permissions and handle the result in onRequestPermissionsResult()
|
|
||||||
requestPermissions(
|
|
||||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
|
|
||||||
CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE);
|
|
||||||
} else {
|
|
||||||
// no permissions required or already grunted, can start crop image activity
|
|
||||||
mCropImageView.setImageUriAsync(mCropImageUri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(
|
|
||||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
||||||
if (requestCode == CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE) {
|
|
||||||
if (mCropImageUri != null
|
|
||||||
&& grantResults.length > 0
|
|
||||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
// required permissions granted, start crop image activity
|
|
||||||
mCropImageView.setImageUriAsync(mCropImageUri);
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, R.string.crop_image_activity_no_permissions, Toast.LENGTH_LONG).show();
|
|
||||||
setResultCancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestCode == CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE) {
|
|
||||||
// Irrespective of whether camera permission was given or not, we show the picker
|
|
||||||
// The picker will not add the camera intent if permission is not available
|
|
||||||
CropImage.startPickImageActivity(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSetImageUriComplete(CropImageView view, Uri uri, Exception error) {
|
|
||||||
if (error == null) {
|
|
||||||
if (mOptions.initialCropWindowRectangle != null) {
|
|
||||||
mCropImageView.setCropRect(mOptions.initialCropWindowRectangle);
|
|
||||||
}
|
|
||||||
if (mOptions.initialRotation > -1) {
|
|
||||||
mCropImageView.setRotatedDegrees(mOptions.initialRotation);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setResult(null, error, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCropImageComplete(CropImageView view, CropImageView.CropResult result) {
|
|
||||||
setResult(result.getUri(), result.getError(), result.getSampleSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
// region: Private methods
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute crop image and save the result tou output uri.
|
|
||||||
*/
|
|
||||||
protected void cropImage() {
|
|
||||||
if (mOptions.noOutputImage) {
|
|
||||||
setResult(null, null, 1);
|
|
||||||
} else {
|
|
||||||
Uri outputUri = getOutputUri();
|
|
||||||
mCropImageView.saveCroppedImageAsync(
|
|
||||||
outputUri,
|
|
||||||
mOptions.outputCompressFormat,
|
|
||||||
mOptions.outputCompressQuality,
|
|
||||||
mOptions.outputRequestWidth,
|
|
||||||
mOptions.outputRequestHeight,
|
|
||||||
mOptions.outputRequestSizeOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rotate the image in the crop image view.
|
|
||||||
*/
|
|
||||||
protected void rotateImage(int degrees) {
|
|
||||||
mCropImageView.rotateImage(degrees);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Android uri to save the cropped image into.<br>
|
|
||||||
* Use the given in options or create a temp file.
|
|
||||||
*/
|
|
||||||
protected Uri getOutputUri() {
|
|
||||||
Uri outputUri = mOptions.outputUri;
|
|
||||||
if (outputUri == null || outputUri.equals(Uri.EMPTY)) {
|
|
||||||
try {
|
|
||||||
String ext =
|
|
||||||
mOptions.outputCompressFormat == Bitmap.CompressFormat.JPEG
|
|
||||||
? ".jpg"
|
|
||||||
: mOptions.outputCompressFormat == Bitmap.CompressFormat.PNG ? ".png" : ".webp";
|
|
||||||
outputUri = Uri.fromFile(File.createTempFile("cropped", ext, getCacheDir()));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Failed to create temp file for output image", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return outputUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result with cropped image data or error if failed.
|
|
||||||
*/
|
|
||||||
protected void setResult(Uri uri, Exception error, int sampleSize) {
|
|
||||||
int resultCode = error == null ? RESULT_OK : CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE;
|
|
||||||
setResult(resultCode, getResultIntent(uri, error, sampleSize));
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel of cropping activity.
|
|
||||||
*/
|
|
||||||
protected void setResultCancel() {
|
|
||||||
setResult(RESULT_CANCELED);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get intent instance to be used for the result of this activity.
|
|
||||||
*/
|
|
||||||
protected Intent getResultIntent(Uri uri, Exception error, int sampleSize) {
|
|
||||||
CropImage.ActivityResult result =
|
|
||||||
new CropImage.ActivityResult(
|
|
||||||
mCropImageView.getImageUri(),
|
|
||||||
uri,
|
|
||||||
error,
|
|
||||||
mCropImageView.getCropPoints(),
|
|
||||||
mCropImageView.getCropRect(),
|
|
||||||
mCropImageView.getRotatedDegrees(),
|
|
||||||
mCropImageView.getWholeImageRect(),
|
|
||||||
sampleSize);
|
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.putExtras(getIntent());
|
|
||||||
intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, result);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the color of a specific menu item to the given color.
|
|
||||||
*/
|
|
||||||
private void updateMenuItemIconColor(Menu menu, int itemId, int color) {
|
|
||||||
MenuItem menuItem = menu.findItem(itemId);
|
|
||||||
if (menuItem != null) {
|
|
||||||
Drawable menuItemIcon = menuItem.getIcon();
|
|
||||||
if (menuItemIcon != null) {
|
|
||||||
try {
|
|
||||||
menuItemIcon.mutate();
|
|
||||||
menuItemIcon.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
|
|
||||||
menuItem.setIcon(menuItemIcon);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
// "Therefore those skilled at the unorthodox
|
|
||||||
// are infinite as heaven and earth,
|
|
||||||
// inexhaustible as the great rivers.
|
|
||||||
// When they come to an end,
|
|
||||||
// they begin again,
|
|
||||||
// like the days and months;
|
|
||||||
// they die and are reborn,
|
|
||||||
// like the four seasons."
|
|
||||||
//
|
|
||||||
// - Sun Tsu,
|
|
||||||
// "The Art of War"
|
|
||||||
|
|
||||||
package com.theartofdev.edmodo.cropper;
|
|
||||||
|
|
||||||
import android.graphics.Matrix;
|
|
||||||
import android.graphics.RectF;
|
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
|
||||||
import android.view.animation.Animation;
|
|
||||||
import android.view.animation.Transformation;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Animation to handle smooth cropping image matrix transformation change, specifically for
|
|
||||||
* zoom-in/out.
|
|
||||||
*/
|
|
||||||
final class CropImageAnimation extends Animation implements Animation.AnimationListener {
|
|
||||||
|
|
||||||
// region: Fields and Consts
|
|
||||||
|
|
||||||
private final ImageView mImageView;
|
|
||||||
|
|
||||||
private final CropOverlayView mCropOverlayView;
|
|
||||||
|
|
||||||
private final float[] mStartBoundPoints = new float[8];
|
|
||||||
|
|
||||||
private final float[] mEndBoundPoints = new float[8];
|
|
||||||
|
|
||||||
private final RectF mStartCropWindowRect = new RectF();
|
|
||||||
|
|
||||||
private final RectF mEndCropWindowRect = new RectF();
|
|
||||||
|
|
||||||
private final float[] mStartImageMatrix = new float[9];
|
|
||||||
|
|
||||||
private final float[] mEndImageMatrix = new float[9];
|
|
||||||
|
|
||||||
private final RectF mAnimRect = new RectF();
|
|
||||||
|
|
||||||
private final float[] mAnimPoints = new float[8];
|
|
||||||
|
|
||||||
private final float[] mAnimMatrix = new float[9];
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
public CropImageAnimation(ImageView cropImageView, CropOverlayView cropOverlayView) {
|
|
||||||
mImageView = cropImageView;
|
|
||||||
mCropOverlayView = cropOverlayView;
|
|
||||||
|
|
||||||
setDuration(300);
|
|
||||||
setFillAfter(true);
|
|
||||||
setInterpolator(new AccelerateDecelerateInterpolator());
|
|
||||||
setAnimationListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStartState(float[] boundPoints, Matrix imageMatrix) {
|
|
||||||
reset();
|
|
||||||
System.arraycopy(boundPoints, 0, mStartBoundPoints, 0, 8);
|
|
||||||
mStartCropWindowRect.set(mCropOverlayView.getCropWindowRect());
|
|
||||||
imageMatrix.getValues(mStartImageMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEndState(float[] boundPoints, Matrix imageMatrix) {
|
|
||||||
System.arraycopy(boundPoints, 0, mEndBoundPoints, 0, 8);
|
|
||||||
mEndCropWindowRect.set(mCropOverlayView.getCropWindowRect());
|
|
||||||
imageMatrix.getValues(mEndImageMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
|
||||||
|
|
||||||
mAnimRect.left =
|
|
||||||
mStartCropWindowRect.left
|
|
||||||
+ (mEndCropWindowRect.left - mStartCropWindowRect.left) * interpolatedTime;
|
|
||||||
mAnimRect.top =
|
|
||||||
mStartCropWindowRect.top
|
|
||||||
+ (mEndCropWindowRect.top - mStartCropWindowRect.top) * interpolatedTime;
|
|
||||||
mAnimRect.right =
|
|
||||||
mStartCropWindowRect.right
|
|
||||||
+ (mEndCropWindowRect.right - mStartCropWindowRect.right) * interpolatedTime;
|
|
||||||
mAnimRect.bottom =
|
|
||||||
mStartCropWindowRect.bottom
|
|
||||||
+ (mEndCropWindowRect.bottom - mStartCropWindowRect.bottom) * interpolatedTime;
|
|
||||||
mCropOverlayView.setCropWindowRect(mAnimRect);
|
|
||||||
|
|
||||||
for (int i = 0; i < mAnimPoints.length; i++) {
|
|
||||||
mAnimPoints[i] =
|
|
||||||
mStartBoundPoints[i] + (mEndBoundPoints[i] - mStartBoundPoints[i]) * interpolatedTime;
|
|
||||||
}
|
|
||||||
mCropOverlayView.setBounds(mAnimPoints, mImageView.getWidth(), mImageView.getHeight());
|
|
||||||
|
|
||||||
for (int i = 0; i < mAnimMatrix.length; i++) {
|
|
||||||
mAnimMatrix[i] =
|
|
||||||
mStartImageMatrix[i] + (mEndImageMatrix[i] - mStartImageMatrix[i]) * interpolatedTime;
|
|
||||||
}
|
|
||||||
Matrix m = mImageView.getImageMatrix();
|
|
||||||
m.setValues(mAnimMatrix);
|
|
||||||
mImageView.setImageMatrix(m);
|
|
||||||
|
|
||||||
mImageView.invalidate();
|
|
||||||
mCropOverlayView.invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationStart(Animation animation) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animation animation) {
|
|
||||||
mImageView.clearAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationRepeat(Animation animation) {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,541 +0,0 @@
|
||||||
// "Therefore those skilled at the unorthodox
|
|
||||||
// are infinite as heaven and earth;
|
|
||||||
// inexhaustible as the great rivers.
|
|
||||||
// When they come to an end;
|
|
||||||
// they begin again;
|
|
||||||
// like the days and months;
|
|
||||||
// they die and are reborn;
|
|
||||||
// like the four seasons."
|
|
||||||
//
|
|
||||||
// - Sun Tsu;
|
|
||||||
// "The Art of War"
|
|
||||||
|
|
||||||
package com.theartofdev.edmodo.cropper;
|
|
||||||
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Parcel;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.util.TypedValue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All the possible options that can be set to customize crop image.<br>
|
|
||||||
* Initialized with default values.
|
|
||||||
*/
|
|
||||||
public class CropImageOptions implements Parcelable {
|
|
||||||
|
|
||||||
public static final Creator<CropImageOptions> CREATOR =
|
|
||||||
new Creator<CropImageOptions>() {
|
|
||||||
@Override
|
|
||||||
public CropImageOptions createFromParcel(Parcel in) {
|
|
||||||
return new CropImageOptions(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CropImageOptions[] newArray(int size) {
|
|
||||||
return new CropImageOptions[size];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The shape of the cropping window.
|
|
||||||
*/
|
|
||||||
public CropImageView.CropShape cropShape;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An edge of the crop window will snap to the corresponding edge of a specified bounding box when
|
|
||||||
* the crop window edge is less than or equal to this distance (in pixels) away from the bounding
|
|
||||||
* box edge. (in pixels)
|
|
||||||
*/
|
|
||||||
public float snapRadius;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The radius of the touchable area around the handle. (in pixels)<br>
|
|
||||||
* We are basing this value off of the recommended 48dp Rhythm.<br>
|
|
||||||
* See: http://developer.android.com/design/style/metrics-grids.html#48dp-rhythm
|
|
||||||
*/
|
|
||||||
public float touchRadius;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* whether the guidelines should be on, off, or only showing when resizing.
|
|
||||||
*/
|
|
||||||
public CropImageView.Guidelines guidelines;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The initial scale type of the image in the crop image view
|
|
||||||
*/
|
|
||||||
public CropImageView.ScaleType scaleType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if to show crop overlay UI what contains the crop window UI surrounded by background over the
|
|
||||||
* cropping image.<br>
|
|
||||||
* default: true, may disable for animation or frame transition.
|
|
||||||
*/
|
|
||||||
public boolean showCropOverlay;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if to show progress bar when image async loading/cropping is in progress.<br>
|
|
||||||
* default: true, disable to provide custom progress bar UI.
|
|
||||||
*/
|
|
||||||
public boolean showProgressBar;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if auto-zoom functionality is enabled.<br>
|
|
||||||
* default: true.
|
|
||||||
*/
|
|
||||||
public boolean autoZoomEnabled;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if multi-touch should be enabled on the crop box default: false
|
|
||||||
*/
|
|
||||||
public boolean multiTouchEnabled;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The max zoom allowed during cropping.
|
|
||||||
*/
|
|
||||||
public int maxZoom;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The initial crop window padding from image borders in percentage of the cropping image
|
|
||||||
* dimensions.
|
|
||||||
*/
|
|
||||||
public float initialCropWindowPaddingRatio;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* whether the width to height aspect ratio should be maintained or free to change.
|
|
||||||
*/
|
|
||||||
public boolean fixAspectRatio;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the X value of the aspect ratio.
|
|
||||||
*/
|
|
||||||
public int aspectRatioX;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the Y value of the aspect ratio.
|
|
||||||
*/
|
|
||||||
public int aspectRatioY;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the thickness of the guidelines lines in pixels. (in pixels)
|
|
||||||
*/
|
|
||||||
public float borderLineThickness;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the color of the guidelines lines
|
|
||||||
*/
|
|
||||||
public int borderLineColor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* thickness of the corner line. (in pixels)
|
|
||||||
*/
|
|
||||||
public float borderCornerThickness;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the offset of corner line from crop window border. (in pixels)
|
|
||||||
*/
|
|
||||||
public float borderCornerOffset;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the length of the corner line away from the corner. (in pixels)
|
|
||||||
*/
|
|
||||||
public float borderCornerLength;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the color of the corner line
|
|
||||||
*/
|
|
||||||
public int borderCornerColor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the thickness of the guidelines lines. (in pixels)
|
|
||||||
*/
|
|
||||||
public float guidelinesThickness;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the color of the guidelines lines
|
|
||||||
*/
|
|
||||||
public int guidelinesColor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the color of the overlay background around the crop window cover the image parts not in the
|
|
||||||
* crop window.
|
|
||||||
*/
|
|
||||||
public int backgroundColor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the min width the crop window is allowed to be. (in pixels)
|
|
||||||
*/
|
|
||||||
public int minCropWindowWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the min height the crop window is allowed to be. (in pixels)
|
|
||||||
*/
|
|
||||||
public int minCropWindowHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the min width the resulting cropping image is allowed to be, affects the cropping window
|
|
||||||
* limits. (in pixels)
|
|
||||||
*/
|
|
||||||
public int minCropResultWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the min height the resulting cropping image is allowed to be, affects the cropping window
|
|
||||||
* limits. (in pixels)
|
|
||||||
*/
|
|
||||||
public int minCropResultHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the max width the resulting cropping image is allowed to be, affects the cropping window
|
|
||||||
* limits. (in pixels)
|
|
||||||
*/
|
|
||||||
public int maxCropResultWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the max height the resulting cropping image is allowed to be, affects the cropping window
|
|
||||||
* limits. (in pixels)
|
|
||||||
*/
|
|
||||||
public int maxCropResultHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the title of the {@link CropImageActivity}
|
|
||||||
*/
|
|
||||||
public CharSequence activityTitle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the color to use for action bar items icons
|
|
||||||
*/
|
|
||||||
public int activityMenuIconColor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the Android Uri to save the cropped image to
|
|
||||||
*/
|
|
||||||
public Uri outputUri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the compression format to use when writing the image
|
|
||||||
*/
|
|
||||||
public Bitmap.CompressFormat outputCompressFormat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the quality (if applicable) to use when writing the image (0 - 100)
|
|
||||||
*/
|
|
||||||
public int outputCompressQuality;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the width to resize the cropped image to (see options)
|
|
||||||
*/
|
|
||||||
public int outputRequestWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the height to resize the cropped image to (see options)
|
|
||||||
*/
|
|
||||||
public int outputRequestHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the resize method to use on the cropped bitmap (see options documentation)
|
|
||||||
*/
|
|
||||||
public CropImageView.RequestSizeOptions outputRequestSizeOptions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if the result of crop image activity should not save the cropped image bitmap
|
|
||||||
*/
|
|
||||||
public boolean noOutputImage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the initial rectangle to set on the cropping image after loading
|
|
||||||
*/
|
|
||||||
public Rect initialCropWindowRectangle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the initial rotation to set on the cropping image after loading (0-360 degrees clockwise)
|
|
||||||
*/
|
|
||||||
public int initialRotation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if to allow (all) rotation during cropping (activity)
|
|
||||||
*/
|
|
||||||
public boolean allowRotation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if to allow (all) flipping during cropping (activity)
|
|
||||||
*/
|
|
||||||
public boolean allowFlipping;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if to allow counter-clockwise rotation during cropping (activity)
|
|
||||||
*/
|
|
||||||
public boolean allowCounterRotation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the amount of degrees to rotate clockwise or counter-clockwise
|
|
||||||
*/
|
|
||||||
public int rotationDegrees;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* whether the image should be flipped horizontally
|
|
||||||
*/
|
|
||||||
public boolean flipHorizontally;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* whether the image should be flipped vertically
|
|
||||||
*/
|
|
||||||
public boolean flipVertically;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* optional, the text of the crop menu crop button
|
|
||||||
*/
|
|
||||||
public CharSequence cropMenuCropButtonTitle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* optional image resource to be used for crop menu crop icon instead of text
|
|
||||||
*/
|
|
||||||
public int cropMenuCropButtonIcon;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init options with defaults.
|
|
||||||
*/
|
|
||||||
public CropImageOptions() {
|
|
||||||
|
|
||||||
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
|
|
||||||
|
|
||||||
cropShape = CropImageView.CropShape.RECTANGLE;
|
|
||||||
snapRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm);
|
|
||||||
touchRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24, dm);
|
|
||||||
guidelines = CropImageView.Guidelines.ON_TOUCH;
|
|
||||||
scaleType = CropImageView.ScaleType.FIT_CENTER;
|
|
||||||
showCropOverlay = true;
|
|
||||||
showProgressBar = true;
|
|
||||||
autoZoomEnabled = true;
|
|
||||||
multiTouchEnabled = false;
|
|
||||||
maxZoom = 4;
|
|
||||||
initialCropWindowPaddingRatio = 0.1f;
|
|
||||||
|
|
||||||
fixAspectRatio = false;
|
|
||||||
aspectRatioX = 1;
|
|
||||||
aspectRatioY = 1;
|
|
||||||
|
|
||||||
borderLineThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm);
|
|
||||||
borderLineColor = Color.argb(170, 255, 255, 255);
|
|
||||||
borderCornerThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, dm);
|
|
||||||
borderCornerOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, dm);
|
|
||||||
borderCornerLength = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14, dm);
|
|
||||||
borderCornerColor = Color.WHITE;
|
|
||||||
|
|
||||||
guidelinesThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, dm);
|
|
||||||
guidelinesColor = Color.argb(170, 255, 255, 255);
|
|
||||||
backgroundColor = Color.argb(119, 0, 0, 0);
|
|
||||||
|
|
||||||
minCropWindowWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm);
|
|
||||||
minCropWindowHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm);
|
|
||||||
minCropResultWidth = 40;
|
|
||||||
minCropResultHeight = 40;
|
|
||||||
maxCropResultWidth = 99999;
|
|
||||||
maxCropResultHeight = 99999;
|
|
||||||
|
|
||||||
activityTitle = "";
|
|
||||||
activityMenuIconColor = 0;
|
|
||||||
|
|
||||||
outputUri = Uri.EMPTY;
|
|
||||||
outputCompressFormat = Bitmap.CompressFormat.JPEG;
|
|
||||||
outputCompressQuality = 90;
|
|
||||||
outputRequestWidth = 0;
|
|
||||||
outputRequestHeight = 0;
|
|
||||||
outputRequestSizeOptions = CropImageView.RequestSizeOptions.NONE;
|
|
||||||
noOutputImage = false;
|
|
||||||
|
|
||||||
initialCropWindowRectangle = null;
|
|
||||||
initialRotation = -1;
|
|
||||||
allowRotation = true;
|
|
||||||
allowFlipping = true;
|
|
||||||
allowCounterRotation = false;
|
|
||||||
rotationDegrees = 90;
|
|
||||||
flipHorizontally = false;
|
|
||||||
flipVertically = false;
|
|
||||||
cropMenuCropButtonTitle = null;
|
|
||||||
|
|
||||||
cropMenuCropButtonIcon = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create object from parcel.
|
|
||||||
*/
|
|
||||||
protected CropImageOptions(Parcel in) {
|
|
||||||
cropShape = CropImageView.CropShape.values()[in.readInt()];
|
|
||||||
snapRadius = in.readFloat();
|
|
||||||
touchRadius = in.readFloat();
|
|
||||||
guidelines = CropImageView.Guidelines.values()[in.readInt()];
|
|
||||||
scaleType = CropImageView.ScaleType.values()[in.readInt()];
|
|
||||||
showCropOverlay = in.readByte() != 0;
|
|
||||||
showProgressBar = in.readByte() != 0;
|
|
||||||
autoZoomEnabled = in.readByte() != 0;
|
|
||||||
multiTouchEnabled = in.readByte() != 0;
|
|
||||||
maxZoom = in.readInt();
|
|
||||||
initialCropWindowPaddingRatio = in.readFloat();
|
|
||||||
fixAspectRatio = in.readByte() != 0;
|
|
||||||
aspectRatioX = in.readInt();
|
|
||||||
aspectRatioY = in.readInt();
|
|
||||||
borderLineThickness = in.readFloat();
|
|
||||||
borderLineColor = in.readInt();
|
|
||||||
borderCornerThickness = in.readFloat();
|
|
||||||
borderCornerOffset = in.readFloat();
|
|
||||||
borderCornerLength = in.readFloat();
|
|
||||||
borderCornerColor = in.readInt();
|
|
||||||
guidelinesThickness = in.readFloat();
|
|
||||||
guidelinesColor = in.readInt();
|
|
||||||
backgroundColor = in.readInt();
|
|
||||||
minCropWindowWidth = in.readInt();
|
|
||||||
minCropWindowHeight = in.readInt();
|
|
||||||
minCropResultWidth = in.readInt();
|
|
||||||
minCropResultHeight = in.readInt();
|
|
||||||
maxCropResultWidth = in.readInt();
|
|
||||||
maxCropResultHeight = in.readInt();
|
|
||||||
activityTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
|
|
||||||
activityMenuIconColor = in.readInt();
|
|
||||||
outputUri = in.readParcelable(Uri.class.getClassLoader());
|
|
||||||
outputCompressFormat = Bitmap.CompressFormat.valueOf(in.readString());
|
|
||||||
outputCompressQuality = in.readInt();
|
|
||||||
outputRequestWidth = in.readInt();
|
|
||||||
outputRequestHeight = in.readInt();
|
|
||||||
outputRequestSizeOptions = CropImageView.RequestSizeOptions.values()[in.readInt()];
|
|
||||||
noOutputImage = in.readByte() != 0;
|
|
||||||
initialCropWindowRectangle = in.readParcelable(Rect.class.getClassLoader());
|
|
||||||
initialRotation = in.readInt();
|
|
||||||
allowRotation = in.readByte() != 0;
|
|
||||||
allowFlipping = in.readByte() != 0;
|
|
||||||
allowCounterRotation = in.readByte() != 0;
|
|
||||||
rotationDegrees = in.readInt();
|
|
||||||
flipHorizontally = in.readByte() != 0;
|
|
||||||
flipVertically = in.readByte() != 0;
|
|
||||||
cropMenuCropButtonTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
|
|
||||||
cropMenuCropButtonIcon = in.readInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeToParcel(Parcel dest, int flags) {
|
|
||||||
dest.writeInt(cropShape.ordinal());
|
|
||||||
dest.writeFloat(snapRadius);
|
|
||||||
dest.writeFloat(touchRadius);
|
|
||||||
dest.writeInt(guidelines.ordinal());
|
|
||||||
dest.writeInt(scaleType.ordinal());
|
|
||||||
dest.writeByte((byte) (showCropOverlay ? 1 : 0));
|
|
||||||
dest.writeByte((byte) (showProgressBar ? 1 : 0));
|
|
||||||
dest.writeByte((byte) (autoZoomEnabled ? 1 : 0));
|
|
||||||
dest.writeByte((byte) (multiTouchEnabled ? 1 : 0));
|
|
||||||
dest.writeInt(maxZoom);
|
|
||||||
dest.writeFloat(initialCropWindowPaddingRatio);
|
|
||||||
dest.writeByte((byte) (fixAspectRatio ? 1 : 0));
|
|
||||||
dest.writeInt(aspectRatioX);
|
|
||||||
dest.writeInt(aspectRatioY);
|
|
||||||
dest.writeFloat(borderLineThickness);
|
|
||||||
dest.writeInt(borderLineColor);
|
|
||||||
dest.writeFloat(borderCornerThickness);
|
|
||||||
dest.writeFloat(borderCornerOffset);
|
|
||||||
dest.writeFloat(borderCornerLength);
|
|
||||||
dest.writeInt(borderCornerColor);
|
|
||||||
dest.writeFloat(guidelinesThickness);
|
|
||||||
dest.writeInt(guidelinesColor);
|
|
||||||
dest.writeInt(backgroundColor);
|
|
||||||
dest.writeInt(minCropWindowWidth);
|
|
||||||
dest.writeInt(minCropWindowHeight);
|
|
||||||
dest.writeInt(minCropResultWidth);
|
|
||||||
dest.writeInt(minCropResultHeight);
|
|
||||||
dest.writeInt(maxCropResultWidth);
|
|
||||||
dest.writeInt(maxCropResultHeight);
|
|
||||||
TextUtils.writeToParcel(activityTitle, dest, flags);
|
|
||||||
dest.writeInt(activityMenuIconColor);
|
|
||||||
dest.writeParcelable(outputUri, flags);
|
|
||||||
dest.writeString(outputCompressFormat.name());
|
|
||||||
dest.writeInt(outputCompressQuality);
|
|
||||||
dest.writeInt(outputRequestWidth);
|
|
||||||
dest.writeInt(outputRequestHeight);
|
|
||||||
dest.writeInt(outputRequestSizeOptions.ordinal());
|
|
||||||
dest.writeInt(noOutputImage ? 1 : 0);
|
|
||||||
dest.writeParcelable(initialCropWindowRectangle, flags);
|
|
||||||
dest.writeInt(initialRotation);
|
|
||||||
dest.writeByte((byte) (allowRotation ? 1 : 0));
|
|
||||||
dest.writeByte((byte) (allowFlipping ? 1 : 0));
|
|
||||||
dest.writeByte((byte) (allowCounterRotation ? 1 : 0));
|
|
||||||
dest.writeInt(rotationDegrees);
|
|
||||||
dest.writeByte((byte) (flipHorizontally ? 1 : 0));
|
|
||||||
dest.writeByte((byte) (flipVertically ? 1 : 0));
|
|
||||||
TextUtils.writeToParcel(cropMenuCropButtonTitle, dest, flags);
|
|
||||||
dest.writeInt(cropMenuCropButtonIcon);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int describeContents() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate all the options are withing valid range.
|
|
||||||
*
|
|
||||||
* @throws IllegalArgumentException if any of the options is not valid
|
|
||||||
*/
|
|
||||||
public void validate() {
|
|
||||||
if (maxZoom < 0) {
|
|
||||||
throw new IllegalArgumentException("Cannot set max zoom to a number < 1");
|
|
||||||
}
|
|
||||||
if (touchRadius < 0) {
|
|
||||||
throw new IllegalArgumentException("Cannot set touch radius value to a number <= 0 ");
|
|
||||||
}
|
|
||||||
if (initialCropWindowPaddingRatio < 0 || initialCropWindowPaddingRatio >= 0.5) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Cannot set initial crop window padding value to a number < 0 or >= 0.5");
|
|
||||||
}
|
|
||||||
if (aspectRatioX <= 0) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Cannot set aspect ratio value to a number less than or equal to 0.");
|
|
||||||
}
|
|
||||||
if (aspectRatioY <= 0) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Cannot set aspect ratio value to a number less than or equal to 0.");
|
|
||||||
}
|
|
||||||
if (borderLineThickness < 0) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Cannot set line thickness value to a number less than 0.");
|
|
||||||
}
|
|
||||||
if (borderCornerThickness < 0) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Cannot set corner thickness value to a number less than 0.");
|
|
||||||
}
|
|
||||||
if (guidelinesThickness < 0) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Cannot set guidelines thickness value to a number less than 0.");
|
|
||||||
}
|
|
||||||
if (minCropWindowHeight < 0) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Cannot set min crop window height value to a number < 0 ");
|
|
||||||
}
|
|
||||||
if (minCropResultWidth < 0) {
|
|
||||||
throw new IllegalArgumentException("Cannot set min crop result width value to a number < 0 ");
|
|
||||||
}
|
|
||||||
if (minCropResultHeight < 0) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Cannot set min crop result height value to a number < 0 ");
|
|
||||||
}
|
|
||||||
if (maxCropResultWidth < minCropResultWidth) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Cannot set max crop result width to smaller value than min crop result width");
|
|
||||||
}
|
|
||||||
if (maxCropResultHeight < minCropResultHeight) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Cannot set max crop result height to smaller value than min crop result height");
|
|
||||||
}
|
|
||||||
if (outputRequestWidth < 0) {
|
|
||||||
throw new IllegalArgumentException("Cannot set request width value to a number < 0 ");
|
|
||||||
}
|
|
||||||
if (outputRequestHeight < 0) {
|
|
||||||
throw new IllegalArgumentException("Cannot set request height value to a number < 0 ");
|
|
||||||
}
|
|
||||||
if (rotationDegrees < 0 || rotationDegrees > 360) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Cannot set rotation degrees value to a number < 0 or > 360");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,405 +0,0 @@
|
||||||
// "Therefore those skilled at the unorthodox
|
|
||||||
// are infinite as heaven and earth,
|
|
||||||
// inexhaustible as the great rivers.
|
|
||||||
// When they come to an end,
|
|
||||||
// they begin again,
|
|
||||||
// like the days and months;
|
|
||||||
// they die and are reborn,
|
|
||||||
// like the four seasons."
|
|
||||||
//
|
|
||||||
// - Sun Tsu,
|
|
||||||
// "The Art of War"
|
|
||||||
|
|
||||||
package com.theartofdev.edmodo.cropper;
|
|
||||||
|
|
||||||
import android.graphics.RectF;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler from crop window stuff, moving and knowing possition.
|
|
||||||
*/
|
|
||||||
final class CropWindowHandler {
|
|
||||||
|
|
||||||
// region: Fields and Consts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The 4 edges of the crop window defining its coordinates and size
|
|
||||||
*/
|
|
||||||
private final RectF mEdges = new RectF();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rectangle used to return the edges rectangle without ability to change it and without creating
|
|
||||||
* new all the time.
|
|
||||||
*/
|
|
||||||
private final RectF mGetEdges = new RectF();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum width in pixels that the crop window can get.
|
|
||||||
*/
|
|
||||||
private float mMinCropWindowWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum height in pixels that the crop window can get.
|
|
||||||
*/
|
|
||||||
private float mMinCropWindowHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum width in pixels that the crop window can CURRENTLY get.
|
|
||||||
*/
|
|
||||||
private float mMaxCropWindowWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum height in pixels that the crop window can CURRENTLY get.
|
|
||||||
*/
|
|
||||||
private float mMaxCropWindowHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum width in pixels that the result of cropping an image can get, affects crop window width
|
|
||||||
* adjusted by width scale factor.
|
|
||||||
*/
|
|
||||||
private float mMinCropResultWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum height in pixels that the result of cropping an image can get, affects crop window
|
|
||||||
* height adjusted by height scale factor.
|
|
||||||
*/
|
|
||||||
private float mMinCropResultHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum width in pixels that the result of cropping an image can get, affects crop window width
|
|
||||||
* adjusted by width scale factor.
|
|
||||||
*/
|
|
||||||
private float mMaxCropResultWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum height in pixels that the result of cropping an image can get, affects crop window
|
|
||||||
* height adjusted by height scale factor.
|
|
||||||
*/
|
|
||||||
private float mMaxCropResultHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The width scale factor of shown image and actual image
|
|
||||||
*/
|
|
||||||
private float mScaleFactorWidth = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The height scale factor of shown image and actual image
|
|
||||||
*/
|
|
||||||
private float mScaleFactorHeight = 1;
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the specified coordinate is in the target touch zone for a corner handle.
|
|
||||||
*
|
|
||||||
* @param x the x-coordinate of the touch point
|
|
||||||
* @param y the y-coordinate of the touch point
|
|
||||||
* @param handleX the x-coordinate of the corner handle
|
|
||||||
* @param handleY the y-coordinate of the corner handle
|
|
||||||
* @param targetRadius the target radius in pixels
|
|
||||||
* @return true if the touch point is in the target touch zone; false otherwise
|
|
||||||
*/
|
|
||||||
private static boolean isInCornerTargetZone(
|
|
||||||
float x, float y, float handleX, float handleY, float targetRadius) {
|
|
||||||
return Math.abs(x - handleX) <= targetRadius && Math.abs(y - handleY) <= targetRadius;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the specified coordinate is in the target touch zone for a horizontal bar handle.
|
|
||||||
*
|
|
||||||
* @param x the x-coordinate of the touch point
|
|
||||||
* @param y the y-coordinate of the touch point
|
|
||||||
* @param handleXStart the left x-coordinate of the horizontal bar handle
|
|
||||||
* @param handleXEnd the right x-coordinate of the horizontal bar handle
|
|
||||||
* @param handleY the y-coordinate of the horizontal bar handle
|
|
||||||
* @param targetRadius the target radius in pixels
|
|
||||||
* @return true if the touch point is in the target touch zone; false otherwise
|
|
||||||
*/
|
|
||||||
private static boolean isInHorizontalTargetZone(
|
|
||||||
float x, float y, float handleXStart, float handleXEnd, float handleY, float targetRadius) {
|
|
||||||
return x > handleXStart && x < handleXEnd && Math.abs(y - handleY) <= targetRadius;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the specified coordinate is in the target touch zone for a vertical bar handle.
|
|
||||||
*
|
|
||||||
* @param x the x-coordinate of the touch point
|
|
||||||
* @param y the y-coordinate of the touch point
|
|
||||||
* @param handleX the x-coordinate of the vertical bar handle
|
|
||||||
* @param handleYStart the top y-coordinate of the vertical bar handle
|
|
||||||
* @param handleYEnd the bottom y-coordinate of the vertical bar handle
|
|
||||||
* @param targetRadius the target radius in pixels
|
|
||||||
* @return true if the touch point is in the target touch zone; false otherwise
|
|
||||||
*/
|
|
||||||
private static boolean isInVerticalTargetZone(
|
|
||||||
float x, float y, float handleX, float handleYStart, float handleYEnd, float targetRadius) {
|
|
||||||
return Math.abs(x - handleX) <= targetRadius && y > handleYStart && y < handleYEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the specified coordinate falls anywhere inside the given bounds.
|
|
||||||
*
|
|
||||||
* @param x the x-coordinate of the touch point
|
|
||||||
* @param y the y-coordinate of the touch point
|
|
||||||
* @param left the x-coordinate of the left bound
|
|
||||||
* @param top the y-coordinate of the top bound
|
|
||||||
* @param right the x-coordinate of the right bound
|
|
||||||
* @param bottom the y-coordinate of the bottom bound
|
|
||||||
* @return true if the touch point is inside the bounding rectangle; false otherwise
|
|
||||||
*/
|
|
||||||
private static boolean isInCenterTargetZone(
|
|
||||||
float x, float y, float left, float top, float right, float bottom) {
|
|
||||||
return x > left && x < right && y > top && y < bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the left/top/right/bottom coordinates of the crop window.
|
|
||||||
*/
|
|
||||||
public RectF getRect() {
|
|
||||||
mGetEdges.set(mEdges);
|
|
||||||
return mGetEdges;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the left/top/right/bottom coordinates of the crop window.
|
|
||||||
*/
|
|
||||||
public void setRect(RectF rect) {
|
|
||||||
mEdges.set(rect);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum width in pixels that the crop window can get.
|
|
||||||
*/
|
|
||||||
public float getMinCropWidth() {
|
|
||||||
return Math.max(mMinCropWindowWidth, mMinCropResultWidth / mScaleFactorWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum height in pixels that the crop window can get.
|
|
||||||
*/
|
|
||||||
public float getMinCropHeight() {
|
|
||||||
return Math.max(mMinCropWindowHeight, mMinCropResultHeight / mScaleFactorHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum width in pixels that the crop window can get.
|
|
||||||
*/
|
|
||||||
public float getMaxCropWidth() {
|
|
||||||
return Math.min(mMaxCropWindowWidth, mMaxCropResultWidth / mScaleFactorWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum height in pixels that the crop window can get.
|
|
||||||
*/
|
|
||||||
public float getMaxCropHeight() {
|
|
||||||
return Math.min(mMaxCropWindowHeight, mMaxCropResultHeight / mScaleFactorHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the scale factor (on width) of the showen image to original image.
|
|
||||||
*/
|
|
||||||
public float getScaleFactorWidth() {
|
|
||||||
return mScaleFactorWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the scale factor (on height) of the showen image to original image.
|
|
||||||
*/
|
|
||||||
public float getScaleFactorHeight() {
|
|
||||||
return mScaleFactorHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the min size the resulting cropping image is allowed to be, affects the cropping window limits
|
|
||||||
* (in pixels).<br>
|
|
||||||
*/
|
|
||||||
public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
|
|
||||||
mMinCropResultWidth = minCropResultWidth;
|
|
||||||
mMinCropResultHeight = minCropResultHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the max size the resulting cropping image is allowed to be, affects the cropping window limits
|
|
||||||
* (in pixels).<br>
|
|
||||||
*/
|
|
||||||
public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
|
|
||||||
mMaxCropResultWidth = maxCropResultWidth;
|
|
||||||
mMaxCropResultHeight = maxCropResultHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// region: Private methods
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set the max width/height and scale factor of the showen image to original image to scale the
|
|
||||||
* limits appropriately.
|
|
||||||
*/
|
|
||||||
public void setCropWindowLimits(
|
|
||||||
float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) {
|
|
||||||
mMaxCropWindowWidth = maxWidth;
|
|
||||||
mMaxCropWindowHeight = maxHeight;
|
|
||||||
mScaleFactorWidth = scaleFactorWidth;
|
|
||||||
mScaleFactorHeight = scaleFactorHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the variables to be used during crop window handling.
|
|
||||||
*/
|
|
||||||
public void setInitialAttributeValues(CropImageOptions options) {
|
|
||||||
mMinCropWindowWidth = options.minCropWindowWidth;
|
|
||||||
mMinCropWindowHeight = options.minCropWindowHeight;
|
|
||||||
mMinCropResultWidth = options.minCropResultWidth;
|
|
||||||
mMinCropResultHeight = options.minCropResultHeight;
|
|
||||||
mMaxCropResultWidth = options.maxCropResultWidth;
|
|
||||||
mMaxCropResultHeight = options.maxCropResultHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether the crop window is small enough that the guidelines should be shown. Public
|
|
||||||
* because this function is also used to determine if the center handle should be focused.
|
|
||||||
*
|
|
||||||
* @return boolean Whether the guidelines should be shown or not
|
|
||||||
*/
|
|
||||||
public boolean showGuidelines() {
|
|
||||||
return !(mEdges.width() < 100 || mEdges.height() < 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
|
|
||||||
* box, and the touch radius.
|
|
||||||
*
|
|
||||||
* @param x the x-coordinate of the touch point
|
|
||||||
* @param y the y-coordinate of the touch point
|
|
||||||
* @param targetRadius the target radius in pixels
|
|
||||||
* @return the Handle that was pressed; null if no Handle was pressed
|
|
||||||
*/
|
|
||||||
public CropWindowMoveHandler getMoveHandler(
|
|
||||||
float x, float y, float targetRadius, CropImageView.CropShape cropShape) {
|
|
||||||
CropWindowMoveHandler.Type type =
|
|
||||||
cropShape == CropImageView.CropShape.OVAL
|
|
||||||
? getOvalPressedMoveType(x, y)
|
|
||||||
: getRectanglePressedMoveType(x, y, targetRadius);
|
|
||||||
return type != null ? new CropWindowMoveHandler(type, this, x, y) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
|
|
||||||
* box, and the touch radius.
|
|
||||||
*
|
|
||||||
* @param x the x-coordinate of the touch point
|
|
||||||
* @param y the y-coordinate of the touch point
|
|
||||||
* @param targetRadius the target radius in pixels
|
|
||||||
* @return the Handle that was pressed; null if no Handle was pressed
|
|
||||||
*/
|
|
||||||
private CropWindowMoveHandler.Type getRectanglePressedMoveType(
|
|
||||||
float x, float y, float targetRadius) {
|
|
||||||
CropWindowMoveHandler.Type moveType = null;
|
|
||||||
|
|
||||||
// Note: corner-handles take precedence, then side-handles, then center.
|
|
||||||
if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.left, mEdges.top, targetRadius)) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.TOP_LEFT;
|
|
||||||
} else if (CropWindowHandler.isInCornerTargetZone(
|
|
||||||
x, y, mEdges.right, mEdges.top, targetRadius)) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.TOP_RIGHT;
|
|
||||||
} else if (CropWindowHandler.isInCornerTargetZone(
|
|
||||||
x, y, mEdges.left, mEdges.bottom, targetRadius)) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT;
|
|
||||||
} else if (CropWindowHandler.isInCornerTargetZone(
|
|
||||||
x, y, mEdges.right, mEdges.bottom, targetRadius)) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT;
|
|
||||||
} else if (CropWindowHandler.isInCenterTargetZone(
|
|
||||||
x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom)
|
|
||||||
&& focusCenter()) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.CENTER;
|
|
||||||
} else if (CropWindowHandler.isInHorizontalTargetZone(
|
|
||||||
x, y, mEdges.left, mEdges.right, mEdges.top, targetRadius)) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.TOP;
|
|
||||||
} else if (CropWindowHandler.isInHorizontalTargetZone(
|
|
||||||
x, y, mEdges.left, mEdges.right, mEdges.bottom, targetRadius)) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.BOTTOM;
|
|
||||||
} else if (CropWindowHandler.isInVerticalTargetZone(
|
|
||||||
x, y, mEdges.left, mEdges.top, mEdges.bottom, targetRadius)) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.LEFT;
|
|
||||||
} else if (CropWindowHandler.isInVerticalTargetZone(
|
|
||||||
x, y, mEdges.right, mEdges.top, mEdges.bottom, targetRadius)) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.RIGHT;
|
|
||||||
} else if (CropWindowHandler.isInCenterTargetZone(
|
|
||||||
x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom)
|
|
||||||
&& !focusCenter()) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.CENTER;
|
|
||||||
}
|
|
||||||
|
|
||||||
return moveType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
|
|
||||||
* box/oval, and the touch radius.
|
|
||||||
*
|
|
||||||
* @param x the x-coordinate of the touch point
|
|
||||||
* @param y the y-coordinate of the touch point
|
|
||||||
* @return the Handle that was pressed; null if no Handle was pressed
|
|
||||||
*/
|
|
||||||
private CropWindowMoveHandler.Type getOvalPressedMoveType(float x, float y) {
|
|
||||||
|
|
||||||
/*
|
|
||||||
Use a 6x6 grid system divided into 9 "handles", with the center the biggest region. While
|
|
||||||
this is not perfect, it's a good quick-to-ship approach.
|
|
||||||
|
|
||||||
TL T T T T TR
|
|
||||||
L C C C C R
|
|
||||||
L C C C C R
|
|
||||||
L C C C C R
|
|
||||||
L C C C C R
|
|
||||||
BL B B B B BR
|
|
||||||
*/
|
|
||||||
|
|
||||||
float cellLength = mEdges.width() / 6;
|
|
||||||
float leftCenter = mEdges.left + cellLength;
|
|
||||||
float rightCenter = mEdges.left + (5 * cellLength);
|
|
||||||
|
|
||||||
float cellHeight = mEdges.height() / 6;
|
|
||||||
float topCenter = mEdges.top + cellHeight;
|
|
||||||
float bottomCenter = mEdges.top + 5 * cellHeight;
|
|
||||||
|
|
||||||
CropWindowMoveHandler.Type moveType;
|
|
||||||
if (x < leftCenter) {
|
|
||||||
if (y < topCenter) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.TOP_LEFT;
|
|
||||||
} else if (y < bottomCenter) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.LEFT;
|
|
||||||
} else {
|
|
||||||
moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT;
|
|
||||||
}
|
|
||||||
} else if (x < rightCenter) {
|
|
||||||
if (y < topCenter) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.TOP;
|
|
||||||
} else if (y < bottomCenter) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.CENTER;
|
|
||||||
} else {
|
|
||||||
moveType = CropWindowMoveHandler.Type.BOTTOM;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (y < topCenter) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.TOP_RIGHT;
|
|
||||||
} else if (y < bottomCenter) {
|
|
||||||
moveType = CropWindowMoveHandler.Type.RIGHT;
|
|
||||||
} else {
|
|
||||||
moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return moveType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if the cropper should focus on the center handle or the side handles. If it is a
|
|
||||||
* small image, focus on the center handle so the user can move it. If it is a large image, focus
|
|
||||||
* on the side handles so user can grab them. Corresponds to the appearance of the
|
|
||||||
* RuleOfThirdsGuidelines.
|
|
||||||
*
|
|
||||||
* @return true if it is small enough such that it should focus on the center; less than
|
|
||||||
* show_guidelines limit
|
|
||||||
*/
|
|
||||||
private boolean focusCenter() {
|
|
||||||
return !showGuidelines();
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
|
@ -1,786 +0,0 @@
|
||||||
// "Therefore those skilled at the unorthodox
|
|
||||||
// are infinite as heaven and earth,
|
|
||||||
// inexhaustible as the great rivers.
|
|
||||||
// When they come to an end,
|
|
||||||
// they begin again,
|
|
||||||
// like the days and months;
|
|
||||||
// they die and are reborn,
|
|
||||||
// like the four seasons."
|
|
||||||
//
|
|
||||||
// - Sun Tsu,
|
|
||||||
// "The Art of War"
|
|
||||||
|
|
||||||
package com.theartofdev.edmodo.cropper;
|
|
||||||
|
|
||||||
import android.graphics.Matrix;
|
|
||||||
import android.graphics.PointF;
|
|
||||||
import android.graphics.RectF;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler to update crop window edges by the move type - Horizontal, Vertical, Corner or Center.
|
|
||||||
* <br>
|
|
||||||
*/
|
|
||||||
final class CropWindowMoveHandler {
|
|
||||||
|
|
||||||
// region: Fields and Consts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Matrix used for rectangle rotation handling
|
|
||||||
*/
|
|
||||||
private static final Matrix MATRIX = new Matrix();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum width in pixels that the crop window can get.
|
|
||||||
*/
|
|
||||||
private final float mMinCropWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum width in pixels that the crop window can get.
|
|
||||||
*/
|
|
||||||
private final float mMinCropHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum height in pixels that the crop window can get.
|
|
||||||
*/
|
|
||||||
private final float mMaxCropWidth;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum height in pixels that the crop window can get.
|
|
||||||
*/
|
|
||||||
private final float mMaxCropHeight;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of crop window move that is handled.
|
|
||||||
*/
|
|
||||||
private final Type mType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the x and y offset between the exact touch location and the exact handle location that is
|
|
||||||
* activated. There may be an offset because we allow for some leeway (specified by mHandleRadius)
|
|
||||||
* in activating a handle. However, we want to maintain these offset values while the handle is
|
|
||||||
* being dragged so that the handle doesn't jump.
|
|
||||||
*/
|
|
||||||
private final PointF mTouchOffset = new PointF();
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param edgeMoveType the type of move this handler is executing
|
|
||||||
* @param horizontalEdge the primary edge associated with this handle; may be null
|
|
||||||
* @param verticalEdge the secondary edge associated with this handle; may be null
|
|
||||||
* @param cropWindowHandler main crop window handle to get and update the crop window edges
|
|
||||||
* @param touchX the location of the initial toch possition to measure move distance
|
|
||||||
* @param touchY the location of the initial toch possition to measure move distance
|
|
||||||
*/
|
|
||||||
public CropWindowMoveHandler(
|
|
||||||
Type type, CropWindowHandler cropWindowHandler, float touchX, float touchY) {
|
|
||||||
mType = type;
|
|
||||||
mMinCropWidth = cropWindowHandler.getMinCropWidth();
|
|
||||||
mMinCropHeight = cropWindowHandler.getMinCropHeight();
|
|
||||||
mMaxCropWidth = cropWindowHandler.getMaxCropWidth();
|
|
||||||
mMaxCropHeight = cropWindowHandler.getMaxCropHeight();
|
|
||||||
calculateTouchOffset(cropWindowHandler.getRect(), touchX, touchY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the aspect ratio given a rectangle.
|
|
||||||
*/
|
|
||||||
private static float calculateAspectRatio(float left, float top, float right, float bottom) {
|
|
||||||
return (right - left) / (bottom - top);
|
|
||||||
}
|
|
||||||
|
|
||||||
// region: Private methods
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the crop window by change in the toch location.<br>
|
|
||||||
* Move type handled by this instance, as initialized in creation, affects how the change in toch
|
|
||||||
* location changes the crop window position and size.<br>
|
|
||||||
* After the crop window position/size is changed by toch move it may result in values that
|
|
||||||
* vialate contraints: outside the bounds of the shown bitmap, smaller/larger than min/max size or
|
|
||||||
* missmatch in aspect ratio. So a series of fixes is executed on "secondary" edges to adjust it
|
|
||||||
* by the "primary" edge movement.<br>
|
|
||||||
* Primary is the edge directly affected by move type, secondary is the other edge.<br>
|
|
||||||
* The crop window is changed by directly setting the Edge coordinates.
|
|
||||||
*
|
|
||||||
* @param x the new x-coordinate of this handle
|
|
||||||
* @param y the new y-coordinate of this handle
|
|
||||||
* @param bounds the bounding rectangle of the image
|
|
||||||
* @param viewWidth The bounding image view width used to know the crop overlay is at view edges.
|
|
||||||
* @param viewHeight The bounding image view height used to know the crop overlay is at view
|
|
||||||
* edges.
|
|
||||||
* @param parentView the parent View containing the image
|
|
||||||
* @param snapMargin the maximum distance (in pixels) at which the crop window should snap to the
|
|
||||||
* image
|
|
||||||
* @param fixedAspectRatio is the aspect ration fixed and 'targetAspectRatio' should be used
|
|
||||||
* @param aspectRatio the aspect ratio to maintain
|
|
||||||
*/
|
|
||||||
public void move(
|
|
||||||
RectF rect,
|
|
||||||
float x,
|
|
||||||
float y,
|
|
||||||
RectF bounds,
|
|
||||||
int viewWidth,
|
|
||||||
int viewHeight,
|
|
||||||
float snapMargin,
|
|
||||||
boolean fixedAspectRatio,
|
|
||||||
float aspectRatio) {
|
|
||||||
|
|
||||||
// Adjust the coordinates for the finger position's offset (i.e. the
|
|
||||||
// distance from the initial touch to the precise handle location).
|
|
||||||
// We want to maintain the initial touch's distance to the pressed
|
|
||||||
// handle so that the crop window size does not "jump".
|
|
||||||
float adjX = x + mTouchOffset.x;
|
|
||||||
float adjY = y + mTouchOffset.y;
|
|
||||||
|
|
||||||
if (mType == Type.CENTER) {
|
|
||||||
moveCenter(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin);
|
|
||||||
} else {
|
|
||||||
if (fixedAspectRatio) {
|
|
||||||
moveSizeWithFixedAspectRatio(
|
|
||||||
rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin, aspectRatio);
|
|
||||||
} else {
|
|
||||||
moveSizeWithFreeAspectRatio(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the offset of the touch point from the precise location of the specified handle.<br>
|
|
||||||
* Save these values in a member variable since we want to maintain this offset as we drag the
|
|
||||||
* handle.
|
|
||||||
*/
|
|
||||||
private void calculateTouchOffset(RectF rect, float touchX, float touchY) {
|
|
||||||
|
|
||||||
float touchOffsetX = 0;
|
|
||||||
float touchOffsetY = 0;
|
|
||||||
|
|
||||||
// Calculate the offset from the appropriate handle.
|
|
||||||
switch (mType) {
|
|
||||||
case TOP_LEFT:
|
|
||||||
touchOffsetX = rect.left - touchX;
|
|
||||||
touchOffsetY = rect.top - touchY;
|
|
||||||
break;
|
|
||||||
case TOP_RIGHT:
|
|
||||||
touchOffsetX = rect.right - touchX;
|
|
||||||
touchOffsetY = rect.top - touchY;
|
|
||||||
break;
|
|
||||||
case BOTTOM_LEFT:
|
|
||||||
touchOffsetX = rect.left - touchX;
|
|
||||||
touchOffsetY = rect.bottom - touchY;
|
|
||||||
break;
|
|
||||||
case BOTTOM_RIGHT:
|
|
||||||
touchOffsetX = rect.right - touchX;
|
|
||||||
touchOffsetY = rect.bottom - touchY;
|
|
||||||
break;
|
|
||||||
case LEFT:
|
|
||||||
touchOffsetX = rect.left - touchX;
|
|
||||||
touchOffsetY = 0;
|
|
||||||
break;
|
|
||||||
case TOP:
|
|
||||||
touchOffsetX = 0;
|
|
||||||
touchOffsetY = rect.top - touchY;
|
|
||||||
break;
|
|
||||||
case RIGHT:
|
|
||||||
touchOffsetX = rect.right - touchX;
|
|
||||||
touchOffsetY = 0;
|
|
||||||
break;
|
|
||||||
case BOTTOM:
|
|
||||||
touchOffsetX = 0;
|
|
||||||
touchOffsetY = rect.bottom - touchY;
|
|
||||||
break;
|
|
||||||
case CENTER:
|
|
||||||
touchOffsetX = rect.centerX() - touchX;
|
|
||||||
touchOffsetY = rect.centerY() - touchY;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
mTouchOffset.x = touchOffsetX;
|
|
||||||
mTouchOffset.y = touchOffsetY;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Center move only changes the position of the crop window without changing the size.
|
|
||||||
*/
|
|
||||||
private void moveCenter(
|
|
||||||
RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapRadius) {
|
|
||||||
float dx = x - rect.centerX();
|
|
||||||
float dy = y - rect.centerY();
|
|
||||||
if (rect.left + dx < 0
|
|
||||||
|| rect.right + dx > viewWidth
|
|
||||||
|| rect.left + dx < bounds.left
|
|
||||||
|| rect.right + dx > bounds.right) {
|
|
||||||
dx /= 1.05f;
|
|
||||||
mTouchOffset.x -= dx / 2;
|
|
||||||
}
|
|
||||||
if (rect.top + dy < 0
|
|
||||||
|| rect.bottom + dy > viewHeight
|
|
||||||
|| rect.top + dy < bounds.top
|
|
||||||
|| rect.bottom + dy > bounds.bottom) {
|
|
||||||
dy /= 1.05f;
|
|
||||||
mTouchOffset.y -= dy / 2;
|
|
||||||
}
|
|
||||||
rect.offset(dx, dy);
|
|
||||||
snapEdgesToBounds(rect, bounds, snapRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the size of the crop window on the required edge (or edges for corner size move) without
|
|
||||||
* affecting "secondary" edges.<br>
|
|
||||||
* Only the primary edge(s) are fixed to stay within limits.
|
|
||||||
*/
|
|
||||||
private void moveSizeWithFreeAspectRatio(
|
|
||||||
RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapMargin) {
|
|
||||||
switch (mType) {
|
|
||||||
case TOP_LEFT:
|
|
||||||
adjustTop(rect, y, bounds, snapMargin, 0, false, false);
|
|
||||||
adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
|
|
||||||
break;
|
|
||||||
case TOP_RIGHT:
|
|
||||||
adjustTop(rect, y, bounds, snapMargin, 0, false, false);
|
|
||||||
adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
|
|
||||||
break;
|
|
||||||
case BOTTOM_LEFT:
|
|
||||||
adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
|
|
||||||
adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
|
|
||||||
break;
|
|
||||||
case BOTTOM_RIGHT:
|
|
||||||
adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
|
|
||||||
adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
|
|
||||||
break;
|
|
||||||
case LEFT:
|
|
||||||
adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
|
|
||||||
break;
|
|
||||||
case TOP:
|
|
||||||
adjustTop(rect, y, bounds, snapMargin, 0, false, false);
|
|
||||||
break;
|
|
||||||
case RIGHT:
|
|
||||||
adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
|
|
||||||
break;
|
|
||||||
case BOTTOM:
|
|
||||||
adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the size of the crop window on the required "primary" edge WITH affect to relevant
|
|
||||||
* "secondary" edge via aspect ratio.<br>
|
|
||||||
* Example: change in the left edge (primary) will affect top and bottom edges (secondary) to
|
|
||||||
* preserve the given aspect ratio.
|
|
||||||
*/
|
|
||||||
private void moveSizeWithFixedAspectRatio(
|
|
||||||
RectF rect,
|
|
||||||
float x,
|
|
||||||
float y,
|
|
||||||
RectF bounds,
|
|
||||||
int viewWidth,
|
|
||||||
int viewHeight,
|
|
||||||
float snapMargin,
|
|
||||||
float aspectRatio) {
|
|
||||||
switch (mType) {
|
|
||||||
case TOP_LEFT:
|
|
||||||
if (calculateAspectRatio(x, y, rect.right, rect.bottom) < aspectRatio) {
|
|
||||||
adjustTop(rect, y, bounds, snapMargin, aspectRatio, true, false);
|
|
||||||
adjustLeftByAspectRatio(rect, aspectRatio);
|
|
||||||
} else {
|
|
||||||
adjustLeft(rect, x, bounds, snapMargin, aspectRatio, true, false);
|
|
||||||
adjustTopByAspectRatio(rect, aspectRatio);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case TOP_RIGHT:
|
|
||||||
if (calculateAspectRatio(rect.left, y, x, rect.bottom) < aspectRatio) {
|
|
||||||
adjustTop(rect, y, bounds, snapMargin, aspectRatio, false, true);
|
|
||||||
adjustRightByAspectRatio(rect, aspectRatio);
|
|
||||||
} else {
|
|
||||||
adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, true, false);
|
|
||||||
adjustTopByAspectRatio(rect, aspectRatio);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case BOTTOM_LEFT:
|
|
||||||
if (calculateAspectRatio(x, rect.top, rect.right, y) < aspectRatio) {
|
|
||||||
adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, true, false);
|
|
||||||
adjustLeftByAspectRatio(rect, aspectRatio);
|
|
||||||
} else {
|
|
||||||
adjustLeft(rect, x, bounds, snapMargin, aspectRatio, false, true);
|
|
||||||
adjustBottomByAspectRatio(rect, aspectRatio);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case BOTTOM_RIGHT:
|
|
||||||
if (calculateAspectRatio(rect.left, rect.top, x, y) < aspectRatio) {
|
|
||||||
adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, false, true);
|
|
||||||
adjustRightByAspectRatio(rect, aspectRatio);
|
|
||||||
} else {
|
|
||||||
adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, false, true);
|
|
||||||
adjustBottomByAspectRatio(rect, aspectRatio);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case LEFT:
|
|
||||||
adjustLeft(rect, x, bounds, snapMargin, aspectRatio, true, true);
|
|
||||||
adjustTopBottomByAspectRatio(rect, bounds, aspectRatio);
|
|
||||||
break;
|
|
||||||
case TOP:
|
|
||||||
adjustTop(rect, y, bounds, snapMargin, aspectRatio, true, true);
|
|
||||||
adjustLeftRightByAspectRatio(rect, bounds, aspectRatio);
|
|
||||||
break;
|
|
||||||
case RIGHT:
|
|
||||||
adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, true, true);
|
|
||||||
adjustTopBottomByAspectRatio(rect, bounds, aspectRatio);
|
|
||||||
break;
|
|
||||||
case BOTTOM:
|
|
||||||
adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, true, true);
|
|
||||||
adjustLeftRightByAspectRatio(rect, bounds, aspectRatio);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if edges have gone out of bounds (including snap margin), and fix if needed.
|
|
||||||
*/
|
|
||||||
private void snapEdgesToBounds(RectF edges, RectF bounds, float margin) {
|
|
||||||
if (edges.left < bounds.left + margin) {
|
|
||||||
edges.offset(bounds.left - edges.left, 0);
|
|
||||||
}
|
|
||||||
if (edges.top < bounds.top + margin) {
|
|
||||||
edges.offset(0, bounds.top - edges.top);
|
|
||||||
}
|
|
||||||
if (edges.right > bounds.right - margin) {
|
|
||||||
edges.offset(bounds.right - edges.right, 0);
|
|
||||||
}
|
|
||||||
if (edges.bottom > bounds.bottom - margin) {
|
|
||||||
edges.offset(0, bounds.bottom - edges.bottom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the resulting x-position of the left edge of the crop window given the handle's position
|
|
||||||
* and the image's bounding box and snap radius.
|
|
||||||
*
|
|
||||||
* @param left the position that the left edge is dragged to
|
|
||||||
* @param bounds the bounding box of the image that is being cropped
|
|
||||||
* @param snapMargin the snap distance to the image edge (in pixels)
|
|
||||||
*/
|
|
||||||
private void adjustLeft(
|
|
||||||
RectF rect,
|
|
||||||
float left,
|
|
||||||
RectF bounds,
|
|
||||||
float snapMargin,
|
|
||||||
float aspectRatio,
|
|
||||||
boolean topMoves,
|
|
||||||
boolean bottomMoves) {
|
|
||||||
|
|
||||||
float newLeft = left;
|
|
||||||
|
|
||||||
if (newLeft < 0) {
|
|
||||||
newLeft /= 1.05f;
|
|
||||||
mTouchOffset.x -= newLeft / 1.1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLeft < bounds.left) {
|
|
||||||
mTouchOffset.x -= (newLeft - bounds.left) / 2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLeft - bounds.left < snapMargin) {
|
|
||||||
newLeft = bounds.left;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the window is too small horizontally
|
|
||||||
if (rect.right - newLeft < mMinCropWidth) {
|
|
||||||
newLeft = rect.right - mMinCropWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the window is too large horizontally
|
|
||||||
if (rect.right - newLeft > mMaxCropWidth) {
|
|
||||||
newLeft = rect.right - mMaxCropWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLeft - bounds.left < snapMargin) {
|
|
||||||
newLeft = bounds.left;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check vertical bounds if aspect ratio is in play
|
|
||||||
if (aspectRatio > 0) {
|
|
||||||
float newHeight = (rect.right - newLeft) / aspectRatio;
|
|
||||||
|
|
||||||
// Checks if the window is too small vertically
|
|
||||||
if (newHeight < mMinCropHeight) {
|
|
||||||
newLeft = Math.max(bounds.left, rect.right - mMinCropHeight * aspectRatio);
|
|
||||||
newHeight = (rect.right - newLeft) / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the window is too large vertically
|
|
||||||
if (newHeight > mMaxCropHeight) {
|
|
||||||
newLeft = Math.max(bounds.left, rect.right - mMaxCropHeight * aspectRatio);
|
|
||||||
newHeight = (rect.right - newLeft) / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if top AND bottom edge moves by aspect ratio check that it is within full height bounds
|
|
||||||
if (topMoves && bottomMoves) {
|
|
||||||
newLeft =
|
|
||||||
Math.max(newLeft, Math.max(bounds.left, rect.right - bounds.height() * aspectRatio));
|
|
||||||
} else {
|
|
||||||
// if top edge moves by aspect ratio check that it is within bounds
|
|
||||||
if (topMoves && rect.bottom - newHeight < bounds.top) {
|
|
||||||
newLeft = Math.max(bounds.left, rect.right - (rect.bottom - bounds.top) * aspectRatio);
|
|
||||||
newHeight = (rect.right - newLeft) / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if bottom edge moves by aspect ratio check that it is within bounds
|
|
||||||
if (bottomMoves && rect.top + newHeight > bounds.bottom) {
|
|
||||||
newLeft =
|
|
||||||
Math.max(
|
|
||||||
newLeft,
|
|
||||||
Math.max(bounds.left, rect.right - (bounds.bottom - rect.top) * aspectRatio));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rect.left = newLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the resulting x-position of the right edge of the crop window given the handle's position
|
|
||||||
* and the image's bounding box and snap radius.
|
|
||||||
*
|
|
||||||
* @param right the position that the right edge is dragged to
|
|
||||||
* @param bounds the bounding box of the image that is being cropped
|
|
||||||
* @param viewWidth
|
|
||||||
* @param snapMargin the snap distance to the image edge (in pixels)
|
|
||||||
*/
|
|
||||||
private void adjustRight(
|
|
||||||
RectF rect,
|
|
||||||
float right,
|
|
||||||
RectF bounds,
|
|
||||||
int viewWidth,
|
|
||||||
float snapMargin,
|
|
||||||
float aspectRatio,
|
|
||||||
boolean topMoves,
|
|
||||||
boolean bottomMoves) {
|
|
||||||
|
|
||||||
float newRight = right;
|
|
||||||
|
|
||||||
if (newRight > viewWidth) {
|
|
||||||
newRight = viewWidth + (newRight - viewWidth) / 1.05f;
|
|
||||||
mTouchOffset.x -= (newRight - viewWidth) / 1.1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newRight > bounds.right) {
|
|
||||||
mTouchOffset.x -= (newRight - bounds.right) / 2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If close to the edge
|
|
||||||
if (bounds.right - newRight < snapMargin) {
|
|
||||||
newRight = bounds.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the window is too small horizontally
|
|
||||||
if (newRight - rect.left < mMinCropWidth) {
|
|
||||||
newRight = rect.left + mMinCropWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the window is too large horizontally
|
|
||||||
if (newRight - rect.left > mMaxCropWidth) {
|
|
||||||
newRight = rect.left + mMaxCropWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If close to the edge
|
|
||||||
if (bounds.right - newRight < snapMargin) {
|
|
||||||
newRight = bounds.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check vertical bounds if aspect ratio is in play
|
|
||||||
if (aspectRatio > 0) {
|
|
||||||
float newHeight = (newRight - rect.left) / aspectRatio;
|
|
||||||
|
|
||||||
// Checks if the window is too small vertically
|
|
||||||
if (newHeight < mMinCropHeight) {
|
|
||||||
newRight = Math.min(bounds.right, rect.left + mMinCropHeight * aspectRatio);
|
|
||||||
newHeight = (newRight - rect.left) / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the window is too large vertically
|
|
||||||
if (newHeight > mMaxCropHeight) {
|
|
||||||
newRight = Math.min(bounds.right, rect.left + mMaxCropHeight * aspectRatio);
|
|
||||||
newHeight = (newRight - rect.left) / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if top AND bottom edge moves by aspect ratio check that it is within full height bounds
|
|
||||||
if (topMoves && bottomMoves) {
|
|
||||||
newRight =
|
|
||||||
Math.min(newRight, Math.min(bounds.right, rect.left + bounds.height() * aspectRatio));
|
|
||||||
} else {
|
|
||||||
// if top edge moves by aspect ratio check that it is within bounds
|
|
||||||
if (topMoves && rect.bottom - newHeight < bounds.top) {
|
|
||||||
newRight = Math.min(bounds.right, rect.left + (rect.bottom - bounds.top) * aspectRatio);
|
|
||||||
newHeight = (newRight - rect.left) / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if bottom edge moves by aspect ratio check that it is within bounds
|
|
||||||
if (bottomMoves && rect.top + newHeight > bounds.bottom) {
|
|
||||||
newRight =
|
|
||||||
Math.min(
|
|
||||||
newRight,
|
|
||||||
Math.min(bounds.right, rect.left + (bounds.bottom - rect.top) * aspectRatio));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rect.right = newRight;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the resulting y-position of the top edge of the crop window given the handle's position and
|
|
||||||
* the image's bounding box and snap radius.
|
|
||||||
*
|
|
||||||
* @param top the x-position that the top edge is dragged to
|
|
||||||
* @param bounds the bounding box of the image that is being cropped
|
|
||||||
* @param snapMargin the snap distance to the image edge (in pixels)
|
|
||||||
*/
|
|
||||||
private void adjustTop(
|
|
||||||
RectF rect,
|
|
||||||
float top,
|
|
||||||
RectF bounds,
|
|
||||||
float snapMargin,
|
|
||||||
float aspectRatio,
|
|
||||||
boolean leftMoves,
|
|
||||||
boolean rightMoves) {
|
|
||||||
|
|
||||||
float newTop = top;
|
|
||||||
|
|
||||||
if (newTop < 0) {
|
|
||||||
newTop /= 1.05f;
|
|
||||||
mTouchOffset.y -= newTop / 1.1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newTop < bounds.top) {
|
|
||||||
mTouchOffset.y -= (newTop - bounds.top) / 2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newTop - bounds.top < snapMargin) {
|
|
||||||
newTop = bounds.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the window is too small vertically
|
|
||||||
if (rect.bottom - newTop < mMinCropHeight) {
|
|
||||||
newTop = rect.bottom - mMinCropHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the window is too large vertically
|
|
||||||
if (rect.bottom - newTop > mMaxCropHeight) {
|
|
||||||
newTop = rect.bottom - mMaxCropHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newTop - bounds.top < snapMargin) {
|
|
||||||
newTop = bounds.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check horizontal bounds if aspect ratio is in play
|
|
||||||
if (aspectRatio > 0) {
|
|
||||||
float newWidth = (rect.bottom - newTop) * aspectRatio;
|
|
||||||
|
|
||||||
// Checks if the crop window is too small horizontally due to aspect ratio adjustment
|
|
||||||
if (newWidth < mMinCropWidth) {
|
|
||||||
newTop = Math.max(bounds.top, rect.bottom - (mMinCropWidth / aspectRatio));
|
|
||||||
newWidth = (rect.bottom - newTop) * aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the crop window is too large horizontally due to aspect ratio adjustment
|
|
||||||
if (newWidth > mMaxCropWidth) {
|
|
||||||
newTop = Math.max(bounds.top, rect.bottom - (mMaxCropWidth / aspectRatio));
|
|
||||||
newWidth = (rect.bottom - newTop) * aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if left AND right edge moves by aspect ratio check that it is within full width bounds
|
|
||||||
if (leftMoves && rightMoves) {
|
|
||||||
newTop = Math.max(newTop, Math.max(bounds.top, rect.bottom - bounds.width() / aspectRatio));
|
|
||||||
} else {
|
|
||||||
// if left edge moves by aspect ratio check that it is within bounds
|
|
||||||
if (leftMoves && rect.right - newWidth < bounds.left) {
|
|
||||||
newTop = Math.max(bounds.top, rect.bottom - (rect.right - bounds.left) / aspectRatio);
|
|
||||||
newWidth = (rect.bottom - newTop) * aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if right edge moves by aspect ratio check that it is within bounds
|
|
||||||
if (rightMoves && rect.left + newWidth > bounds.right) {
|
|
||||||
newTop =
|
|
||||||
Math.max(
|
|
||||||
newTop,
|
|
||||||
Math.max(bounds.top, rect.bottom - (bounds.right - rect.left) / aspectRatio));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rect.top = newTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the resulting y-position of the bottom edge of the crop window given the handle's position
|
|
||||||
* and the image's bounding box and snap radius.
|
|
||||||
*
|
|
||||||
* @param bottom the position that the bottom edge is dragged to
|
|
||||||
* @param bounds the bounding box of the image that is being cropped
|
|
||||||
* @param viewHeight
|
|
||||||
* @param snapMargin the snap distance to the image edge (in pixels)
|
|
||||||
*/
|
|
||||||
private void adjustBottom(
|
|
||||||
RectF rect,
|
|
||||||
float bottom,
|
|
||||||
RectF bounds,
|
|
||||||
int viewHeight,
|
|
||||||
float snapMargin,
|
|
||||||
float aspectRatio,
|
|
||||||
boolean leftMoves,
|
|
||||||
boolean rightMoves) {
|
|
||||||
|
|
||||||
float newBottom = bottom;
|
|
||||||
|
|
||||||
if (newBottom > viewHeight) {
|
|
||||||
newBottom = viewHeight + (newBottom - viewHeight) / 1.05f;
|
|
||||||
mTouchOffset.y -= (newBottom - viewHeight) / 1.1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newBottom > bounds.bottom) {
|
|
||||||
mTouchOffset.y -= (newBottom - bounds.bottom) / 2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bounds.bottom - newBottom < snapMargin) {
|
|
||||||
newBottom = bounds.bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the window is too small vertically
|
|
||||||
if (newBottom - rect.top < mMinCropHeight) {
|
|
||||||
newBottom = rect.top + mMinCropHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the window is too small vertically
|
|
||||||
if (newBottom - rect.top > mMaxCropHeight) {
|
|
||||||
newBottom = rect.top + mMaxCropHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bounds.bottom - newBottom < snapMargin) {
|
|
||||||
newBottom = bounds.bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check horizontal bounds if aspect ratio is in play
|
|
||||||
if (aspectRatio > 0) {
|
|
||||||
float newWidth = (newBottom - rect.top) * aspectRatio;
|
|
||||||
|
|
||||||
// Checks if the window is too small horizontally
|
|
||||||
if (newWidth < mMinCropWidth) {
|
|
||||||
newBottom = Math.min(bounds.bottom, rect.top + mMinCropWidth / aspectRatio);
|
|
||||||
newWidth = (newBottom - rect.top) * aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the window is too large horizontally
|
|
||||||
if (newWidth > mMaxCropWidth) {
|
|
||||||
newBottom = Math.min(bounds.bottom, rect.top + mMaxCropWidth / aspectRatio);
|
|
||||||
newWidth = (newBottom - rect.top) * aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if left AND right edge moves by aspect ratio check that it is within full width bounds
|
|
||||||
if (leftMoves && rightMoves) {
|
|
||||||
newBottom =
|
|
||||||
Math.min(newBottom, Math.min(bounds.bottom, rect.top + bounds.width() / aspectRatio));
|
|
||||||
} else {
|
|
||||||
// if left edge moves by aspect ratio check that it is within bounds
|
|
||||||
if (leftMoves && rect.right - newWidth < bounds.left) {
|
|
||||||
newBottom = Math.min(bounds.bottom, rect.top + (rect.right - bounds.left) / aspectRatio);
|
|
||||||
newWidth = (newBottom - rect.top) * aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if right edge moves by aspect ratio check that it is within bounds
|
|
||||||
if (rightMoves && rect.left + newWidth > bounds.right) {
|
|
||||||
newBottom =
|
|
||||||
Math.min(
|
|
||||||
newBottom,
|
|
||||||
Math.min(bounds.bottom, rect.top + (bounds.right - rect.left) / aspectRatio));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rect.bottom = newBottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjust left edge by current crop window height and the given aspect ratio, the right edge
|
|
||||||
* remains in possition while the left adjusts to keep aspect ratio to the height.
|
|
||||||
*/
|
|
||||||
private void adjustLeftByAspectRatio(RectF rect, float aspectRatio) {
|
|
||||||
rect.left = rect.right - rect.height() * aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjust top edge by current crop window width and the given aspect ratio, the bottom edge
|
|
||||||
* remains in possition while the top adjusts to keep aspect ratio to the width.
|
|
||||||
*/
|
|
||||||
private void adjustTopByAspectRatio(RectF rect, float aspectRatio) {
|
|
||||||
rect.top = rect.bottom - rect.width() / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjust right edge by current crop window height and the given aspect ratio, the left edge
|
|
||||||
* remains in possition while the left adjusts to keep aspect ratio to the height.
|
|
||||||
*/
|
|
||||||
private void adjustRightByAspectRatio(RectF rect, float aspectRatio) {
|
|
||||||
rect.right = rect.left + rect.height() * aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjust bottom edge by current crop window width and the given aspect ratio, the top edge
|
|
||||||
* remains in possition while the top adjusts to keep aspect ratio to the width.
|
|
||||||
*/
|
|
||||||
private void adjustBottomByAspectRatio(RectF rect, float aspectRatio) {
|
|
||||||
rect.bottom = rect.top + rect.width() / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjust left and right edges by current crop window height and the given aspect ratio, both
|
|
||||||
* right and left edges adjusts equally relative to center to keep aspect ratio to the height.
|
|
||||||
*/
|
|
||||||
private void adjustLeftRightByAspectRatio(RectF rect, RectF bounds, float aspectRatio) {
|
|
||||||
rect.inset((rect.width() - rect.height() * aspectRatio) / 2, 0);
|
|
||||||
if (rect.left < bounds.left) {
|
|
||||||
rect.offset(bounds.left - rect.left, 0);
|
|
||||||
}
|
|
||||||
if (rect.right > bounds.right) {
|
|
||||||
rect.offset(bounds.right - rect.right, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjust top and bottom edges by current crop window width and the given aspect ratio, both top
|
|
||||||
* and bottom edges adjusts equally relative to center to keep aspect ratio to the width.
|
|
||||||
*/
|
|
||||||
private void adjustTopBottomByAspectRatio(RectF rect, RectF bounds, float aspectRatio) {
|
|
||||||
rect.inset(0, (rect.height() - rect.width() / aspectRatio) / 2);
|
|
||||||
if (rect.top < bounds.top) {
|
|
||||||
rect.offset(0, bounds.top - rect.top);
|
|
||||||
}
|
|
||||||
if (rect.bottom > bounds.bottom) {
|
|
||||||
rect.offset(0, bounds.bottom - rect.bottom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region: Inner class: Type
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of crop window move that is handled.
|
|
||||||
*/
|
|
||||||
public enum Type {
|
|
||||||
TOP_LEFT,
|
|
||||||
TOP_RIGHT,
|
|
||||||
BOTTOM_LEFT,
|
|
||||||
BOTTOM_RIGHT,
|
|
||||||
LEFT,
|
|
||||||
TOP,
|
|
||||||
RIGHT,
|
|
||||||
BOTTOM,
|
|
||||||
CENTER
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
Before Width: | Height: | Size: 262 B |
Before Width: | Height: | Size: 634 B |
Before Width: | Height: | Size: 617 B |
Before Width: | Height: | Size: 259 B |
Before Width: | Height: | Size: 798 B |
Before Width: | Height: | Size: 787 B |
Before Width: | Height: | Size: 417 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 508 B |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<com.theartofdev.edmodo.cropper.CropImageView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/cropImageView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
|
@ -1,25 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/ImageView_image"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:adjustViewBounds="true"
|
|
||||||
android:scaleType="centerInside"
|
|
||||||
tools:ignore="contentDescription" />
|
|
||||||
|
|
||||||
<com.theartofdev.edmodo.cropper.CropOverlayView
|
|
||||||
android:id="@+id/CropOverlayView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:visibility="invisible" />
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/CropProgressBar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center" />
|
|
||||||
|
|
||||||
</merge>
|
|
|
@ -1,35 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/crop_image_menu_rotate_left"
|
|
||||||
android:icon="@drawable/crop_image_menu_rotate_left"
|
|
||||||
android:title="@string/crop_image_menu_rotate_left"
|
|
||||||
android:visible="false"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/crop_image_menu_rotate_right"
|
|
||||||
android:icon="@drawable/crop_image_menu_rotate_right"
|
|
||||||
android:title="@string/crop_image_menu_rotate_right"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/crop_image_menu_flip"
|
|
||||||
android:icon="@drawable/crop_image_menu_flip"
|
|
||||||
android:title="@string/crop_image_menu_flip"
|
|
||||||
app:showAsAction="ifRoom">
|
|
||||||
<menu>
|
|
||||||
<item
|
|
||||||
android:id="@+id/crop_image_menu_flip_horizontally"
|
|
||||||
android:title="@string/crop_image_menu_flip_horizontally" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/crop_image_menu_flip_vertically"
|
|
||||||
android:title="@string/crop_image_menu_flip_vertically" />
|
|
||||||
</menu>
|
|
||||||
</item>
|
|
||||||
<item
|
|
||||||
android:id="@+id/crop_image_menu_crop"
|
|
||||||
android:title="@string/crop_image_menu_crop"
|
|
||||||
app:showAsAction="always" />
|
|
||||||
|
|
||||||
</menu>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">أدر عكس اتجاه عقارب الساعة</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">أدر</string>
|
|
||||||
<string name="crop_image_menu_crop">قُصّ</string>
|
|
||||||
<string name="crop_image_menu_flip">اقلب</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">اقلب أفقيًا</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">اقلب رأسيًا</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">اختر مصدرًا</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">إلغاء؛ الأذونات المطلوبة غير ممنوحة</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">Otočit proti směru hodinových ručiček</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Otočit</string>
|
|
||||||
<string name="crop_image_menu_crop">Oříznout</string>
|
|
||||||
<string name="crop_image_menu_flip">Překlopit</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Překlopit vodorovně</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Překlopit svisle</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">Vybrat zdroj</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">Probíhá storno, požadovaná povolení nejsou udělena</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">gegen den Uhrzeigersinn drehen</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">drehen</string>
|
|
||||||
<string name="crop_image_menu_crop">zuschneiden</string>
|
|
||||||
<string name="crop_image_menu_flip">spiegeln</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">horizontal spiegeln</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">vertikal spiegeln</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">Quelle wählen</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">Vorgang wird abgebrochen, benötigte Berechtigungen wurden nicht erteilt.</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">Girar a la izquierda</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Girar a la derecha</string>
|
|
||||||
<string name="crop_image_menu_crop">Cortar</string>
|
|
||||||
<string name="crop_image_menu_flip">Dar la vuelta</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Voltear horizontalmente</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Voltear verticalmente</string>
|
|
||||||
<string name="pick_image_intent_chooser_title">Seleccionar fuente</string>
|
|
||||||
<string name="crop_image_activity_no_permissions">Cancelando, los permisos requeridos no se otorgaron</string>
|
|
||||||
</resources>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">Rotar a la izquierda</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Rotar a la derecha</string>
|
|
||||||
<string name="crop_image_menu_crop">Cortar</string>
|
|
||||||
<string name="crop_image_menu_flip">Dar la vuelta</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Voltear horizontalmente</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Voltear verticalmente</string>
|
|
||||||
<string name="pick_image_intent_chooser_title">Seleccionar fuente</string>
|
|
||||||
<string name="crop_image_activity_no_permissions">Cancelando, los permisos requeridos no han sido otorgados</string>
|
|
||||||
</resources>
|
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="crop_image_menu_rotate_left">چرخش در جهت عقربه های ساعت</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">چرخش</string>
|
|
||||||
<string name="crop_image_menu_crop">بریدن (کراپ)</string>
|
|
||||||
<string name="crop_image_menu_flip">آیینه کردن</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">آیینه کردن به صورت افقی</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">آیینه کردن به صورت عمودی</string>
|
|
||||||
<string name="pick_image_intent_chooser_title">منبع را انتخاب کنید</string>
|
|
||||||
<string name="crop_image_activity_no_permissions">لغو، مجوزهای مورد نیاز ارائه نشده</string>
|
|
||||||
</resources>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">Pivoter à gauche</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Pivoter à droite</string>
|
|
||||||
<string name="crop_image_menu_crop">Redimensionner</string>
|
|
||||||
<string name="crop_image_menu_flip">Retourner</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Retourner horizontalement</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Retourner verticalement</string>
|
|
||||||
<string name="pick_image_intent_chooser_title">Sélectionner la source</string>
|
|
||||||
<string name="crop_image_activity_no_permissions">Annulation, il manque des permissions requises</string>
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">סובב נגד כיוון השעון</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">סובב</string>
|
|
||||||
<string name="crop_image_menu_crop">חתוך</string>
|
|
||||||
<string name="crop_image_menu_flip">הפוך</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">הפוך אופקית</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">הפוך אנכית</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">בחר מקור</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">ההרשאות הנדרשות חסרות, מבטל</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="crop_image_menu_rotate_left">घड़ी की सुई के विपरीत दिशा में घुमाइए</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">घुमाएँ</string>
|
|
||||||
<string name="crop_image_menu_crop">फ़सल</string>
|
|
||||||
<string name="crop_image_menu_flip">फ्लिप</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">क्षैतिज फ्लिप</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">लंबवत फ्लिप करें</string>
|
|
||||||
<string name="pick_image_intent_chooser_title">सोर्स चुनें</string>
|
|
||||||
<string name="crop_image_activity_no_permissions">रद्द करना, आवश्यक अनुमतियां नहीं दी गई हैं</string>
|
|
||||||
</resources>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="crop_image_menu_rotate_left">Putar berlawanan arah jarum jam</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Putar</string>
|
|
||||||
<string name="crop_image_menu_crop">Potong</string>
|
|
||||||
<string name="crop_image_menu_flip">Balik</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Balik secara horizontal</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Balik secara vertikal</string>
|
|
||||||
<string name="pick_image_intent_chooser_title">Pilih sumber</string>
|
|
||||||
<string name="crop_image_activity_no_permissions">Membatalkan, tidak mendapatkan izin yang diperlukan</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="crop_image_menu_rotate_left">Putar berlawanan arah jarum jam</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Putar</string>
|
|
||||||
<string name="crop_image_menu_crop">Potong</string>
|
|
||||||
<string name="crop_image_menu_flip">Balik</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Balik secara horizontal</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Balik secara vertikal</string>
|
|
||||||
<string name="pick_image_intent_chooser_title">Pilih sumber</string>
|
|
||||||
<string name="crop_image_activity_no_permissions">Membatalkan, tidak mendapatkan izin yang diperlukan</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">Ruota in senso antiorario</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Ruota</string>
|
|
||||||
<string name="crop_image_menu_crop">Ritaglia</string>
|
|
||||||
<string name="crop_image_menu_flip">Capovolgi</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Capovolgi orizzontalmente</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Capovolgi verticalmente</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">Seleziona origine</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">Annullamento in corso, autorizzazione richieste non concesse</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="crop_image_menu_rotate_left">左回転</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">右回転</string>
|
|
||||||
<string name="crop_image_menu_crop">切り取り</string>
|
|
||||||
<string name="crop_image_menu_flip">反転</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">左右反転</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">上下反転</string>
|
|
||||||
<string name="pick_image_intent_chooser_title">画像を選択</string>
|
|
||||||
<string name="crop_image_activity_no_permissions">必要な権限がありません、キャンセルしています。</string>
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">반시계 회전</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">회전</string>
|
|
||||||
<string name="crop_image_menu_crop">자르기</string>
|
|
||||||
<string name="crop_image_menu_flip">반전</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">좌우반전</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">상하반전</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">이미지 선택</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">필수 권한이 없어서 취소합니다.</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="crop_image_menu_rotate_left">Putar arah berlawanan jam</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Putar</string>
|
|
||||||
<string name="crop_image_menu_crop">Potong</string>
|
|
||||||
<string name="crop_image_menu_flip">Flip</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Flip melintang</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Flip menegak</string>
|
|
||||||
<string name="pick_image_intent_chooser_title">Pilih sumber</string>
|
|
||||||
<string name="crop_image_activity_no_permissions">Membatal, tidak mendapat kebenaran yang diperlukan</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">Roter teller med urviseren</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Roter</string>
|
|
||||||
<string name="crop_image_menu_crop">Beskjær</string>
|
|
||||||
<string name="crop_image_menu_flip">Vend</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Vend vannrett</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Vend loddrett</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">Velg kilde</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">Avbryter, nødvendige tillatelser er ikke gitt</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">Tegen de klok in draaien</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Draaien</string>
|
|
||||||
<string name="crop_image_menu_crop">Bijsnijden</string>
|
|
||||||
<string name="crop_image_menu_flip">Spiegelen</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Horizontaal spiegelen</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Verticaal spiegelen</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">Bron selecteren</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">Wordt geannuleerd, vereiste machtigingen zijn niet toegekend</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">Obróć w lewo</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Obróć</string>
|
|
||||||
<string name="crop_image_menu_crop">Przytnij</string>
|
|
||||||
<string name="crop_image_menu_flip">Odbij</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Odbij poziomo</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Odbij pionowo</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">Wybierz źródło</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">Przerywaniem, potrzebne uprawnienia nie zostały nadane</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">Girar para a esquerda</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Girar para a direita</string>
|
|
||||||
<string name="crop_image_menu_crop">Cortar</string>
|
|
||||||
<string name="crop_image_menu_flip">Espelhar</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Espelhar na horizontal</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Espelhar na vertifcal</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">Escolher foto a partir de</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="crop_image_menu_rotate_left">Повернуть налево</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Повернуть направо</string>
|
|
||||||
<string name="crop_image_menu_crop">Обрезать</string>
|
|
||||||
<string name="crop_image_menu_flip">Отразить</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Отразить по горизонтали</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Отразить по вертикали</string>
|
|
||||||
<string name="pick_image_intent_chooser_title">Выбрать источник</string>
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">Rotera vänster</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Rotera höger</string>
|
|
||||||
<string name="crop_image_menu_crop">Beskär</string>
|
|
||||||
<string name="crop_image_menu_flip">Vänd</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Vänd horisontellt</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Vänd vertikalt</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">Välj bild</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">Avbryter, nödvändiga behörigheter beviljas inte</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="crop_image_menu_rotate_left">Saat yönünde döndür</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">döndürmek</string>
|
|
||||||
<string name="crop_image_menu_crop">ekin</string>
|
|
||||||
<string name="crop_image_menu_flip">fiske</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Yatay olarak çevir</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Dikey olarak çevir</string>
|
|
||||||
<string name="pick_image_intent_chooser_title">Kaynağı seçin</string>
|
|
||||||
<string name="crop_image_activity_no_permissions">İptal ediliyor, gerekli izinler verilmiyor</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="crop_image_menu_rotate_left">گھڑی وار گھڑی گھومیں</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">گھمائیں</string>
|
|
||||||
<string name="crop_image_menu_crop">فصل</string>
|
|
||||||
<string name="crop_image_menu_flip">پلٹائیں</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">افقی پلٹائیں</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">عمودی طور پر پلٹائیں</string>
|
|
||||||
<string name="pick_image_intent_chooser_title">ذریعہ منتخب کریں</string>
|
|
||||||
<string name="crop_image_activity_no_permissions">منسوخ کرنا، ضروری اجازت نہیں دی جاتی ہیں</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">Xoay theo chiều kim đồng hồ</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Xoay</string>
|
|
||||||
<string name="crop_image_menu_crop">Cắt</string>
|
|
||||||
<string name="crop_image_menu_flip">Lật</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Lật theo chiều ngang</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Lật theo chiều dọc</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">Chọn nguồn</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">Đang hủy, các quyền đã yêu cầu không được cấp</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">逆时针旋转</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">旋转</string>
|
|
||||||
<string name="crop_image_menu_crop">裁切</string>
|
|
||||||
<string name="crop_image_menu_flip">翻转</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">水平翻转</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">垂直翻转</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">选择来源</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">取消中,未授予所需权限</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">逆時針旋轉</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">旋轉</string>
|
|
||||||
<string name="crop_image_menu_crop">裁切</string>
|
|
||||||
<string name="crop_image_menu_flip">翻轉</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">水平翻轉</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">垂直翻轉</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">選擇來源</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">取消中,未授予所需權限</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">逆时针旋转</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">旋转</string>
|
|
||||||
<string name="crop_image_menu_crop">裁剪</string>
|
|
||||||
<string name="crop_image_menu_flip">翻转</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">水平翻转</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">垂直翻转</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">选择来源</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">正在取消,该操作未获得所需权限。</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,50 +0,0 @@
|
||||||
<resources>
|
|
||||||
|
|
||||||
<declare-styleable name="CropImageView">
|
|
||||||
<attr name="cropGuidelines">
|
|
||||||
<enum name="off" value="0" />
|
|
||||||
<enum name="onTouch" value="1" />
|
|
||||||
<enum name="on" value="2" />
|
|
||||||
</attr>
|
|
||||||
<attr name="cropScaleType">
|
|
||||||
<enum name="fitCenter" value="0" />
|
|
||||||
<enum name="center" value="1" />
|
|
||||||
<enum name="centerCrop" value="2" />
|
|
||||||
<enum name="centerInside" value="3" />
|
|
||||||
</attr>
|
|
||||||
<attr name="cropShape">
|
|
||||||
<enum name="rectangle" value="0" />
|
|
||||||
<enum name="oval" value="1" />
|
|
||||||
</attr>
|
|
||||||
<attr name="cropAutoZoomEnabled" format="boolean" />
|
|
||||||
<attr name="cropMaxZoom" format="integer" />
|
|
||||||
<attr name="cropMultiTouchEnabled" format="boolean" />
|
|
||||||
<attr name="cropFixAspectRatio" format="boolean" />
|
|
||||||
<attr name="cropAspectRatioX" format="integer" />
|
|
||||||
<attr name="cropAspectRatioY" format="integer" />
|
|
||||||
<attr name="cropInitialCropWindowPaddingRatio" format="float" />
|
|
||||||
<attr name="cropBorderLineThickness" format="dimension" />
|
|
||||||
<attr name="cropBorderLineColor" format="color" />
|
|
||||||
<attr name="cropBorderCornerThickness" format="dimension" />
|
|
||||||
<attr name="cropBorderCornerOffset" format="dimension" />
|
|
||||||
<attr name="cropBorderCornerLength" format="dimension" />
|
|
||||||
<attr name="cropBorderCornerColor" format="color" />
|
|
||||||
<attr name="cropGuidelinesThickness" format="dimension" />
|
|
||||||
<attr name="cropGuidelinesColor" format="color" />
|
|
||||||
<attr name="cropBackgroundColor" format="color" />
|
|
||||||
<attr name="cropSnapRadius" format="dimension" />
|
|
||||||
<attr name="cropTouchRadius" format="dimension" />
|
|
||||||
<attr name="cropSaveBitmapToInstanceState" format="boolean" />
|
|
||||||
<attr name="cropShowCropOverlay" format="boolean" />
|
|
||||||
<attr name="cropShowProgressBar" format="boolean" />
|
|
||||||
<attr name="cropMinCropWindowWidth" format="dimension" />
|
|
||||||
<attr name="cropMinCropWindowHeight" format="dimension" />
|
|
||||||
<attr name="cropMinCropResultWidthPX" format="float" />
|
|
||||||
<attr name="cropMinCropResultHeightPX" format="float" />
|
|
||||||
<attr name="cropMaxCropResultWidthPX" format="float" />
|
|
||||||
<attr name="cropMaxCropResultHeightPX" format="float" />
|
|
||||||
<attr name="cropFlipHorizontally" format="boolean" />
|
|
||||||
<attr name="cropFlipVertically" format="boolean" />
|
|
||||||
</declare-styleable>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_title"></string>
|
|
||||||
<string name="crop_image_menu_rotate_left">Rotate counter clockwise</string>
|
|
||||||
<string name="crop_image_menu_rotate_right">Rotate</string>
|
|
||||||
<string name="crop_image_menu_crop">Crop</string>
|
|
||||||
<string name="crop_image_menu_flip">Flip</string>
|
|
||||||
<string name="crop_image_menu_flip_horizontally">Flip horizontally</string>
|
|
||||||
<string name="crop_image_menu_flip_vertically">Flip vertically</string>
|
|
||||||
|
|
||||||
<string name="pick_image_intent_chooser_title">Select source</string>
|
|
||||||
|
|
||||||
<string name="crop_image_activity_no_permissions">Cancelling, required permissions are not granted</string>
|
|
||||||
|
|
||||||
</resources>
|
|
|
@ -3,4 +3,3 @@ include ':app'
|
||||||
include ':autoimageslider'
|
include ':autoimageslider'
|
||||||
include ':mytransl'
|
include ':mytransl'
|
||||||
include ':ratethisapp'
|
include ':ratethisapp'
|
||||||
include ':cropper'
|
|
||||||
|
|