diff --git a/.gitignore b/.gitignore
index 050d0118..471debea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@
.externalNativeBuild
.cxx
local.properties
+/cropper/build/
diff --git a/app/build.gradle b/app/build.gradle
index df014722..3d47323d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -71,7 +71,7 @@ allprojects {
}
dependencies {
implementation project(':autoimageslider')
- implementation 'androidx.appcompat:appcompat:1.4.1'
+ implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation "com.google.code.gson:gson:2.8.6"
@@ -91,6 +91,11 @@ dependencies {
}
implementation project(path: ':mytransl')
implementation project(path: ':ratethisapp')
+
+
+
+ implementation 'com.burhanrashid52:photoeditor:1.5.1'
+ implementation project(path: ':cropper')
annotationProcessor "com.github.bumptech.glide:compiler:4.12.0"
implementation 'jp.wasabeef:glide-transformations:4.3.0'
implementation 'com.github.penfeizhou.android.animation:apng:2.17.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c5fd2dbb..45a825d1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -57,6 +57,9 @@
+
{
+
+ private final List colorPickerColors;
+ private Context context;
+ private LayoutInflater inflater;
+ private OnColorPickerClickListener onColorPickerClickListener;
+
+ ColorPickerAdapter(@NonNull Context context, @NonNull List colorPickerColors) {
+ this.context = context;
+ this.inflater = LayoutInflater.from(context);
+ this.colorPickerColors = colorPickerColors;
+ }
+
+ ColorPickerAdapter(@NonNull Context context) {
+ this(context, getDefaultColors(context));
+ this.context = context;
+ this.inflater = LayoutInflater.from(context);
+ }
+
+ public static List getDefaultColors(Context context) {
+ ArrayList colorPickerColors = new ArrayList<>();
+ colorPickerColors.add(ContextCompat.getColor(context, R.color.blue_color_picker));
+ colorPickerColors.add(ContextCompat.getColor(context, R.color.brown_color_picker));
+ colorPickerColors.add(ContextCompat.getColor(context, R.color.green_color_picker));
+ colorPickerColors.add(ContextCompat.getColor(context, R.color.orange_color_picker));
+ colorPickerColors.add(ContextCompat.getColor(context, R.color.red_color_picker));
+ colorPickerColors.add(ContextCompat.getColor(context, R.color.black));
+ colorPickerColors.add(ContextCompat.getColor(context, R.color.red_orange_color_picker));
+ colorPickerColors.add(ContextCompat.getColor(context, R.color.sky_blue_color_picker));
+ colorPickerColors.add(ContextCompat.getColor(context, R.color.violet_color_picker));
+ colorPickerColors.add(ContextCompat.getColor(context, R.color.white));
+ colorPickerColors.add(ContextCompat.getColor(context, R.color.yellow_color_picker));
+ colorPickerColors.add(ContextCompat.getColor(context, R.color.yellow_green_color_picker));
+ return colorPickerColors;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = inflater.inflate(R.layout.color_picker_item_list, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ holder.colorPickerView.setBackgroundColor(colorPickerColors.get(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return colorPickerColors.size();
+ }
+
+ private void buildColorPickerView(View view, int colorCode) {
+ view.setVisibility(View.VISIBLE);
+
+ ShapeDrawable biggerCircle = new ShapeDrawable(new OvalShape());
+ biggerCircle.setIntrinsicHeight(20);
+ biggerCircle.setIntrinsicWidth(20);
+ biggerCircle.setBounds(new Rect(0, 0, 20, 20));
+ biggerCircle.getPaint().setColor(colorCode);
+
+ ShapeDrawable smallerCircle = new ShapeDrawable(new OvalShape());
+ smallerCircle.setIntrinsicHeight(5);
+ smallerCircle.setIntrinsicWidth(5);
+ smallerCircle.setBounds(new Rect(0, 0, 5, 5));
+ smallerCircle.getPaint().setColor(Color.WHITE);
+ smallerCircle.setPadding(10, 10, 10, 10);
+ Drawable[] drawables = {smallerCircle, biggerCircle};
+
+ LayerDrawable layerDrawable = new LayerDrawable(drawables);
+
+ view.setBackgroundDrawable(layerDrawable);
+ }
+
+ public void setOnColorPickerClickListener(OnColorPickerClickListener onColorPickerClickListener) {
+ this.onColorPickerClickListener = onColorPickerClickListener;
+ }
+
+ public interface OnColorPickerClickListener {
+ void onColorPickerClickListener(int colorCode);
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder {
+ View colorPickerView;
+
+ public ViewHolder(View itemView) {
+ super(itemView);
+ colorPickerView = itemView.findViewById(R.id.color_picker_view);
+ itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (onColorPickerClickListener != null)
+ onColorPickerClickListener.onColorPickerClickListener(colorPickerColors.get(getAdapterPosition()));
+ }
+ });
+ }
+ }
+}
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/EditImageActivity.java b/app/src/main/java/app/fedilab/android/imageeditor/EditImageActivity.java
new file mode 100644
index 00000000..cc02bb14
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/EditImageActivity.java
@@ -0,0 +1,585 @@
+package app.fedilab.android.imageeditor;
+
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static app.fedilab.android.imageeditor.FileSaveHelper.isSdkHigherThan28;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.AnticipateOvershootInterpolator;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.app.AlertDialog;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.constraintlayout.widget.ConstraintSet;
+import androidx.core.content.ContextCompat;
+import androidx.core.content.FileProvider;
+import androidx.exifinterface.media.ExifInterface;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.transition.ChangeBounds;
+import androidx.transition.TransitionManager;
+
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+import com.theartofdev.edmodo.cropper.CropImage;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import app.fedilab.android.R;
+import app.fedilab.android.imageeditor.base.BaseActivity;
+import app.fedilab.android.imageeditor.filters.FilterListener;
+import app.fedilab.android.imageeditor.filters.FilterViewAdapter;
+import app.fedilab.android.imageeditor.tools.EditingToolsAdapter;
+import app.fedilab.android.imageeditor.tools.ToolType;
+import es.dmoral.toasty.Toasty;
+import ja.burhanrashid52.photoeditor.OnPhotoEditorListener;
+import ja.burhanrashid52.photoeditor.PhotoEditor;
+import ja.burhanrashid52.photoeditor.PhotoEditorView;
+import ja.burhanrashid52.photoeditor.PhotoFilter;
+import ja.burhanrashid52.photoeditor.SaveSettings;
+import ja.burhanrashid52.photoeditor.TextStyleBuilder;
+import ja.burhanrashid52.photoeditor.ViewType;
+import ja.burhanrashid52.photoeditor.shape.ShapeBuilder;
+import ja.burhanrashid52.photoeditor.shape.ShapeType;
+
+public class EditImageActivity extends BaseActivity implements OnPhotoEditorListener,
+ View.OnClickListener,
+ PropertiesBSFragment.Properties,
+ ShapeBSFragment.Properties,
+ EmojiBSFragment.EmojiListener,
+ StickerBSFragment.StickerListener, EditingToolsAdapter.OnItemSelected, FilterListener {
+
+ public static final String FILE_PROVIDER_AUTHORITY = "com.burhanrashid52.photoeditor.fileprovider";
+ public static final String ACTION_NEXTGEN_EDIT = "action_nextgen_edit";
+ public static final String PINCH_TEXT_SCALABLE_INTENT_KEY = "PINCH_TEXT_SCALABLE";
+ private static final int CAMERA_REQUEST = 52;
+ private static final int PICK_REQUEST = 53;
+ private final EditingToolsAdapter mEditingToolsAdapter = new EditingToolsAdapter(this);
+ private final FilterViewAdapter mFilterViewAdapter = new FilterViewAdapter(this);
+ private final ConstraintSet mConstraintSet = new ConstraintSet();
+ PhotoEditor mPhotoEditor;
+ @Nullable
+ @VisibleForTesting
+ Uri mSaveImageUri;
+ private PhotoEditorView mPhotoEditorView;
+ private PropertiesBSFragment mPropertiesBSFragment;
+ private ShapeBSFragment mShapeBSFragment;
+ private ShapeBuilder mShapeBuilder;
+ private EmojiBSFragment mEmojiBSFragment;
+ private StickerBSFragment mStickerBSFragment;
+ private TextView mTxtCurrentTool;
+ private Typeface mWonderFont;
+ private RecyclerView mRvTools, mRvFilters;
+ private ConstraintLayout mRootView;
+ private boolean mIsFilterVisible;
+ private Uri uri;
+ private boolean exit;
+ private FileSaveHelper mSaveFileHelper;
+
+ private static int exifToDegrees(int exifOrientation) {
+ if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) {
+ return 90;
+ } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_180) {
+ return 180;
+ } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
+ return 270;
+ }
+ return 0;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ makeFullScreen();
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_edit_image);
+ Bundle b = getIntent().getExtras();
+ if (getSupportActionBar() != null)
+ getSupportActionBar().hide();
+ String path = null;
+ if (b != null)
+ path = b.getString("imageUri", null);
+ if (path == null) {
+ finish();
+ }
+ uri = Uri.parse(path);
+
+
+ exit = false;
+
+
+ initViews();
+
+ handleIntentImage(mPhotoEditorView.getSource());
+
+ mWonderFont = Typeface.createFromAsset(getAssets(), "beyond_wonderland.ttf");
+
+ mPropertiesBSFragment = new PropertiesBSFragment();
+ mEmojiBSFragment = new EmojiBSFragment();
+ mStickerBSFragment = new StickerBSFragment();
+ mShapeBSFragment = new ShapeBSFragment();
+ mStickerBSFragment.setStickerListener(this);
+ mEmojiBSFragment.setEmojiListener(this);
+ mPropertiesBSFragment.setPropertiesChangeListener(this);
+ mShapeBSFragment.setPropertiesChangeListener(this);
+
+ LinearLayoutManager llmTools = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
+ mRvTools.setLayoutManager(llmTools);
+ mRvTools.setAdapter(mEditingToolsAdapter);
+
+ LinearLayoutManager llmFilters = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
+ mRvFilters.setLayoutManager(llmFilters);
+ mRvFilters.setAdapter(mFilterViewAdapter);
+
+ // NOTE(lucianocheng): Used to set integration testing parameters to PhotoEditor
+ boolean pinchTextScalable = getIntent().getBooleanExtra(PINCH_TEXT_SCALABLE_INTENT_KEY, true);
+
+ //Typeface mTextRobotoTf = ResourcesCompat.getFont(this, R.font.roboto_medium);
+ //Typeface mEmojiTypeFace = Typeface.createFromAsset(getAssets(), "emojione-android.ttf");
+ Typeface mEmojiTypeFace = Typeface.createFromAsset(getAssets(), "emojione-android.ttf");
+
+ mPhotoEditor = new PhotoEditor.Builder(this, mPhotoEditorView)
+ .setPinchTextScalable(pinchTextScalable) // set flag to make text scalable when pinch
+ //.setDefaultTextTypeface(mTextRobotoTf)
+ .setPinchTextScalable(true)
+ .setDefaultEmojiTypeface(mEmojiTypeFace)
+ .build(); // build photo editor sdk
+
+ mPhotoEditor.setOnPhotoEditorListener(this);
+
+
+ //Set Image Dynamically
+ try {
+ mPhotoEditorView.getSource().setImageURI(uri);
+ } catch (Exception e) {
+ Toasty.error(EditImageActivity.this, getString(R.string.toast_error)).show();
+ }
+
+ if (uri != null) {
+ try (InputStream inputStream = getContentResolver().openInputStream(uri)) {
+ assert inputStream != null;
+ ExifInterface exif = new ExifInterface(inputStream);
+ int rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
+ int rotationInDegrees = exifToDegrees(rotation);
+ mPhotoEditorView.getSource().setRotation(rotationInDegrees);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ Button send = findViewById(R.id.send);
+
+ send.setOnClickListener(v -> {
+ exit = true;
+ saveImage();
+ });
+ }
+
+ private void handleIntentImage(ImageView source) {
+ Intent intent = getIntent();
+ if (intent != null) {
+ // NOTE(lucianocheng): Using "yoda conditions" here to guard against
+ // a null Action in the Intent.
+ if (Intent.ACTION_EDIT.equals(intent.getAction()) ||
+ ACTION_NEXTGEN_EDIT.equals(intent.getAction())) {
+ try {
+ Uri uri = intent.getData();
+ Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
+ source.setImageBitmap(bitmap);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ String intentType = intent.getType();
+ if (intentType != null && intentType.startsWith("image/")) {
+ Uri imageUri = intent.getData();
+ if (imageUri != null) {
+ source.setImageURI(imageUri);
+ }
+ }
+ }
+ }
+ }
+
+ private void initViews() {
+ ImageView imgUndo;
+ ImageView imgRedo;
+ ImageView imgCamera;
+ ImageView imgGallery;
+ ImageView imgSave;
+ ImageView imgClose;
+ ImageView imgCrop;
+
+ mPhotoEditorView = findViewById(R.id.photoEditorView);
+ mTxtCurrentTool = findViewById(R.id.txtCurrentTool);
+ mRvTools = findViewById(R.id.rvConstraintTools);
+ mRvFilters = findViewById(R.id.rvFilterView);
+ mRootView = findViewById(R.id.rootView);
+
+ imgUndo = findViewById(R.id.imgUndo);
+ imgUndo.setOnClickListener(this);
+
+ imgRedo = findViewById(R.id.imgRedo);
+ imgRedo.setOnClickListener(this);
+
+ imgCamera = findViewById(R.id.imgCamera);
+ imgCamera.setOnClickListener(this);
+
+ imgGallery = findViewById(R.id.imgGallery);
+ imgGallery.setOnClickListener(this);
+
+ imgSave = findViewById(R.id.imgSave);
+ imgSave.setOnClickListener(this);
+
+ imgClose = findViewById(R.id.imgClose);
+ imgClose.setOnClickListener(this);
+
+ imgCrop = findViewById(R.id.imgCrop);
+ imgCrop.setOnClickListener(this);
+
+ }
+
+ @Override
+ public void onEditTextChangeListener(final View rootView, String text, int colorCode) {
+ TextEditorDialogFragment textEditorDialogFragment =
+ TextEditorDialogFragment.show(this, text, colorCode);
+ textEditorDialogFragment.setOnTextEditorListener((inputText, newColorCode) -> {
+ final TextStyleBuilder styleBuilder = new TextStyleBuilder();
+ styleBuilder.withTextColor(newColorCode);
+
+ mPhotoEditor.editText(rootView, inputText, styleBuilder);
+ mTxtCurrentTool.setText(R.string.label_text);
+ });
+ }
+
+ @Override
+ public void onAddViewListener(ViewType viewType, int numberOfAddedViews) {
+ }
+
+ @Override
+ public void onRemoveViewListener(ViewType viewType, int numberOfAddedViews) {
+ }
+
+ @Override
+ public void onStartViewChangeListener(ViewType viewType) {
+ }
+
+ @Override
+ public void onStopViewChangeListener(ViewType viewType) {
+ }
+
+ @Override
+ public void onTouchSourceImage(MotionEvent event) {
+ }
+
+ @SuppressLint("NonConstantResourceId")
+ @Override
+ public void onClick(View view) {
+ switch (view.getId()) {
+
+ case R.id.imgUndo:
+ mPhotoEditor.undo();
+ break;
+
+ case R.id.imgRedo:
+ mPhotoEditor.redo();
+ break;
+
+ case R.id.imgSave:
+ saveImage();
+ break;
+
+ case R.id.imgClose:
+ onBackPressed();
+ break;
+ case R.id.imgCrop:
+ shareImage();
+ break;
+
+ case R.id.imgCamera:
+ Intent cameraIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
+ startActivityForResult(cameraIntent, CAMERA_REQUEST);
+ break;
+
+ case R.id.imgGallery:
+ Intent intent = new Intent();
+ intent.setType("image/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ startActivityForResult(Intent.createChooser(intent, "Select Picture"), PICK_REQUEST);
+ break;
+ }
+ }
+
+ private void shareImage() {
+ if (mSaveImageUri == null) {
+ //showSnackbar(getString(R.string.msg_save_image_to_share));
+ return;
+ }
+
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("image/*");
+ intent.putExtra(Intent.EXTRA_STREAM, buildFileProviderUri(mSaveImageUri));
+ startActivity(Intent.createChooser(intent, getString(R.string.msg_share_image)));
+ }
+
+ private Uri buildFileProviderUri(@NonNull Uri uri) {
+ return FileProvider.getUriForFile(this,
+ FILE_PROVIDER_AUTHORITY,
+ new File(uri.getPath()));
+ }
+
+
+ private void saveImage() {
+ final String fileName = System.currentTimeMillis() + ".png";
+ final boolean hasStoragePermission =
+ ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED;
+ if (hasStoragePermission || isSdkHigherThan28()) {
+ showLoading("Saving...");
+ mSaveFileHelper.createFile(fileName, (fileCreated, filePath, error, uri) -> {
+ if (fileCreated) {
+ SaveSettings saveSettings = new SaveSettings.Builder()
+ .setClearViewsEnabled(true)
+ .setTransparencyEnabled(true)
+ .build();
+
+ mPhotoEditor.saveAsFile(filePath, saveSettings, new PhotoEditor.OnSaveListener() {
+ @Override
+ public void onSuccess(@NonNull String imagePath) {
+ mSaveFileHelper.notifyThatFileIsNowPubliclyAvailable(getContentResolver());
+ hideLoading();
+ showSnackbar("Image Saved Successfully");
+ mSaveImageUri = uri;
+ mPhotoEditorView.getSource().setImageURI(mSaveImageUri);
+ }
+
+ @Override
+ public void onFailure(@NonNull Exception exception) {
+ hideLoading();
+ showSnackbar("Failed to save Image");
+ }
+ });
+
+ } else {
+ hideLoading();
+ showSnackbar(error);
+ }
+ });
+ } else {
+ requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ }
+ }
+
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ ExifInterface exif;
+ int rotation;
+ int rotationInDegrees = 0;
+ if (data != null && data.getData() != null) {
+ try (InputStream inputStream = getContentResolver().openInputStream(data.getData())) {
+ assert inputStream != null;
+ exif = new ExifInterface(inputStream);
+ rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
+ rotationInDegrees = exifToDegrees(rotation);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ switch (requestCode) {
+ case CAMERA_REQUEST:
+ if (data != null && data.getExtras() != null) {
+ mPhotoEditor.clearAllViews();
+ Bitmap photo = (Bitmap) data.getExtras().get("data");
+ mPhotoEditorView.getSource().setImageBitmap(photo);
+ mPhotoEditorView.getSource().setRotation(rotationInDegrees);
+ }
+ break;
+ case PICK_REQUEST:
+ if (data != null && data.getData() != null) {
+ try {
+ mPhotoEditor.clearAllViews();
+ Uri uri = data.getData();
+ Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
+ mPhotoEditorView.getSource().setImageBitmap(bitmap);
+ mPhotoEditorView.getSource().setRotation(rotationInDegrees);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ break;
+ case CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE:
+
+ CropImage.ActivityResult result = CropImage.getActivityResult(data);
+ if (result != null) {
+ Uri resultUri = result.getUri();
+ if (resultUri != null) {
+ mPhotoEditorView.getSource().setImageURI(resultUri);
+ mPhotoEditorView.getSource().setRotation(rotationInDegrees);
+ if (uri != null && uri.getPath() != null) {
+ File fdelete = new File(uri.getPath());
+ if (fdelete.exists()) {
+ //noinspection ResultOfMethodCallIgnored
+ fdelete.delete();
+ }
+ }
+ uri = resultUri;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onColorChanged(int colorCode) {
+ mPhotoEditor.setShape(mShapeBuilder.withShapeColor(colorCode));
+ mTxtCurrentTool.setText(R.string.label_brush);
+ }
+
+ @Override
+ public void onOpacityChanged(int opacity) {
+ mPhotoEditor.setShape(mShapeBuilder.withShapeOpacity(opacity));
+ mTxtCurrentTool.setText(R.string.label_brush);
+ }
+
+ @Override
+ public void onShapeSizeChanged(int shapeSize) {
+ mPhotoEditor.setShape(mShapeBuilder.withShapeSize(shapeSize));
+ mTxtCurrentTool.setText(R.string.label_brush);
+ }
+
+ @Override
+ public void onShapePicked(ShapeType shapeType) {
+ mPhotoEditor.setShape(mShapeBuilder.withShapeType(shapeType));
+ }
+
+ @Override
+ public void onEmojiClick(String emojiUnicode) {
+ mPhotoEditor.addEmoji(emojiUnicode);
+ mTxtCurrentTool.setText(R.string.label_emoji);
+ }
+
+ @Override
+ public void onStickerClick(Bitmap bitmap) {
+ mPhotoEditor.addImage(bitmap);
+ mTxtCurrentTool.setText(R.string.label_sticker);
+ }
+
+
+ private void showSaveDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(getString(R.string.msg_save_image));
+ builder.setPositiveButton("Save", (dialog, which) -> saveImage());
+ builder.setNegativeButton("Cancel", (dialog, which) -> dialog.dismiss());
+ builder.setNeutralButton("Discard", (dialog, which) -> finish());
+ builder.create().show();
+
+ }
+
+ @Override
+ public void onFilterSelected(PhotoFilter photoFilter) {
+ mPhotoEditor.setFilterEffect(photoFilter);
+ }
+
+ @Override
+ public void onToolSelected(ToolType toolType) {
+ switch (toolType) {
+ case SHAPE:
+ mPhotoEditor.setBrushDrawingMode(true);
+ mShapeBuilder = new ShapeBuilder();
+ mPhotoEditor.setShape(mShapeBuilder);
+ mTxtCurrentTool.setText(R.string.label_shape);
+ showBottomSheetDialogFragment(mShapeBSFragment);
+ break;
+ case TEXT:
+ TextEditorDialogFragment textEditorDialogFragment = TextEditorDialogFragment.show(this);
+ textEditorDialogFragment.setOnTextEditorListener((inputText, colorCode) -> {
+ final TextStyleBuilder styleBuilder = new TextStyleBuilder();
+ styleBuilder.withTextColor(colorCode);
+
+ mPhotoEditor.addText(inputText, styleBuilder);
+ mTxtCurrentTool.setText(R.string.label_text);
+ });
+ break;
+ case ERASER:
+ mPhotoEditor.brushEraser();
+ mTxtCurrentTool.setText(R.string.label_eraser_mode);
+ break;
+ case FILTER:
+ mTxtCurrentTool.setText(R.string.label_filter);
+ showFilter(true);
+ break;
+ case EMOJI:
+ showBottomSheetDialogFragment(mEmojiBSFragment);
+ break;
+ case STICKER:
+ showBottomSheetDialogFragment(mStickerBSFragment);
+ break;
+ case BRUSH:
+ mPhotoEditor.setBrushDrawingMode(true);
+ mTxtCurrentTool.setText(R.string.label_brush);
+ mPropertiesBSFragment.show(getSupportFragmentManager(), mPropertiesBSFragment.getTag());
+ break;
+ }
+ }
+
+ private void showBottomSheetDialogFragment(BottomSheetDialogFragment fragment) {
+ if (fragment == null || fragment.isAdded()) {
+ return;
+ }
+ fragment.show(getSupportFragmentManager(), fragment.getTag());
+ }
+
+
+ void showFilter(boolean isVisible) {
+ mIsFilterVisible = isVisible;
+ mConstraintSet.clone(mRootView);
+
+ if (isVisible) {
+ mConstraintSet.clear(mRvFilters.getId(), ConstraintSet.START);
+ mConstraintSet.connect(mRvFilters.getId(), ConstraintSet.START,
+ ConstraintSet.PARENT_ID, ConstraintSet.START);
+ mConstraintSet.connect(mRvFilters.getId(), ConstraintSet.END,
+ ConstraintSet.PARENT_ID, ConstraintSet.END);
+ } else {
+ mConstraintSet.connect(mRvFilters.getId(), ConstraintSet.START,
+ ConstraintSet.PARENT_ID, ConstraintSet.END);
+ mConstraintSet.clear(mRvFilters.getId(), ConstraintSet.END);
+ }
+
+ ChangeBounds changeBounds = new ChangeBounds();
+ changeBounds.setDuration(350);
+ changeBounds.setInterpolator(new AnticipateOvershootInterpolator(1.0f));
+ TransitionManager.beginDelayedTransition(mRootView, changeBounds);
+
+ mConstraintSet.applyTo(mRootView);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mIsFilterVisible) {
+ showFilter(false);
+ mTxtCurrentTool.setText(R.string.app_name);
+ } else if (!mPhotoEditor.isCacheEmpty()) {
+ showSaveDialog();
+ } else {
+ super.onBackPressed();
+ }
+ }
+}
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/EmojiBSFragment.java b/app/src/main/java/app/fedilab/android/imageeditor/EmojiBSFragment.java
new file mode 100644
index 00000000..61baa135
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/EmojiBSFragment.java
@@ -0,0 +1,140 @@
+package app.fedilab.android.imageeditor;
+
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import java.util.ArrayList;
+
+import app.fedilab.android.R;
+
+public class EmojiBSFragment extends BottomSheetDialogFragment {
+
+ private final BottomSheetBehavior.BottomSheetCallback mBottomSheetBehaviorCallback = new BottomSheetBehavior.BottomSheetCallback() {
+
+ @Override
+ public void onStateChanged(@NonNull View bottomSheet, int newState) {
+ if (newState == BottomSheetBehavior.STATE_HIDDEN) {
+ dismiss();
+ }
+
+ }
+
+ @Override
+ public void onSlide(@NonNull View bottomSheet, float slideOffset) {
+ }
+ };
+ private EmojiListener mEmojiListener;
+
+ public EmojiBSFragment() {
+ // Required empty public constructor
+ }
+
+ /**
+ * Provide the list of emoji in form of unicode string
+ *
+ * @param context context
+ * @return list of emoji unicode
+ */
+ public static ArrayList getEmojis(Context context) {
+ ArrayList convertedEmojiList = new ArrayList<>();
+ String[] emojiList = context.getResources().getStringArray(R.array.photo_editor_emoji);
+ for (String emojiUnicode : emojiList) {
+ convertedEmojiList.add(convertEmoji(emojiUnicode));
+ }
+ return convertedEmojiList;
+ }
+
+ private static String convertEmoji(String emoji) {
+ String returnedEmoji;
+ try {
+ int convertEmojiToInt = Integer.parseInt(emoji.substring(2), 16);
+ returnedEmoji = new String(Character.toChars(convertEmojiToInt));
+ } catch (NumberFormatException e) {
+ returnedEmoji = "";
+ }
+ return returnedEmoji;
+ }
+
+ @SuppressLint("RestrictedApi")
+ @Override
+ public void setupDialog(Dialog dialog, int style) {
+ super.setupDialog(dialog, style);
+ View contentView = View.inflate(getContext(), R.layout.fragment_bottom_sticker_emoji_dialog, null);
+ dialog.setContentView(contentView);
+ CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) ((View) contentView.getParent()).getLayoutParams();
+ CoordinatorLayout.Behavior behavior = params.getBehavior();
+
+ if (behavior != null && behavior instanceof BottomSheetBehavior) {
+ ((BottomSheetBehavior) behavior).setBottomSheetCallback(mBottomSheetBehaviorCallback);
+ }
+ ((View) contentView.getParent()).setBackgroundColor(getResources().getColor(android.R.color.transparent));
+ RecyclerView rvEmoji = contentView.findViewById(R.id.rvEmoji);
+
+ GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), 5);
+ rvEmoji.setLayoutManager(gridLayoutManager);
+ EmojiAdapter emojiAdapter = new EmojiAdapter();
+ rvEmoji.setAdapter(emojiAdapter);
+ }
+
+ public void setEmojiListener(EmojiListener emojiListener) {
+ mEmojiListener = emojiListener;
+ }
+
+ public interface EmojiListener {
+ void onEmojiClick(String emojiUnicode);
+ }
+
+ public class EmojiAdapter extends RecyclerView.Adapter {
+
+ ArrayList emojisList = getEmojis(getActivity());
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_emoji, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ holder.txtEmoji.setText(emojisList.get(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return emojisList.size();
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder {
+ TextView txtEmoji;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ txtEmoji = itemView.findViewById(R.id.txtEmoji);
+
+ itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mEmojiListener != null) {
+ mEmojiListener.onEmojiClick(emojisList.get(getLayoutPosition()));
+ }
+ dismiss();
+ }
+ });
+ }
+ }
+ }
+}
+
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/FileSaveHelper.java b/app/src/main/java/app/fedilab/android/imageeditor/FileSaveHelper.java
new file mode 100644
index 00000000..78fd8be4
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/FileSaveHelper.java
@@ -0,0 +1,185 @@
+package app.fedilab.android.imageeditor;
+
+import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.MediaStore;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.OnLifecycleEvent;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * General contract of this class is to
+ * create a file on a device.
+ *
+ * How to Use it-
+ * Call {@linkplain FileSaveHelper#createFile(String, OnFileCreateResult)}
+ * if file is created you would receive it's file path and Uri
+ * and after you are done with File call {@linkplain FileSaveHelper#notifyThatFileIsNowPubliclyAvailable(ContentResolver)}
+ *
+ * Remember! in order to shutdown executor call {@linkplain FileSaveHelper#addObserver(LifecycleOwner)} or
+ * create object with the {@linkplain FileSaveHelper#FileSaveHelper(AppCompatActivity)}
+ */
+public class FileSaveHelper implements LifecycleObserver {
+ private final ContentResolver mContentResolver;
+ private final ExecutorService executor;
+ private final MutableLiveData fileCreatedResult;
+ private OnFileCreateResult resultListener;
+ private final Observer observer = fileMeta -> {
+ if (resultListener != null) {
+ resultListener.onFileCreateResult(fileMeta.isCreated,
+ fileMeta.filePath,
+ fileMeta.error,
+ fileMeta.uri);
+ }
+ };
+
+
+ public FileSaveHelper(ContentResolver contentResolver) {
+ mContentResolver = contentResolver;
+ executor = Executors.newSingleThreadExecutor();
+ fileCreatedResult = new MutableLiveData<>();
+ }
+
+ public FileSaveHelper(AppCompatActivity activity) {
+ this(activity.getContentResolver());
+ addObserver(activity);
+ }
+
+ public static boolean isSdkHigherThan28() {
+ return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q);
+ }
+
+ private void addObserver(LifecycleOwner lifecycleOwner) {
+ fileCreatedResult.observe(lifecycleOwner, observer);
+ lifecycleOwner.getLifecycle().addObserver(this);
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ public void release() {
+ if (null != executor) {
+ executor.shutdownNow();
+ }
+ }
+
+ /**
+ * The effects of this method are
+ * 1- insert new Image File data in MediaStore.Images column
+ * 2- create File on Disk.
+ *
+ * @param fileNameToSave fileName
+ * @param listener result listener
+ */
+
+ public void createFile(final String fileNameToSave, OnFileCreateResult listener) {
+ this.resultListener = listener;
+ executor.submit(() -> {
+ Cursor cursor = null;
+ String filePath;
+ try {
+ final ContentValues newImageDetails = new ContentValues();
+ Uri imageCollection = buildUriCollection(newImageDetails);
+ final Uri editedImageUri = getEditedImageUri(fileNameToSave, newImageDetails, imageCollection);
+ filePath = getFilePath(cursor, editedImageUri);
+ updateResult(true, filePath, null, editedImageUri, newImageDetails);
+ } catch (final Exception ex) {
+ ex.printStackTrace();
+ updateResult(false, null, ex.getMessage(), null, null);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ });
+ }
+
+ private String getFilePath(Cursor cursor, Uri editedImageUri) {
+ String[] proj = {MediaStore.Images.Media.DATA};
+ cursor = mContentResolver.query(editedImageUri, proj, null, null, null);
+ int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
+ cursor.moveToFirst();
+ return cursor.getString(column_index);
+ }
+
+ private Uri getEditedImageUri(String fileNameToSave, ContentValues newImageDetails, Uri imageCollection) throws IOException {
+ newImageDetails.put(MediaStore.Images.Media.DISPLAY_NAME, fileNameToSave);
+ final Uri editedImageUri = mContentResolver.insert(imageCollection, newImageDetails);
+ final OutputStream outputStream = mContentResolver.openOutputStream(editedImageUri);
+ outputStream.close();
+ return editedImageUri;
+ }
+
+ @SuppressLint("InlinedApi")
+ private Uri buildUriCollection(ContentValues newImageDetails) {
+ Uri imageCollection;
+ if (isSdkHigherThan28()) {
+ imageCollection = MediaStore.Images.Media.getContentUri(
+ MediaStore.VOLUME_EXTERNAL_PRIMARY
+ );
+ newImageDetails.put(MediaStore.Images.Media.IS_PENDING, 1);
+ } else {
+ imageCollection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ }
+ return imageCollection;
+ }
+
+ @SuppressLint("InlinedApi")
+ public void notifyThatFileIsNowPubliclyAvailable(ContentResolver contentResolver) {
+ if (isSdkHigherThan28()) {
+ executor.submit(() -> {
+ FileMeta value = fileCreatedResult.getValue();
+ if (value != null) {
+ value.imageDetails.clear();
+ value.imageDetails.put(MediaStore.Images.Media.IS_PENDING, 0);
+ contentResolver.update(value.uri, value.imageDetails, null, null);
+ }
+ });
+ }
+ }
+
+ private void updateResult(boolean result, String filePath, String error, Uri uri, ContentValues newImageDetails) {
+ fileCreatedResult.postValue(new FileMeta(result, filePath, uri, error, newImageDetails));
+ }
+
+ public interface OnFileCreateResult {
+ /**
+ * @param created whether file creation is success or failure
+ * @param filePath filepath on disk. null in case of failure
+ * @param error in case file creation is failed . it would represent the cause
+ * @param Uri Uri to the newly created file. null in case of failure
+ */
+ void onFileCreateResult(boolean created, String filePath, String error, Uri Uri);
+ }
+
+ private static class FileMeta {
+ public ContentValues imageDetails;
+ public boolean isCreated;
+ public String filePath;
+ public Uri uri;
+ public String error;
+
+ public FileMeta(boolean isCreated, String filePath,
+ Uri uri, String error,
+ ContentValues newImageDetails) {
+ this.isCreated = isCreated;
+ this.filePath = filePath;
+ this.uri = uri;
+ this.error = error;
+ this.imageDetails = newImageDetails;
+ }
+ }
+
+}
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/PhotoApp.java b/app/src/main/java/app/fedilab/android/imageeditor/PhotoApp.java
new file mode 100644
index 00000000..23d1c058
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/PhotoApp.java
@@ -0,0 +1,53 @@
+package app.fedilab.android.imageeditor;
+
+import android.app.Application;
+import android.content.Context;
+
+/**
+ * Created by Burhanuddin Rashid on 1/23/2018.
+ */
+
+public class PhotoApp extends Application {
+ private static final String TAG = PhotoApp.class.getSimpleName();
+ private static PhotoApp sPhotoApp;
+
+ public static PhotoApp getPhotoApp() {
+ return sPhotoApp;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ sPhotoApp = this;
+ /* FontRequest fontRequest = new FontRequest(
+ "com.google.android.gms.fonts",
+ "com.google.android.gms",
+ "Noto Color Emoji Compat",
+ R.array.com_google_android_gms_fonts_certs);
+
+ EmojiCompat.Config config = new FontRequestEmojiCompatConfig(this, fontRequest)
+ .setReplaceAll(true)
+ // .setEmojiSpanIndicatorEnabled(true)
+ // .setEmojiSpanIndicatorColor(Color.GREEN)
+ .registerInitCallback(new EmojiCompat.InitCallback() {
+ @Override
+ public void onInitialized() {
+ super.onInitialized();
+ Log.e(TAG, "Success");
+ }
+
+ @Override
+ public void onFailed(@Nullable Throwable throwable) {
+ super.onFailed(throwable);
+ Log.e(TAG, "onFailed: " + throwable.getMessage());
+ }
+ });
+
+ // BundledEmojiCompatConfig bundledEmojiCompatConfig = new BundledEmojiCompatConfig(this);
+ EmojiCompat.init(config);*/
+ }
+
+ public Context getContext() {
+ return sPhotoApp.getContext();
+ }
+}
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/PropertiesBSFragment.java b/app/src/main/java/app/fedilab/android/imageeditor/PropertiesBSFragment.java
new file mode 100644
index 00000000..aac1ed59
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/PropertiesBSFragment.java
@@ -0,0 +1,99 @@
+package app.fedilab.android.imageeditor;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SeekBar;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import app.fedilab.android.R;
+
+public class PropertiesBSFragment extends BottomSheetDialogFragment implements SeekBar.OnSeekBarChangeListener {
+
+ private Properties mProperties;
+
+ public PropertiesBSFragment() {
+ // Required empty public constructor
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_bottom_properties_dialog, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ RecyclerView rvColor = view.findViewById(R.id.rvColors);
+ SeekBar sbOpacity = view.findViewById(R.id.sbOpacity);
+ SeekBar sbBrushSize = view.findViewById(R.id.sbSize);
+
+ sbOpacity.setOnSeekBarChangeListener(this);
+ sbBrushSize.setOnSeekBarChangeListener(this);
+
+ LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false);
+ rvColor.setLayoutManager(layoutManager);
+ rvColor.setHasFixedSize(true);
+ ColorPickerAdapter colorPickerAdapter = new ColorPickerAdapter(getActivity());
+ colorPickerAdapter.setOnColorPickerClickListener(new ColorPickerAdapter.OnColorPickerClickListener() {
+ @Override
+ public void onColorPickerClickListener(int colorCode) {
+ if (mProperties != null) {
+ dismiss();
+ mProperties.onColorChanged(colorCode);
+ }
+ }
+ });
+ rvColor.setAdapter(colorPickerAdapter);
+ }
+
+ public void setPropertiesChangeListener(Properties properties) {
+ mProperties = properties;
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
+ switch (seekBar.getId()) {
+ case R.id.sbOpacity:
+ if (mProperties != null) {
+ mProperties.onOpacityChanged(i);
+ }
+ break;
+ case R.id.sbSize:
+ if (mProperties != null) {
+ mProperties.onShapeSizeChanged(i);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ public interface Properties {
+ void onColorChanged(int colorCode);
+
+ void onOpacityChanged(int opacity);
+
+ void onShapeSizeChanged(int shapeSize);
+ }
+}
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/ShapeBSFragment.java b/app/src/main/java/app/fedilab/android/imageeditor/ShapeBSFragment.java
new file mode 100644
index 00000000..12dca879
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/ShapeBSFragment.java
@@ -0,0 +1,112 @@
+package app.fedilab.android.imageeditor;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RadioGroup;
+import android.widget.SeekBar;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import app.fedilab.android.R;
+import ja.burhanrashid52.photoeditor.shape.ShapeType;
+
+public class ShapeBSFragment extends BottomSheetDialogFragment implements SeekBar.OnSeekBarChangeListener {
+
+ private Properties mProperties;
+
+ public ShapeBSFragment() {
+ // Required empty public constructor
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_bottom_shapes_dialog, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ RecyclerView rvColor = view.findViewById(R.id.shapeColors);
+ SeekBar sbOpacity = view.findViewById(R.id.shapeOpacity);
+ SeekBar sbBrushSize = view.findViewById(R.id.shapeSize);
+ RadioGroup shapeGroup = view.findViewById(R.id.shapeRadioGroup);
+
+ // shape picker
+ shapeGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ if (checkedId == R.id.lineRadioButton) {
+ mProperties.onShapePicked(ShapeType.LINE);
+ } else if (checkedId == R.id.ovalRadioButton) {
+ mProperties.onShapePicked(ShapeType.OVAL);
+ } else if (checkedId == R.id.rectRadioButton) {
+ mProperties.onShapePicked(ShapeType.RECTANGLE);
+ } else {
+ mProperties.onShapePicked(ShapeType.BRUSH);
+ }
+ });
+
+ sbOpacity.setOnSeekBarChangeListener(this);
+ sbBrushSize.setOnSeekBarChangeListener(this);
+
+ LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false);
+ rvColor.setLayoutManager(layoutManager);
+ rvColor.setHasFixedSize(true);
+ ColorPickerAdapter colorPickerAdapter = new ColorPickerAdapter(getActivity());
+ colorPickerAdapter.setOnColorPickerClickListener(colorCode -> {
+ if (mProperties != null) {
+ dismiss();
+ mProperties.onColorChanged(colorCode);
+ }
+ });
+ rvColor.setAdapter(colorPickerAdapter);
+ }
+
+ public void setPropertiesChangeListener(Properties properties) {
+ mProperties = properties;
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
+ switch (seekBar.getId()) {
+ case R.id.shapeOpacity:
+ if (mProperties != null) {
+ mProperties.onOpacityChanged(i);
+ }
+ break;
+ case R.id.shapeSize:
+ if (mProperties != null) {
+ mProperties.onShapeSizeChanged(i);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+
+ public interface Properties {
+ void onColorChanged(int colorCode);
+
+ void onOpacityChanged(int opacity);
+
+ void onShapeSizeChanged(int shapeSize);
+
+ void onShapePicked(ShapeType shapeType);
+ }
+}
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/StickerBSFragment.java b/app/src/main/java/app/fedilab/android/imageeditor/StickerBSFragment.java
new file mode 100644
index 00000000..370f9a11
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/StickerBSFragment.java
@@ -0,0 +1,137 @@
+package app.fedilab.android.imageeditor;
+
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import app.fedilab.android.R;
+
+public class StickerBSFragment extends BottomSheetDialogFragment {
+
+ private final BottomSheetBehavior.BottomSheetCallback mBottomSheetBehaviorCallback = new BottomSheetBehavior.BottomSheetCallback() {
+
+ @Override
+ public void onStateChanged(@NonNull View bottomSheet, int newState) {
+ if (newState == BottomSheetBehavior.STATE_HIDDEN) {
+ dismiss();
+ }
+
+ }
+
+ @Override
+ public void onSlide(@NonNull View bottomSheet, float slideOffset) {
+ }
+ };
+ private StickerListener mStickerListener;
+
+ public StickerBSFragment() {
+ // Required empty public constructor
+ }
+
+ public void setStickerListener(StickerListener stickerListener) {
+ mStickerListener = stickerListener;
+ }
+
+ @SuppressLint("RestrictedApi")
+ @Override
+ public void setupDialog(Dialog dialog, int style) {
+ super.setupDialog(dialog, style);
+ View contentView = View.inflate(getContext(), R.layout.fragment_bottom_sticker_emoji_dialog, null);
+ dialog.setContentView(contentView);
+ CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) ((View) contentView.getParent()).getLayoutParams();
+ CoordinatorLayout.Behavior behavior = params.getBehavior();
+
+ if (behavior != null && behavior instanceof BottomSheetBehavior) {
+ ((BottomSheetBehavior) behavior).setBottomSheetCallback(mBottomSheetBehaviorCallback);
+ }
+ ((View) contentView.getParent()).setBackgroundColor(getResources().getColor(android.R.color.transparent));
+ RecyclerView rvEmoji = contentView.findViewById(R.id.rvEmoji);
+
+ GridLayoutManager gridLayoutManager = new GridLayoutManager(getActivity(), 3);
+ rvEmoji.setLayoutManager(gridLayoutManager);
+ StickerAdapter stickerAdapter = new StickerAdapter();
+ rvEmoji.setAdapter(stickerAdapter);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ }
+
+ private String convertEmoji(String emoji) {
+ String returnedEmoji = "";
+ try {
+ int convertEmojiToInt = Integer.parseInt(emoji.substring(2), 16);
+ returnedEmoji = getEmojiByUnicode(convertEmojiToInt);
+ } catch (NumberFormatException e) {
+ returnedEmoji = "";
+ }
+ return returnedEmoji;
+ }
+
+ private String getEmojiByUnicode(int unicode) {
+ return new String(Character.toChars(unicode));
+ }
+
+ public interface StickerListener {
+ void onStickerClick(Bitmap bitmap);
+ }
+
+ public class StickerAdapter extends RecyclerView.Adapter {
+
+ int[] stickerList = new int[]{R.drawable.aa, R.drawable.bb};
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_sticker, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ holder.imgSticker.setImageResource(stickerList[position]);
+ }
+
+ @Override
+ public int getItemCount() {
+ return stickerList.length;
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder {
+ ImageView imgSticker;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ imgSticker = itemView.findViewById(R.id.imgSticker);
+
+ itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mStickerListener != null) {
+ mStickerListener.onStickerClick(
+ BitmapFactory.decodeResource(getResources(),
+ stickerList[getLayoutPosition()]));
+ }
+ dismiss();
+ }
+ });
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/TextEditorDialogFragment.java b/app/src/main/java/app/fedilab/android/imageeditor/TextEditorDialogFragment.java
new file mode 100644
index 00000000..936056d5
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/TextEditorDialogFragment.java
@@ -0,0 +1,130 @@
+package app.fedilab.android.imageeditor;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.DialogFragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import app.fedilab.android.R;
+
+/**
+ * Created by Burhanuddin Rashid on 1/16/2018.
+ */
+
+public class TextEditorDialogFragment extends DialogFragment {
+
+ public static final String TAG = TextEditorDialogFragment.class.getSimpleName();
+ public static final String EXTRA_INPUT_TEXT = "extra_input_text";
+ public static final String EXTRA_COLOR_CODE = "extra_color_code";
+ private EditText mAddTextEditText;
+ private TextView mAddTextDoneTextView;
+ private InputMethodManager mInputMethodManager;
+ private int mColorCode;
+ private TextEditor mTextEditor;
+
+ //Show dialog with provide text and text color
+ public static TextEditorDialogFragment show(@NonNull AppCompatActivity appCompatActivity,
+ @NonNull String inputText,
+ @ColorInt int colorCode) {
+ Bundle args = new Bundle();
+ args.putString(EXTRA_INPUT_TEXT, inputText);
+ args.putInt(EXTRA_COLOR_CODE, colorCode);
+ TextEditorDialogFragment fragment = new TextEditorDialogFragment();
+ fragment.setArguments(args);
+ fragment.show(appCompatActivity.getSupportFragmentManager(), TAG);
+ return fragment;
+ }
+
+ //Show dialog with default text input as empty and text color white
+ public static TextEditorDialogFragment show(@NonNull AppCompatActivity appCompatActivity) {
+ return show(appCompatActivity,
+ "", ContextCompat.getColor(appCompatActivity, R.color.white));
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Dialog dialog = getDialog();
+ //Make dialog full screen with transparent background
+ if (dialog != null) {
+ int width = ViewGroup.LayoutParams.MATCH_PARENT;
+ int height = ViewGroup.LayoutParams.MATCH_PARENT;
+ dialog.getWindow().setLayout(width, height);
+ dialog.getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
+ }
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.add_text_dialog, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mAddTextEditText = view.findViewById(R.id.add_text_edit_text);
+ mInputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ mAddTextDoneTextView = view.findViewById(R.id.add_text_done_tv);
+
+ //Setup the color picker for text color
+ RecyclerView addTextColorPickerRecyclerView = view.findViewById(R.id.add_text_color_picker_recycler_view);
+ LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false);
+ addTextColorPickerRecyclerView.setLayoutManager(layoutManager);
+ addTextColorPickerRecyclerView.setHasFixedSize(true);
+ ColorPickerAdapter colorPickerAdapter = new ColorPickerAdapter(getActivity());
+ //This listener will change the text color when clicked on any color from picker
+ colorPickerAdapter.setOnColorPickerClickListener(new ColorPickerAdapter.OnColorPickerClickListener() {
+ @Override
+ public void onColorPickerClickListener(int colorCode) {
+ mColorCode = colorCode;
+ mAddTextEditText.setTextColor(colorCode);
+ }
+ });
+ addTextColorPickerRecyclerView.setAdapter(colorPickerAdapter);
+ mAddTextEditText.setText(getArguments().getString(EXTRA_INPUT_TEXT));
+ mColorCode = getArguments().getInt(EXTRA_COLOR_CODE);
+ mAddTextEditText.setTextColor(mColorCode);
+ mInputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
+
+ //Make a callback on activity when user is done with text editing
+ mAddTextDoneTextView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mInputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ dismiss();
+ String inputText = mAddTextEditText.getText().toString();
+ if (!TextUtils.isEmpty(inputText) && mTextEditor != null) {
+ mTextEditor.onDone(inputText, mColorCode);
+ }
+ }
+ });
+
+ }
+
+ //Callback to listener if user is done with text editing
+ public void setOnTextEditorListener(TextEditor textEditor) {
+ mTextEditor = textEditor;
+ }
+
+
+ public interface TextEditor {
+ void onDone(String inputText, int colorCode);
+ }
+}
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/base/BaseActivity.java b/app/src/main/java/app/fedilab/android/imageeditor/base/BaseActivity.java
new file mode 100644
index 00000000..3a7d262d
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/base/BaseActivity.java
@@ -0,0 +1,79 @@
+package app.fedilab.android.imageeditor.base;
+
+import android.app.ProgressDialog;
+import android.content.pm.PackageManager;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import com.google.android.material.snackbar.Snackbar;
+
+/**
+ * Created by Burhanuddin Rashid on 1/17/2018.
+ */
+
+public class BaseActivity extends AppCompatActivity {
+
+ public static final int READ_WRITE_STORAGE = 52;
+ private ProgressDialog mProgressDialog;
+
+
+ public boolean requestPermission(String permission) {
+ boolean isGranted = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED;
+ if (!isGranted) {
+ ActivityCompat.requestPermissions(
+ this,
+ new String[]{permission},
+ READ_WRITE_STORAGE);
+ }
+ return isGranted;
+ }
+
+ public void isPermissionGranted(boolean isGranted, String permission) {
+
+ }
+
+ public void makeFullScreen() {
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ switch (requestCode) {
+ case READ_WRITE_STORAGE:
+ isPermissionGranted(grantResults[0] == PackageManager.PERMISSION_GRANTED, permissions[0]);
+ break;
+ }
+ }
+
+ protected void showLoading(@NonNull String message) {
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setMessage(message);
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.show();
+ }
+
+ protected void hideLoading() {
+ if (mProgressDialog != null) {
+ mProgressDialog.dismiss();
+ }
+ }
+
+ protected void showSnackbar(@NonNull String message) {
+ View view = findViewById(android.R.id.content);
+ if (view != null) {
+ Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+ }
+ }
+}
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/base/BaseFragment.java b/app/src/main/java/app/fedilab/android/imageeditor/base/BaseFragment.java
new file mode 100644
index 00000000..8e798b42
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/base/BaseFragment.java
@@ -0,0 +1,29 @@
+package app.fedilab.android.imageeditor.base;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+/**
+ * @author Burhanuddin Rashid
+ * @version 0.1.2
+ * @since 5/25/2018
+ */
+public abstract class BaseFragment extends Fragment {
+
+ protected abstract int getLayoutId();
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ if (getLayoutId() == 0) {
+ throw new IllegalArgumentException("Invalid layout id");
+ }
+ return inflater.inflate(getLayoutId(), container, false);
+ }
+}
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/filters/FilterListener.java b/app/src/main/java/app/fedilab/android/imageeditor/filters/FilterListener.java
new file mode 100644
index 00000000..1d627ce9
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/filters/FilterListener.java
@@ -0,0 +1,7 @@
+package app.fedilab.android.imageeditor.filters;
+
+import ja.burhanrashid52.photoeditor.PhotoFilter;
+
+public interface FilterListener {
+ void onFilterSelected(PhotoFilter photoFilter);
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/filters/FilterViewAdapter.java b/app/src/main/java/app/fedilab/android/imageeditor/filters/FilterViewAdapter.java
new file mode 100644
index 00000000..077e21c0
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/filters/FilterViewAdapter.java
@@ -0,0 +1,115 @@
+package app.fedilab.android.imageeditor.filters;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import app.fedilab.android.R;
+import ja.burhanrashid52.photoeditor.PhotoFilter;
+
+/**
+ * @author Burhanuddin Rashid
+ * @version 0.1.2
+ * @since 5/23/2018
+ */
+public class FilterViewAdapter extends RecyclerView.Adapter {
+
+ private final FilterListener mFilterListener;
+ private final List> mPairList = new ArrayList<>();
+
+ public FilterViewAdapter(FilterListener filterListener) {
+ mFilterListener = filterListener;
+ setupFilters();
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_filter_view, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ Pair filterPair = mPairList.get(position);
+ Bitmap fromAsset = getBitmapFromAsset(holder.itemView.getContext(), filterPair.first);
+ holder.mImageFilterView.setImageBitmap(fromAsset);
+ holder.mTxtFilterName.setText(filterPair.second.name().replace("_", " "));
+ }
+
+ @Override
+ public int getItemCount() {
+ return mPairList.size();
+ }
+
+ private Bitmap getBitmapFromAsset(Context context, String strName) {
+ AssetManager assetManager = context.getAssets();
+ InputStream istr = null;
+ try {
+ istr = assetManager.open(strName);
+ return BitmapFactory.decodeStream(istr);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private void setupFilters() {
+ mPairList.add(new Pair<>("filters/original.jpg", PhotoFilter.NONE));
+ mPairList.add(new Pair<>("filters/auto_fix.png", PhotoFilter.AUTO_FIX));
+ mPairList.add(new Pair<>("filters/brightness.png", PhotoFilter.BRIGHTNESS));
+ mPairList.add(new Pair<>("filters/contrast.png", PhotoFilter.CONTRAST));
+ mPairList.add(new Pair<>("filters/documentary.png", PhotoFilter.DOCUMENTARY));
+ mPairList.add(new Pair<>("filters/dual_tone.png", PhotoFilter.DUE_TONE));
+ mPairList.add(new Pair<>("filters/fill_light.png", PhotoFilter.FILL_LIGHT));
+ mPairList.add(new Pair<>("filters/fish_eye.png", PhotoFilter.FISH_EYE));
+ mPairList.add(new Pair<>("filters/grain.png", PhotoFilter.GRAIN));
+ mPairList.add(new Pair<>("filters/gray_scale.png", PhotoFilter.GRAY_SCALE));
+ mPairList.add(new Pair<>("filters/lomish.png", PhotoFilter.LOMISH));
+ mPairList.add(new Pair<>("filters/negative.png", PhotoFilter.NEGATIVE));
+ mPairList.add(new Pair<>("filters/posterize.png", PhotoFilter.POSTERIZE));
+ mPairList.add(new Pair<>("filters/saturate.png", PhotoFilter.SATURATE));
+ mPairList.add(new Pair<>("filters/sepia.png", PhotoFilter.SEPIA));
+ mPairList.add(new Pair<>("filters/sharpen.png", PhotoFilter.SHARPEN));
+ mPairList.add(new Pair<>("filters/temprature.png", PhotoFilter.TEMPERATURE));
+ mPairList.add(new Pair<>("filters/tint.png", PhotoFilter.TINT));
+ mPairList.add(new Pair<>("filters/vignette.png", PhotoFilter.VIGNETTE));
+ mPairList.add(new Pair<>("filters/cross_process.png", PhotoFilter.CROSS_PROCESS));
+ mPairList.add(new Pair<>("filters/b_n_w.png", PhotoFilter.BLACK_WHITE));
+ mPairList.add(new Pair<>("filters/flip_horizental.png", PhotoFilter.FLIP_HORIZONTAL));
+ mPairList.add(new Pair<>("filters/flip_vertical.png", PhotoFilter.FLIP_VERTICAL));
+ mPairList.add(new Pair<>("filters/rotate.png", PhotoFilter.ROTATE));
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder {
+ ImageView mImageFilterView;
+ TextView mTxtFilterName;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ mImageFilterView = itemView.findViewById(R.id.imgFilterView);
+ mTxtFilterName = itemView.findViewById(R.id.txtFilterName);
+ itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mFilterListener.onFilterSelected(mPairList.get(getLayoutPosition()).second);
+ }
+ });
+ }
+ }
+}
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/tools/EditingToolsAdapter.java b/app/src/main/java/app/fedilab/android/imageeditor/tools/EditingToolsAdapter.java
new file mode 100644
index 00000000..f994af1f
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/tools/EditingToolsAdapter.java
@@ -0,0 +1,85 @@
+package app.fedilab.android.imageeditor.tools;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import app.fedilab.android.R;
+
+/**
+ * @author Burhanuddin Rashid
+ * @version 0.1.2
+ * @since 5/23/2018
+ */
+public class EditingToolsAdapter extends RecyclerView.Adapter {
+
+ private final List mToolList = new ArrayList<>();
+ private final OnItemSelected mOnItemSelected;
+
+ public EditingToolsAdapter(OnItemSelected onItemSelected) {
+ mOnItemSelected = onItemSelected;
+ mToolList.add(new ToolModel("Shape", R.drawable.ic_oval, ToolType.SHAPE));
+ mToolList.add(new ToolModel("Text", R.drawable.ic_text, ToolType.TEXT));
+ mToolList.add(new ToolModel("Eraser", R.drawable.ic_eraser, ToolType.ERASER));
+ mToolList.add(new ToolModel("Filter", R.drawable.ic_photo_filter, ToolType.FILTER));
+ mToolList.add(new ToolModel("Emoji", R.drawable.ic_insert_emoticon, ToolType.EMOJI));
+ mToolList.add(new ToolModel("Sticker", R.drawable.ic_sticker, ToolType.STICKER));
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.row_editing_tools, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ ToolModel item = mToolList.get(position);
+ holder.txtTool.setText(item.mToolName);
+ holder.imgToolIcon.setImageResource(item.mToolIcon);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mToolList.size();
+ }
+
+ public interface OnItemSelected {
+ void onToolSelected(ToolType toolType);
+ }
+
+ class ToolModel {
+ private final String mToolName;
+ private final int mToolIcon;
+ private final ToolType mToolType;
+
+ ToolModel(String toolName, int toolIcon, ToolType toolType) {
+ mToolName = toolName;
+ mToolIcon = toolIcon;
+ mToolType = toolType;
+ }
+
+ }
+
+ class ViewHolder extends RecyclerView.ViewHolder {
+ ImageView imgToolIcon;
+ TextView txtTool;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ imgToolIcon = itemView.findViewById(R.id.imgToolIcon);
+ txtTool = itemView.findViewById(R.id.txtTool);
+ itemView.setOnClickListener(v -> mOnItemSelected.onToolSelected(mToolList.get(getLayoutPosition()).mToolType));
+ }
+ }
+}
diff --git a/app/src/main/java/app/fedilab/android/imageeditor/tools/ToolType.java b/app/src/main/java/app/fedilab/android/imageeditor/tools/ToolType.java
new file mode 100644
index 00000000..0054e22d
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/imageeditor/tools/ToolType.java
@@ -0,0 +1,16 @@
+package app.fedilab.android.imageeditor.tools;
+
+/**
+ * @author Burhanuddin Rashid
+ * @version 0.1.2
+ * @since 5/23/2018
+ */
+public enum ToolType {
+ BRUSH,
+ SHAPE,
+ TEXT,
+ ERASER,
+ FILTER,
+ EMOJI,
+ STICKER
+}
diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java
index c1614ccd..d6528c84 100644
--- a/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java
+++ b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java
@@ -30,6 +30,7 @@ import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
+import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
@@ -99,6 +100,7 @@ import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.MastodonHelper;
import app.fedilab.android.helper.ThemeHelper;
+import app.fedilab.android.imageeditor.EditImageActivity;
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
import app.fedilab.android.viewmodel.mastodon.SearchVM;
import es.dmoral.toasty.Toasty;
@@ -476,7 +478,13 @@ public class ComposeAdapter extends RecyclerView.Adapter {
+ Intent intent = new Intent(context, EditImageActivity.class);
+ Bundle b = new Bundle();
+ intent.putExtra("imageUri", attachment.local_path);
+ intent.putExtras(b);
+ context.startActivity(intent);
+ });
composeAttachmentItemBinding.buttonDescription.setOnClickListener(v -> {
AlertDialog.Builder builderInner = new AlertDialog.Builder(context, Helper.dialogStyle());
builderInner.setTitle(R.string.upload_form_description);
diff --git a/app/src/main/res/drawable/aa.png b/app/src/main/res/drawable/aa.png
new file mode 100644
index 00000000..ae288b86
Binary files /dev/null and b/app/src/main/res/drawable/aa.png differ
diff --git a/app/src/main/res/drawable/bb.png b/app/src/main/res/drawable/bb.png
new file mode 100644
index 00000000..cd18da80
Binary files /dev/null and b/app/src/main/res/drawable/bb.png differ
diff --git a/app/src/main/res/drawable/blank_image.jpg b/app/src/main/res/drawable/blank_image.jpg
new file mode 100644
index 00000000..93e8dbd9
Binary files /dev/null and b/app/src/main/res/drawable/blank_image.jpg differ
diff --git a/app/src/main/res/drawable/ic_brush.xml b/app/src/main/res/drawable/ic_brush.xml
new file mode 100644
index 00000000..154dbcf2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_brush.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml
new file mode 100644
index 00000000..a7c3363a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_camera.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml
new file mode 100644
index 00000000..299773a8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_close.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_crop.xml b/app/src/main/res/drawable/ic_crop.xml
new file mode 100644
index 00000000..cb2455ff
--- /dev/null
+++ b/app/src/main/res/drawable/ic_crop.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_eraser.xml b/app/src/main/res/drawable/ic_eraser.xml
new file mode 100644
index 00000000..212d2ae1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_eraser.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_gallery.xml b/app/src/main/res/drawable/ic_gallery.xml
new file mode 100644
index 00000000..0c981fc9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gallery.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_insert_emoticon.xml b/app/src/main/res/drawable/ic_insert_emoticon.xml
new file mode 100644
index 00000000..4622233c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_insert_emoticon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..a339898e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_oval.xml b/app/src/main/res/drawable/ic_oval.xml
new file mode 100644
index 00000000..b516ac23
--- /dev/null
+++ b/app/src/main/res/drawable/ic_oval.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_photo_filter.xml b/app/src/main/res/drawable/ic_photo_filter.xml
new file mode 100644
index 00000000..f7f1370c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_photo_filter.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_rectangle.xml b/app/src/main/res/drawable/ic_rectangle.xml
new file mode 100644
index 00000000..79ad219c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_rectangle.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_redo.xml b/app/src/main/res/drawable/ic_redo.xml
new file mode 100644
index 00000000..45e34649
--- /dev/null
+++ b/app/src/main/res/drawable/ic_redo.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_save.xml b/app/src/main/res/drawable/ic_save.xml
new file mode 100644
index 00000000..b639a3bb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_save.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_save_gallery.xml b/app/src/main/res/drawable/ic_save_gallery.xml
new file mode 100644
index 00000000..555a07c8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_save_gallery.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml
new file mode 100644
index 00000000..9dad7b85
--- /dev/null
+++ b/app/src/main/res/drawable/ic_share.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_sticker.xml b/app/src/main/res/drawable/ic_sticker.xml
new file mode 100644
index 00000000..8fa27070
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sticker.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_text.xml b/app/src/main/res/drawable/ic_text.xml
new file mode 100644
index 00000000..f01eeaeb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_text.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_undo.xml b/app/src/main/res/drawable/ic_undo.xml
new file mode 100644
index 00000000..b83d4dce
--- /dev/null
+++ b/app/src/main/res/drawable/ic_undo.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/original.jpg b/app/src/main/res/drawable/original.jpg
new file mode 100644
index 00000000..353089ed
Binary files /dev/null and b/app/src/main/res/drawable/original.jpg differ
diff --git a/app/src/main/res/drawable/paris_tower.jpg b/app/src/main/res/drawable/paris_tower.jpg
new file mode 100644
index 00000000..fab7eee2
Binary files /dev/null and b/app/src/main/res/drawable/paris_tower.jpg differ
diff --git a/app/src/main/res/drawable/rounded_border_text_view.xml b/app/src/main/res/drawable/rounded_border_text_view.xml
new file mode 100644
index 00000000..52cf54c4
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_border_text_view.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_edit_image.xml b/app/src/main/res/layout/activity_edit_image.xml
new file mode 100644
index 00000000..85d10454
--- /dev/null
+++ b/app/src/main/res/layout/activity_edit_image.xml
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_photoeditor.xml b/app/src/main/res/layout/activity_photoeditor.xml
new file mode 100644
index 00000000..d424e423
--- /dev/null
+++ b/app/src/main/res/layout/activity_photoeditor.xml
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/add_text_dialog.xml b/app/src/main/res/layout/add_text_dialog.xml
new file mode 100644
index 00000000..30bcac2b
--- /dev/null
+++ b/app/src/main/res/layout/add_text_dialog.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/color_picker_item_list.xml b/app/src/main/res/layout/color_picker_item_list.xml
new file mode 100644
index 00000000..010f0741
--- /dev/null
+++ b/app/src/main/res/layout/color_picker_item_list.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_bottom_custom_effect_dialog.xml b/app/src/main/res/layout/fragment_bottom_custom_effect_dialog.xml
new file mode 100644
index 00000000..aa4bb4f0
--- /dev/null
+++ b/app/src/main/res/layout/fragment_bottom_custom_effect_dialog.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_bottom_properties_dialog.xml b/app/src/main/res/layout/fragment_bottom_properties_dialog.xml
new file mode 100644
index 00000000..7bbc26d7
--- /dev/null
+++ b/app/src/main/res/layout/fragment_bottom_properties_dialog.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_bottom_shapes_dialog.xml b/app/src/main/res/layout/fragment_bottom_shapes_dialog.xml
new file mode 100644
index 00000000..b23b2515
--- /dev/null
+++ b/app/src/main/res/layout/fragment_bottom_shapes_dialog.xml
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_bottom_sticker_emoji_dialog.xml b/app/src/main/res/layout/fragment_bottom_sticker_emoji_dialog.xml
new file mode 100644
index 00000000..f64f0190
--- /dev/null
+++ b/app/src/main/res/layout/fragment_bottom_sticker_emoji_dialog.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/row_editing_tools.xml b/app/src/main/res/layout/row_editing_tools.xml
new file mode 100644
index 00000000..36b20ce7
--- /dev/null
+++ b/app/src/main/res/layout/row_editing_tools.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/row_emoji.xml b/app/src/main/res/layout/row_emoji.xml
new file mode 100644
index 00000000..824a9dfb
--- /dev/null
+++ b/app/src/main/res/layout/row_emoji.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/row_filter_view.xml b/app/src/main/res/layout/row_filter_view.xml
new file mode 100644
index 00000000..0827a775
--- /dev/null
+++ b/app/src/main/res/layout/row_filter_view.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/row_sticker.xml b/app/src/main/res/layout/row_sticker.xml
new file mode 100644
index 00000000..02e0e42a
--- /dev/null
+++ b/app/src/main/res/layout/row_sticker.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index c45fb085..09645154 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -96,7 +96,8 @@
#8359A3
#FBE870
#C5E17A
-
+ #151414
+ #99000000
#144365
@@ -159,4 +160,5 @@
#7986CB
#9E9E9E
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 27c38586..368075fc 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -15,5 +15,10 @@
10dp
50dp
12sp
+
+
35dp
+ 8dp
+ 16dp
+ 50dp
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1ec7001d..e194a070 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1616,4 +1616,416 @@
Fetch more messages…
+ Shape
+ Oval
+ Rectangle
+ Line
+
+ Eraser Mode
+ Adjust
+ Are you want to exit without saving image ?
+ Share Image
+
+
+
+ - u+1f604
+ - u+1f603
+ - u+1f600
+ - u+1f60a
+ - u+263a
+ - u+1f609
+ - u+1f60d
+ - u+1f618
+ - u+1f61a
+ - u+1f617
+ - u+1f619
+ - u+1f61c
+ - u+1f61d
+ - u+1f61b
+ - u+1f633
+ - u+1f601
+ - u+1f614
+ - u+1f60c
+ - u+1f612
+ - u+1f61e
+ - u+1f623
+ - u+1f622
+ - u+1f602
+ - u+1f62d
+ - u+1f62a
+ - u+1f625
+ - u+1f630
+ - u+1f605
+ - u+1f613
+ - u+1f629
+ - u+1f62b
+ - u+1f628
+ - u+1f631
+ - u+1f620
+ - u+1f621
+ - u+1f624
+
+ - u+1f616
+ - u+1f606
+ - u+1f60b
+ - u+1f637
+ - u+1f60e
+ - u+1f634
+ - u+1f635
+ - u+1f632
+ - u+1f61f
+ - u+1f626
+ - u+1f627
+ - u+1f608
+ - u+1f47f
+ - u+1f62e
+ - u+1f62c
+ - u+1f610
+ - u+1f615
+ - u+1f62f
+ - u+1f636
+ - u+1f607
+ - u+1f60f
+ - u+1f611
+ - u+1f472
+ - u+1f473
+ - u+1f46e
+ - u+1f477
+ - u+1f482
+ - u+1f476
+ - u+1f466
+ - u+1f467
+ - u+1f468
+ - u+1f469
+ - u+1f474
+ - u+1f475
+ - u+1f471
+ - u+1f47c
+ - u+1f478
+ - u+1f63a
+ - u+1f638
+ - u+1f63b
+ - u+1f63d
+ - u+1f63c
+ - u+1f640
+ - u+1f63f
+ - u+1f639
+ - u+1f63e
+ - u+1f479
+ - u+1f47a
+ - u+1f648
+ - u+1f649
+ - u+1f64a
+ - u+1f480
+ - u+1f47d
+ - u+1f4a9
+ - u+1f525
+ - u+2728
+ - u+1f31f
+ - u+1f4ab
+ - u+1f4a5
+ - u+1f4a2
+ - u+1f4a6
+ - u+1f4a7
+ - u+1f4a4
+ - u+1f4a8
+ - u+1f442
+ - u+1f440
+ - u+1f443
+ - u+1f445
+ - u+1f444
+ - u+1f44d
+ - u+1f44e
+ - u+1f44c
+ - u+1f44a
+ - u+270a
+ - u+270c
+ - u+1f44b
+ - u+270b
+ - u+1f450
+ - u+1f446
+ - u+1f447
+ - u+1f449
+ - u+1f448
+ - u+1f64c
+ - u+1f64f
+ - u+261d
+ - u+1f44f
+ - u+1f4aa
+ - u+1f6b6
+ - u+1f3c3
+ - u+1f483
+ - u+1f46b
+ - u+1f46a
+ - u+1f46c
+ - u+1f46d
+ - u+1f48f
+ - u+1f491
+ - u+1f46f
+ - u+1f646
+ - u+1f645
+ - u+1f481
+ - u+1f64b
+ - u+1f486
+ - u+1f487
+ - u+1f485
+ - u+1f470
+ - u+1f64e
+ - u+1f64d
+ - u+1f647
+ - u+1f3a9
+ - u+1f451
+ - u+1f452
+ - u+1f45f
+ - u+1f45e
+ - u+1f461
+ - u+1f460
+ - u+1f462
+ - u+1f455
+ - u+1f454
+ - u+1f45a
+ - u+1f457
+ - u+1f3bd
+ - u+1f456
+ - u+1f458
+ - u+1f459
+ - u+1f4bc
+ - u+1f45c
+ - u+1f45d
+ - u+1f45b
+ - u+1f453
+ - u+1f380
+ - u+1f302
+ - u+1f484
+ - u+1f49b
+ - u+1f499
+ - u+1f49c
+ - u+1f49a
+ - u+2764
+ - u+1f494
+ - u+1f497
+ - u+1f493
+ - u+1f495
+ - u+1f496
+ - u+1f49e
+ - u+1f498
+ - u+1f48c
+ - u+1f48b
+ - u+1f48d
+ - u+1f48e
+ - u+1f464
+ - u+1f465
+ - u+1f4ac
+ - u+1f463
+ - u+1f4ad
+
+ - u+1f436
+ - u+1f43a
+ - u+1f431
+ - u+1f42d
+ - u+1f439
+ - u+1f430
+ - u+1f438
+ - u+1f42f
+ - u+1f428
+ - u+1f43b
+ - u+1f437
+ - u+1f43d
+ - u+1f42e
+ - u+1f417
+ - u+1f435
+ - u+1f412
+ - u+1f434
+ - u+1f411
+ - u+1f418
+ - u+1f43c
+ - u+1f427
+ - u+1f426
+ - u+1f424
+ - u+1f425
+ - u+1f423
+ - u+1f414
+ - u+1f40d
+ - u+1f422
+ - u+1f41b
+ - u+1f41d
+ - u+1f41c
+ - u+1f41e
+ - u+1f40c
+ - u+1f419
+ - u+1f41a
+ - u+1f420
+ - u+1f41f
+ - u+1f42c
+ - u+1f433
+ - u+1f40b
+ - u+1f404
+ - u+1f40f
+ - u+1f400
+ - u+1f403
+ - u+1f405
+ - u+1f407
+ - u+1f409
+ - u+1f40e
+ - u+1f410
+ - u+1f413
+ - u+1f415
+ - u+1f416
+ - u+1f401
+ - u+1f402
+ - u+1f432
+ - u+1f421
+ - u+1f40a
+ - u+1f42b
+ - u+1f42a
+ - u+1f406
+ - u+1f408
+ - u+1f429
+ - u+1f43e
+ - u+1f490
+ - u+1f338
+ - u+1f337
+ - u+1f340
+ - u+1f339
+ - u+1f33b
+ - u+1f33a
+ - u+1f341
+ - u+1f343
+ - u+1f342
+ - u+1f33f
+ - u+1f33e
+ - u+1f344
+ - u+1f335
+ - u+1f334
+ - u+1f332
+ - u+1f333
+ - u+1f330
+ - u+1f331
+ - u+1f33c
+ - u+1f310
+ - u+1f31e
+ - u+1f31d
+ - u+1f31a
+ - u+1f311
+ - u+1f312
+ - u+1f313
+ - u+1f314
+ - u+1f315
+ - u+1f316
+ - u+1f317
+ - u+1f318
+ - u+1f31c
+ - u+1f31b
+ - u+1f319
+ - u+1f30d
+ - u+1f30e
+ - u+1f30f
+ - u+1f30b
+ - u+1f30c
+ - u+1f320
+ - u+2b50
+ - u+2600
+ - u+26c5
+ - u+2601
+ - u+26a1
+ - u+2614
+ - u+2744
+ - u+26c4
+ - u+1f300
+ - u+1f301
+ - u+1f308
+ - u+1f30a
+
+ - u+1f3e0
+ - u+1f3e1
+ - u+1f3eb
+ - u+1f3e2
+ - u+1f3e3
+ - u+1f3e5
+ - u+1f3e6
+ - u+1f3ea
+ - u+1f3e9
+ - u+1f3e8
+ - u+1f492
+ - u+26ea
+ - u+1f3ec
+ - u+1f3e4
+ - u+1f307
+ - u+1f306
+ - u+1f3ef
+ - u+1f3f0
+ - u+26fa
+ - u+1f3ed
+ - u+1f5fc
+ - u+1f5fe
+ - u+1f5fb
+ - u+1f304
+ - u+1f305
+ - u+1f303
+ - u+1f5fd
+ - u+1f309
+ - u+1f3a0
+ - u+1f3a1
+ - u+26f2
+ - u+1f3a2
+ - u+1f6a2
+ - u+26f5
+ - u+1f6a4
+ - u+1f6a3
+ - u+2693
+ - u+1f680
+ - u+2708
+ - u+1f4ba
+ - u+1f681
+ - u+1f682
+ - u+1f68a
+ - u+1f689
+ - u+1f69e
+ - u+1f686
+ - u+1f684
+ - u+1f685
+ - u+1f688
+ - u+1f687
+ - u+1f69d
+ - u+1f68b
+ - u+1f683
+ - u+1f68e
+ - u+1f68c
+ - u+1f68d
+ - u+1f699
+ - u+1f698
+ - u+1f697
+ - u+1f695
+ - u+1f696
+ - u+1f69b
+ - u+1f69a
+ - u+1f6a8
+ - u+1f693
+ - u+1f694
+ - u+1f692
+ - u+1f691
+ - u+1f690
+ - u+1f6b2
+ - u+1f6a1
+ - u+1f69f
+ - u+1f6a0
+ - u+1f69c
+ - u+1f488
+ - u+1f68f
+ - u+1f3ab
+ - u+1f6a6
+ - u+1f6a5
+ - u+26a0
+ - u+1f6a7
+ - u+1f530
+ - u+26fd
+ - u+1f3ee
+ - u+1f3b0
+ - u+2668
+ - u+1f5ff
+ - u+1f3aa
+ - u+1f3ad
+ - u+1f4cd
+ - u+1f6a9
+
diff --git a/cropper/build.gradle b/cropper/build.gradle
new file mode 100644
index 00000000..f603264a
--- /dev/null
+++ b/cropper/build.gradle
@@ -0,0 +1,23 @@
+apply plugin: 'com.android.library'
+
+android {
+
+ compileSdk 31
+ defaultConfig {
+ minSdkVersion 14
+ versionCode 1
+ }
+ 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"
+}
+
diff --git a/cropper/src/main/AndroidManifest.xml b/cropper/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fe8baa84
--- /dev/null
+++ b/cropper/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapCroppingWorkerTask.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapCroppingWorkerTask.java
new file mode 100644
index 00000000..54d33df4
--- /dev/null
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapCroppingWorkerTask.java
@@ -0,0 +1,354 @@
+// "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 {
+
+ // region: Fields and Consts
+
+ /**
+ * Use a WeakReference to ensure the ImageView can be garbage collected
+ */
+ private final WeakReference 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
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapLoadingWorkerTask.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapLoadingWorkerTask.java
new file mode 100644
index 00000000..a6ecf939
--- /dev/null
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapLoadingWorkerTask.java
@@ -0,0 +1,176 @@
+// "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 {
+
+ // region: Fields and Consts
+
+ /**
+ * Use a WeakReference to ensure the ImageView can be garbage collected
+ */
+ private final WeakReference 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
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapUtils.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapUtils.java
new file mode 100644
index 00000000..7190bd42
--- /dev/null
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/BitmapUtils.java
@@ -0,0 +1,923 @@
+// "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> 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).
+ * If no rotation is required the image will not be rotated.
+ * 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.
+ * If no rotation is required the image will not be rotated.
+ * 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.
+ * 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.
+ * 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.
+ * 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.
+ * 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.
+ * 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.
+ */
+ 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.
+ * 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.
+ * 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.
+ * 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
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImage.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImage.java
new file mode 100644
index 00000000..07d36ea0
--- /dev/null
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImage.java
@@ -0,0 +1,1048 @@
+// "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.app.Activity;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.MediaStore;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.fragment.app.Fragment;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper to simplify crop image work like starting pick-image acitvity and handling camera/gallery
+ * intents.
+ * The goal of the helper is to simplify the starting and most-common usage of image cropping and
+ * not all porpose all possible scenario one-to-rule-them-all code base. So feel free to use it as
+ * is and as a wiki to make your own.
+ * Added value you get out-of-the-box is some edge case handling that you may miss otherwise, like
+ * the stupid-ass Android camera result URI that may differ from version to version and from device
+ * to device.
+ */
+@SuppressWarnings("WeakerAccess, unused")
+public final class CropImage {
+
+ // region: Fields and Consts
+
+ /**
+ * The key used to pass crop image source URI to {@link CropImageActivity}.
+ */
+ public static final String CROP_IMAGE_EXTRA_SOURCE = "CROP_IMAGE_EXTRA_SOURCE";
+
+ /**
+ * The key used to pass crop image options to {@link CropImageActivity}.
+ */
+ public static final String CROP_IMAGE_EXTRA_OPTIONS = "CROP_IMAGE_EXTRA_OPTIONS";
+
+ /**
+ * The key used to pass crop image bundle data to {@link CropImageActivity}.
+ */
+ public static final String CROP_IMAGE_EXTRA_BUNDLE = "CROP_IMAGE_EXTRA_BUNDLE";
+
+ /**
+ * The key used to pass crop image result data back from {@link CropImageActivity}.
+ */
+ public static final String CROP_IMAGE_EXTRA_RESULT = "CROP_IMAGE_EXTRA_RESULT";
+
+ /**
+ * The request code used to start pick image activity to be used on result to identify the this
+ * specific request.
+ */
+ public static final int PICK_IMAGE_CHOOSER_REQUEST_CODE = 200;
+
+ /**
+ * The request code used to request permission to pick image from external storage.
+ */
+ public static final int PICK_IMAGE_PERMISSIONS_REQUEST_CODE = 201;
+
+ /**
+ * The request code used to request permission to capture image from camera.
+ */
+ public static final int CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE = 2011;
+
+ /**
+ * The request code used to start {@link CropImageActivity} to be used on result to identify the
+ * this specific request.
+ */
+ public static final int CROP_IMAGE_ACTIVITY_REQUEST_CODE = 203;
+
+ /**
+ * The result code used to return error from {@link CropImageActivity}.
+ */
+ public static final int CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE = 204;
+ // endregion
+
+ private CropImage() {
+ }
+
+ /**
+ * Create a new bitmap that has all pixels beyond the oval shape transparent. Old bitmap is
+ * recycled.
+ */
+ public static Bitmap toOvalBitmap(@NonNull Bitmap bitmap) {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+
+ Canvas canvas = new Canvas(output);
+
+ int color = 0xff424242;
+ Paint paint = new Paint();
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(color);
+
+ RectF rect = new RectF(0, 0, width, height);
+ canvas.drawOval(rect, paint);
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, 0, 0, paint);
+
+ bitmap.recycle();
+
+ return output;
+ }
+
+ /**
+ * Start an activity to get image for cropping using chooser intent that will have all the
+ * available applications for the device like camera (MyCamera), galery (Photos), store apps
+ * (Dropbox), etc.
+ * Use "pick_image_intent_chooser_title" string resource to override pick chooser title.
+ *
+ * @param activity the activity to be used to start activity from
+ */
+ public static void startPickImageActivity(@NonNull Activity activity) {
+ activity.startActivityForResult(
+ getPickImageChooserIntent(activity), PICK_IMAGE_CHOOSER_REQUEST_CODE);
+ }
+
+ /**
+ * Same as {@link #startPickImageActivity(Activity) startPickImageActivity} method but instead of
+ * being called and returning to an Activity, this method can be called and return to a Fragment.
+ *
+ * @param context The Fragments context. Use getContext()
+ * @param fragment The calling Fragment to start and return the image to
+ */
+ public static void startPickImageActivity(@NonNull Context context, @NonNull Fragment fragment) {
+ fragment.startActivityForResult(
+ getPickImageChooserIntent(context), PICK_IMAGE_CHOOSER_REQUEST_CODE);
+ }
+
+ /**
+ * Create a chooser intent to select the source to get image from.
+ * The source can be camera's (ACTION_IMAGE_CAPTURE) or gallery's (ACTION_GET_CONTENT).
+ * All possible sources are added to the intent chooser.
+ * Use "pick_image_intent_chooser_title" string resource to override chooser title.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ */
+ public static Intent getPickImageChooserIntent(@NonNull Context context) {
+ return getPickImageChooserIntent(
+ context, context.getString(R.string.pick_image_intent_chooser_title), false, true);
+ }
+
+ /**
+ * Create a chooser intent to select the source to get image from.
+ * The source can be camera's (ACTION_IMAGE_CAPTURE) or gallery's (ACTION_GET_CONTENT).
+ * All possible sources are added to the intent chooser.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ * @param title the title to use for the chooser UI
+ * @param includeDocuments if to include KitKat documents activity containing all sources
+ * @param includeCamera if to include camera intents
+ */
+ public static Intent getPickImageChooserIntent(
+ @NonNull Context context,
+ CharSequence title,
+ boolean includeDocuments,
+ boolean includeCamera) {
+
+ List allIntents = new ArrayList<>();
+ PackageManager packageManager = context.getPackageManager();
+
+ // collect all camera intents if Camera permission is available
+ if (!isExplicitCameraPermissionRequired(context) && includeCamera) {
+ allIntents.addAll(getCameraIntents(context, packageManager));
+ }
+
+ List galleryIntents =
+ getGalleryIntents(packageManager, Intent.ACTION_GET_CONTENT, includeDocuments);
+ if (galleryIntents.size() == 0) {
+ // if no intents found for get-content try pick intent action (Huawei P9).
+ galleryIntents = getGalleryIntents(packageManager, Intent.ACTION_PICK, includeDocuments);
+ }
+ allIntents.addAll(galleryIntents);
+
+ Intent target;
+ if (allIntents.isEmpty()) {
+ target = new Intent();
+ } else {
+ target = allIntents.get(allIntents.size() - 1);
+ allIntents.remove(allIntents.size() - 1);
+ }
+
+ // Create a chooser from the main intent
+ Intent chooserIntent = Intent.createChooser(target, title);
+
+ // Add all other intents
+ chooserIntent.putExtra(
+ Intent.EXTRA_INITIAL_INTENTS, allIntents.toArray(new Parcelable[allIntents.size()]));
+
+ return chooserIntent;
+ }
+
+ /**
+ * Get the main Camera intent for capturing image using device camera app. If the outputFileUri is
+ * null, a default Uri will be created with {@link #getCaptureImageOutputUri(Context)}, so then
+ * you will be able to get the pictureUri using {@link #getPickImageResultUri(Context, Intent)}.
+ * Otherwise, it is just you use the Uri passed to this method.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ * @param outputFileUri the Uri where the picture will be placed.
+ */
+ public static Intent getCameraIntent(@NonNull Context context, Uri outputFileUri) {
+ Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+ if (outputFileUri == null) {
+ outputFileUri = getCaptureImageOutputUri(context);
+ }
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
+ return intent;
+ }
+
+ /**
+ * Get all Camera intents for capturing image using device camera apps.
+ */
+ public static List getCameraIntents(
+ @NonNull Context context, @NonNull PackageManager packageManager) {
+
+ List allIntents = new ArrayList<>();
+
+ // Determine Uri of camera image to save.
+ Uri outputFileUri = getCaptureImageOutputUri(context);
+
+ Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+ List listCam = packageManager.queryIntentActivities(captureIntent, 0);
+ for (ResolveInfo res : listCam) {
+ Intent intent = new Intent(captureIntent);
+ intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
+ intent.setPackage(res.activityInfo.packageName);
+ if (outputFileUri != null) {
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
+ }
+ allIntents.add(intent);
+ }
+
+ return allIntents;
+ }
+
+ /**
+ * Get all Gallery intents for getting image from one of the apps of the device that handle
+ * images.
+ */
+ public static List getGalleryIntents(
+ @NonNull PackageManager packageManager, String action, boolean includeDocuments) {
+ List intents = new ArrayList<>();
+ Intent galleryIntent =
+ action == Intent.ACTION_GET_CONTENT
+ ? new Intent(action)
+ : new Intent(action, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ galleryIntent.setType("image/*");
+ List listGallery = packageManager.queryIntentActivities(galleryIntent, 0);
+ for (ResolveInfo res : listGallery) {
+ Intent intent = new Intent(galleryIntent);
+ intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
+ intent.setPackage(res.activityInfo.packageName);
+ intents.add(intent);
+ }
+
+ // remove documents intent
+ if (!includeDocuments) {
+ for (Intent intent : intents) {
+ if (intent
+ .getComponent()
+ .getClassName()
+ .equals("com.android.documentsui.DocumentsActivity")) {
+ intents.remove(intent);
+ break;
+ }
+ }
+ }
+ return intents;
+ }
+
+ /**
+ * Check if explicetly requesting camera permission is required.
+ * It is required in Android Marshmellow and above if "CAMERA" permission is requested in the
+ * manifest.
+ * See StackOverflow
+ * question.
+ */
+ public static boolean isExplicitCameraPermissionRequired(@NonNull Context context) {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && hasPermissionInManifest(context, "android.permission.CAMERA")
+ && context.checkSelfPermission(Manifest.permission.CAMERA)
+ != PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * Check if the app requests a specific permission in the manifest.
+ *
+ * @param permissionName the permission to check
+ * @return true - the permission in requested in manifest, false - not.
+ */
+ public static boolean hasPermissionInManifest(
+ @NonNull Context context, @NonNull String permissionName) {
+ String packageName = context.getPackageName();
+ try {
+ PackageInfo packageInfo =
+ context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
+ final String[] declaredPermisisons = packageInfo.requestedPermissions;
+ if (declaredPermisisons != null && declaredPermisisons.length > 0) {
+ for (String p : declaredPermisisons) {
+ if (p.equalsIgnoreCase(permissionName)) {
+ return true;
+ }
+ }
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+ return false;
+ }
+
+ /**
+ * Get URI to image received from capture by camera.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ */
+ public static Uri getCaptureImageOutputUri(@NonNull Context context) {
+ Uri outputFileUri = null;
+ File getImage = context.getExternalCacheDir();
+ if (getImage != null) {
+ outputFileUri = Uri.fromFile(new File(getImage.getPath(), "pickImageResult.jpeg"));
+ }
+ return outputFileUri;
+ }
+
+ /**
+ * Get the URI of the selected image from {@link #getPickImageChooserIntent(Context)}.
+ * Will return the correct URI for camera and gallery image.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ * @param data the returned data of the activity result
+ */
+ public static Uri getPickImageResultUri(@NonNull Context context, @Nullable Intent data) {
+ boolean isCamera = true;
+ if (data != null && data.getData() != null) {
+ String action = data.getAction();
+ isCamera = action != null && action.equals(MediaStore.ACTION_IMAGE_CAPTURE);
+ }
+ return isCamera || data.getData() == null ? getCaptureImageOutputUri(context) : data.getData();
+ }
+
+ /**
+ * Check if the given picked image URI requires READ_EXTERNAL_STORAGE permissions.
+ * Only relevant for API version 23 and above and not required for all URI's depends on the
+ * implementation of the app that was used for picking the image. So we just test if we can open
+ * the stream or do we get an exception when we try, Android is awesome.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ * @param uri the result URI of image pick.
+ * @return true - required permission are not granted, false - either no need for permissions or
+ * they are granted
+ */
+ public static boolean isReadExternalStoragePermissionsRequired(
+ @NonNull Context context, @NonNull Uri uri) {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED
+ && isUriRequiresPermissions(context, uri);
+ }
+
+ /**
+ * Test if we can open the given Android URI to test if permission required error is thrown.
+ * Only relevant for API version 23 and above.
+ *
+ * @param context used to access Android APIs, like content resolve, it is your
+ * activity/fragment/widget.
+ * @param uri the result URI of image pick.
+ */
+ public static boolean isUriRequiresPermissions(@NonNull Context context, @NonNull Uri uri) {
+ try {
+ ContentResolver resolver = context.getContentResolver();
+ InputStream stream = resolver.openInputStream(uri);
+ if (stream != null) {
+ stream.close();
+ }
+ return false;
+ } catch (Exception e) {
+ return true;
+ }
+ }
+
+ /**
+ * Create {@link ActivityBuilder} instance to open image picker for cropping and then start {@link
+ * CropImageActivity} to crop the selected image.
+ * Result will be received in {@link Activity#onActivityResult(int, int, Intent)} and can be
+ * retrieved using {@link #getActivityResult(Intent)}.
+ *
+ * @return builder for Crop Image Activity
+ */
+ public static ActivityBuilder activity() {
+ return new ActivityBuilder(null);
+ }
+
+ /**
+ * Create {@link ActivityBuilder} instance to start {@link CropImageActivity} to crop the given
+ * image.
+ * Result will be received in {@link Activity#onActivityResult(int, int, Intent)} and can be
+ * retrieved using {@link #getActivityResult(Intent)}.
+ *
+ * @param uri the image Android uri source to crop or null to start a picker
+ * @return builder for Crop Image Activity
+ */
+ public static ActivityBuilder activity(@Nullable Uri uri) {
+ return new ActivityBuilder(uri);
+ }
+
+ /**
+ * Get {@link CropImageActivity} result data object for crop image activity started using {@link
+ * #activity(Uri)}.
+ *
+ * @param data result data intent as received in {@link Activity#onActivityResult(int, int,
+ * Intent)}.
+ * @return Crop Image Activity Result object or null if none exists
+ */
+ public static ActivityResult getActivityResult(@Nullable Intent data) {
+ return data != null ? (ActivityResult) data.getParcelableExtra(CROP_IMAGE_EXTRA_RESULT) : null;
+ }
+
+ // region: Inner class: ActivityBuilder
+
+ /**
+ * Builder used for creating Image Crop Activity by user request.
+ */
+ public static final class ActivityBuilder {
+
+ /**
+ * The image to crop source Android uri.
+ */
+ @Nullable
+ private final Uri mSource;
+
+ /**
+ * Options for image crop UX
+ */
+ private final CropImageOptions mOptions;
+
+ private ActivityBuilder(@Nullable Uri source) {
+ mSource = source;
+ mOptions = new CropImageOptions();
+ }
+
+ /**
+ * Get {@link CropImageActivity} intent to start the activity.
+ */
+ public Intent getIntent(@NonNull Context context) {
+ return getIntent(context, CropImageActivity.class);
+ }
+
+ /**
+ * Get {@link CropImageActivity} intent to start the activity.
+ */
+ public Intent getIntent(@NonNull Context context, @Nullable Class> cls) {
+ mOptions.validate();
+
+ Intent intent = new Intent();
+ intent.setClass(context, cls);
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(CROP_IMAGE_EXTRA_SOURCE, mSource);
+ bundle.putParcelable(CROP_IMAGE_EXTRA_OPTIONS, mOptions);
+ intent.putExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE, bundle);
+ return intent;
+ }
+
+ /**
+ * Start {@link CropImageActivity}.
+ *
+ * @param activity activity to receive result
+ */
+ public void start(@NonNull Activity activity) {
+ mOptions.validate();
+ activity.startActivityForResult(getIntent(activity), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
+ }
+
+ /**
+ * Start {@link CropImageActivity}.
+ *
+ * @param activity activity to receive result
+ */
+ public void start(@NonNull Activity activity, @Nullable Class> cls) {
+ mOptions.validate();
+ activity.startActivityForResult(getIntent(activity, cls), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
+ }
+
+ /**
+ * Start {@link CropImageActivity}.
+ *
+ * @param fragment fragment to receive result
+ */
+ public void start(@NonNull Context context, @NonNull Fragment fragment) {
+ fragment.startActivityForResult(getIntent(context), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
+ }
+
+ /**
+ * Start {@link CropImageActivity}.
+ *
+ * @param fragment fragment to receive result
+ */
+ @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
+ public void start(@NonNull Context context, @NonNull android.app.Fragment fragment) {
+ fragment.startActivityForResult(getIntent(context), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
+ }
+
+ /**
+ * Start {@link CropImageActivity}.
+ *
+ * @param fragment fragment to receive result
+ */
+ public void start(
+ @NonNull Context context, @NonNull Fragment fragment, @Nullable Class> cls) {
+ fragment.startActivityForResult(getIntent(context, cls), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
+ }
+
+ /**
+ * Start {@link CropImageActivity}.
+ *
+ * @param fragment fragment to receive result
+ */
+ @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
+ public void start(
+ @NonNull Context context, @NonNull android.app.Fragment fragment, @Nullable Class> cls) {
+ fragment.startActivityForResult(getIntent(context, cls), CROP_IMAGE_ACTIVITY_REQUEST_CODE);
+ }
+
+ /**
+ * The shape of the cropping window.
+ * To set square/circle crop shape set aspect ratio to 1:1.
+ * Default: RECTANGLE
+ */
+ public ActivityBuilder setCropShape(@NonNull CropImageView.CropShape cropShape) {
+ mOptions.cropShape = cropShape;
+ return this;
+ }
+
+ /**
+ * 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).
+ * Default: 3dp
+ */
+ public ActivityBuilder setSnapRadius(float snapRadius) {
+ mOptions.snapRadius = snapRadius;
+ return this;
+ }
+
+ /**
+ * The radius of the touchable area around the handle (in pixels).
+ * We are basing this value off of the recommended 48dp Rhythm.
+ * See: http://developer.android.com/design/style/metrics-grids.html#48dp-rhythm
+ * Default: 48dp
+ */
+ public ActivityBuilder setTouchRadius(float touchRadius) {
+ mOptions.touchRadius = touchRadius;
+ return this;
+ }
+
+ /**
+ * whether the guidelines should be on, off, or only showing when resizing.
+ * Default: ON_TOUCH
+ */
+ public ActivityBuilder setGuidelines(@NonNull CropImageView.Guidelines guidelines) {
+ mOptions.guidelines = guidelines;
+ return this;
+ }
+
+ /**
+ * The initial scale type of the image in the crop image view
+ * Default: FIT_CENTER
+ */
+ public ActivityBuilder setScaleType(@NonNull CropImageView.ScaleType scaleType) {
+ mOptions.scaleType = scaleType;
+ return this;
+ }
+
+ /**
+ * if to show crop overlay UI what contains the crop window UI surrounded by background over the
+ * cropping image.
+ * default: true, may disable for animation or frame transition.
+ */
+ public ActivityBuilder setShowCropOverlay(boolean showCropOverlay) {
+ mOptions.showCropOverlay = showCropOverlay;
+ return this;
+ }
+
+ /**
+ * if auto-zoom functionality is enabled.
+ * default: true.
+ */
+ public ActivityBuilder setAutoZoomEnabled(boolean autoZoomEnabled) {
+ mOptions.autoZoomEnabled = autoZoomEnabled;
+ return this;
+ }
+
+ /**
+ * if multi touch functionality is enabled.
+ * default: true.
+ */
+ public ActivityBuilder setMultiTouchEnabled(boolean multiTouchEnabled) {
+ mOptions.multiTouchEnabled = multiTouchEnabled;
+ return this;
+ }
+
+ /**
+ * The max zoom allowed during cropping.
+ * Default: 4
+ */
+ public ActivityBuilder setMaxZoom(int maxZoom) {
+ mOptions.maxZoom = maxZoom;
+ return this;
+ }
+
+ /**
+ * The initial crop window padding from image borders in percentage of the cropping image
+ * dimensions.
+ * Default: 0.1
+ */
+ public ActivityBuilder setInitialCropWindowPaddingRatio(float initialCropWindowPaddingRatio) {
+ mOptions.initialCropWindowPaddingRatio = initialCropWindowPaddingRatio;
+ return this;
+ }
+
+ /**
+ * whether the width to height aspect ratio should be maintained or free to change.
+ * Default: false
+ */
+ public ActivityBuilder setFixAspectRatio(boolean fixAspectRatio) {
+ mOptions.fixAspectRatio = fixAspectRatio;
+ return this;
+ }
+
+ /**
+ * the X,Y value of the aspect ratio.
+ * Also sets fixes aspect ratio to TRUE.
+ * Default: 1/1
+ *
+ * @param aspectRatioX the width
+ * @param aspectRatioY the height
+ */
+ public ActivityBuilder setAspectRatio(int aspectRatioX, int aspectRatioY) {
+ mOptions.aspectRatioX = aspectRatioX;
+ mOptions.aspectRatioY = aspectRatioY;
+ mOptions.fixAspectRatio = true;
+ return this;
+ }
+
+ /**
+ * the thickness of the guidelines lines (in pixels).
+ * Default: 3dp
+ */
+ public ActivityBuilder setBorderLineThickness(float borderLineThickness) {
+ mOptions.borderLineThickness = borderLineThickness;
+ return this;
+ }
+
+ /**
+ * the color of the guidelines lines.
+ * Default: Color.argb(170, 255, 255, 255)
+ */
+ public ActivityBuilder setBorderLineColor(int borderLineColor) {
+ mOptions.borderLineColor = borderLineColor;
+ return this;
+ }
+
+ /**
+ * thickness of the corner line (in pixels).
+ * Default: 2dp
+ */
+ public ActivityBuilder setBorderCornerThickness(float borderCornerThickness) {
+ mOptions.borderCornerThickness = borderCornerThickness;
+ return this;
+ }
+
+ /**
+ * the offset of corner line from crop window border (in pixels).
+ * Default: 5dp
+ */
+ public ActivityBuilder setBorderCornerOffset(float borderCornerOffset) {
+ mOptions.borderCornerOffset = borderCornerOffset;
+ return this;
+ }
+
+ /**
+ * the length of the corner line away from the corner (in pixels).
+ * Default: 14dp
+ */
+ public ActivityBuilder setBorderCornerLength(float borderCornerLength) {
+ mOptions.borderCornerLength = borderCornerLength;
+ return this;
+ }
+
+ /**
+ * the color of the corner line.
+ * Default: WHITE
+ */
+ public ActivityBuilder setBorderCornerColor(int borderCornerColor) {
+ mOptions.borderCornerColor = borderCornerColor;
+ return this;
+ }
+
+ /**
+ * the thickness of the guidelines lines (in pixels).
+ * Default: 1dp
+ */
+ public ActivityBuilder setGuidelinesThickness(float guidelinesThickness) {
+ mOptions.guidelinesThickness = guidelinesThickness;
+ return this;
+ }
+
+ /**
+ * the color of the guidelines lines.
+ * Default: Color.argb(170, 255, 255, 255)
+ */
+ public ActivityBuilder setGuidelinesColor(int guidelinesColor) {
+ mOptions.guidelinesColor = guidelinesColor;
+ return this;
+ }
+
+ /**
+ * the color of the overlay background around the crop window cover the image parts not in the
+ * crop window.
+ * Default: Color.argb(119, 0, 0, 0)
+ */
+ public ActivityBuilder setBackgroundColor(int backgroundColor) {
+ mOptions.backgroundColor = backgroundColor;
+ return this;
+ }
+
+ /**
+ * the min size the crop window is allowed to be (in pixels).
+ * Default: 42dp, 42dp
+ */
+ public ActivityBuilder setMinCropWindowSize(int minCropWindowWidth, int minCropWindowHeight) {
+ mOptions.minCropWindowWidth = minCropWindowWidth;
+ mOptions.minCropWindowHeight = minCropWindowHeight;
+ return this;
+ }
+
+ /**
+ * the min size the resulting cropping image is allowed to be, affects the cropping window
+ * limits (in pixels).
+ * Default: 40px, 40px
+ */
+ public ActivityBuilder setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
+ mOptions.minCropResultWidth = minCropResultWidth;
+ mOptions.minCropResultHeight = minCropResultHeight;
+ return this;
+ }
+
+ /**
+ * the max size the resulting cropping image is allowed to be, affects the cropping window
+ * limits (in pixels).
+ * Default: 99999, 99999
+ */
+ public ActivityBuilder setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
+ mOptions.maxCropResultWidth = maxCropResultWidth;
+ mOptions.maxCropResultHeight = maxCropResultHeight;
+ return this;
+ }
+
+ /**
+ * the title of the {@link CropImageActivity}.
+ * Default: ""
+ */
+ public ActivityBuilder setActivityTitle(CharSequence activityTitle) {
+ mOptions.activityTitle = activityTitle;
+ return this;
+ }
+
+ /**
+ * the color to use for action bar items icons.
+ * Default: NONE
+ */
+ public ActivityBuilder setActivityMenuIconColor(int activityMenuIconColor) {
+ mOptions.activityMenuIconColor = activityMenuIconColor;
+ return this;
+ }
+
+ /**
+ * the Android Uri to save the cropped image to.
+ * Default: NONE, will create a temp file
+ */
+ public ActivityBuilder setOutputUri(Uri outputUri) {
+ mOptions.outputUri = outputUri;
+ return this;
+ }
+
+ /**
+ * the compression format to use when writting the image.
+ * Default: JPEG
+ */
+ public ActivityBuilder setOutputCompressFormat(Bitmap.CompressFormat outputCompressFormat) {
+ mOptions.outputCompressFormat = outputCompressFormat;
+ return this;
+ }
+
+ /**
+ * the quility (if applicable) to use when writting the image (0 - 100).
+ * Default: 90
+ */
+ public ActivityBuilder setOutputCompressQuality(int outputCompressQuality) {
+ mOptions.outputCompressQuality = outputCompressQuality;
+ return this;
+ }
+
+ /**
+ * the size to resize the cropped image to.
+ * Uses {@link CropImageView.RequestSizeOptions#RESIZE_INSIDE} option.
+ * Default: 0, 0 - not set, will not resize
+ */
+ public ActivityBuilder setRequestedSize(int reqWidth, int reqHeight) {
+ return setRequestedSize(reqWidth, reqHeight, CropImageView.RequestSizeOptions.RESIZE_INSIDE);
+ }
+
+ /**
+ * the size to resize the cropped image to.
+ * Default: 0, 0 - not set, will not resize
+ */
+ public ActivityBuilder setRequestedSize(
+ int reqWidth, int reqHeight, CropImageView.RequestSizeOptions options) {
+ mOptions.outputRequestWidth = reqWidth;
+ mOptions.outputRequestHeight = reqHeight;
+ mOptions.outputRequestSizeOptions = options;
+ return this;
+ }
+
+ /**
+ * if the result of crop image activity should not save the cropped image bitmap.
+ * Used if you want to crop the image manually and need only the crop rectangle and rotation
+ * data.
+ * Default: false
+ */
+ public ActivityBuilder setNoOutputImage(boolean noOutputImage) {
+ mOptions.noOutputImage = noOutputImage;
+ return this;
+ }
+
+ /**
+ * the initial rectangle to set on the cropping image after loading.
+ * Default: NONE - will initialize using initial crop window padding ratio
+ */
+ public ActivityBuilder setInitialCropWindowRectangle(Rect initialCropWindowRectangle) {
+ mOptions.initialCropWindowRectangle = initialCropWindowRectangle;
+ return this;
+ }
+
+ /**
+ * the initial rotation to set on the cropping image after loading (0-360 degrees clockwise).
+ *
+ * Default: NONE - will read image exif data
+ */
+ public ActivityBuilder setInitialRotation(int initialRotation) {
+ mOptions.initialRotation = (initialRotation + 360) % 360;
+ return this;
+ }
+
+ /**
+ * if to allow rotation during cropping.
+ * Default: true
+ */
+ public ActivityBuilder setAllowRotation(boolean allowRotation) {
+ mOptions.allowRotation = allowRotation;
+ return this;
+ }
+
+ /**
+ * if to allow flipping during cropping.
+ * Default: true
+ */
+ public ActivityBuilder setAllowFlipping(boolean allowFlipping) {
+ mOptions.allowFlipping = allowFlipping;
+ return this;
+ }
+
+ /**
+ * if to allow counter-clockwise rotation during cropping.
+ * Note: if rotation is disabled this option has no effect.
+ * Default: false
+ */
+ public ActivityBuilder setAllowCounterRotation(boolean allowCounterRotation) {
+ mOptions.allowCounterRotation = allowCounterRotation;
+ return this;
+ }
+
+ /**
+ * The amount of degreees to rotate clockwise or counter-clockwise (0-360).
+ * Default: 90
+ */
+ public ActivityBuilder setRotationDegrees(int rotationDegrees) {
+ mOptions.rotationDegrees = (rotationDegrees + 360) % 360;
+ return this;
+ }
+
+ /**
+ * whether the image should be flipped horizontally.
+ * Default: false
+ */
+ public ActivityBuilder setFlipHorizontally(boolean flipHorizontally) {
+ mOptions.flipHorizontally = flipHorizontally;
+ return this;
+ }
+
+ /**
+ * whether the image should be flipped vertically.
+ * Default: false
+ */
+ public ActivityBuilder setFlipVertically(boolean flipVertically) {
+ mOptions.flipVertically = flipVertically;
+ return this;
+ }
+
+ /**
+ * optional, set crop menu crop button title.
+ * Default: null, will use resource string: crop_image_menu_crop
+ */
+ public ActivityBuilder setCropMenuCropButtonTitle(CharSequence title) {
+ mOptions.cropMenuCropButtonTitle = title;
+ return this;
+ }
+
+ /**
+ * Image resource id to use for crop icon instead of text.
+ * Default: 0
+ */
+ public ActivityBuilder setCropMenuCropButtonIcon(@DrawableRes int drawableResource) {
+ mOptions.cropMenuCropButtonIcon = drawableResource;
+ return this;
+ }
+ }
+ // endregion
+
+ // region: Inner class: ActivityResult
+
+ /**
+ * Result data of Crop Image Activity.
+ */
+ public static final class ActivityResult extends CropImageView.CropResult implements Parcelable {
+
+ public static final Creator CREATOR =
+ new Creator() {
+ @Override
+ public ActivityResult createFromParcel(Parcel in) {
+ return new ActivityResult(in);
+ }
+
+ @Override
+ public ActivityResult[] newArray(int size) {
+ return new ActivityResult[size];
+ }
+ };
+
+ public ActivityResult(
+ Uri originalUri,
+ Uri uri,
+ Exception error,
+ float[] cropPoints,
+ Rect cropRect,
+ int rotation,
+ Rect wholeImageRect,
+ int sampleSize) {
+ super(
+ null,
+ originalUri,
+ null,
+ uri,
+ error,
+ cropPoints,
+ cropRect,
+ wholeImageRect,
+ rotation,
+ sampleSize);
+ }
+
+ protected ActivityResult(Parcel in) {
+ super(
+ null,
+ (Uri) in.readParcelable(Uri.class.getClassLoader()),
+ null,
+ (Uri) in.readParcelable(Uri.class.getClassLoader()),
+ (Exception) in.readSerializable(),
+ in.createFloatArray(),
+ (Rect) in.readParcelable(Rect.class.getClassLoader()),
+ (Rect) in.readParcelable(Rect.class.getClassLoader()),
+ in.readInt(),
+ in.readInt());
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(getOriginalUri(), flags);
+ dest.writeParcelable(getUri(), flags);
+ dest.writeSerializable(getError());
+ dest.writeFloatArray(getCropPoints());
+ dest.writeParcelable(getCropRect(), flags);
+ dest.writeParcelable(getWholeImageRect(), flags);
+ dest.writeInt(getRotation());
+ dest.writeInt(getSampleSize());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+ }
+ // endregion
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageActivity.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageActivity.java
new file mode 100644
index 00000000..bd9d0afe
--- /dev/null
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageActivity.java
@@ -0,0 +1,367 @@
+// "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.
+ * 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.
+ * 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();
+ }
+ }
+ }
+ }
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageAnimation.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageAnimation.java
new file mode 100644
index 00000000..2861b857
--- /dev/null
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageAnimation.java
@@ -0,0 +1,123 @@
+// "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) {
+ }
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageOptions.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageOptions.java
new file mode 100644
index 00000000..5a9256a8
--- /dev/null
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageOptions.java
@@ -0,0 +1,541 @@
+// "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.
+ * Initialized with default values.
+ */
+public class CropImageOptions implements Parcelable {
+
+ public static final Creator CREATOR =
+ new Creator() {
+ @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)
+ * We are basing this value off of the recommended 48dp Rhythm.
+ * 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.
+ * 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.
+ * default: true, disable to provide custom progress bar UI.
+ */
+ public boolean showProgressBar;
+
+ /**
+ * if auto-zoom functionality is enabled.
+ * 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");
+ }
+ }
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageView.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageView.java
new file mode 100644
index 00000000..2e835b07
--- /dev/null
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropImageView.java
@@ -0,0 +1,2296 @@
+// "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.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+
+import androidx.exifinterface.media.ExifInterface;
+
+import java.lang.ref.WeakReference;
+import java.util.UUID;
+
+/**
+ * Custom view that provides cropping capabilities to an image.
+ */
+public class CropImageView extends FrameLayout {
+
+ // region: Fields and Consts
+
+ /**
+ * Image view widget used to show the image for cropping.
+ */
+ private final ImageView mImageView;
+
+ /**
+ * Overlay over the image view to show cropping UI.
+ */
+ private final CropOverlayView mCropOverlayView;
+
+ /**
+ * The matrix used to transform the cropping image in the image view
+ */
+ private final Matrix mImageMatrix = new Matrix();
+
+ /**
+ * Reusing matrix instance for reverse matrix calculations.
+ */
+ private final Matrix mImageInverseMatrix = new Matrix();
+
+ /**
+ * Progress bar widget to show progress bar on async image loading and cropping.
+ */
+ private final ProgressBar mProgressBar;
+
+ /**
+ * Rectangle used in image matrix transformation calculation (reusing rect instance)
+ */
+ private final float[] mImagePoints = new float[8];
+
+ /**
+ * Rectangle used in image matrix transformation for scale calculation (reusing rect instance)
+ */
+ private final float[] mScaleImagePoints = new float[8];
+
+ /**
+ * Animation class to smooth animate zoom-in/out
+ */
+ private CropImageAnimation mAnimation;
+
+ private Bitmap mBitmap;
+
+ /**
+ * The image rotation value used during loading of the image so we can reset to it
+ */
+ private int mInitialDegreesRotated;
+
+ /**
+ * How much the image is rotated from original clockwise
+ */
+ private int mDegreesRotated;
+
+ /**
+ * if the image flipped horizontally
+ */
+ private boolean mFlipHorizontally;
+
+ /**
+ * if the image flipped vertically
+ */
+ private boolean mFlipVertically;
+
+ private int mLayoutWidth;
+
+ private int mLayoutHeight;
+
+ private int mImageResource;
+
+ /**
+ * The initial scale type of the image in the crop image view
+ */
+ private ScaleType mScaleType;
+
+ /**
+ * if to save bitmap on save instance state.
+ * It is best to avoid it by using URI in setting image for cropping.
+ * If false the bitmap is not saved and if restore is required to view will be empty, storing the
+ * bitmap requires saving it to file which can be expensive. default: false.
+ */
+ private boolean mSaveBitmapToInstanceState = false;
+
+ /**
+ * if to show crop overlay UI what contains the crop window UI surrounded by background over the
+ * cropping image.
+ * default: true, may disable for animation or frame transition.
+ */
+ private boolean mShowCropOverlay = true;
+
+ /**
+ * if to show progress bar when image async loading/cropping is in progress.
+ * default: true, disable to provide custom progress bar UI.
+ */
+ private boolean mShowProgressBar = true;
+
+ /**
+ * if auto-zoom functionality is enabled.
+ * default: true.
+ */
+ private boolean mAutoZoomEnabled = true;
+
+ /**
+ * The max zoom allowed during cropping
+ */
+ private int mMaxZoom;
+
+ /**
+ * callback to be invoked when crop overlay is released.
+ */
+ private OnSetCropOverlayReleasedListener mOnCropOverlayReleasedListener;
+
+ /**
+ * callback to be invoked when crop overlay is moved.
+ */
+ private OnSetCropOverlayMovedListener mOnSetCropOverlayMovedListener;
+
+ /**
+ * callback to be invoked when crop window is changed.
+ */
+ private OnSetCropWindowChangeListener mOnSetCropWindowChangeListener;
+
+ /**
+ * callback to be invoked when image async loading is complete.
+ */
+ private OnSetImageUriCompleteListener mOnSetImageUriCompleteListener;
+
+ /**
+ * callback to be invoked when image async cropping is complete.
+ */
+ private OnCropImageCompleteListener mOnCropImageCompleteListener;
+
+ /**
+ * The URI that the image was loaded from (if loaded from URI)
+ */
+ private Uri mLoadedImageUri;
+
+ /**
+ * The sample size the image was loaded by if was loaded by URI
+ */
+ private int mLoadedSampleSize = 1;
+
+ /**
+ * The current zoom level to to scale the cropping image
+ */
+ private float mZoom = 1;
+
+ /**
+ * The X offset that the cropping image was translated after zooming
+ */
+ private float mZoomOffsetX;
+
+ /**
+ * The Y offset that the cropping image was translated after zooming
+ */
+ private float mZoomOffsetY;
+
+ /**
+ * Used to restore the cropping windows rectangle after state restore
+ */
+ private RectF mRestoreCropWindowRect;
+
+ /**
+ * Used to restore image rotation after state restore
+ */
+ private int mRestoreDegreesRotated;
+
+ /**
+ * Used to detect size change to handle auto-zoom using {@link #handleCropWindowChanged(boolean,
+ * boolean)} in {@link #layout(int, int, int, int)}.
+ */
+ private boolean mSizeChanged;
+
+ /**
+ * Temp URI used to save bitmap image to disk to preserve for instance state in case cropped was
+ * set with bitmap
+ */
+ private Uri mSaveInstanceStateBitmapUri;
+
+ /**
+ * Task used to load bitmap async from UI thread
+ */
+ private WeakReference mBitmapLoadingWorkerTask;
+
+ /**
+ * Task used to crop bitmap async from UI thread
+ */
+ private WeakReference mBitmapCroppingWorkerTask;
+ // endregion
+
+ public CropImageView(Context context) {
+ this(context, null);
+ }
+
+ public CropImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ CropImageOptions options = null;
+ Intent intent = context instanceof Activity ? ((Activity) context).getIntent() : null;
+ if (intent != null) {
+ Bundle bundle = intent.getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE);
+ if (bundle != null) {
+ options = bundle.getParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS);
+ }
+ }
+
+ if (options == null) {
+
+ options = new CropImageOptions();
+
+ if (attrs != null) {
+ TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CropImageView, 0, 0);
+ try {
+ options.fixAspectRatio =
+ ta.getBoolean(R.styleable.CropImageView_cropFixAspectRatio, options.fixAspectRatio);
+ options.aspectRatioX =
+ ta.getInteger(R.styleable.CropImageView_cropAspectRatioX, options.aspectRatioX);
+ options.aspectRatioY =
+ ta.getInteger(R.styleable.CropImageView_cropAspectRatioY, options.aspectRatioY);
+ options.scaleType =
+ ScaleType.values()[
+ ta.getInt(R.styleable.CropImageView_cropScaleType, options.scaleType.ordinal())];
+ options.autoZoomEnabled =
+ ta.getBoolean(R.styleable.CropImageView_cropAutoZoomEnabled, options.autoZoomEnabled);
+ options.multiTouchEnabled =
+ ta.getBoolean(
+ R.styleable.CropImageView_cropMultiTouchEnabled, options.multiTouchEnabled);
+ options.maxZoom = ta.getInteger(R.styleable.CropImageView_cropMaxZoom, options.maxZoom);
+ options.cropShape =
+ CropShape.values()[
+ ta.getInt(R.styleable.CropImageView_cropShape, options.cropShape.ordinal())];
+ options.guidelines =
+ Guidelines.values()[
+ ta.getInt(
+ R.styleable.CropImageView_cropGuidelines, options.guidelines.ordinal())];
+ options.snapRadius =
+ ta.getDimension(R.styleable.CropImageView_cropSnapRadius, options.snapRadius);
+ options.touchRadius =
+ ta.getDimension(R.styleable.CropImageView_cropTouchRadius, options.touchRadius);
+ options.initialCropWindowPaddingRatio =
+ ta.getFloat(
+ R.styleable.CropImageView_cropInitialCropWindowPaddingRatio,
+ options.initialCropWindowPaddingRatio);
+ options.borderLineThickness =
+ ta.getDimension(
+ R.styleable.CropImageView_cropBorderLineThickness, options.borderLineThickness);
+ options.borderLineColor =
+ ta.getInteger(R.styleable.CropImageView_cropBorderLineColor, options.borderLineColor);
+ options.borderCornerThickness =
+ ta.getDimension(
+ R.styleable.CropImageView_cropBorderCornerThickness,
+ options.borderCornerThickness);
+ options.borderCornerOffset =
+ ta.getDimension(
+ R.styleable.CropImageView_cropBorderCornerOffset, options.borderCornerOffset);
+ options.borderCornerLength =
+ ta.getDimension(
+ R.styleable.CropImageView_cropBorderCornerLength, options.borderCornerLength);
+ options.borderCornerColor =
+ ta.getInteger(
+ R.styleable.CropImageView_cropBorderCornerColor, options.borderCornerColor);
+ options.guidelinesThickness =
+ ta.getDimension(
+ R.styleable.CropImageView_cropGuidelinesThickness, options.guidelinesThickness);
+ options.guidelinesColor =
+ ta.getInteger(R.styleable.CropImageView_cropGuidelinesColor, options.guidelinesColor);
+ options.backgroundColor =
+ ta.getInteger(R.styleable.CropImageView_cropBackgroundColor, options.backgroundColor);
+ options.showCropOverlay =
+ ta.getBoolean(R.styleable.CropImageView_cropShowCropOverlay, mShowCropOverlay);
+ options.showProgressBar =
+ ta.getBoolean(R.styleable.CropImageView_cropShowProgressBar, mShowProgressBar);
+ options.borderCornerThickness =
+ ta.getDimension(
+ R.styleable.CropImageView_cropBorderCornerThickness,
+ options.borderCornerThickness);
+ options.minCropWindowWidth =
+ (int)
+ ta.getDimension(
+ R.styleable.CropImageView_cropMinCropWindowWidth, options.minCropWindowWidth);
+ options.minCropWindowHeight =
+ (int)
+ ta.getDimension(
+ R.styleable.CropImageView_cropMinCropWindowHeight,
+ options.minCropWindowHeight);
+ options.minCropResultWidth =
+ (int)
+ ta.getFloat(
+ R.styleable.CropImageView_cropMinCropResultWidthPX,
+ options.minCropResultWidth);
+ options.minCropResultHeight =
+ (int)
+ ta.getFloat(
+ R.styleable.CropImageView_cropMinCropResultHeightPX,
+ options.minCropResultHeight);
+ options.maxCropResultWidth =
+ (int)
+ ta.getFloat(
+ R.styleable.CropImageView_cropMaxCropResultWidthPX,
+ options.maxCropResultWidth);
+ options.maxCropResultHeight =
+ (int)
+ ta.getFloat(
+ R.styleable.CropImageView_cropMaxCropResultHeightPX,
+ options.maxCropResultHeight);
+ options.flipHorizontally =
+ ta.getBoolean(
+ R.styleable.CropImageView_cropFlipHorizontally, options.flipHorizontally);
+ options.flipVertically =
+ ta.getBoolean(R.styleable.CropImageView_cropFlipHorizontally, options.flipVertically);
+
+ mSaveBitmapToInstanceState =
+ ta.getBoolean(
+ R.styleable.CropImageView_cropSaveBitmapToInstanceState,
+ mSaveBitmapToInstanceState);
+
+ // if aspect ratio is set then set fixed to true
+ if (ta.hasValue(R.styleable.CropImageView_cropAspectRatioX)
+ && ta.hasValue(R.styleable.CropImageView_cropAspectRatioX)
+ && !ta.hasValue(R.styleable.CropImageView_cropFixAspectRatio)) {
+ options.fixAspectRatio = true;
+ }
+ } finally {
+ ta.recycle();
+ }
+ }
+ }
+
+ options.validate();
+
+ mScaleType = options.scaleType;
+ mAutoZoomEnabled = options.autoZoomEnabled;
+ mMaxZoom = options.maxZoom;
+ mShowCropOverlay = options.showCropOverlay;
+ mShowProgressBar = options.showProgressBar;
+ mFlipHorizontally = options.flipHorizontally;
+ mFlipVertically = options.flipVertically;
+
+ LayoutInflater inflater = LayoutInflater.from(context);
+ View v = inflater.inflate(R.layout.crop_image_view, this, true);
+
+ mImageView = v.findViewById(R.id.ImageView_image);
+ mImageView.setScaleType(ImageView.ScaleType.MATRIX);
+
+ mCropOverlayView = v.findViewById(R.id.CropOverlayView);
+ mCropOverlayView.setCropWindowChangeListener(
+ new CropOverlayView.CropWindowChangeListener() {
+ @Override
+ public void onCropWindowChanged(boolean inProgress) {
+ handleCropWindowChanged(inProgress, true);
+ OnSetCropOverlayReleasedListener listener = mOnCropOverlayReleasedListener;
+ if (listener != null && !inProgress) {
+ listener.onCropOverlayReleased(getCropRect());
+ }
+ OnSetCropOverlayMovedListener movedListener = mOnSetCropOverlayMovedListener;
+ if (movedListener != null && inProgress) {
+ movedListener.onCropOverlayMoved(getCropRect());
+ }
+ }
+ });
+ mCropOverlayView.setInitialAttributeValues(options);
+
+ mProgressBar = v.findViewById(R.id.CropProgressBar);
+ setProgressBarVisibility();
+ }
+
+ /**
+ * Determines the specs for the onMeasure function. Calculates the width or height depending on
+ * the mode.
+ *
+ * @param measureSpecMode The mode of the measured width or height.
+ * @param measureSpecSize The size of the measured width or height.
+ * @param desiredSize The desired size of the measured width or height.
+ * @return The final size of the width or height.
+ */
+ private static int getOnMeasureSpec(int measureSpecMode, int measureSpecSize, int desiredSize) {
+
+ // Measure Width
+ int spec;
+ if (measureSpecMode == MeasureSpec.EXACTLY) {
+ // Must be this size
+ spec = measureSpecSize;
+ } else if (measureSpecMode == MeasureSpec.AT_MOST) {
+ // Can't be bigger than...; match_parent value
+ spec = Math.min(desiredSize, measureSpecSize);
+ } else {
+ // Be whatever you want; wrap_content
+ spec = desiredSize;
+ }
+
+ return spec;
+ }
+
+ /**
+ * Get the scale type of the image in the crop view.
+ */
+ public ScaleType getScaleType() {
+ return mScaleType;
+ }
+
+ /**
+ * Set the scale type of the image in the crop view
+ */
+ public void setScaleType(ScaleType scaleType) {
+ if (scaleType != mScaleType) {
+ mScaleType = scaleType;
+ mZoom = 1;
+ mZoomOffsetX = mZoomOffsetY = 0;
+ mCropOverlayView.resetCropOverlayView();
+ requestLayout();
+ }
+ }
+
+ /**
+ * The shape of the cropping area - rectangle/circular.
+ */
+ public CropShape getCropShape() {
+ return mCropOverlayView.getCropShape();
+ }
+
+ /**
+ * The shape of the cropping area - rectangle/circular.
+ * To set square/circle crop shape set aspect ratio to 1:1.
+ */
+ public void setCropShape(CropShape cropShape) {
+ mCropOverlayView.setCropShape(cropShape);
+ }
+
+ /**
+ * if auto-zoom functionality is enabled. default: true.
+ */
+ public boolean isAutoZoomEnabled() {
+ return mAutoZoomEnabled;
+ }
+
+ /**
+ * Set auto-zoom functionality to enabled/disabled.
+ */
+ public void setAutoZoomEnabled(boolean autoZoomEnabled) {
+ if (mAutoZoomEnabled != autoZoomEnabled) {
+ mAutoZoomEnabled = autoZoomEnabled;
+ handleCropWindowChanged(false, false);
+ mCropOverlayView.invalidate();
+ }
+ }
+
+ /**
+ * Set multi touch functionality to enabled/disabled.
+ */
+ public void setMultiTouchEnabled(boolean multiTouchEnabled) {
+ if (mCropOverlayView.setMultiTouchEnabled(multiTouchEnabled)) {
+ handleCropWindowChanged(false, false);
+ mCropOverlayView.invalidate();
+ }
+ }
+
+ /**
+ * The max zoom allowed during cropping.
+ */
+ public int getMaxZoom() {
+ return mMaxZoom;
+ }
+
+ /**
+ * The max zoom allowed during cropping.
+ */
+ public void setMaxZoom(int maxZoom) {
+ if (mMaxZoom != maxZoom && maxZoom > 0) {
+ mMaxZoom = maxZoom;
+ handleCropWindowChanged(false, false);
+ mCropOverlayView.invalidate();
+ }
+ }
+
+ /**
+ * the min size the resulting cropping image is allowed to be, affects the cropping window limits
+ * (in pixels).
+ */
+ public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
+ mCropOverlayView.setMinCropResultSize(minCropResultWidth, minCropResultHeight);
+ }
+
+ /**
+ * the max size the resulting cropping image is allowed to be, affects the cropping window limits
+ * (in pixels).
+ */
+ public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
+ mCropOverlayView.setMaxCropResultSize(maxCropResultWidth, maxCropResultHeight);
+ }
+
+ /**
+ * Get the amount of degrees the cropping image is rotated cloackwise.
+ *
+ * @return 0-360
+ */
+ public int getRotatedDegrees() {
+ return mDegreesRotated;
+ }
+
+ /**
+ * Set the amount of degrees the cropping image is rotated cloackwise.
+ *
+ * @param degrees 0-360
+ */
+ public void setRotatedDegrees(int degrees) {
+ if (mDegreesRotated != degrees) {
+ rotateImage(degrees - mDegreesRotated);
+ }
+ }
+
+ /**
+ * whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows it to
+ * be changed.
+ */
+ public boolean isFixAspectRatio() {
+ return mCropOverlayView.isFixAspectRatio();
+ }
+
+ /**
+ * Sets whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows
+ * it to be changed.
+ */
+ public void setFixedAspectRatio(boolean fixAspectRatio) {
+ mCropOverlayView.setFixedAspectRatio(fixAspectRatio);
+ }
+
+ /**
+ * whether the image should be flipped horizontally
+ */
+ public boolean isFlippedHorizontally() {
+ return mFlipHorizontally;
+ }
+
+ /**
+ * Sets whether the image should be flipped horizontally
+ */
+ public void setFlippedHorizontally(boolean flipHorizontally) {
+ if (mFlipHorizontally != flipHorizontally) {
+ mFlipHorizontally = flipHorizontally;
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+ }
+ }
+
+ /**
+ * whether the image should be flipped vertically
+ */
+ public boolean isFlippedVertically() {
+ return mFlipVertically;
+ }
+
+ /**
+ * Sets whether the image should be flipped vertically
+ */
+ public void setFlippedVertically(boolean flipVertically) {
+ if (mFlipVertically != flipVertically) {
+ mFlipVertically = flipVertically;
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+ }
+ }
+
+ /**
+ * Get the current guidelines option set.
+ */
+ public Guidelines getGuidelines() {
+ return mCropOverlayView.getGuidelines();
+ }
+
+ /**
+ * Sets the guidelines for the CropOverlayView to be either on, off, or to show when resizing the
+ * application.
+ */
+ public void setGuidelines(Guidelines guidelines) {
+ mCropOverlayView.setGuidelines(guidelines);
+ }
+
+ /**
+ * both the X and Y values of the aspectRatio.
+ */
+ public Pair getAspectRatio() {
+ return new Pair<>(mCropOverlayView.getAspectRatioX(), mCropOverlayView.getAspectRatioY());
+ }
+
+ /**
+ * Sets the both the X and Y values of the aspectRatio.
+ * Sets fixed aspect ratio to TRUE.
+ *
+ * @param aspectRatioX int that specifies the new X value of the aspect ratio
+ * @param aspectRatioY int that specifies the new Y value of the aspect ratio
+ */
+ public void setAspectRatio(int aspectRatioX, int aspectRatioY) {
+ mCropOverlayView.setAspectRatioX(aspectRatioX);
+ mCropOverlayView.setAspectRatioY(aspectRatioY);
+ setFixedAspectRatio(true);
+ }
+
+ /**
+ * Clears set aspect ratio values and sets fixed aspect ratio to FALSE.
+ */
+ public void clearAspectRatio() {
+ mCropOverlayView.setAspectRatioX(1);
+ mCropOverlayView.setAspectRatioY(1);
+ setFixedAspectRatio(false);
+ }
+
+ /**
+ * 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. (default: 3dp)
+ */
+ public void setSnapRadius(float snapRadius) {
+ if (snapRadius >= 0) {
+ mCropOverlayView.setSnapRadius(snapRadius);
+ }
+ }
+
+ /**
+ * if to show progress bar when image async loading/cropping is in progress.
+ * default: true, disable to provide custom progress bar UI.
+ */
+ public boolean isShowProgressBar() {
+ return mShowProgressBar;
+ }
+
+ /**
+ * if to show progress bar when image async loading/cropping is in progress.
+ * default: true, disable to provide custom progress bar UI.
+ */
+ public void setShowProgressBar(boolean showProgressBar) {
+ if (mShowProgressBar != showProgressBar) {
+ mShowProgressBar = showProgressBar;
+ setProgressBarVisibility();
+ }
+ }
+
+ /**
+ * if to show crop overlay UI what contains the crop window UI surrounded by background over the
+ * cropping image.
+ * default: true, may disable for animation or frame transition.
+ */
+ public boolean isShowCropOverlay() {
+ return mShowCropOverlay;
+ }
+
+ /**
+ * if to show crop overlay UI what contains the crop window UI surrounded by background over the
+ * cropping image.
+ * default: true, may disable for animation or frame transition.
+ */
+ public void setShowCropOverlay(boolean showCropOverlay) {
+ if (mShowCropOverlay != showCropOverlay) {
+ mShowCropOverlay = showCropOverlay;
+ setCropOverlayVisibility();
+ }
+ }
+
+ /**
+ * if to save bitmap on save instance state.
+ * It is best to avoid it by using URI in setting image for cropping.
+ * If false the bitmap is not saved and if restore is required to view will be empty, storing the
+ * bitmap requires saving it to file which can be expensive. default: false.
+ */
+ public boolean isSaveBitmapToInstanceState() {
+ return mSaveBitmapToInstanceState;
+ }
+
+ /**
+ * if to save bitmap on save instance state.
+ * It is best to avoid it by using URI in setting image for cropping.
+ * If false the bitmap is not saved and if restore is required to view will be empty, storing the
+ * bitmap requires saving it to file which can be expensive. default: false.
+ */
+ public void setSaveBitmapToInstanceState(boolean saveBitmapToInstanceState) {
+ mSaveBitmapToInstanceState = saveBitmapToInstanceState;
+ }
+
+ /**
+ * Returns the integer of the imageResource
+ */
+ public int getImageResource() {
+ return mImageResource;
+ }
+
+ /**
+ * Sets a Drawable as the content of the CropImageView.
+ *
+ * @param resId the drawable resource ID to set
+ */
+ public void setImageResource(int resId) {
+ if (resId != 0) {
+ mCropOverlayView.setInitialCropWindowRect(null);
+ Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
+ setBitmap(bitmap, resId, null, 1, 0);
+ }
+ }
+
+ /**
+ * Get the URI of an image that was set by URI, null otherwise.
+ */
+ public Uri getImageUri() {
+ return mLoadedImageUri;
+ }
+
+ /**
+ * Gets the source Bitmap's dimensions. This represents the largest possible crop rectangle.
+ *
+ * @return a Rect instance dimensions of the source Bitmap
+ */
+ public Rect getWholeImageRect() {
+ int loadedSampleSize = mLoadedSampleSize;
+ Bitmap bitmap = mBitmap;
+ if (bitmap == null) {
+ return null;
+ }
+
+ int orgWidth = bitmap.getWidth() * loadedSampleSize;
+ int orgHeight = bitmap.getHeight() * loadedSampleSize;
+ return new Rect(0, 0, orgWidth, orgHeight);
+ }
+
+ /**
+ * Gets the crop window's position relative to the source Bitmap (not the image displayed in the
+ * CropImageView) using the original image rotation.
+ *
+ * @return a Rect instance containing cropped area boundaries of the source Bitmap
+ */
+ public Rect getCropRect() {
+ int loadedSampleSize = mLoadedSampleSize;
+ Bitmap bitmap = mBitmap;
+ if (bitmap == null) {
+ return null;
+ }
+
+ // get the points of the crop rectangle adjusted to source bitmap
+ float[] points = getCropPoints();
+
+ int orgWidth = bitmap.getWidth() * loadedSampleSize;
+ int orgHeight = bitmap.getHeight() * loadedSampleSize;
+
+ // get the rectangle for the points (it may be larger than original if rotation is not stright)
+ return BitmapUtils.getRectFromPoints(
+ points,
+ orgWidth,
+ orgHeight,
+ mCropOverlayView.isFixAspectRatio(),
+ mCropOverlayView.getAspectRatioX(),
+ mCropOverlayView.getAspectRatioY());
+ }
+
+ /**
+ * Set the crop window position and size to the given rectangle.
+ * Image to crop must be first set before invoking this, for async - after complete callback.
+ *
+ * @param rect window rectangle (position and size) relative to source bitmap
+ */
+ public void setCropRect(Rect rect) {
+ mCropOverlayView.setInitialCropWindowRect(rect);
+ }
+
+ /**
+ * Gets the crop window's position relative to the parent's view at screen.
+ *
+ * @return a Rect instance containing cropped area boundaries of the source Bitmap
+ */
+ public RectF getCropWindowRect() {
+ if (mCropOverlayView == null) {
+ return null;
+ }
+ return mCropOverlayView.getCropWindowRect();
+ }
+
+ /**
+ * Gets the 4 points of crop window's position relative to the source Bitmap (not the image
+ * displayed in the CropImageView) using the original image rotation.
+ * Note: the 4 points may not be a rectangle if the image was rotates to NOT stright angle (!=
+ * 90/180/270).
+ *
+ * @return 4 points (x0,y0,x1,y1,x2,y2,x3,y3) of cropped area boundaries
+ */
+ public float[] getCropPoints() {
+
+ // Get crop window position relative to the displayed image.
+ RectF cropWindowRect = mCropOverlayView.getCropWindowRect();
+
+ float[] points =
+ new float[]{
+ cropWindowRect.left,
+ cropWindowRect.top,
+ cropWindowRect.right,
+ cropWindowRect.top,
+ cropWindowRect.right,
+ cropWindowRect.bottom,
+ cropWindowRect.left,
+ cropWindowRect.bottom
+ };
+
+ mImageMatrix.invert(mImageInverseMatrix);
+ mImageInverseMatrix.mapPoints(points);
+
+ for (int i = 0; i < points.length; i++) {
+ points[i] *= mLoadedSampleSize;
+ }
+
+ return points;
+ }
+
+ /**
+ * Reset crop window to initial rectangle.
+ */
+ public void resetCropRect() {
+ mZoom = 1;
+ mZoomOffsetX = 0;
+ mZoomOffsetY = 0;
+ mDegreesRotated = mInitialDegreesRotated;
+ mFlipHorizontally = false;
+ mFlipVertically = false;
+ applyImageMatrix(getWidth(), getHeight(), false, false);
+ mCropOverlayView.resetCropWindowRect();
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ *
+ * @return a new Bitmap representing the cropped image
+ */
+ public Bitmap getCroppedImage() {
+ return getCroppedImage(0, 0, RequestSizeOptions.NONE);
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
+ *
+ * @param reqWidth the width to resize the cropped image to
+ * @param reqHeight the height to resize the cropped image to
+ * @return a new Bitmap representing the cropped image
+ */
+ public Bitmap getCroppedImage(int reqWidth, int reqHeight) {
+ return getCroppedImage(reqWidth, reqHeight, RequestSizeOptions.RESIZE_INSIDE);
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ *
+ * @param reqWidth the width to resize the cropped image to (see options)
+ * @param reqHeight the height to resize the cropped image to (see options)
+ * @param options the resize method to use, see its documentation
+ * @return a new Bitmap representing the cropped image
+ */
+ public Bitmap getCroppedImage(int reqWidth, int reqHeight, RequestSizeOptions options) {
+ Bitmap croppedBitmap = null;
+ if (mBitmap != null) {
+ mImageView.clearAnimation();
+
+ reqWidth = options != RequestSizeOptions.NONE ? reqWidth : 0;
+ reqHeight = options != RequestSizeOptions.NONE ? reqHeight : 0;
+
+ if (mLoadedImageUri != null
+ && (mLoadedSampleSize > 1 || options == RequestSizeOptions.SAMPLING)) {
+ int orgWidth = mBitmap.getWidth() * mLoadedSampleSize;
+ int orgHeight = mBitmap.getHeight() * mLoadedSampleSize;
+ BitmapUtils.BitmapSampled bitmapSampled =
+ BitmapUtils.cropBitmap(
+ getContext(),
+ mLoadedImageUri,
+ getCropPoints(),
+ mDegreesRotated,
+ orgWidth,
+ orgHeight,
+ mCropOverlayView.isFixAspectRatio(),
+ mCropOverlayView.getAspectRatioX(),
+ mCropOverlayView.getAspectRatioY(),
+ reqWidth,
+ reqHeight,
+ mFlipHorizontally,
+ mFlipVertically);
+ croppedBitmap = bitmapSampled.bitmap;
+ } else {
+ croppedBitmap =
+ BitmapUtils.cropBitmapObjectHandleOOM(
+ mBitmap,
+ getCropPoints(),
+ mDegreesRotated,
+ mCropOverlayView.isFixAspectRatio(),
+ mCropOverlayView.getAspectRatioX(),
+ mCropOverlayView.getAspectRatioY(),
+ mFlipHorizontally,
+ mFlipVertically)
+ .bitmap;
+ }
+
+ croppedBitmap = BitmapUtils.resizeBitmap(croppedBitmap, reqWidth, reqHeight, options);
+ }
+
+ return croppedBitmap;
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ */
+ public void getCroppedImageAsync() {
+ getCroppedImageAsync(0, 0, RequestSizeOptions.NONE);
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param reqWidth the width to resize the cropped image to
+ * @param reqHeight the height to resize the cropped image to
+ */
+ public void getCroppedImageAsync(int reqWidth, int reqHeight) {
+ getCroppedImageAsync(reqWidth, reqHeight, RequestSizeOptions.RESIZE_INSIDE);
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param reqWidth the width to resize the cropped image to (see options)
+ * @param reqHeight the height to resize the cropped image to (see options)
+ * @param options the resize method to use, see its documentation
+ */
+ public void getCroppedImageAsync(int reqWidth, int reqHeight, RequestSizeOptions options) {
+ if (mOnCropImageCompleteListener == null) {
+ throw new IllegalArgumentException("mOnCropImageCompleteListener is not set");
+ }
+ startCropWorkerTask(reqWidth, reqHeight, options, null, null, 0);
+ }
+
+ /**
+ * Save the cropped image based on the current crop window to the given uri.
+ * Uses JPEG image compression with 90 compression quality.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param saveUri the Android Uri to save the cropped image to
+ */
+ public void saveCroppedImageAsync(Uri saveUri) {
+ saveCroppedImageAsync(saveUri, Bitmap.CompressFormat.JPEG, 90, 0, 0, RequestSizeOptions.NONE);
+ }
+
+ /**
+ * Save the cropped image based on the current crop window to the given uri.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param saveUri the Android Uri to save the cropped image to
+ * @param saveCompressFormat the compression format to use when writing the image
+ * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
+ */
+ public void saveCroppedImageAsync(
+ Uri saveUri, Bitmap.CompressFormat saveCompressFormat, int saveCompressQuality) {
+ saveCroppedImageAsync(
+ saveUri, saveCompressFormat, saveCompressQuality, 0, 0, RequestSizeOptions.NONE);
+ }
+
+ /**
+ * Save the cropped image based on the current crop window to the given uri.
+ * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param saveUri the Android Uri to save the cropped image to
+ * @param saveCompressFormat the compression format to use when writing the image
+ * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
+ * @param reqWidth the width to resize the cropped image to
+ * @param reqHeight the height to resize the cropped image to
+ */
+ public void saveCroppedImageAsync(
+ Uri saveUri,
+ Bitmap.CompressFormat saveCompressFormat,
+ int saveCompressQuality,
+ int reqWidth,
+ int reqHeight) {
+ saveCroppedImageAsync(
+ saveUri,
+ saveCompressFormat,
+ saveCompressQuality,
+ reqWidth,
+ reqHeight,
+ RequestSizeOptions.RESIZE_INSIDE);
+ }
+
+ /**
+ * Save the cropped image based on the current crop window to the given uri.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param saveUri the Android Uri to save the cropped image to
+ * @param saveCompressFormat the compression format to use when writing the image
+ * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
+ * @param reqWidth the width to resize the cropped image to (see options)
+ * @param reqHeight the height to resize the cropped image to (see options)
+ * @param options the resize method to use, see its documentation
+ */
+ public void saveCroppedImageAsync(
+ Uri saveUri,
+ Bitmap.CompressFormat saveCompressFormat,
+ int saveCompressQuality,
+ int reqWidth,
+ int reqHeight,
+ RequestSizeOptions options) {
+ if (mOnCropImageCompleteListener == null) {
+ throw new IllegalArgumentException("mOnCropImageCompleteListener is not set");
+ }
+ startCropWorkerTask(
+ reqWidth, reqHeight, options, saveUri, saveCompressFormat, saveCompressQuality);
+ }
+
+ /**
+ * Set the callback t
+ */
+ public void setOnSetCropOverlayReleasedListener(OnSetCropOverlayReleasedListener listener) {
+ mOnCropOverlayReleasedListener = listener;
+ }
+
+ /**
+ * Set the callback when the cropping is moved
+ */
+ public void setOnSetCropOverlayMovedListener(OnSetCropOverlayMovedListener listener) {
+ mOnSetCropOverlayMovedListener = listener;
+ }
+
+ /**
+ * Set the callback when the crop window is changed
+ */
+ public void setOnCropWindowChangedListener(OnSetCropWindowChangeListener listener) {
+ mOnSetCropWindowChangeListener = listener;
+ }
+
+ /**
+ * Set the callback to be invoked when image async loading ({@link #setImageUriAsync(Uri)}) is
+ * complete (successful or failed).
+ */
+ public void setOnSetImageUriCompleteListener(OnSetImageUriCompleteListener listener) {
+ mOnSetImageUriCompleteListener = listener;
+ }
+
+ /**
+ * Set the callback to be invoked when image async cropping image ({@link #getCroppedImageAsync()}
+ * or {@link #saveCroppedImageAsync(Uri)}) is complete (successful or failed).
+ */
+ public void setOnCropImageCompleteListener(OnCropImageCompleteListener listener) {
+ mOnCropImageCompleteListener = listener;
+ }
+
+ /**
+ * Sets a Bitmap as the content of the CropImageView.
+ *
+ * @param bitmap the Bitmap to set
+ */
+ public void setImageBitmap(Bitmap bitmap) {
+ mCropOverlayView.setInitialCropWindowRect(null);
+ setBitmap(bitmap, 0, null, 1, 0);
+ }
+
+ /**
+ * Sets a Bitmap and initializes the image rotation according to the EXIT data.
+ *
+ * The EXIF can be retrieved by doing the following:
+ * ExifInterface exif = new ExifInterface(path);
+ *
+ * @param bitmap the original bitmap to set; if null, this
+ * @param exif the EXIF information about this bitmap; may be null
+ */
+ public void setImageBitmap(Bitmap bitmap, ExifInterface exif) {
+ Bitmap setBitmap;
+ int degreesRotated = 0;
+ if (bitmap != null && exif != null) {
+ BitmapUtils.RotateBitmapResult result = BitmapUtils.rotateBitmapByExif(bitmap, exif);
+ setBitmap = result.bitmap;
+ degreesRotated = result.degrees;
+ mInitialDegreesRotated = result.degrees;
+ } else {
+ setBitmap = bitmap;
+ }
+ mCropOverlayView.setInitialCropWindowRect(null);
+ setBitmap(setBitmap, 0, null, 1, degreesRotated);
+ }
+
+ /**
+ * Sets a bitmap loaded from the given Android URI as the content of the CropImageView.
+ * Can be used with URI from gallery or camera source.
+ * Will rotate the image by exif data.
+ *
+ * @param uri the URI to load the image from
+ */
+ public void setImageUriAsync(Uri uri) {
+ if (uri != null) {
+ BitmapLoadingWorkerTask currentTask =
+ mBitmapLoadingWorkerTask != null ? mBitmapLoadingWorkerTask.get() : null;
+ if (currentTask != null) {
+ // cancel previous loading (no check if the same URI because camera URI can be the same for
+ // different images)
+ currentTask.cancel(true);
+ }
+
+ // either no existing task is working or we canceled it, need to load new URI
+ clearImageInt();
+ mRestoreCropWindowRect = null;
+ mRestoreDegreesRotated = 0;
+ mCropOverlayView.setInitialCropWindowRect(null);
+ mBitmapLoadingWorkerTask = new WeakReference<>(new BitmapLoadingWorkerTask(this, uri));
+ mBitmapLoadingWorkerTask.get().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ setProgressBarVisibility();
+ }
+ }
+
+ /**
+ * Clear the current image set for cropping.
+ */
+ public void clearImage() {
+ clearImageInt();
+ mCropOverlayView.setInitialCropWindowRect(null);
+ }
+
+ /**
+ * Rotates image by the specified number of degrees clockwise.
+ * Negative values represent counter-clockwise rotations.
+ *
+ * @param degrees Integer specifying the number of degrees to rotate.
+ */
+ public void rotateImage(int degrees) {
+ if (mBitmap != null) {
+ // Force degrees to be a non-zero value between 0 and 360 (inclusive)
+ if (degrees < 0) {
+ degrees = (degrees % 360) + 360;
+ } else {
+ degrees = degrees % 360;
+ }
+
+ boolean flipAxes =
+ !mCropOverlayView.isFixAspectRatio()
+ && ((degrees > 45 && degrees < 135) || (degrees > 215 && degrees < 305));
+ BitmapUtils.RECT.set(mCropOverlayView.getCropWindowRect());
+ float halfWidth = (flipAxes ? BitmapUtils.RECT.height() : BitmapUtils.RECT.width()) / 2f;
+ float halfHeight = (flipAxes ? BitmapUtils.RECT.width() : BitmapUtils.RECT.height()) / 2f;
+ if (flipAxes) {
+ boolean isFlippedHorizontally = mFlipHorizontally;
+ mFlipHorizontally = mFlipVertically;
+ mFlipVertically = isFlippedHorizontally;
+ }
+
+ mImageMatrix.invert(mImageInverseMatrix);
+
+ BitmapUtils.POINTS[0] = BitmapUtils.RECT.centerX();
+ BitmapUtils.POINTS[1] = BitmapUtils.RECT.centerY();
+ BitmapUtils.POINTS[2] = 0;
+ BitmapUtils.POINTS[3] = 0;
+ BitmapUtils.POINTS[4] = 1;
+ BitmapUtils.POINTS[5] = 0;
+ mImageInverseMatrix.mapPoints(BitmapUtils.POINTS);
+
+ // This is valid because degrees is not negative.
+ mDegreesRotated = (mDegreesRotated + degrees) % 360;
+
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+
+ // adjust the zoom so the crop window size remains the same even after image scale change
+ mImageMatrix.mapPoints(BitmapUtils.POINTS2, BitmapUtils.POINTS);
+ mZoom /=
+ Math.sqrt(
+ Math.pow(BitmapUtils.POINTS2[4] - BitmapUtils.POINTS2[2], 2)
+ + Math.pow(BitmapUtils.POINTS2[5] - BitmapUtils.POINTS2[3], 2));
+ mZoom = Math.max(mZoom, 1);
+
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+
+ mImageMatrix.mapPoints(BitmapUtils.POINTS2, BitmapUtils.POINTS);
+
+ // adjust the width/height by the changes in scaling to the image
+ double change =
+ Math.sqrt(
+ Math.pow(BitmapUtils.POINTS2[4] - BitmapUtils.POINTS2[2], 2)
+ + Math.pow(BitmapUtils.POINTS2[5] - BitmapUtils.POINTS2[3], 2));
+ halfWidth *= change;
+ halfHeight *= change;
+
+ // calculate the new crop window rectangle to center in the same location and have proper
+ // width/height
+ BitmapUtils.RECT.set(
+ BitmapUtils.POINTS2[0] - halfWidth,
+ BitmapUtils.POINTS2[1] - halfHeight,
+ BitmapUtils.POINTS2[0] + halfWidth,
+ BitmapUtils.POINTS2[1] + halfHeight);
+
+ mCropOverlayView.resetCropOverlayView();
+ mCropOverlayView.setCropWindowRect(BitmapUtils.RECT);
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+ handleCropWindowChanged(false, false);
+
+ // make sure the crop window rectangle is within the cropping image bounds after all the
+ // changes
+ mCropOverlayView.fixCurrentCropWindowRect();
+ }
+ }
+
+ /**
+ * Flips the image horizontally.
+ */
+ public void flipImageHorizontally() {
+ mFlipHorizontally = !mFlipHorizontally;
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+ }
+
+ // region: Private methods
+
+ /**
+ * Flips the image vertically.
+ */
+ public void flipImageVertically() {
+ mFlipVertically = !mFlipVertically;
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+ }
+
+ /**
+ * On complete of the async bitmap loading by {@link #setImageUriAsync(Uri)} set the result to the
+ * widget if still relevant and call listener if set.
+ *
+ * @param result the result of bitmap loading
+ */
+ void onSetImageUriAsyncComplete(BitmapLoadingWorkerTask.Result result) {
+
+ mBitmapLoadingWorkerTask = null;
+ setProgressBarVisibility();
+
+ if (result.error == null) {
+ mInitialDegreesRotated = result.degreesRotated;
+ setBitmap(result.bitmap, 0, result.uri, result.loadSampleSize, result.degreesRotated);
+ }
+
+ OnSetImageUriCompleteListener listener = mOnSetImageUriCompleteListener;
+ if (listener != null) {
+ listener.onSetImageUriComplete(this, result.uri, result.error);
+ }
+ }
+
+ /**
+ * On complete of the async bitmap cropping by {@link #getCroppedImageAsync()} call listener if
+ * set.
+ *
+ * @param result the result of bitmap cropping
+ */
+ void onImageCroppingAsyncComplete(BitmapCroppingWorkerTask.Result result) {
+
+ mBitmapCroppingWorkerTask = null;
+ setProgressBarVisibility();
+
+ OnCropImageCompleteListener listener = mOnCropImageCompleteListener;
+ if (listener != null) {
+ CropResult cropResult =
+ new CropResult(
+ mBitmap,
+ mLoadedImageUri,
+ result.bitmap,
+ result.uri,
+ result.error,
+ getCropPoints(),
+ getCropRect(),
+ getWholeImageRect(),
+ getRotatedDegrees(),
+ result.sampleSize);
+ listener.onCropImageComplete(this, cropResult);
+ }
+ }
+
+ /**
+ * Set the given bitmap to be used in for cropping
+ * Optionally clear full if the bitmap is new, or partial clear if the bitmap has been
+ * manipulated.
+ */
+ private void setBitmap(
+ Bitmap bitmap, int imageResource, Uri imageUri, int loadSampleSize, int degreesRotated) {
+ if (mBitmap == null || !mBitmap.equals(bitmap)) {
+
+ mImageView.clearAnimation();
+
+ clearImageInt();
+
+ mBitmap = bitmap;
+ mImageView.setImageBitmap(mBitmap);
+
+ mLoadedImageUri = imageUri;
+ mImageResource = imageResource;
+ mLoadedSampleSize = loadSampleSize;
+ mDegreesRotated = degreesRotated;
+
+ applyImageMatrix(getWidth(), getHeight(), true, false);
+
+ if (mCropOverlayView != null) {
+ mCropOverlayView.resetCropOverlayView();
+ setCropOverlayVisibility();
+ }
+ }
+ }
+
+ /**
+ * Clear the current image set for cropping.
+ * Full clear will also clear the data of the set image like Uri or Resource id while partial
+ * clear will only clear the bitmap and recycle if required.
+ */
+ private void clearImageInt() {
+
+ // if we allocated the bitmap, release it as fast as possible
+ if (mBitmap != null && (mImageResource > 0 || mLoadedImageUri != null)) {
+ mBitmap.recycle();
+ }
+ mBitmap = null;
+
+ // clean the loaded image flags for new image
+ mImageResource = 0;
+ mLoadedImageUri = null;
+ mLoadedSampleSize = 1;
+ mDegreesRotated = 0;
+ mZoom = 1;
+ mZoomOffsetX = 0;
+ mZoomOffsetY = 0;
+ mImageMatrix.reset();
+ mSaveInstanceStateBitmapUri = null;
+
+ mImageView.setImageBitmap(null);
+
+ setCropOverlayVisibility();
+ }
+
+ /**
+ * Gets the cropped image based on the current crop window.
+ * If (reqWidth,reqHeight) is given AND image is loaded from URI cropping will try to use sample
+ * size to fit in the requested width and height down-sampling if possible - optimization to get
+ * best size to quality.
+ * The result will be invoked to listener set by {@link
+ * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
+ *
+ * @param reqWidth the width to resize the cropped image to (see options)
+ * @param reqHeight the height to resize the cropped image to (see options)
+ * @param options the resize method to use on the cropped bitmap
+ * @param saveUri optional: to save the cropped image to
+ * @param saveCompressFormat if saveUri is given, the given compression will be used for saving
+ * the image
+ * @param saveCompressQuality if saveUri is given, the given quality will be used for the
+ * compression.
+ */
+ public void startCropWorkerTask(
+ int reqWidth,
+ int reqHeight,
+ RequestSizeOptions options,
+ Uri saveUri,
+ Bitmap.CompressFormat saveCompressFormat,
+ int saveCompressQuality) {
+ Bitmap bitmap = mBitmap;
+ if (bitmap != null) {
+ mImageView.clearAnimation();
+
+ BitmapCroppingWorkerTask currentTask =
+ mBitmapCroppingWorkerTask != null ? mBitmapCroppingWorkerTask.get() : null;
+ if (currentTask != null) {
+ // cancel previous cropping
+ currentTask.cancel(true);
+ }
+
+ reqWidth = options != RequestSizeOptions.NONE ? reqWidth : 0;
+ reqHeight = options != RequestSizeOptions.NONE ? reqHeight : 0;
+
+ int orgWidth = bitmap.getWidth() * mLoadedSampleSize;
+ int orgHeight = bitmap.getHeight() * mLoadedSampleSize;
+ if (mLoadedImageUri != null
+ && (mLoadedSampleSize > 1 || options == RequestSizeOptions.SAMPLING)) {
+ mBitmapCroppingWorkerTask =
+ new WeakReference<>(
+ new BitmapCroppingWorkerTask(
+ this,
+ mLoadedImageUri,
+ getCropPoints(),
+ mDegreesRotated,
+ orgWidth,
+ orgHeight,
+ mCropOverlayView.isFixAspectRatio(),
+ mCropOverlayView.getAspectRatioX(),
+ mCropOverlayView.getAspectRatioY(),
+ reqWidth,
+ reqHeight,
+ mFlipHorizontally,
+ mFlipVertically,
+ options,
+ saveUri,
+ saveCompressFormat,
+ saveCompressQuality));
+ } else {
+ mBitmapCroppingWorkerTask =
+ new WeakReference<>(
+ new BitmapCroppingWorkerTask(
+ this,
+ bitmap,
+ getCropPoints(),
+ mDegreesRotated,
+ mCropOverlayView.isFixAspectRatio(),
+ mCropOverlayView.getAspectRatioX(),
+ mCropOverlayView.getAspectRatioY(),
+ reqWidth,
+ reqHeight,
+ mFlipHorizontally,
+ mFlipVertically,
+ options,
+ saveUri,
+ saveCompressFormat,
+ saveCompressQuality));
+ }
+ mBitmapCroppingWorkerTask.get().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ setProgressBarVisibility();
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ if (mLoadedImageUri == null && mBitmap == null && mImageResource < 1) {
+ return super.onSaveInstanceState();
+ }
+
+ Bundle bundle = new Bundle();
+ Uri imageUri = mLoadedImageUri;
+ if (mSaveBitmapToInstanceState && imageUri == null && mImageResource < 1) {
+ mSaveInstanceStateBitmapUri =
+ imageUri =
+ BitmapUtils.writeTempStateStoreBitmap(
+ getContext(), mBitmap, mSaveInstanceStateBitmapUri);
+ }
+ if (imageUri != null && mBitmap != null) {
+ String key = UUID.randomUUID().toString();
+ BitmapUtils.mStateBitmap = new Pair<>(key, new WeakReference<>(mBitmap));
+ bundle.putString("LOADED_IMAGE_STATE_BITMAP_KEY", key);
+ }
+ if (mBitmapLoadingWorkerTask != null) {
+ BitmapLoadingWorkerTask task = mBitmapLoadingWorkerTask.get();
+ if (task != null) {
+ bundle.putParcelable("LOADING_IMAGE_URI", task.getUri());
+ }
+ }
+ bundle.putParcelable("instanceState", super.onSaveInstanceState());
+ bundle.putParcelable("LOADED_IMAGE_URI", imageUri);
+ bundle.putInt("LOADED_IMAGE_RESOURCE", mImageResource);
+ bundle.putInt("LOADED_SAMPLE_SIZE", mLoadedSampleSize);
+ bundle.putInt("DEGREES_ROTATED", mDegreesRotated);
+ bundle.putParcelable("INITIAL_CROP_RECT", mCropOverlayView.getInitialCropWindowRect());
+
+ BitmapUtils.RECT.set(mCropOverlayView.getCropWindowRect());
+
+ mImageMatrix.invert(mImageInverseMatrix);
+ mImageInverseMatrix.mapRect(BitmapUtils.RECT);
+
+ bundle.putParcelable("CROP_WINDOW_RECT", BitmapUtils.RECT);
+ bundle.putString("CROP_SHAPE", mCropOverlayView.getCropShape().name());
+ bundle.putBoolean("CROP_AUTO_ZOOM_ENABLED", mAutoZoomEnabled);
+ bundle.putInt("CROP_MAX_ZOOM", mMaxZoom);
+ bundle.putBoolean("CROP_FLIP_HORIZONTALLY", mFlipHorizontally);
+ bundle.putBoolean("CROP_FLIP_VERTICALLY", mFlipVertically);
+
+ return bundle;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+
+ if (state instanceof Bundle) {
+ Bundle bundle = (Bundle) state;
+
+ // prevent restoring state if already set by outside code
+ if (mBitmapLoadingWorkerTask == null
+ && mLoadedImageUri == null
+ && mBitmap == null
+ && mImageResource == 0) {
+
+ Uri uri = bundle.getParcelable("LOADED_IMAGE_URI");
+ if (uri != null) {
+ String key = bundle.getString("LOADED_IMAGE_STATE_BITMAP_KEY");
+ if (key != null) {
+ Bitmap stateBitmap =
+ BitmapUtils.mStateBitmap != null && BitmapUtils.mStateBitmap.first.equals(key)
+ ? BitmapUtils.mStateBitmap.second.get()
+ : null;
+ BitmapUtils.mStateBitmap = null;
+ if (stateBitmap != null && !stateBitmap.isRecycled()) {
+ setBitmap(stateBitmap, 0, uri, bundle.getInt("LOADED_SAMPLE_SIZE"), 0);
+ }
+ }
+ if (mLoadedImageUri == null) {
+ setImageUriAsync(uri);
+ }
+ } else {
+ int resId = bundle.getInt("LOADED_IMAGE_RESOURCE");
+ if (resId > 0) {
+ setImageResource(resId);
+ } else {
+ uri = bundle.getParcelable("LOADING_IMAGE_URI");
+ if (uri != null) {
+ setImageUriAsync(uri);
+ }
+ }
+ }
+
+ mDegreesRotated = mRestoreDegreesRotated = bundle.getInt("DEGREES_ROTATED");
+
+ Rect initialCropRect = bundle.getParcelable("INITIAL_CROP_RECT");
+ if (initialCropRect != null
+ && (initialCropRect.width() > 0 || initialCropRect.height() > 0)) {
+ mCropOverlayView.setInitialCropWindowRect(initialCropRect);
+ }
+
+ RectF cropWindowRect = bundle.getParcelable("CROP_WINDOW_RECT");
+ if (cropWindowRect != null && (cropWindowRect.width() > 0 || cropWindowRect.height() > 0)) {
+ mRestoreCropWindowRect = cropWindowRect;
+ }
+
+ mCropOverlayView.setCropShape(CropShape.valueOf(bundle.getString("CROP_SHAPE")));
+
+ mAutoZoomEnabled = bundle.getBoolean("CROP_AUTO_ZOOM_ENABLED");
+ mMaxZoom = bundle.getInt("CROP_MAX_ZOOM");
+
+ mFlipHorizontally = bundle.getBoolean("CROP_FLIP_HORIZONTALLY");
+ mFlipVertically = bundle.getBoolean("CROP_FLIP_VERTICALLY");
+ }
+
+ super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
+ } else {
+ super.onRestoreInstanceState(state);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (mBitmap != null) {
+
+ // Bypasses a baffling bug when used within a ScrollView, where heightSize is set to 0.
+ if (heightSize == 0) {
+ heightSize = mBitmap.getHeight();
+ }
+
+ int desiredWidth;
+ int desiredHeight;
+
+ double viewToBitmapWidthRatio = Double.POSITIVE_INFINITY;
+ double viewToBitmapHeightRatio = Double.POSITIVE_INFINITY;
+
+ // Checks if either width or height needs to be fixed
+ if (widthSize < mBitmap.getWidth()) {
+ viewToBitmapWidthRatio = (double) widthSize / (double) mBitmap.getWidth();
+ }
+ if (heightSize < mBitmap.getHeight()) {
+ viewToBitmapHeightRatio = (double) heightSize / (double) mBitmap.getHeight();
+ }
+
+ // If either needs to be fixed, choose smallest ratio and calculate from there
+ if (viewToBitmapWidthRatio != Double.POSITIVE_INFINITY
+ || viewToBitmapHeightRatio != Double.POSITIVE_INFINITY) {
+ if (viewToBitmapWidthRatio <= viewToBitmapHeightRatio) {
+ desiredWidth = widthSize;
+ desiredHeight = (int) (mBitmap.getHeight() * viewToBitmapWidthRatio);
+ } else {
+ desiredHeight = heightSize;
+ desiredWidth = (int) (mBitmap.getWidth() * viewToBitmapHeightRatio);
+ }
+ } else {
+ // Otherwise, the picture is within frame layout bounds. Desired width is simply picture
+ // size
+ desiredWidth = mBitmap.getWidth();
+ desiredHeight = mBitmap.getHeight();
+ }
+
+ int width = getOnMeasureSpec(widthMode, widthSize, desiredWidth);
+ int height = getOnMeasureSpec(heightMode, heightSize, desiredHeight);
+
+ mLayoutWidth = width;
+ mLayoutHeight = height;
+
+ setMeasuredDimension(mLayoutWidth, mLayoutHeight);
+
+ } else {
+ setMeasuredDimension(widthSize, heightSize);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+
+ super.onLayout(changed, l, t, r, b);
+
+ if (mLayoutWidth > 0 && mLayoutHeight > 0) {
+ // Gets original parameters, and creates the new parameters
+ ViewGroup.LayoutParams origParams = this.getLayoutParams();
+ origParams.width = mLayoutWidth;
+ origParams.height = mLayoutHeight;
+ setLayoutParams(origParams);
+
+ if (mBitmap != null) {
+ applyImageMatrix(r - l, b - t, true, false);
+
+ // after state restore we want to restore the window crop, possible only after widget size
+ // is known
+ if (mRestoreCropWindowRect != null) {
+ if (mRestoreDegreesRotated != mInitialDegreesRotated) {
+ mDegreesRotated = mRestoreDegreesRotated;
+ applyImageMatrix(r - l, b - t, true, false);
+ }
+ mImageMatrix.mapRect(mRestoreCropWindowRect);
+ mCropOverlayView.setCropWindowRect(mRestoreCropWindowRect);
+ handleCropWindowChanged(false, false);
+ mCropOverlayView.fixCurrentCropWindowRect();
+ mRestoreCropWindowRect = null;
+ } else if (mSizeChanged) {
+ mSizeChanged = false;
+ handleCropWindowChanged(false, false);
+ }
+ } else {
+ updateImageBounds(true);
+ }
+ } else {
+ updateImageBounds(true);
+ }
+ }
+
+ /**
+ * Detect size change to handle auto-zoom using {@link #handleCropWindowChanged(boolean, boolean)}
+ * in {@link #layout(int, int, int, int)}.
+ */
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ mSizeChanged = oldw > 0 && oldh > 0;
+ }
+
+ /**
+ * Handle crop window change to:
+ * 1. Execute auto-zoom-in/out depending on the area covered of cropping window relative to the
+ * available view area.
+ * 2. Slide the zoomed sub-area if the cropping window is outside of the visible view sub-area.
+ *
+ *
+ * @param inProgress is the crop window change is still in progress by the user
+ * @param animate if to animate the change to the image matrix, or set it directly
+ */
+ private void handleCropWindowChanged(boolean inProgress, boolean animate) {
+ int width = getWidth();
+ int height = getHeight();
+ if (mBitmap != null && width > 0 && height > 0) {
+
+ RectF cropRect = mCropOverlayView.getCropWindowRect();
+ if (inProgress) {
+ if (cropRect.left < 0
+ || cropRect.top < 0
+ || cropRect.right > width
+ || cropRect.bottom > height) {
+ applyImageMatrix(width, height, false, false);
+ }
+ } else if (mAutoZoomEnabled || mZoom > 1) {
+ float newZoom = 0;
+ // keep the cropping window covered area to 50%-65% of zoomed sub-area
+ if (mZoom < mMaxZoom
+ && cropRect.width() < width * 0.5f
+ && cropRect.height() < height * 0.5f) {
+ newZoom =
+ Math.min(
+ mMaxZoom,
+ Math.min(
+ width / (cropRect.width() / mZoom / 0.64f),
+ height / (cropRect.height() / mZoom / 0.64f)));
+ }
+ if (mZoom > 1 && (cropRect.width() > width * 0.65f || cropRect.height() > height * 0.65f)) {
+ newZoom =
+ Math.max(
+ 1,
+ Math.min(
+ width / (cropRect.width() / mZoom / 0.51f),
+ height / (cropRect.height() / mZoom / 0.51f)));
+ }
+ if (!mAutoZoomEnabled) {
+ newZoom = 1;
+ }
+
+ if (newZoom > 0 && newZoom != mZoom) {
+ if (animate) {
+ if (mAnimation == null) {
+ // lazy create animation single instance
+ mAnimation = new CropImageAnimation(mImageView, mCropOverlayView);
+ }
+ // set the state for animation to start from
+ mAnimation.setStartState(mImagePoints, mImageMatrix);
+ }
+
+ mZoom = newZoom;
+
+ applyImageMatrix(width, height, true, animate);
+ }
+ }
+ if (mOnSetCropWindowChangeListener != null && !inProgress) {
+ mOnSetCropWindowChangeListener.onCropWindowChanged();
+ }
+ }
+ }
+
+ /**
+ * Apply matrix to handle the image inside the image view.
+ *
+ * @param width the width of the image view
+ * @param height the height of the image view
+ */
+ private void applyImageMatrix(float width, float height, boolean center, boolean animate) {
+ if (mBitmap != null && width > 0 && height > 0) {
+
+ mImageMatrix.invert(mImageInverseMatrix);
+ RectF cropRect = mCropOverlayView.getCropWindowRect();
+ mImageInverseMatrix.mapRect(cropRect);
+
+ mImageMatrix.reset();
+
+ // move the image to the center of the image view first so we can manipulate it from there
+ mImageMatrix.postTranslate(
+ (width - mBitmap.getWidth()) / 2, (height - mBitmap.getHeight()) / 2);
+ mapImagePointsByImageMatrix();
+
+ // rotate the image the required degrees from center of image
+ if (mDegreesRotated > 0) {
+ mImageMatrix.postRotate(
+ mDegreesRotated,
+ BitmapUtils.getRectCenterX(mImagePoints),
+ BitmapUtils.getRectCenterY(mImagePoints));
+ mapImagePointsByImageMatrix();
+ }
+
+ // scale the image to the image view, image rect transformed to know new width/height
+ float scale =
+ Math.min(
+ width / BitmapUtils.getRectWidth(mImagePoints),
+ height / BitmapUtils.getRectHeight(mImagePoints));
+ if (mScaleType == ScaleType.FIT_CENTER
+ || (mScaleType == ScaleType.CENTER_INSIDE && scale < 1)
+ || (scale > 1 && mAutoZoomEnabled)) {
+ mImageMatrix.postScale(
+ scale,
+ scale,
+ BitmapUtils.getRectCenterX(mImagePoints),
+ BitmapUtils.getRectCenterY(mImagePoints));
+ mapImagePointsByImageMatrix();
+ }
+
+ // scale by the current zoom level
+ float scaleX = mFlipHorizontally ? -mZoom : mZoom;
+ float scaleY = mFlipVertically ? -mZoom : mZoom;
+ mImageMatrix.postScale(
+ scaleX,
+ scaleY,
+ BitmapUtils.getRectCenterX(mImagePoints),
+ BitmapUtils.getRectCenterY(mImagePoints));
+ mapImagePointsByImageMatrix();
+
+ mImageMatrix.mapRect(cropRect);
+
+ if (center) {
+ // set the zoomed area to be as to the center of cropping window as possible
+ mZoomOffsetX =
+ width > BitmapUtils.getRectWidth(mImagePoints)
+ ? 0
+ : Math.max(
+ Math.min(
+ width / 2 - cropRect.centerX(), -BitmapUtils.getRectLeft(mImagePoints)),
+ getWidth() - BitmapUtils.getRectRight(mImagePoints))
+ / scaleX;
+ mZoomOffsetY =
+ height > BitmapUtils.getRectHeight(mImagePoints)
+ ? 0
+ : Math.max(
+ Math.min(
+ height / 2 - cropRect.centerY(), -BitmapUtils.getRectTop(mImagePoints)),
+ getHeight() - BitmapUtils.getRectBottom(mImagePoints))
+ / scaleY;
+ } else {
+ // adjust the zoomed area so the crop window rectangle will be inside the area in case it
+ // was moved outside
+ mZoomOffsetX =
+ Math.min(Math.max(mZoomOffsetX * scaleX, -cropRect.left), -cropRect.right + width)
+ / scaleX;
+ mZoomOffsetY =
+ Math.min(Math.max(mZoomOffsetY * scaleY, -cropRect.top), -cropRect.bottom + height)
+ / scaleY;
+ }
+
+ // apply to zoom offset translate and update the crop rectangle to offset correctly
+ mImageMatrix.postTranslate(mZoomOffsetX * scaleX, mZoomOffsetY * scaleY);
+ cropRect.offset(mZoomOffsetX * scaleX, mZoomOffsetY * scaleY);
+ mCropOverlayView.setCropWindowRect(cropRect);
+ mapImagePointsByImageMatrix();
+ mCropOverlayView.invalidate();
+
+ // set matrix to apply
+ if (animate) {
+ // set the state for animation to end in, start animation now
+ mAnimation.setEndState(mImagePoints, mImageMatrix);
+ mImageView.startAnimation(mAnimation);
+ } else {
+ mImageView.setImageMatrix(mImageMatrix);
+ }
+
+ // update the image rectangle in the crop overlay
+ updateImageBounds(false);
+ }
+ }
+
+ /**
+ * Adjust the given image rectangle by image transformation matrix to know the final rectangle of
+ * the image.
+ * To get the proper rectangle it must be first reset to original image rectangle.
+ */
+ private void mapImagePointsByImageMatrix() {
+ mImagePoints[0] = 0;
+ mImagePoints[1] = 0;
+ mImagePoints[2] = mBitmap.getWidth();
+ mImagePoints[3] = 0;
+ mImagePoints[4] = mBitmap.getWidth();
+ mImagePoints[5] = mBitmap.getHeight();
+ mImagePoints[6] = 0;
+ mImagePoints[7] = mBitmap.getHeight();
+ mImageMatrix.mapPoints(mImagePoints);
+ mScaleImagePoints[0] = 0;
+ mScaleImagePoints[1] = 0;
+ mScaleImagePoints[2] = 100;
+ mScaleImagePoints[3] = 0;
+ mScaleImagePoints[4] = 100;
+ mScaleImagePoints[5] = 100;
+ mScaleImagePoints[6] = 0;
+ mScaleImagePoints[7] = 100;
+ mImageMatrix.mapPoints(mScaleImagePoints);
+ }
+
+ /**
+ * Set visibility of crop overlay to hide it when there is no image or specificly set by client.
+ */
+ private void setCropOverlayVisibility() {
+ if (mCropOverlayView != null) {
+ mCropOverlayView.setVisibility(mShowCropOverlay && mBitmap != null ? VISIBLE : INVISIBLE);
+ }
+ }
+
+ /**
+ * Set visibility of progress bar when async loading/cropping is in process and show is enabled.
+ */
+ private void setProgressBarVisibility() {
+ boolean visible =
+ mShowProgressBar
+ && (mBitmap == null && mBitmapLoadingWorkerTask != null
+ || mBitmapCroppingWorkerTask != null);
+ mProgressBar.setVisibility(visible ? VISIBLE : INVISIBLE);
+ }
+
+ /**
+ * Update the scale factor between the actual image bitmap and the shown image.
+ */
+ private void updateImageBounds(boolean clear) {
+ if (mBitmap != null && !clear) {
+
+ // Get the scale factor between the actual Bitmap dimensions and the displayed dimensions for
+ // width/height.
+ float scaleFactorWidth =
+ 100f * mLoadedSampleSize / BitmapUtils.getRectWidth(mScaleImagePoints);
+ float scaleFactorHeight =
+ 100f * mLoadedSampleSize / BitmapUtils.getRectHeight(mScaleImagePoints);
+ mCropOverlayView.setCropWindowLimits(
+ getWidth(), getHeight(), scaleFactorWidth, scaleFactorHeight);
+ }
+
+ // set the bitmap rectangle and update the crop window after scale factor is set
+ mCropOverlayView.setBounds(clear ? null : mImagePoints, getWidth(), getHeight());
+ }
+ // endregion
+
+ // region: Inner class: CropShape
+
+ /**
+ * The possible cropping area shape.
+ * To set square/circle crop shape set aspect ratio to 1:1.
+ */
+ public enum CropShape {
+ RECTANGLE,
+ OVAL
+ }
+ // endregion
+
+ // region: Inner class: ScaleType
+
+ /**
+ * Options for scaling the bounds of cropping image to the bounds of Crop Image View.
+ * Note: Some options are affected by auto-zoom, if enabled.
+ */
+ public enum ScaleType {
+
+ /**
+ * Scale the image uniformly (maintain the image's aspect ratio) to fit in crop image view.
+ * The largest dimension will be equals to crop image view and the second dimension will be
+ * smaller.
+ */
+ FIT_CENTER,
+
+ /**
+ * Center the image in the view, but perform no scaling.
+ * Note: If auto-zoom is enabled and the source image is smaller than crop image view then it
+ * will be scaled uniformly to fit the crop image view.
+ */
+ CENTER,
+
+ /**
+ * Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
+ * and height) of the image will be equal to or larger than the corresponding dimension
+ * of the view (minus padding).
+ * The image is then centered in the view.
+ */
+ CENTER_CROP,
+
+ /**
+ * Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
+ * and height) of the image will be equal to or less than the corresponding dimension of
+ * the view (minus padding).
+ * The image is then centered in the view.
+ * Note: If auto-zoom is enabled and the source image is smaller than crop image view then it
+ * will be scaled uniformly to fit the crop image view.
+ */
+ CENTER_INSIDE
+ }
+ // endregion
+
+ // region: Inner class: Guidelines
+
+ /**
+ * The possible guidelines showing types.
+ */
+ public enum Guidelines {
+ /**
+ * Never show
+ */
+ OFF,
+
+ /**
+ * Show when crop move action is live
+ */
+ ON_TOUCH,
+
+ /**
+ * Always show
+ */
+ ON
+ }
+ // endregion
+
+ // region: Inner class: RequestSizeOptions
+
+ /**
+ * Possible options for handling requested width/height for cropping.
+ */
+ public enum RequestSizeOptions {
+
+ /**
+ * No resize/sampling is done unless required for memory management (OOM).
+ */
+ NONE,
+
+ /**
+ * Only sample the image during loading (if image set using URI) so the smallest of the image
+ * dimensions will be between the requested size and x2 requested size.
+ * NOTE: resulting image will not be exactly requested width/height see: Loading
+ * Large Bitmaps Efficiently.
+ */
+ SAMPLING,
+
+ /**
+ * Resize the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
+ * and height) of the image will be equal to or less than the corresponding requested
+ * dimension.
+ * If the image is smaller than the requested size it will NOT change.
+ */
+ RESIZE_INSIDE,
+
+ /**
+ * Resize the image uniformly (maintain the image's aspect ratio) to fit in the given
+ * width/height.
+ * The largest dimension will be equals to the requested and the second dimension will be
+ * smaller.
+ * If the image is smaller than the requested size it will enlarge it.
+ */
+ RESIZE_FIT,
+
+ /**
+ * Resize the image to fit exactly in the given width/height.
+ * This resize method does NOT preserve aspect ratio.
+ * If the image is smaller than the requested size it will enlarge it.
+ */
+ RESIZE_EXACT
+ }
+ // endregion
+
+ // region: Inner class: OnSetImageUriCompleteListener
+
+ /**
+ * Interface definition for a callback to be invoked when the crop overlay is released.
+ */
+ public interface OnSetCropOverlayReleasedListener {
+
+ /**
+ * Called when the crop overlay changed listener is called and inProgress is false.
+ *
+ * @param rect The rect coordinates of the cropped overlay
+ */
+ void onCropOverlayReleased(Rect rect);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the crop overlay is released.
+ */
+ public interface OnSetCropOverlayMovedListener {
+
+ /**
+ * Called when the crop overlay is moved
+ *
+ * @param rect The rect coordinates of the cropped overlay
+ */
+ void onCropOverlayMoved(Rect rect);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the crop overlay is released.
+ */
+ public interface OnSetCropWindowChangeListener {
+
+ /**
+ * Called when the crop window is changed
+ */
+ void onCropWindowChanged();
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when image async loading is complete.
+ */
+ public interface OnSetImageUriCompleteListener {
+
+ /**
+ * Called when a crop image view has completed loading image for cropping.
+ * If loading failed error parameter will contain the error.
+ *
+ * @param view The crop image view that loading of image was complete.
+ * @param uri the URI of the image that was loading
+ * @param error if error occurred during loading will contain the error, otherwise null.
+ */
+ void onSetImageUriComplete(CropImageView view, Uri uri, Exception error);
+ }
+ // endregion
+
+ // region: Inner class: OnGetCroppedImageCompleteListener
+
+ /**
+ * Interface definition for a callback to be invoked when image async crop is complete.
+ */
+ public interface OnCropImageCompleteListener {
+
+ /**
+ * Called when a crop image view has completed cropping image.
+ * Result object contains the cropped bitmap, saved cropped image uri, crop points data or the
+ * error occured during cropping.
+ *
+ * @param view The crop image view that cropping of image was complete.
+ * @param result the crop image result data (with cropped image or error)
+ */
+ void onCropImageComplete(CropImageView view, CropResult result);
+ }
+ // endregion
+
+ // region: Inner class: ActivityResult
+
+ /**
+ * Result data of crop image.
+ */
+ public static class CropResult {
+
+ /**
+ * The image bitmap of the original image loaded for cropping.
+ * Null if uri used to load image or activity result is used.
+ */
+ private final Bitmap mOriginalBitmap;
+
+ /**
+ * The Android uri of the original image loaded for cropping.
+ * Null if bitmap was used to load image.
+ */
+ private final Uri mOriginalUri;
+
+ /**
+ * The cropped image bitmap result.
+ * Null if save cropped image was executed, no output requested or failure.
+ */
+ private final Bitmap mBitmap;
+
+ /**
+ * The Android uri of the saved cropped image result.
+ * Null if get cropped image was executed, no output requested or failure.
+ */
+ private final Uri mUri;
+
+ /**
+ * The error that failed the loading/cropping (null if successful)
+ */
+ private final Exception mError;
+
+ /**
+ * The 4 points of the cropping window in the source image
+ */
+ private final float[] mCropPoints;
+
+ /**
+ * The rectangle of the cropping window in the source image
+ */
+ private final Rect mCropRect;
+
+ /**
+ * The rectangle of the source image dimensions
+ */
+ private final Rect mWholeImageRect;
+
+ /**
+ * The final rotation of the cropped image relative to source
+ */
+ private final int mRotation;
+
+ /**
+ * sample size used creating the crop bitmap to lower its size
+ */
+ private final int mSampleSize;
+
+ CropResult(
+ Bitmap originalBitmap,
+ Uri originalUri,
+ Bitmap bitmap,
+ Uri uri,
+ Exception error,
+ float[] cropPoints,
+ Rect cropRect,
+ Rect wholeImageRect,
+ int rotation,
+ int sampleSize) {
+ mOriginalBitmap = originalBitmap;
+ mOriginalUri = originalUri;
+ mBitmap = bitmap;
+ mUri = uri;
+ mError = error;
+ mCropPoints = cropPoints;
+ mCropRect = cropRect;
+ mWholeImageRect = wholeImageRect;
+ mRotation = rotation;
+ mSampleSize = sampleSize;
+ }
+
+ /**
+ * The image bitmap of the original image loaded for cropping.
+ * Null if uri used to load image or activity result is used.
+ */
+ public Bitmap getOriginalBitmap() {
+ return mOriginalBitmap;
+ }
+
+ /**
+ * The Android uri of the original image loaded for cropping.
+ * Null if bitmap was used to load image.
+ */
+ public Uri getOriginalUri() {
+ return mOriginalUri;
+ }
+
+ /**
+ * Is the result is success or error.
+ */
+ public boolean isSuccessful() {
+ return mError == null;
+ }
+
+ /**
+ * The cropped image bitmap result.
+ * Null if save cropped image was executed, no output requested or failure.
+ */
+ public Bitmap getBitmap() {
+ return mBitmap;
+ }
+
+ /**
+ * The Android uri of the saved cropped image result Null if get cropped image was executed, no
+ * output requested or failure.
+ */
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * The error that failed the loading/cropping (null if successful)
+ */
+ public Exception getError() {
+ return mError;
+ }
+
+ /**
+ * The 4 points of the cropping window in the source image
+ */
+ public float[] getCropPoints() {
+ return mCropPoints;
+ }
+
+ /**
+ * The rectangle of the cropping window in the source image
+ */
+ public Rect getCropRect() {
+ return mCropRect;
+ }
+
+ /**
+ * The rectangle of the source image dimensions
+ */
+ public Rect getWholeImageRect() {
+ return mWholeImageRect;
+ }
+
+ /**
+ * The final rotation of the cropped image relative to source
+ */
+ public int getRotation() {
+ return mRotation;
+ }
+
+ /**
+ * sample size used creating the crop bitmap to lower its size
+ */
+ public int getSampleSize() {
+ return mSampleSize;
+ }
+ }
+ // endregion
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropOverlayView.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropOverlayView.java
new file mode 100644
index 00000000..2a774fe6
--- /dev/null
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropOverlayView.java
@@ -0,0 +1,1118 @@
+// "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.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+
+import java.util.Arrays;
+
+/**
+ * A custom View representing the crop window and the shaded background outside the crop window.
+ */
+public class CropOverlayView extends View {
+
+ // region: Fields and Consts
+
+ /**
+ * Handler from crop window stuff, moving and knowing possition.
+ */
+ private final CropWindowHandler mCropWindowHandler = new CropWindowHandler();
+ /**
+ * Rectangle used for drawing
+ */
+ private final RectF mDrawRect = new RectF();
+ /**
+ * Used for oval crop window shape or non-straight rotation drawing.
+ */
+ private final Path mPath = new Path();
+ /**
+ * The bounding box around the Bitmap that we are cropping.
+ */
+ private final float[] mBoundsPoints = new float[8];
+ /**
+ * The bounding box around the Bitmap that we are cropping.
+ */
+ private final RectF mCalcBounds = new RectF();
+ /**
+ * the initial crop window rectangle to set
+ */
+ private final Rect mInitialCropWindowRect = new Rect();
+ /**
+ * Gesture detector used for multi touch box scaling
+ */
+ private ScaleGestureDetector mScaleDetector;
+ /**
+ * Boolean to see if multi touch is enabled for the crop rectangle
+ */
+ private boolean mMultiTouchEnabled;
+ /**
+ * Listener to publicj crop window changes
+ */
+ private CropWindowChangeListener mCropWindowChangeListener;
+ /**
+ * The Paint used to draw the white rectangle around the crop area.
+ */
+ private Paint mBorderPaint;
+ /**
+ * The Paint used to draw the corners of the Border
+ */
+ private Paint mBorderCornerPaint;
+ /**
+ * The Paint used to draw the guidelines within the crop area when pressed.
+ */
+ private Paint mGuidelinePaint;
+ /**
+ * The Paint used to darken the surrounding areas outside the crop area.
+ */
+ private Paint mBackgroundPaint;
+ /**
+ * The bounding image view width used to know the crop overlay is at view edges.
+ */
+ private int mViewWidth;
+ /**
+ * The bounding image view height used to know the crop overlay is at view edges.
+ */
+ private int mViewHeight;
+ /**
+ * The offset to draw the border corener from the border
+ */
+ private float mBorderCornerOffset;
+ /**
+ * the length of the border corner to draw
+ */
+ private float mBorderCornerLength;
+ /**
+ * The initial crop window padding from image borders
+ */
+ private float mInitialCropWindowPaddingRatio;
+ /**
+ * The radius of the touch zone (in pixels) around a given Handle.
+ */
+ private float mTouchRadius;
+ /**
+ * 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.
+ */
+ private float mSnapRadius;
+ /**
+ * The Handle that is currently pressed; null if no Handle is pressed.
+ */
+ private CropWindowMoveHandler mMoveHandler;
+ /**
+ * Flag indicating if the crop area should always be a certain aspect ratio (indicated by
+ * mTargetAspectRatio).
+ */
+ private boolean mFixAspectRatio;
+ /**
+ * save the current aspect ratio of the image
+ */
+ private int mAspectRatioX;
+ /**
+ * save the current aspect ratio of the image
+ */
+ private int mAspectRatioY;
+ /**
+ * The aspect ratio that the crop area should maintain; this variable is only used when
+ * mMaintainAspectRatio is true.
+ */
+ private float mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
+ /**
+ * Instance variables for customizable attributes
+ */
+ private CropImageView.Guidelines mGuidelines;
+ /**
+ * The shape of the cropping area - rectangle/circular.
+ */
+ private CropImageView.CropShape mCropShape;
+ /**
+ * Whether the Crop View has been initialized for the first time
+ */
+ private boolean initializedCropWindow;
+
+ /**
+ * Used to set back LayerType after changing to software.
+ */
+ private Integer mOriginalLayerType;
+ // endregion
+
+ public CropOverlayView(Context context) {
+ this(context, null);
+ }
+
+ public CropOverlayView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Creates the Paint object for drawing.
+ */
+ private static Paint getNewPaint(int color) {
+ Paint paint = new Paint();
+ paint.setColor(color);
+ return paint;
+ }
+
+ /**
+ * Creates the Paint object for given thickness and color, if thickness < 0 return null.
+ */
+ private static Paint getNewPaintOrNull(float thickness, int color) {
+ if (thickness > 0) {
+ Paint borderPaint = new Paint();
+ borderPaint.setColor(color);
+ borderPaint.setStrokeWidth(thickness);
+ borderPaint.setStyle(Paint.Style.STROKE);
+ borderPaint.setAntiAlias(true);
+ return borderPaint;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Set the crop window change listener.
+ */
+ public void setCropWindowChangeListener(CropWindowChangeListener listener) {
+ mCropWindowChangeListener = listener;
+ }
+
+ /**
+ * Get the left/top/right/bottom coordinates of the crop window.
+ */
+ public RectF getCropWindowRect() {
+ return mCropWindowHandler.getRect();
+ }
+
+ /**
+ * Set the left/top/right/bottom coordinates of the crop window.
+ */
+ public void setCropWindowRect(RectF rect) {
+ mCropWindowHandler.setRect(rect);
+ }
+
+ /**
+ * Fix the current crop window rectangle if it is outside of cropping image or view bounds.
+ */
+ public void fixCurrentCropWindowRect() {
+ RectF rect = getCropWindowRect();
+ fixCropWindowRectByRules(rect);
+ mCropWindowHandler.setRect(rect);
+ }
+
+ /**
+ * Informs the CropOverlayView of the image's position relative to the ImageView. This is
+ * necessary to call in order to draw the crop window.
+ *
+ * @param boundsPoints the image's bounding points
+ * @param viewWidth The bounding image view width.
+ * @param viewHeight The bounding image view height.
+ */
+ public void setBounds(float[] boundsPoints, int viewWidth, int viewHeight) {
+ if (boundsPoints == null || !Arrays.equals(mBoundsPoints, boundsPoints)) {
+ if (boundsPoints == null) {
+ Arrays.fill(mBoundsPoints, 0);
+ } else {
+ System.arraycopy(boundsPoints, 0, mBoundsPoints, 0, boundsPoints.length);
+ }
+ mViewWidth = viewWidth;
+ mViewHeight = viewHeight;
+ RectF cropRect = mCropWindowHandler.getRect();
+ if (cropRect.width() == 0 || cropRect.height() == 0) {
+ initCropWindow();
+ }
+ }
+ }
+
+ /**
+ * Resets the crop overlay view.
+ */
+ public void resetCropOverlayView() {
+ if (initializedCropWindow) {
+ setCropWindowRect(BitmapUtils.EMPTY_RECT_F);
+ initCropWindow();
+ invalidate();
+ }
+ }
+
+ /**
+ * The shape of the cropping area - rectangle/circular.
+ */
+ public CropImageView.CropShape getCropShape() {
+ return mCropShape;
+ }
+
+ /**
+ * The shape of the cropping area - rectangle/circular.
+ */
+ public void setCropShape(CropImageView.CropShape cropShape) {
+ if (mCropShape != cropShape) {
+ mCropShape = cropShape;
+ if (Build.VERSION.SDK_INT <= 17) {
+ if (mCropShape == CropImageView.CropShape.OVAL) {
+ mOriginalLayerType = getLayerType();
+ if (mOriginalLayerType != View.LAYER_TYPE_SOFTWARE) {
+ // TURN off hardware acceleration
+ setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+ } else {
+ mOriginalLayerType = null;
+ }
+ } else if (mOriginalLayerType != null) {
+ // return hardware acceleration back
+ setLayerType(mOriginalLayerType, null);
+ mOriginalLayerType = null;
+ }
+ }
+ invalidate();
+ }
+ }
+
+ /**
+ * Get the current guidelines option set.
+ */
+ public CropImageView.Guidelines getGuidelines() {
+ return mGuidelines;
+ }
+
+ /**
+ * Sets the guidelines for the CropOverlayView to be either on, off, or to show when resizing the
+ * application.
+ */
+ public void setGuidelines(CropImageView.Guidelines guidelines) {
+ if (mGuidelines != guidelines) {
+ mGuidelines = guidelines;
+ if (initializedCropWindow) {
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows it to
+ * be changed.
+ */
+ public boolean isFixAspectRatio() {
+ return mFixAspectRatio;
+ }
+
+ /**
+ * Sets whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows
+ * it to be changed.
+ */
+ public void setFixedAspectRatio(boolean fixAspectRatio) {
+ if (mFixAspectRatio != fixAspectRatio) {
+ mFixAspectRatio = fixAspectRatio;
+ if (initializedCropWindow) {
+ initCropWindow();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * the X value of the aspect ratio;
+ */
+ public int getAspectRatioX() {
+ return mAspectRatioX;
+ }
+
+ /**
+ * Sets the X value of the aspect ratio; is defaulted to 1.
+ */
+ public void setAspectRatioX(int aspectRatioX) {
+ if (aspectRatioX <= 0) {
+ throw new IllegalArgumentException(
+ "Cannot set aspect ratio value to a number less than or equal to 0.");
+ } else if (mAspectRatioX != aspectRatioX) {
+ mAspectRatioX = aspectRatioX;
+ mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
+
+ if (initializedCropWindow) {
+ initCropWindow();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * the Y value of the aspect ratio;
+ */
+ public int getAspectRatioY() {
+ return mAspectRatioY;
+ }
+
+ /**
+ * Sets the Y value of the aspect ratio; is defaulted to 1.
+ *
+ * @param aspectRatioY int that specifies the new Y value of the aspect ratio
+ */
+ public void setAspectRatioY(int aspectRatioY) {
+ if (aspectRatioY <= 0) {
+ throw new IllegalArgumentException(
+ "Cannot set aspect ratio value to a number less than or equal to 0.");
+ } else if (mAspectRatioY != aspectRatioY) {
+ mAspectRatioY = aspectRatioY;
+ mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
+
+ if (initializedCropWindow) {
+ initCropWindow();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * 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. (default: 3)
+ */
+ public void setSnapRadius(float snapRadius) {
+ mSnapRadius = snapRadius;
+ }
+
+ /**
+ * Set multi touch functionality to enabled/disabled.
+ */
+ public boolean setMultiTouchEnabled(boolean multiTouchEnabled) {
+ if (mMultiTouchEnabled != multiTouchEnabled) {
+ mMultiTouchEnabled = multiTouchEnabled;
+ if (mMultiTouchEnabled && mScaleDetector == null) {
+ mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * the min size the resulting cropping image is allowed to be, affects the cropping window limits
+ * (in pixels).
+ */
+ public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
+ mCropWindowHandler.setMinCropResultSize(minCropResultWidth, minCropResultHeight);
+ }
+
+ /**
+ * the max size the resulting cropping image is allowed to be, affects the cropping window limits
+ * (in pixels).
+ */
+ public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
+ mCropWindowHandler.setMaxCropResultSize(maxCropResultWidth, maxCropResultHeight);
+ }
+
+ /**
+ * set the max width/height and scale factor of the shown image to original image to scale the
+ * limits appropriately.
+ */
+ public void setCropWindowLimits(
+ float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) {
+ mCropWindowHandler.setCropWindowLimits(
+ maxWidth, maxHeight, scaleFactorWidth, scaleFactorHeight);
+ }
+
+ /**
+ * Get crop window initial rectangle.
+ */
+ public Rect getInitialCropWindowRect() {
+ return mInitialCropWindowRect;
+ }
+
+ /**
+ * Set crop window initial rectangle to be used instead of default.
+ */
+ public void setInitialCropWindowRect(Rect rect) {
+ mInitialCropWindowRect.set(rect != null ? rect : BitmapUtils.EMPTY_RECT);
+ if (initializedCropWindow) {
+ initCropWindow();
+ invalidate();
+ callOnCropWindowChanged(false);
+ }
+ }
+
+ // region: Private methods
+
+ /**
+ * Reset crop window to initial rectangle.
+ */
+ public void resetCropWindowRect() {
+ if (initializedCropWindow) {
+ initCropWindow();
+ invalidate();
+ callOnCropWindowChanged(false);
+ }
+ }
+
+ /**
+ * Sets all initial values, but does not call initCropWindow to reset the views.
+ * Used once at the very start to initialize the attributes.
+ */
+ public void setInitialAttributeValues(CropImageOptions options) {
+
+ mCropWindowHandler.setInitialAttributeValues(options);
+
+ setCropShape(options.cropShape);
+
+ setSnapRadius(options.snapRadius);
+
+ setGuidelines(options.guidelines);
+
+ setFixedAspectRatio(options.fixAspectRatio);
+
+ setAspectRatioX(options.aspectRatioX);
+
+ setAspectRatioY(options.aspectRatioY);
+
+ setMultiTouchEnabled(options.multiTouchEnabled);
+
+ mTouchRadius = options.touchRadius;
+
+ mInitialCropWindowPaddingRatio = options.initialCropWindowPaddingRatio;
+
+ mBorderPaint = getNewPaintOrNull(options.borderLineThickness, options.borderLineColor);
+
+ mBorderCornerOffset = options.borderCornerOffset;
+ mBorderCornerLength = options.borderCornerLength;
+ mBorderCornerPaint =
+ getNewPaintOrNull(options.borderCornerThickness, options.borderCornerColor);
+
+ mGuidelinePaint = getNewPaintOrNull(options.guidelinesThickness, options.guidelinesColor);
+
+ mBackgroundPaint = getNewPaint(options.backgroundColor);
+ }
+
+ /**
+ * Set the initial crop window size and position. This is dependent on the size and position of
+ * the image being cropped.
+ */
+ private void initCropWindow() {
+
+ float leftLimit = Math.max(BitmapUtils.getRectLeft(mBoundsPoints), 0);
+ float topLimit = Math.max(BitmapUtils.getRectTop(mBoundsPoints), 0);
+ float rightLimit = Math.min(BitmapUtils.getRectRight(mBoundsPoints), getWidth());
+ float bottomLimit = Math.min(BitmapUtils.getRectBottom(mBoundsPoints), getHeight());
+
+ if (rightLimit <= leftLimit || bottomLimit <= topLimit) {
+ return;
+ }
+
+ RectF rect = new RectF();
+
+ // Tells the attribute functions the crop window has already been initialized
+ initializedCropWindow = true;
+
+ float horizontalPadding = mInitialCropWindowPaddingRatio * (rightLimit - leftLimit);
+ float verticalPadding = mInitialCropWindowPaddingRatio * (bottomLimit - topLimit);
+
+ if (mInitialCropWindowRect.width() > 0 && mInitialCropWindowRect.height() > 0) {
+ // Get crop window position relative to the displayed image.
+ rect.left =
+ leftLimit + mInitialCropWindowRect.left / mCropWindowHandler.getScaleFactorWidth();
+ rect.top = topLimit + mInitialCropWindowRect.top / mCropWindowHandler.getScaleFactorHeight();
+ rect.right =
+ rect.left + mInitialCropWindowRect.width() / mCropWindowHandler.getScaleFactorWidth();
+ rect.bottom =
+ rect.top + mInitialCropWindowRect.height() / mCropWindowHandler.getScaleFactorHeight();
+
+ // Correct for floating point errors. Crop rect boundaries should not exceed the source Bitmap
+ // bounds.
+ rect.left = Math.max(leftLimit, rect.left);
+ rect.top = Math.max(topLimit, rect.top);
+ rect.right = Math.min(rightLimit, rect.right);
+ rect.bottom = Math.min(bottomLimit, rect.bottom);
+
+ } else if (mFixAspectRatio && rightLimit > leftLimit && bottomLimit > topLimit) {
+
+ // If the image aspect ratio is wider than the crop aspect ratio,
+ // then the image height is the determining initial length. Else, vice-versa.
+ float bitmapAspectRatio = (rightLimit - leftLimit) / (bottomLimit - topLimit);
+ if (bitmapAspectRatio > mTargetAspectRatio) {
+
+ rect.top = topLimit + verticalPadding;
+ rect.bottom = bottomLimit - verticalPadding;
+
+ float centerX = getWidth() / 2f;
+
+ // dirty fix for wrong crop overlay aspect ratio when using fixed aspect ratio
+ mTargetAspectRatio = (float) mAspectRatioX / mAspectRatioY;
+
+ // Limits the aspect ratio to no less than 40 wide or 40 tall
+ float cropWidth =
+ Math.max(mCropWindowHandler.getMinCropWidth(), rect.height() * mTargetAspectRatio);
+
+ float halfCropWidth = cropWidth / 2f;
+ rect.left = centerX - halfCropWidth;
+ rect.right = centerX + halfCropWidth;
+
+ } else {
+
+ rect.left = leftLimit + horizontalPadding;
+ rect.right = rightLimit - horizontalPadding;
+
+ float centerY = getHeight() / 2f;
+
+ // Limits the aspect ratio to no less than 40 wide or 40 tall
+ float cropHeight =
+ Math.max(mCropWindowHandler.getMinCropHeight(), rect.width() / mTargetAspectRatio);
+
+ float halfCropHeight = cropHeight / 2f;
+ rect.top = centerY - halfCropHeight;
+ rect.bottom = centerY + halfCropHeight;
+ }
+ } else {
+ // Initialize crop window to have 10% padding w/ respect to image.
+ rect.left = leftLimit + horizontalPadding;
+ rect.top = topLimit + verticalPadding;
+ rect.right = rightLimit - horizontalPadding;
+ rect.bottom = bottomLimit - verticalPadding;
+ }
+
+ fixCropWindowRectByRules(rect);
+
+ mCropWindowHandler.setRect(rect);
+ }
+
+ /**
+ * Fix the given rect to fit into bitmap rect and follow min, max and aspect ratio rules.
+ */
+ private void fixCropWindowRectByRules(RectF rect) {
+ if (rect.width() < mCropWindowHandler.getMinCropWidth()) {
+ float adj = (mCropWindowHandler.getMinCropWidth() - rect.width()) / 2;
+ rect.left -= adj;
+ rect.right += adj;
+ }
+ if (rect.height() < mCropWindowHandler.getMinCropHeight()) {
+ float adj = (mCropWindowHandler.getMinCropHeight() - rect.height()) / 2;
+ rect.top -= adj;
+ rect.bottom += adj;
+ }
+ if (rect.width() > mCropWindowHandler.getMaxCropWidth()) {
+ float adj = (rect.width() - mCropWindowHandler.getMaxCropWidth()) / 2;
+ rect.left += adj;
+ rect.right -= adj;
+ }
+ if (rect.height() > mCropWindowHandler.getMaxCropHeight()) {
+ float adj = (rect.height() - mCropWindowHandler.getMaxCropHeight()) / 2;
+ rect.top += adj;
+ rect.bottom -= adj;
+ }
+
+ calculateBounds(rect);
+ if (mCalcBounds.width() > 0 && mCalcBounds.height() > 0) {
+ float leftLimit = Math.max(mCalcBounds.left, 0);
+ float topLimit = Math.max(mCalcBounds.top, 0);
+ float rightLimit = Math.min(mCalcBounds.right, getWidth());
+ float bottomLimit = Math.min(mCalcBounds.bottom, getHeight());
+ if (rect.left < leftLimit) {
+ rect.left = leftLimit;
+ }
+ if (rect.top < topLimit) {
+ rect.top = topLimit;
+ }
+ if (rect.right > rightLimit) {
+ rect.right = rightLimit;
+ }
+ if (rect.bottom > bottomLimit) {
+ rect.bottom = bottomLimit;
+ }
+ }
+ if (mFixAspectRatio && Math.abs(rect.width() - rect.height() * mTargetAspectRatio) > 0.1) {
+ if (rect.width() > rect.height() * mTargetAspectRatio) {
+ float adj = Math.abs(rect.height() * mTargetAspectRatio - rect.width()) / 2;
+ rect.left += adj;
+ rect.right -= adj;
+ } else {
+ float adj = Math.abs(rect.width() / mTargetAspectRatio - rect.height()) / 2;
+ rect.top += adj;
+ rect.bottom -= adj;
+ }
+ }
+ }
+
+ /**
+ * Draw crop overview by drawing background over image not in the cripping area, then borders and
+ * guidelines.
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+
+ super.onDraw(canvas);
+
+ // Draw translucent background for the cropped area.
+ drawBackground(canvas);
+
+ if (mCropWindowHandler.showGuidelines()) {
+ // Determines whether guidelines should be drawn or not
+ if (mGuidelines == CropImageView.Guidelines.ON) {
+ drawGuidelines(canvas);
+ } else if (mGuidelines == CropImageView.Guidelines.ON_TOUCH && mMoveHandler != null) {
+ // Draw only when resizing
+ drawGuidelines(canvas);
+ }
+ }
+
+ drawBorders(canvas);
+
+ drawCorners(canvas);
+ }
+
+ /**
+ * Draw shadow background over the image not including the crop area.
+ */
+ private void drawBackground(Canvas canvas) {
+
+ RectF rect = mCropWindowHandler.getRect();
+
+ float left = Math.max(BitmapUtils.getRectLeft(mBoundsPoints), 0);
+ float top = Math.max(BitmapUtils.getRectTop(mBoundsPoints), 0);
+ float right = Math.min(BitmapUtils.getRectRight(mBoundsPoints), getWidth());
+ float bottom = Math.min(BitmapUtils.getRectBottom(mBoundsPoints), getHeight());
+
+ if (mCropShape == CropImageView.CropShape.RECTANGLE) {
+ if (!isNonStraightAngleRotated() || Build.VERSION.SDK_INT <= 17) {
+ canvas.drawRect(left, top, right, rect.top, mBackgroundPaint);
+ canvas.drawRect(left, rect.bottom, right, bottom, mBackgroundPaint);
+ canvas.drawRect(left, rect.top, rect.left, rect.bottom, mBackgroundPaint);
+ canvas.drawRect(rect.right, rect.top, right, rect.bottom, mBackgroundPaint);
+ } else {
+ mPath.reset();
+ mPath.moveTo(mBoundsPoints[0], mBoundsPoints[1]);
+ mPath.lineTo(mBoundsPoints[2], mBoundsPoints[3]);
+ mPath.lineTo(mBoundsPoints[4], mBoundsPoints[5]);
+ mPath.lineTo(mBoundsPoints[6], mBoundsPoints[7]);
+ mPath.close();
+
+ canvas.save();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ canvas.clipOutPath(mPath);
+ } else {
+ canvas.clipPath(mPath, Region.Op.INTERSECT);
+ }
+ canvas.clipRect(rect, Region.Op.XOR);
+ canvas.drawRect(left, top, right, bottom, mBackgroundPaint);
+ canvas.restore();
+ }
+ } else {
+ mPath.reset();
+ if (Build.VERSION.SDK_INT <= 17 && mCropShape == CropImageView.CropShape.OVAL) {
+ mDrawRect.set(rect.left + 2, rect.top + 2, rect.right - 2, rect.bottom - 2);
+ } else {
+ mDrawRect.set(rect.left, rect.top, rect.right, rect.bottom);
+ }
+ mPath.addOval(mDrawRect, Path.Direction.CW);
+ canvas.save();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ canvas.clipOutPath(mPath);
+ } else {
+ canvas.clipPath(mPath, Region.Op.XOR);
+ }
+ canvas.drawRect(left, top, right, bottom, mBackgroundPaint);
+ canvas.restore();
+ }
+ }
+
+ /**
+ * Draw 2 veritcal and 2 horizontal guidelines inside the cropping area to split it into 9 equal
+ * parts.
+ */
+ private void drawGuidelines(Canvas canvas) {
+ if (mGuidelinePaint != null) {
+ float sw = mBorderPaint != null ? mBorderPaint.getStrokeWidth() : 0;
+ RectF rect = mCropWindowHandler.getRect();
+ rect.inset(sw, sw);
+
+ float oneThirdCropWidth = rect.width() / 3;
+ float oneThirdCropHeight = rect.height() / 3;
+
+ if (mCropShape == CropImageView.CropShape.OVAL) {
+
+ float w = rect.width() / 2 - sw;
+ float h = rect.height() / 2 - sw;
+
+ // Draw vertical guidelines.
+ float x1 = rect.left + oneThirdCropWidth;
+ float x2 = rect.right - oneThirdCropWidth;
+ float yv = (float) (h * Math.sin(Math.acos((w - oneThirdCropWidth) / w)));
+ canvas.drawLine(x1, rect.top + h - yv, x1, rect.bottom - h + yv, mGuidelinePaint);
+ canvas.drawLine(x2, rect.top + h - yv, x2, rect.bottom - h + yv, mGuidelinePaint);
+
+ // Draw horizontal guidelines.
+ float y1 = rect.top + oneThirdCropHeight;
+ float y2 = rect.bottom - oneThirdCropHeight;
+ float xv = (float) (w * Math.cos(Math.asin((h - oneThirdCropHeight) / h)));
+ canvas.drawLine(rect.left + w - xv, y1, rect.right - w + xv, y1, mGuidelinePaint);
+ canvas.drawLine(rect.left + w - xv, y2, rect.right - w + xv, y2, mGuidelinePaint);
+ } else {
+
+ // Draw vertical guidelines.
+ float x1 = rect.left + oneThirdCropWidth;
+ float x2 = rect.right - oneThirdCropWidth;
+ canvas.drawLine(x1, rect.top, x1, rect.bottom, mGuidelinePaint);
+ canvas.drawLine(x2, rect.top, x2, rect.bottom, mGuidelinePaint);
+
+ // Draw horizontal guidelines.
+ float y1 = rect.top + oneThirdCropHeight;
+ float y2 = rect.bottom - oneThirdCropHeight;
+ canvas.drawLine(rect.left, y1, rect.right, y1, mGuidelinePaint);
+ canvas.drawLine(rect.left, y2, rect.right, y2, mGuidelinePaint);
+ }
+ }
+ }
+
+ /**
+ * Draw borders of the crop area.
+ */
+ private void drawBorders(Canvas canvas) {
+ if (mBorderPaint != null) {
+ float w = mBorderPaint.getStrokeWidth();
+ RectF rect = mCropWindowHandler.getRect();
+ rect.inset(w / 2, w / 2);
+
+ if (mCropShape == CropImageView.CropShape.RECTANGLE) {
+ // Draw rectangle crop window border.
+ canvas.drawRect(rect, mBorderPaint);
+ } else {
+ // Draw circular crop window border
+ canvas.drawOval(rect, mBorderPaint);
+ }
+ }
+ }
+
+ /**
+ * Draw the corner of crop overlay.
+ */
+ private void drawCorners(Canvas canvas) {
+ if (mBorderCornerPaint != null) {
+
+ float lineWidth = mBorderPaint != null ? mBorderPaint.getStrokeWidth() : 0;
+ float cornerWidth = mBorderCornerPaint.getStrokeWidth();
+
+ // for rectangle crop shape we allow the corners to be offset from the borders
+ float w =
+ cornerWidth / 2
+ + (mCropShape == CropImageView.CropShape.RECTANGLE ? mBorderCornerOffset : 0);
+
+ RectF rect = mCropWindowHandler.getRect();
+ rect.inset(w, w);
+
+ float cornerOffset = (cornerWidth - lineWidth) / 2;
+ float cornerExtension = cornerWidth / 2 + cornerOffset;
+
+ // Top left
+ canvas.drawLine(
+ rect.left - cornerOffset,
+ rect.top - cornerExtension,
+ rect.left - cornerOffset,
+ rect.top + mBorderCornerLength,
+ mBorderCornerPaint);
+ canvas.drawLine(
+ rect.left - cornerExtension,
+ rect.top - cornerOffset,
+ rect.left + mBorderCornerLength,
+ rect.top - cornerOffset,
+ mBorderCornerPaint);
+
+ // Top right
+ canvas.drawLine(
+ rect.right + cornerOffset,
+ rect.top - cornerExtension,
+ rect.right + cornerOffset,
+ rect.top + mBorderCornerLength,
+ mBorderCornerPaint);
+ canvas.drawLine(
+ rect.right + cornerExtension,
+ rect.top - cornerOffset,
+ rect.right - mBorderCornerLength,
+ rect.top - cornerOffset,
+ mBorderCornerPaint);
+
+ // Bottom left
+ canvas.drawLine(
+ rect.left - cornerOffset,
+ rect.bottom + cornerExtension,
+ rect.left - cornerOffset,
+ rect.bottom - mBorderCornerLength,
+ mBorderCornerPaint);
+ canvas.drawLine(
+ rect.left - cornerExtension,
+ rect.bottom + cornerOffset,
+ rect.left + mBorderCornerLength,
+ rect.bottom + cornerOffset,
+ mBorderCornerPaint);
+
+ // Bottom left
+ canvas.drawLine(
+ rect.right + cornerOffset,
+ rect.bottom + cornerExtension,
+ rect.right + cornerOffset,
+ rect.bottom - mBorderCornerLength,
+ mBorderCornerPaint);
+ canvas.drawLine(
+ rect.right + cornerExtension,
+ rect.bottom + cornerOffset,
+ rect.right - mBorderCornerLength,
+ rect.bottom + cornerOffset,
+ mBorderCornerPaint);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // If this View is not enabled, don't allow for touch interactions.
+ if (isEnabled()) {
+ if (mMultiTouchEnabled) {
+ mScaleDetector.onTouchEvent(event);
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ onActionDown(event.getX(), event.getY());
+ return true;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ getParent().requestDisallowInterceptTouchEvent(false);
+ onActionUp();
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ onActionMove(event.getX(), event.getY());
+ getParent().requestDisallowInterceptTouchEvent(true);
+ return true;
+ default:
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * On press down start crop window movment depending on the location of the press.
+ * if press is far from crop window then no move handler is returned (null).
+ */
+ private void onActionDown(float x, float y) {
+ mMoveHandler = mCropWindowHandler.getMoveHandler(x, y, mTouchRadius, mCropShape);
+ if (mMoveHandler != null) {
+ invalidate();
+ }
+ }
+
+ /**
+ * Clear move handler starting in {@link #onActionDown(float, float)} if exists.
+ */
+ private void onActionUp() {
+ if (mMoveHandler != null) {
+ mMoveHandler = null;
+ callOnCropWindowChanged(false);
+ invalidate();
+ }
+ }
+
+ /**
+ * Handle move of crop window using the move handler created in {@link #onActionDown(float,
+ * float)}.
+ * The move handler will do the proper move/resize of the crop window.
+ */
+ private void onActionMove(float x, float y) {
+ if (mMoveHandler != null) {
+ float snapRadius = mSnapRadius;
+ RectF rect = mCropWindowHandler.getRect();
+
+ if (calculateBounds(rect)) {
+ snapRadius = 0;
+ }
+
+ mMoveHandler.move(
+ rect,
+ x,
+ y,
+ mCalcBounds,
+ mViewWidth,
+ mViewHeight,
+ snapRadius,
+ mFixAspectRatio,
+ mTargetAspectRatio);
+ mCropWindowHandler.setRect(rect);
+ callOnCropWindowChanged(true);
+ invalidate();
+ }
+ }
+
+ /**
+ * Calculate the bounding rectangle for current crop window, handle non-straight rotation angles.
+ *
+ * If the rotation angle is straight then the bounds rectangle is the bitmap rectangle, otherwsie
+ * we find the max rectangle that is within the image bounds starting from the crop window
+ * rectangle.
+ *
+ * @param rect the crop window rectangle to start finsing bounded rectangle from
+ * @return true - non straight rotation in place, false - otherwise.
+ */
+ private boolean calculateBounds(RectF rect) {
+
+ float left = BitmapUtils.getRectLeft(mBoundsPoints);
+ float top = BitmapUtils.getRectTop(mBoundsPoints);
+ float right = BitmapUtils.getRectRight(mBoundsPoints);
+ float bottom = BitmapUtils.getRectBottom(mBoundsPoints);
+
+ if (!isNonStraightAngleRotated()) {
+ mCalcBounds.set(left, top, right, bottom);
+ return false;
+ } else {
+ float x0 = mBoundsPoints[0];
+ float y0 = mBoundsPoints[1];
+ float x2 = mBoundsPoints[4];
+ float y2 = mBoundsPoints[5];
+ float x3 = mBoundsPoints[6];
+ float y3 = mBoundsPoints[7];
+
+ if (mBoundsPoints[7] < mBoundsPoints[1]) {
+ if (mBoundsPoints[1] < mBoundsPoints[3]) {
+ x0 = mBoundsPoints[6];
+ y0 = mBoundsPoints[7];
+ x2 = mBoundsPoints[2];
+ y2 = mBoundsPoints[3];
+ x3 = mBoundsPoints[4];
+ y3 = mBoundsPoints[5];
+ } else {
+ x0 = mBoundsPoints[4];
+ y0 = mBoundsPoints[5];
+ x2 = mBoundsPoints[0];
+ y2 = mBoundsPoints[1];
+ x3 = mBoundsPoints[2];
+ y3 = mBoundsPoints[3];
+ }
+ } else if (mBoundsPoints[1] > mBoundsPoints[3]) {
+ x0 = mBoundsPoints[2];
+ y0 = mBoundsPoints[3];
+ x2 = mBoundsPoints[6];
+ y2 = mBoundsPoints[7];
+ x3 = mBoundsPoints[0];
+ y3 = mBoundsPoints[1];
+ }
+
+ float a0 = (y3 - y0) / (x3 - x0);
+ float a1 = -1f / a0;
+ float b0 = y0 - a0 * x0;
+ float b1 = y0 - a1 * x0;
+ float b2 = y2 - a0 * x2;
+ float b3 = y2 - a1 * x2;
+
+ float c0 = (rect.centerY() - rect.top) / (rect.centerX() - rect.left);
+ float c1 = -c0;
+ float d0 = rect.top - c0 * rect.left;
+ float d1 = rect.top - c1 * rect.right;
+
+ left = Math.max(left, (d0 - b0) / (a0 - c0) < rect.right ? (d0 - b0) / (a0 - c0) : left);
+ left = Math.max(left, (d0 - b1) / (a1 - c0) < rect.right ? (d0 - b1) / (a1 - c0) : left);
+ left = Math.max(left, (d1 - b3) / (a1 - c1) < rect.right ? (d1 - b3) / (a1 - c1) : left);
+ right = Math.min(right, (d1 - b1) / (a1 - c1) > rect.left ? (d1 - b1) / (a1 - c1) : right);
+ right = Math.min(right, (d1 - b2) / (a0 - c1) > rect.left ? (d1 - b2) / (a0 - c1) : right);
+ right = Math.min(right, (d0 - b2) / (a0 - c0) > rect.left ? (d0 - b2) / (a0 - c0) : right);
+
+ top = Math.max(top, Math.max(a0 * left + b0, a1 * right + b1));
+ bottom = Math.min(bottom, Math.min(a1 * left + b3, a0 * right + b2));
+
+ mCalcBounds.left = left;
+ mCalcBounds.top = top;
+ mCalcBounds.right = right;
+ mCalcBounds.bottom = bottom;
+ return true;
+ }
+ }
+
+ /**
+ * Is the cropping image has been rotated by NOT 0,90,180 or 270 degrees.
+ */
+ private boolean isNonStraightAngleRotated() {
+ return mBoundsPoints[0] != mBoundsPoints[6] && mBoundsPoints[1] != mBoundsPoints[7];
+ }
+
+ /**
+ * Invoke on crop change listener safe, don't let the app crash on exception.
+ */
+ private void callOnCropWindowChanged(boolean inProgress) {
+ try {
+ if (mCropWindowChangeListener != null) {
+ mCropWindowChangeListener.onCropWindowChanged(inProgress);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ // endregion
+
+ // region: Inner class: CropWindowChangeListener
+
+ /**
+ * Interface definition for a callback to be invoked when crop window rectangle is changing.
+ */
+ public interface CropWindowChangeListener {
+
+ /**
+ * Called after a change in crop window rectangle.
+ *
+ * @param inProgress is the crop window change operation is still in progress by user touch
+ */
+ void onCropWindowChanged(boolean inProgress);
+ }
+ // endregion
+
+ // region: Inner class: ScaleListener
+
+ /**
+ * Handle scaling the rectangle based on two finger input
+ */
+ private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public boolean onScale(ScaleGestureDetector detector) {
+ RectF rect = mCropWindowHandler.getRect();
+
+ float x = detector.getFocusX();
+ float y = detector.getFocusY();
+ float dY = detector.getCurrentSpanY() / 2;
+ float dX = detector.getCurrentSpanX() / 2;
+
+ float newTop = y - dY;
+ float newLeft = x - dX;
+ float newRight = x + dX;
+ float newBottom = y + dY;
+
+ if (newLeft < newRight
+ && newTop <= newBottom
+ && newLeft >= 0
+ && newRight <= mCropWindowHandler.getMaxCropWidth()
+ && newTop >= 0
+ && newBottom <= mCropWindowHandler.getMaxCropHeight()) {
+
+ rect.set(newLeft, newTop, newRight, newBottom);
+ mCropWindowHandler.setRect(rect);
+ invalidate();
+ }
+
+ return true;
+ }
+ }
+ // endregion
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowHandler.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowHandler.java
new file mode 100644
index 00000000..d9785531
--- /dev/null
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowHandler.java
@@ -0,0 +1,405 @@
+// "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).
+ */
+ 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).
+ */
+ 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
+}
diff --git a/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowMoveHandler.java b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowMoveHandler.java
new file mode 100644
index 00000000..0f1eaab4
--- /dev/null
+++ b/cropper/src/main/java/com/theartofdev/edmodo/cropper/CropWindowMoveHandler.java
@@ -0,0 +1,786 @@
+// "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.
+ *
+ */
+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.
+ * Move type handled by this instance, as initialized in creation, affects how the change in toch
+ * location changes the crop window position and size.
+ * 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.
+ * Primary is the edge directly affected by move type, secondary is the other edge.
+ * 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.
+ * 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.
+ * 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.
+ * 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
+}
diff --git a/cropper/src/main/res/drawable-hdpi/crop_image_menu_flip.png b/cropper/src/main/res/drawable-hdpi/crop_image_menu_flip.png
new file mode 100644
index 00000000..133395df
Binary files /dev/null and b/cropper/src/main/res/drawable-hdpi/crop_image_menu_flip.png differ
diff --git a/cropper/src/main/res/drawable-hdpi/crop_image_menu_rotate_left.png b/cropper/src/main/res/drawable-hdpi/crop_image_menu_rotate_left.png
new file mode 100644
index 00000000..e4e26f8c
Binary files /dev/null and b/cropper/src/main/res/drawable-hdpi/crop_image_menu_rotate_left.png differ
diff --git a/cropper/src/main/res/drawable-hdpi/crop_image_menu_rotate_right.png b/cropper/src/main/res/drawable-hdpi/crop_image_menu_rotate_right.png
new file mode 100644
index 00000000..2311d1a0
Binary files /dev/null and b/cropper/src/main/res/drawable-hdpi/crop_image_menu_rotate_right.png differ
diff --git a/cropper/src/main/res/drawable-xhdpi/crop_image_menu_flip.png b/cropper/src/main/res/drawable-xhdpi/crop_image_menu_flip.png
new file mode 100644
index 00000000..79910ffe
Binary files /dev/null and b/cropper/src/main/res/drawable-xhdpi/crop_image_menu_flip.png differ
diff --git a/cropper/src/main/res/drawable-xhdpi/crop_image_menu_rotate_left.png b/cropper/src/main/res/drawable-xhdpi/crop_image_menu_rotate_left.png
new file mode 100644
index 00000000..bdfcbca8
Binary files /dev/null and b/cropper/src/main/res/drawable-xhdpi/crop_image_menu_rotate_left.png differ
diff --git a/cropper/src/main/res/drawable-xhdpi/crop_image_menu_rotate_right.png b/cropper/src/main/res/drawable-xhdpi/crop_image_menu_rotate_right.png
new file mode 100644
index 00000000..6d730125
Binary files /dev/null and b/cropper/src/main/res/drawable-xhdpi/crop_image_menu_rotate_right.png differ
diff --git a/cropper/src/main/res/drawable-xxhdpi/crop_image_menu_flip.png b/cropper/src/main/res/drawable-xxhdpi/crop_image_menu_flip.png
new file mode 100644
index 00000000..3629e38d
Binary files /dev/null and b/cropper/src/main/res/drawable-xxhdpi/crop_image_menu_flip.png differ
diff --git a/cropper/src/main/res/drawable-xxhdpi/crop_image_menu_rotate_left.png b/cropper/src/main/res/drawable-xxhdpi/crop_image_menu_rotate_left.png
new file mode 100644
index 00000000..5ae4f53e
Binary files /dev/null and b/cropper/src/main/res/drawable-xxhdpi/crop_image_menu_rotate_left.png differ
diff --git a/cropper/src/main/res/drawable-xxhdpi/crop_image_menu_rotate_right.png b/cropper/src/main/res/drawable-xxhdpi/crop_image_menu_rotate_right.png
new file mode 100644
index 00000000..796114cc
Binary files /dev/null and b/cropper/src/main/res/drawable-xxhdpi/crop_image_menu_rotate_right.png differ
diff --git a/cropper/src/main/res/drawable-xxxhdpi/crop_image_menu_flip.png b/cropper/src/main/res/drawable-xxxhdpi/crop_image_menu_flip.png
new file mode 100644
index 00000000..4200cb86
Binary files /dev/null and b/cropper/src/main/res/drawable-xxxhdpi/crop_image_menu_flip.png differ
diff --git a/cropper/src/main/res/drawable-xxxhdpi/crop_image_menu_rotate_left.png b/cropper/src/main/res/drawable-xxxhdpi/crop_image_menu_rotate_left.png
new file mode 100644
index 00000000..1eb68612
Binary files /dev/null and b/cropper/src/main/res/drawable-xxxhdpi/crop_image_menu_rotate_left.png differ
diff --git a/cropper/src/main/res/drawable-xxxhdpi/crop_image_menu_rotate_right.png b/cropper/src/main/res/drawable-xxxhdpi/crop_image_menu_rotate_right.png
new file mode 100644
index 00000000..33ce6709
Binary files /dev/null and b/cropper/src/main/res/drawable-xxxhdpi/crop_image_menu_rotate_right.png differ
diff --git a/cropper/src/main/res/layout/crop_image_activity.xml b/cropper/src/main/res/layout/crop_image_activity.xml
new file mode 100644
index 00000000..82e84fcf
--- /dev/null
+++ b/cropper/src/main/res/layout/crop_image_activity.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/layout/crop_image_view.xml b/cropper/src/main/res/layout/crop_image_view.xml
new file mode 100644
index 00000000..50f897b5
--- /dev/null
+++ b/cropper/src/main/res/layout/crop_image_view.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/menu/crop_image_menu.xml b/cropper/src/main/res/menu/crop_image_menu.xml
new file mode 100644
index 00000000..ff42bc69
--- /dev/null
+++ b/cropper/src/main/res/menu/crop_image_menu.xml
@@ -0,0 +1,35 @@
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-ar/strings.xml b/cropper/src/main/res/values-ar/strings.xml
new file mode 100644
index 00000000..4b570989
--- /dev/null
+++ b/cropper/src/main/res/values-ar/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ أدر عكس اتجاه عقارب الساعة
+ أدر
+ قُصّ
+ اقلب
+ اقلب أفقيًا
+ اقلب رأسيًا
+
+ اختر مصدرًا
+
+ إلغاء؛ الأذونات المطلوبة غير ممنوحة
+
+
diff --git a/cropper/src/main/res/values-cs/strings.xml b/cropper/src/main/res/values-cs/strings.xml
new file mode 100644
index 00000000..a8cef3c9
--- /dev/null
+++ b/cropper/src/main/res/values-cs/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Otočit proti směru hodinových ručiček
+ Otočit
+ Oříznout
+ Překlopit
+ Překlopit vodorovně
+ Překlopit svisle
+
+ Vybrat zdroj
+
+ Probíhá storno, požadovaná povolení nejsou udělena
+
+
diff --git a/cropper/src/main/res/values-de/strings.xml b/cropper/src/main/res/values-de/strings.xml
new file mode 100644
index 00000000..1ef0f3d7
--- /dev/null
+++ b/cropper/src/main/res/values-de/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ gegen den Uhrzeigersinn drehen
+ drehen
+ zuschneiden
+ spiegeln
+ horizontal spiegeln
+ vertikal spiegeln
+
+ Quelle wählen
+
+ Vorgang wird abgebrochen, benötigte Berechtigungen wurden nicht erteilt.
+
+
diff --git a/cropper/src/main/res/values-es-rGT/strings.xml b/cropper/src/main/res/values-es-rGT/strings.xml
new file mode 100644
index 00000000..c9b0864f
--- /dev/null
+++ b/cropper/src/main/res/values-es-rGT/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Girar a la izquierda
+ Girar a la derecha
+ Cortar
+ Dar la vuelta
+ Voltear horizontalmente
+ Voltear verticalmente
+ Seleccionar fuente
+ Cancelando, los permisos requeridos no se otorgaron
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-es/strings.xml b/cropper/src/main/res/values-es/strings.xml
new file mode 100644
index 00000000..a14c240a
--- /dev/null
+++ b/cropper/src/main/res/values-es/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Rotar a la izquierda
+ Rotar a la derecha
+ Cortar
+ Dar la vuelta
+ Voltear horizontalmente
+ Voltear verticalmente
+ Seleccionar fuente
+ Cancelando, los permisos requeridos no han sido otorgados
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-fa/strings.xml b/cropper/src/main/res/values-fa/strings.xml
new file mode 100644
index 00000000..b6745743
--- /dev/null
+++ b/cropper/src/main/res/values-fa/strings.xml
@@ -0,0 +1,11 @@
+
+
+ چرخش در جهت عقربه های ساعت
+ چرخش
+ بریدن (کراپ)
+ آیینه کردن
+ آیینه کردن به صورت افقی
+ آیینه کردن به صورت عمودی
+ منبع را انتخاب کنید
+ لغو، مجوزهای مورد نیاز ارائه نشده
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-fr/strings.xml b/cropper/src/main/res/values-fr/strings.xml
new file mode 100644
index 00000000..b0ec3bd0
--- /dev/null
+++ b/cropper/src/main/res/values-fr/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Pivoter à gauche
+ Pivoter à droite
+ Redimensionner
+ Retourner
+ Retourner horizontalement
+ Retourner verticalement
+ Sélectionner la source
+ Annulation, il manque des permissions requises
+
diff --git a/cropper/src/main/res/values-he/strings.xml b/cropper/src/main/res/values-he/strings.xml
new file mode 100644
index 00000000..8a781d36
--- /dev/null
+++ b/cropper/src/main/res/values-he/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ סובב נגד כיוון השעון
+ סובב
+ חתוך
+ הפוך
+ הפוך אופקית
+ הפוך אנכית
+
+ בחר מקור
+
+ ההרשאות הנדרשות חסרות, מבטל
+
+
diff --git a/cropper/src/main/res/values-hi/strings.xml b/cropper/src/main/res/values-hi/strings.xml
new file mode 100644
index 00000000..8549a125
--- /dev/null
+++ b/cropper/src/main/res/values-hi/strings.xml
@@ -0,0 +1,11 @@
+
+
+ घड़ी की सुई के विपरीत दिशा में घुमाइए
+ घुमाएँ
+ फ़सल
+ फ्लिप
+ क्षैतिज फ्लिप
+ लंबवत फ्लिप करें
+ सोर्स चुनें
+ रद्द करना, आवश्यक अनुमतियां नहीं दी गई हैं
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-id/strings.xml b/cropper/src/main/res/values-id/strings.xml
new file mode 100644
index 00000000..5d0570ea
--- /dev/null
+++ b/cropper/src/main/res/values-id/strings.xml
@@ -0,0 +1,12 @@
+
+
+ Putar berlawanan arah jarum jam
+ Putar
+ Potong
+ Balik
+ Balik secara horizontal
+ Balik secara vertikal
+ Pilih sumber
+ Membatalkan, tidak mendapatkan izin yang diperlukan
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-in/strings.xml b/cropper/src/main/res/values-in/strings.xml
new file mode 100644
index 00000000..5d0570ea
--- /dev/null
+++ b/cropper/src/main/res/values-in/strings.xml
@@ -0,0 +1,12 @@
+
+
+ Putar berlawanan arah jarum jam
+ Putar
+ Potong
+ Balik
+ Balik secara horizontal
+ Balik secara vertikal
+ Pilih sumber
+ Membatalkan, tidak mendapatkan izin yang diperlukan
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-it/strings.xml b/cropper/src/main/res/values-it/strings.xml
new file mode 100644
index 00000000..fa266660
--- /dev/null
+++ b/cropper/src/main/res/values-it/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Ruota in senso antiorario
+ Ruota
+ Ritaglia
+ Capovolgi
+ Capovolgi orizzontalmente
+ Capovolgi verticalmente
+
+ Seleziona origine
+
+ Annullamento in corso, autorizzazione richieste non concesse
+
+
diff --git a/cropper/src/main/res/values-ja/strings.xml b/cropper/src/main/res/values-ja/strings.xml
new file mode 100644
index 00000000..4ab0ca56
--- /dev/null
+++ b/cropper/src/main/res/values-ja/strings.xml
@@ -0,0 +1,11 @@
+
+
+ 左回転
+ 右回転
+ 切り取り
+ 反転
+ 左右反転
+ 上下反転
+ 画像を選択
+ 必要な権限がありません、キャンセルしています。
+
diff --git a/cropper/src/main/res/values-ko/strings.xml b/cropper/src/main/res/values-ko/strings.xml
new file mode 100644
index 00000000..a33967bd
--- /dev/null
+++ b/cropper/src/main/res/values-ko/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ 반시계 회전
+ 회전
+ 자르기
+ 반전
+ 좌우반전
+ 상하반전
+
+ 이미지 선택
+
+ 필수 권한이 없어서 취소합니다.
+
+
diff --git a/cropper/src/main/res/values-ms/strings.xml b/cropper/src/main/res/values-ms/strings.xml
new file mode 100644
index 00000000..000ac2eb
--- /dev/null
+++ b/cropper/src/main/res/values-ms/strings.xml
@@ -0,0 +1,12 @@
+
+
+ Putar arah berlawanan jam
+ Putar
+ Potong
+ Flip
+ Flip melintang
+ Flip menegak
+ Pilih sumber
+ Membatal, tidak mendapat kebenaran yang diperlukan
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-nb/strings.xml b/cropper/src/main/res/values-nb/strings.xml
new file mode 100644
index 00000000..c177d252
--- /dev/null
+++ b/cropper/src/main/res/values-nb/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Roter teller med urviseren
+ Roter
+ Beskjær
+ Vend
+ Vend vannrett
+ Vend loddrett
+
+ Velg kilde
+
+ Avbryter, nødvendige tillatelser er ikke gitt
+
+
diff --git a/cropper/src/main/res/values-nl/strings.xml b/cropper/src/main/res/values-nl/strings.xml
new file mode 100644
index 00000000..6b25e03b
--- /dev/null
+++ b/cropper/src/main/res/values-nl/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Tegen de klok in draaien
+ Draaien
+ Bijsnijden
+ Spiegelen
+ Horizontaal spiegelen
+ Verticaal spiegelen
+
+ Bron selecteren
+
+ Wordt geannuleerd, vereiste machtigingen zijn niet toegekend
+
+
diff --git a/cropper/src/main/res/values-pl/strings.xml b/cropper/src/main/res/values-pl/strings.xml
new file mode 100644
index 00000000..9db22a14
--- /dev/null
+++ b/cropper/src/main/res/values-pl/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Obróć w lewo
+ Obróć
+ Przytnij
+ Odbij
+ Odbij poziomo
+ Odbij pionowo
+
+ Wybierz źródło
+
+ Przerywaniem, potrzebne uprawnienia nie zostały nadane
+
+
diff --git a/cropper/src/main/res/values-pt-rBR/strings.xml b/cropper/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 00000000..e60b8bfb
--- /dev/null
+++ b/cropper/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ Girar para a esquerda
+ Girar para a direita
+ Cortar
+ Espelhar
+ Espelhar na horizontal
+ Espelhar na vertifcal
+
+ Escolher foto a partir de
+
+
diff --git a/cropper/src/main/res/values-ru-rRU/strings.xml b/cropper/src/main/res/values-ru-rRU/strings.xml
new file mode 100644
index 00000000..cd8e62d9
--- /dev/null
+++ b/cropper/src/main/res/values-ru-rRU/strings.xml
@@ -0,0 +1,10 @@
+
+
+ Повернуть налево
+ Повернуть направо
+ Обрезать
+ Отразить
+ Отразить по горизонтали
+ Отразить по вертикали
+ Выбрать источник
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-sv/strings.xml b/cropper/src/main/res/values-sv/strings.xml
new file mode 100644
index 00000000..ce8beb96
--- /dev/null
+++ b/cropper/src/main/res/values-sv/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Rotera vänster
+ Rotera höger
+ Beskär
+ Vänd
+ Vänd horisontellt
+ Vänd vertikalt
+
+ Välj bild
+
+ Avbryter, nödvändiga behörigheter beviljas inte
+
+
diff --git a/cropper/src/main/res/values-tr/strings.xml b/cropper/src/main/res/values-tr/strings.xml
new file mode 100644
index 00000000..ba0f6f4e
--- /dev/null
+++ b/cropper/src/main/res/values-tr/strings.xml
@@ -0,0 +1,12 @@
+
+
+ Saat yönünde döndür
+ döndürmek
+ ekin
+ fiske
+ Yatay olarak çevir
+ Dikey olarak çevir
+ Kaynağı seçin
+ İptal ediliyor, gerekli izinler verilmiyor
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-ur/strings.xml b/cropper/src/main/res/values-ur/strings.xml
new file mode 100644
index 00000000..b62a923f
--- /dev/null
+++ b/cropper/src/main/res/values-ur/strings.xml
@@ -0,0 +1,12 @@
+
+
+ گھڑی وار گھڑی گھومیں
+ گھمائیں
+ فصل
+ پلٹائیں
+ افقی پلٹائیں
+ عمودی طور پر پلٹائیں
+ ذریعہ منتخب کریں
+ منسوخ کرنا، ضروری اجازت نہیں دی جاتی ہیں
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-vi/strings.xml b/cropper/src/main/res/values-vi/strings.xml
new file mode 100644
index 00000000..d6301f41
--- /dev/null
+++ b/cropper/src/main/res/values-vi/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Xoay theo chiều kim đồng hồ
+ Xoay
+ Cắt
+ Lật
+ Lật theo chiều ngang
+ Lật theo chiều dọc
+
+ Chọn nguồn
+
+ Đang hủy, các quyền đã yêu cầu không được cấp
+
+
diff --git a/cropper/src/main/res/values-zh-rCN/strings.xml b/cropper/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 00000000..7ceb799a
--- /dev/null
+++ b/cropper/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ 逆时针旋转
+ 旋转
+ 裁切
+ 翻转
+ 水平翻转
+ 垂直翻转
+
+ 选择来源
+
+ 取消中,未授予所需权限
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-zh-rTW/strings.xml b/cropper/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 00000000..269b9653
--- /dev/null
+++ b/cropper/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ 逆時針旋轉
+ 旋轉
+ 裁切
+ 翻轉
+ 水平翻轉
+ 垂直翻轉
+
+ 選擇來源
+
+ 取消中,未授予所需權限
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values-zh/strings.xml b/cropper/src/main/res/values-zh/strings.xml
new file mode 100644
index 00000000..b197f48f
--- /dev/null
+++ b/cropper/src/main/res/values-zh/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ 逆时针旋转
+ 旋转
+ 裁剪
+ 翻转
+ 水平翻转
+ 垂直翻转
+
+ 选择来源
+
+ 正在取消,该操作未获得所需权限。
+
+
diff --git a/cropper/src/main/res/values/attrs.xml b/cropper/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..756a2c1b
--- /dev/null
+++ b/cropper/src/main/res/values/attrs.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cropper/src/main/res/values/strings.xml b/cropper/src/main/res/values/strings.xml
new file mode 100644
index 00000000..6dc1fd22
--- /dev/null
+++ b/cropper/src/main/res/values/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Rotate counter clockwise
+ Rotate
+ Crop
+ Flip
+ Flip horizontally
+ Flip vertically
+
+ Select source
+
+ Cancelling, required permissions are not granted
+
+
diff --git a/settings.gradle b/settings.gradle
index eea69bc0..48eb5e95 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -3,3 +3,4 @@ include ':app'
include ':autoimageslider'
include ':mytransl'
include ':ratethisapp'
+include ':cropper'