Start adding media edition

pull/254/head
Thomas 2 years ago
parent ed76c97e8b
commit 07ba1d2219

1
.gitignore vendored

@ -8,3 +8,4 @@
.externalNativeBuild
.cxx
local.properties
/cropper/build/

@ -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'

@ -57,6 +57,9 @@
<activity
android:name=".activities.DraftActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />
<activity
android:name=".imageeditor.EditImageActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />
<activity
android:name=".activities.ComposeActivity"
android:configChanges="keyboardHidden|orientation|screenSize"

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

@ -250,7 +250,7 @@ public class Helper {
public static final int NOTIFICATION_INTENT = 1;
public static final int OPEN_NOTIFICATION = 2;
public static final String INTENT_TARGETED_ACCOUNT = "INTENT_TARGETED_ACCOUNT";
public static final String INTENT_SEND_MODIFIED_IMAGE = "INTENT_SEND_MODIFIED_IMAGE";
public static final String TEMP_MEDIA_DIRECTORY = "TEMP_MEDIA_DIRECTORY";

@ -0,0 +1,124 @@
package app.fedilab.android.imageeditor;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import app.fedilab.android.R;
/**
* Created by Ahmed Adel on 5/8/17.
*/
public class ColorPickerAdapter extends RecyclerView.Adapter<ColorPickerAdapter.ViewHolder> {
private final List<Integer> colorPickerColors;
private Context context;
private LayoutInflater inflater;
private OnColorPickerClickListener onColorPickerClickListener;
ColorPickerAdapter(@NonNull Context context, @NonNull List<Integer> 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<Integer> getDefaultColors(Context context) {
ArrayList<Integer> 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()));
}
});
}
}
}

@ -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();
}
}
}

@ -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<String> getEmojis(Context context) {
ArrayList<String> 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<EmojiAdapter.ViewHolder> {
ArrayList<String> 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();
}
});
}
}
}
}

@ -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.
* </br>
* 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)}
* </br>
* 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<FileMeta> fileCreatedResult;
private OnFileCreateResult resultListener;
private final Observer<FileMeta> 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;
}
}
}

@ -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();
}
}

@ -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);
}
}

@ -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);
}
}

@ -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<StickerAdapter.ViewHolder> {
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();
}
});
}
}
}
}

@ -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);
}
}

@ -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();
}
}
}

@ -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 <a href="https://github.com/burhanrashid52">Burhanuddin Rashid</a>
* @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);
}
}

@ -0,0 +1,7 @@
package app.fedilab.android.imageeditor.filters;
import ja.burhanrashid52.photoeditor.PhotoFilter;
public interface FilterListener {
void onFilterSelected(PhotoFilter photoFilter);
}

@ -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 <a href="https://github.com/burhanrashid52">Burhanuddin Rashid</a>
* @version 0.1.2
* @since 5/23/2018
*/
public class FilterViewAdapter extends RecyclerView.Adapter<FilterViewAdapter.ViewHolder> {
private final FilterListener mFilterListener;
private final List<Pair<String, PhotoFilter>> 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<String, PhotoFilter> 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);
}
});
}
}
}

@ -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 <a href="https://github.com/burhanrashid52">Burhanuddin Rashid</a>
* @version 0.1.2
* @since 5/23/2018
*/
public class EditingToolsAdapter extends RecyclerView.Adapter<EditingToolsAdapter.ViewHolder> {
private final List<ToolModel> 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));
}
}
}

@ -0,0 +1,16 @@
package app.fedilab.android.imageeditor.tools;
/**
* @author <a href="https://github.com/burhanrashid52">Burhanuddin Rashid</a>
* @version 0.1.2
* @since 5/23/2018
*/
public enum ToolType {
BRUSH,
SHAPE,
TEXT,
ERASER,
FILTER,
EMOJI,
STICKER
}

@ -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<RecyclerView.ViewHolder
composeAttachmentItemBinding.buttonOrderDown.setVisibility(View.INVISIBLE);
}
int finalMediaPosition = mediaPosition;
composeAttachmentItemBinding.editPreview.setOnClickListener(v -> {
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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M7,14c-1.66,0 -3,1.34 -3,3 0,1.31 -1.16,2 -2,2 0.92,1.22 2.49,2 4,2 2.21,0 4,-1.79 4,-4 0,-1.66 -1.34,-3 -3,-3zM20.71,4.63l-1.34,-1.34c-0.39,-0.39 -1.02,-0.39 -1.41,0L9,12.25 11.75,15l8.96,-8.96c0.39,-0.39 0.39,-1.02 0,-1.41z" />
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0" />
<path
android:fillColor="#FFFFFF"
android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,15h2V7c0,-1.1 -0.9,-2 -2,-2H9v2h8v8zM7,17V1H5v4H1v2h4v10c0,1.1 0.9,2 2,2h10v4h2v-4h4v-2H7z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="511.999"
android:viewportHeight="511.999">
<path
android:fillColor="#FFFFFF"
android:pathData="M498.8,140.6L371.6,13.3c-17.5,-17.5 -46.1,-17.5 -63.6,-0L22,299c-29.3,29.3 -29.3,76.7 -0,106.1l84.8,84.8c29.2,29.2 76.8,29.2 106,0c0,0 281.7,-281.4 286,-285.7C516.4,186.6 516.4,158.1 498.8,140.6zM191.6,468.7c-17.5,17.5 -46.1,17.5 -63.6,-0l-84.8,-84.8c-17.6,-17.6 -17.6,-46.1 0,-63.6l64,-63.9l148.5,148.5L191.6,468.7zM276.8,383.6L128.4,235.1L276.1,87.5l148.5,148.5L276.8,383.6zM477.6,183l-31.8,31.8L297.3,66.3l31.8,-31.8c5.8,-5.8 15.4,-5.8 21.2,0l127.3,127.3C483.5,167.6 483.5,177.1 477.6,183z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M22,16L22,4c0,-1.1 -0.9,-2 -2,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zM11,12l2.03,2.71L16,11l4,5L8,16l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM15.5,11c0.83,0 1.5,-0.67 1.5,-1.5S16.33,8 15.5,8 14,8.67 14,9.5s0.67,1.5 1.5,1.5zM8.5,11c0.83,0 1.5,-0.67 1.5,-1.5S9.33,8 8.5,8 7,8.67 7,9.5 7.67,11 8.5,11zM12,17.5c2.33,0 4.31,-1.46 5.11,-3.5L6.89,14c0.8,2.04 2.78,3.5 5.11,3.5z" />
</vector>

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19.02,10v9L5,19L5,5h9L14,3L5.02,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2v-9h-2zM17,10l0.94,-2.06L20,7l-2.06,-0.94L17,4l-0.94,2.06L14,7l2.06,0.94zM13.25,10.75L12,8l-1.25,2.75L8,12l2.75,1.25L12,16l1.25,-2.75L16,12z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.99h18v14.02z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M18.4,10.6C16.55,8.99 14.15,8 11.5,8c-4.65,0 -8.58,3.03 -9.96,7.22L3.9,16c1.05,-3.19 4.05,-5.5 7.6,-5.5 1.95,0 3.73,0.72 5.12,1.88L13,16h9V7l-3.6,3.6z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M2.5,4v3h5v12h3L10.5,7h5L15.5,4h-13zM21.5,9h-9v3h3v7h3v-7h3L21.5,9z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12.5,8c-2.65,0 -5.05,0.99 -6.9,2.6L2,7v9h9l-3.62,-3.62c1.39,-1.16 3.16,-1.88 5.12,-1.88 3.54,0 6.55,2.31 7.6,5.5l2.37,-0.78C21.08,11.03 17.15,8 12.5,8z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="2dp"
android:color="#FFFFFF" />
<padding
android:bottom="2dp"
android:left="2dp"
android:right="2dp"
android:top="2dp" />
<corners android:radius="50dp" />
</shape>

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rootView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_end="?attr/actionBarSize" />
<ja.burhanrashid52.photoeditor.PhotoEditorView
android:id="@+id/photoEditorView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/rvConstraintTools"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:photo_src="@drawable/blank_image" />
<ImageView
android:id="@+id/imgUndo"
android:layout_width="@dimen/top_tool_icon_width"
android:layout_height="wrap_content"
android:background="@color/semi_black_transparent"
android:padding="8dp"
android:src="@drawable/ic_undo"
app:layout_constraintBottom_toTopOf="@+id/rvConstraintTools"
app:layout_constraintEnd_toStartOf="@+id/imgRedo" />
<ImageView
android:id="@+id/imgRedo"
android:layout_width="@dimen/top_tool_icon_width"
android:layout_height="wrap_content"
android:background="@color/semi_black_transparent"
android:padding="8dp"
android:src="@drawable/ic_redo"
app:layout_constraintBottom_toTopOf="@+id/rvConstraintTools"
app:layout_constraintEnd_toEndOf="parent" />
<ImageView
android:id="@+id/imgGallery"
android:layout_width="@dimen/top_tool_icon_width"
android:layout_height="wrap_content"
android:background="@color/semi_black_transparent"
android:padding="8dp"
android:src="@drawable/ic_gallery"
app:layout_constraintBottom_toTopOf="@+id/rvConstraintTools"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:id="@+id/imgCamera"
android:layout_width="@dimen/top_tool_icon_width"
android:layout_height="wrap_content"
android:background="@color/semi_black_transparent"
android:padding="8dp"
android:src="@drawable/ic_camera"
app:layout_constraintBottom_toTopOf="@+id/rvConstraintTools"
app:layout_constraintStart_toEndOf="@id/imgGallery" />
<ImageView
android:id="@+id/imgClose"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_margin="8dp"
android:src="@drawable/ic_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/guideline" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvConstraintTools"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/tool_bg"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="4dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:listitem="@layout/row_editing_tools" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFilterView"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/tool_bg"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="@+id/rvConstraintTools"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/rvConstraintTools"
tools:listitem="@layout/row_filter_view" />
<TextView
android:id="@+id/txtCurrentTool"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:singleLine="true"
android:text="@string/app_name"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/guideline" />
<ImageView
android:id="@+id/imgSave"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:src="@drawable/ic_save"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/guideline" />
<ImageView
android:id="@+id/imgCrop"
android:layout_width="@dimen/top_tool_icon_width"
android:layout_height="wrap_content"
android:background="@color/semi_black_transparent"
android:padding="8dp"
android:src="@drawable/ic_crop"
app:layout_constraintBottom_toBottomOf="@+id/photoEditorView"
app:layout_constraintEnd_toStartOf="@+id/imgUndo"
app:layout_constraintStart_toEndOf="@+id/imgCamera" />
<Button
android:id="@+id/send"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:layout_gravity="end"
android:layout_margin="8dp"
android:gravity="center"
android:padding="0dp"
android:text="@string/validate"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/imgSave"
app:layout_constraintTop_toBottomOf="@+id/guideline" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2022 Thomas Schneider
This file is a part of Fedilab
This program is free software; you can redistribute it and/or modify it under the terms of the
GNU General Public License as published by the Free Software Foundation; either version 3 of the
License, or (at your option) any later version.
Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
Public License for more details.
You should have received a copy of the GNU General Public License along with Fedilab; if not,
see <http://www.gnu.org/licenses>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rootView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_end="?attr/actionBarSize" />
<ja.burhanrashid52.photoeditor.PhotoEditorView
android:id="@+id/photoEditorView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/rvConstraintTools"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imgUndo"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:contentDescription="@string/undo"
android:padding="8dp"
android:src="@drawable/ic_undo"
app:layout_constraintBottom_toTopOf="@+id/rvConstraintTools"
app:layout_constraintEnd_toStartOf="@+id/imgRedo" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imgRedo"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:contentDescription="@string/redo"
android:padding="8dp"
android:src="@drawable/ic_redo"
app:layout_constraintBottom_toTopOf="@+id/rvConstraintTools"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imgGallery"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:contentDescription="@string/gallery"
android:padding="8dp"
android:src="@drawable/ic_gallery"
app:layout_constraintBottom_toTopOf="@+id/rvConstraintTools"
app:layout_constraintStart_toStartOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imgCamera"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:contentDescription="@string/camera"
android:padding="8dp"
android:src="@drawable/ic_camera"
app:layout_constraintBottom_toTopOf="@+id/rvConstraintTools"
app:layout_constraintStart_toEndOf="@id/imgGallery" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imgClose"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_margin="8dp"
android:contentDescription="@string/close"
android:src="@drawable/ic_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/guideline" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvConstraintTools"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="4dp"
android:paddingBottom="4dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:listitem="@layout/row_editing_tools" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFilterView"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="@+id/rvConstraintTools"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/rvConstraintTools"
tools:listitem="@layout/row_filter_view" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txtCurrentTool"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:singleLine="true"
android:text="@string/app_name"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/guideline" />
<Button
android:id="@+id/send"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:layout_gravity="end"
android:layout_margin="8dp"
android:gravity="center"
android:padding="0dp"
android:text="@string/validate"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/imgSave"
app:layout_constraintTop_toBottomOf="@+id/guideline" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imgSave"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:contentDescription="@string/save"
android:src="@drawable/ic_save_gallery"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/guideline" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#B3000000"
android:orientation="vertical">
<TextView
android:id="@+id/add_text_done_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_margin="20dp"
android:background="@drawable/rounded_border_text_view"
android:padding="10dp"
android:text="Done"
android:textAllCaps="false"
android:textColor="@color/white"
android:textSize="15sp" />
<EditText
android:id="@+id/add_text_edit_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/add_text_color_picker_relative_layout"
android:layout_below="@+id/add_text_done_tv"
android:background="@null"
android:gravity="center"
android:inputType="textMultiLine"
android:textSize="40sp" />
<RelativeLayout
android:id="@+id/add_text_color_picker_relative_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/add_text_color_picker_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:background="@android:color/black"
tools:listitem="@layout/color_picker_item_list" />
</RelativeLayout>
</RelativeLayout>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<View
android:id="@+id/color_picker_view"
android:layout_width="40dp"
android:layout_height="50dp"
android:layout_alignParentTop="true"
android:background="@android:color/white"
tools:text="" />
</RelativeLayout>

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<SeekBar
android:id="@+id/sbRotate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:max="100"
android:progress="0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/txtRotation" />
<TextView
android:id="@+id/txtRotation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="Rotation"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvColors"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginStart="8dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sbOpacity"
tools:background="@android:color/black"
tools:listitem="@layout/color_picker_item_list" />
<SeekBar
android:id="@+id/sbOpacity"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:progress="100"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/txtOpacity" />
<TextView
android:id="@+id/txtOpacity"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="8dp"
android:text="Opacity"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sbSize" />
<SeekBar
android:id="@+id/sbSize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:progress="25"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/txtBrushSize" />
<TextView
android:id="@+id/txtBrushSize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="Brush"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioGroup
android:id="@+id/shapeRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/shapeType">
<RadioButton
android:id="@+id/brushRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/label_brush" />
<RadioButton
android:id="@+id/lineRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_line" />
<RadioButton
android:id="@+id/ovalRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_oval" />
<RadioButton
android:id="@+id/rectRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_rectangle" />
</RadioGroup>
<SeekBar
android:id="@+id/shapeSize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:progress="25"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/txtShapeSize" />
<SeekBar
android:id="@+id/shapeOpacity"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:max="255"
android:progress="255"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/txtOpacity" />
<TextView
android:id="@+id/txtShapeSize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="Brush"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/shapeRadioGroup" />
<TextView
android:id="@+id/txtOpacity"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="8dp"
android:text="Opacity"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/txtShapeSize" />
<TextView
android:id="@+id/shapeType"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="Shape"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/shapeColors"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginStart="8dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/shapeOpacity"
tools:background="@android:color/black"
tools:listitem="@layout/color_picker_item_list" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp">
<TextView
android:id="@+id/txtClose"
android:layout_width="wrap_content"
android:layout_height="?attr/actionBarSize"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:gravity="center"
android:text="Close"
android:textAppearance="@style/TextAppearance.AppCompat.Large.Inverse"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/lineView"
android:layout_width="match_parent"
android:layout_height="2px"
android:background="@android:color/white"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/txtClose" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvEmoji"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_behavior="@string/bottom_sheet_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lineView" />
<TextView
android:id="@+id/txtDone"
android:layout_width="wrap_content"
android:layout_height="?attr/actionBarSize"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:gravity="center"
android:text="Done"
android:textAppearance="@style/TextAppearance.AppCompat.Large.Inverse"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:padding="4dp"
tools:background="@android:color/black">
<ImageView
android:id="@+id/imgToolIcon"
android:layout_width="@dimen/editor_size"
android:layout_height="@dimen/editor_size"
android:layout_margin="4dp"
android:layout_marginBottom="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_brush" />
<TextView
android:id="@+id/txtTool"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imgToolIcon"
tools:text="@string/label_brush" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/txtEmoji"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:padding="2dp"
android:textColor="#FFFFFF"
android:textSize="35sp"
tools:android="Burhanuddin" />
</LinearLayout>

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_margin="2dp">
<ImageView
android:id="@+id/imgFilterView"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/txtFilterName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#90000000"
android:gravity="center"
android:padding="4dp"
android:textColor="@android:color/white"
android:textSize="8dp"
app:layout_constraintBottom_toBottomOf="@+id/imgFilterView"
app:layout_constraintEnd_toEndOf="@+id/imgFilterView"
app:layout_constraintStart_toStartOf="@+id/imgFilterView"
tools:text="@tools:sample/full_names" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp">
<ImageView
android:id="@+id/imgSticker"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -96,7 +96,8 @@
<color name="violet_color_picker">#8359A3</color>
<color name="yellow_color_picker">#FBE870</color>
<color name="yellow_green_color_picker">#C5E17A</color>
<color name="tool_bg">#151414</color>
<color name="semi_black_transparent">#99000000</color>
<color name="custom_features_button_black">#144365</color>
@ -159,4 +160,5 @@
<color name="decoration_14">#7986CB</color> <!-- Indigo 300 -->
<color name="decoration_15">#9E9E9E</color> <!-- Gray 500 -->
</resources>

@ -15,5 +15,10 @@
<dimen name="settings_option_margin">10dp</dimen>
<dimen name="activity_vertical_margin_login">50dp</dimen>
<dimen name="badge_count_textsize">12sp</dimen>
<dimen name="editor_size">35dp</dimen>
<dimen name="normal_margin">8dp</dimen>
<dimen name="large_margin">16dp</dimen>
<dimen name="top_tool_icon_width">50dp</dimen>
</resources>

@ -1616,4 +1616,416 @@
<string name="fetch_more_messages">Fetch more messages…</string>
<string name="label_shape">Shape</string>
<string name="label_oval">Oval</string>
<string name="label_rectangle">Rectangle</string>
<string name="label_line">Line</string>
<string name="label_eraser_mode">Eraser Mode</string>
<string name="label_adjust">Adjust</string>
<string name="msg_save_image">Are you want to exit without saving image ?</string>
<string name="msg_share_image">Share Image</string>
<string-array name="photo_editor_emoji" translatable="false">
<!-- Smiles -->
<item>u+1f604</item>
<item>u+1f603</item>
<item>u+1f600</item>
<item>u+1f60a</item>
<item>u+263a</item>
<item>u+1f609</item>
<item>u+1f60d</item>
<item>u+1f618</item>
<item>u+1f61a</item>
<item>u+1f617</item>
<item>u+1f619</item>
<item>u+1f61c</item>
<item>u+1f61d</item>
<item>u+1f61b</item>
<item>u+1f633</item>
<item>u+1f601</item>
<item>u+1f614</item>
<item>u+1f60c</item>
<item>u+1f612</item>
<item>u+1f61e</item>
<item>u+1f623</item>
<item>u+1f622</item>
<item>u+1f602</item>
<item>u+1f62d</item>
<item>u+1f62a</item>
<item>u+1f625</item>
<item>u+1f630</item>
<item>u+1f605</item>
<item>u+1f613</item>
<item>u+1f629</item>
<item>u+1f62b</item>
<item>u+1f628</item>
<item>u+1f631</item>
<item>u+1f620</item>
<item>u+1f621</item>
<item>u+1f624</item>
<!-- People -->
<item>u+1f616</item>
<item>u+1f606</item>
<item>u+1f60b</item>
<item>u+1f637</item>
<item>u+1f60e</item>
<item>u+1f634</item>
<item>u+1f635</item>
<item>u+1f632</item>
<item>u+1f61f</item>
<item>u+1f626</item>
<item>u+1f627</item>
<item>u+1f608</item>
<item>u+1f47f</item>
<item>u+1f62e</item>
<item>u+1f62c</item>
<item>u+1f610</item>
<item>u+1f615</item>
<item>u+1f62f</item>
<item>u+1f636</item>
<item>u+1f607</item>
<item>u+1f60f</item>
<item>u+1f611</item>
<item>u+1f472</item>
<item>u+1f473</item>
<item>u+1f46e</item>
<item>u+1f477</item>
<item>u+1f482</item>
<item>u+1f476</item>
<item>u+1f466</item>
<item>u+1f467</item>
<item>u+1f468</item>
<item>u+1f469</item>
<item>u+1f474</item>
<item>u+1f475</item>
<item>u+1f471</item>
<item>u+1f47c</item>
<item>u+1f478</item>
<item>u+1f63a</item>
<item>u+1f638</item>
<item>u+1f63b</item>
<item>u+1f63d</item>
<item>u+1f63c</item>
<item>u+1f640</item>
<item>u+1f63f</item>
<item>u+1f639</item>
<item>u+1f63e</item>
<item>u+1f479</item>
<item>u+1f47a</item>
<item>u+1f648</item>
<item>u+1f649</item>
<item>u+1f64a</item>
<item>u+1f480</item>
<item>u+1f47d</item>
<item>u+1f4a9</item>
<item>u+1f525</item>
<item>u+2728</item>
<item>u+1f31f</item>
<item>u+1f4ab</item>
<item>u+1f4a5</item>
<item>u+1f4a2</item>
<item>u+1f4a6</item>
<item>u+1f4a7</item>
<item>u+1f4a4</item>
<item>u+1f4a8</item>
<item>u+1f442</item>
<item>u+1f440</item>
<item>u+1f443</item>
<item>u+1f445</item>
<item>u+1f444</item>
<item>u+1f44d</item>
<item>u+1f44e</item>
<item>u+1f44c</item>
<item>u+1f44a</item>
<item>u+270a</item>
<item>u+270c</item>
<item>u+1f44b</item>
<item>u+270b</item>
<item>u+1f450</item>
<item>u+1f446</item>
<item>u+1f447</item>
<item>u+1f449</item>
<item>u+1f448</item>
<item>u+1f64c</item>
<item>u+1f64f</item>
<item>u+261d</item>
<item>u+1f44f</item>
<item>u+1f4aa</item>
<item>u+1f6b6</item>
<item>u+1f3c3</item>
<item>u+1f483</item>
<item>u+1f46b</item>
<item>u+1f46a</item>
<item>u+1f46c</item>
<item>u+1f46d</item>
<item>u+1f48f</item>
<item>u+1f491</item>
<item>u+1f46f</item>
<item>u+1f646</item>
<item>u+1f645</item>
<item>u+1f481</item>
<item>u+1f64b</item>
<item>u+1f486</item>
<item>u+1f487</item>
<item>u+1f485</item>
<item>u+1f470</item>
<item>u+1f64e</item>
<item>u+1f64d</item>
<item>u+1f647</item>
<item>u+1f3a9</item>
<item>u+1f451</item>
<item>u+1f452</item>
<item>u+1f45f</item>
<item>u+1f45e</item>
<item>u+1f461</item>
<item>u+1f460</item>
<item>u+1f462</item>
<item>u+1f455</item>
<item>u+1f454</item>
<item>u+1f45a</item>
<item>u+1f457</item>
<item>u+1f3bd</item>
<item>u+1f456</item>
<item>u+1f458</item>
<item>u+1f459</item>
<item>u+1f4bc</item>
<item>u+1f45c</item>
<item>u+1f45d</item>
<item>u+1f45b</item>
<item>u+1f453</item>
<item>u+1f380</item>
<item>u+1f302</item>
<item>u+1f484</item>
<item>u+1f49b</item>
<item>u+1f499</item>
<item>u+1f49c</item>
<item>u+1f49a</item>
<item>u+2764</item>
<item>u+1f494</item>
<item>u+1f497</item>
<item>u+1f493</item>
<item>u+1f495</item>
<item>u+1f496</item>
<item>u+1f49e</item>
<item>u+1f498</item>
<item>u+1f48c</item>
<item>u+1f48b</item>
<item>u+1f48d</item>
<item>u+1f48e</item>
<item>u+1f464</item>
<item>u+1f465</item>
<item>u+1f4ac</item>
<item>u+1f463</item>
<item>u+1f4ad</item>
<!-- Nature -->
<item>u+1f436</item>
<item>u+1f43a</item>
<item>u+1f431</item>
<item>u+1f42d</item>
<item>u+1f439</item>
<item>u+1f430</item>
<item>u+1f438</item>
<item>u+1f42f</item>
<item>u+1f428</item>
<item>u+1f43b</item>
<item>u+1f437</item>
<item>u+1f43d</item>
<item>u+1f42e</item>
<item>u+1f417</item>
<item>u+1f435</item>
<item>u+1f412</item>
<item>u+1f434</item>
<item>u+1f411</item>
<item>u+1f418</item>
<item>u+1f43c</item>
<item>u+1f427</item>
<item>u+1f426</item>
<item>u+1f424</item>
<item>u+1f425</item>
<item>u+1f423</item>
<item>u+1f414</item>
<item>u+1f40d</item>
<item>u+1f422</item>
<item>u+1f41b</item>
<item>u+1f41d</item>
<item>u+1f41c</item>
<item>u+1f41e</item>
<item>u+1f40c</item>
<item>u+1f419</item>
<item>u+1f41a</item>
<item>u+1f420</item>
<item>u+1f41f</item>
<item>u+1f42c</item>
<item>u+1f433</item>
<item>u+1f40b</item>
<item>u+1f404</item>
<item>u+1f40f</item>
<item>u+1f400</item>
<item>u+1f403</item>
<item>u+1f405</item>
<item>u+1f407</item>
<item>u+1f409</item>
<item>u+1f40e</item>
<item>u+1f410</item>
<item>u+1f413</item>
<item>u+1f415</item>
<item>u+1f416</item>
<item>u+1f401</item>
<item>u+1f402</item>
<item>u+1f432</item>
<item>u+1f421</item>
<item>u+1f40a</item>
<item>u+1f42b</item>
<item>u+1f42a</item>
<item>u+1f406</item>
<item>u+1f408</item>
<item>u+1f429</item>
<item>u+1f43e</item>
<item>u+1f490</item>
<item>u+1f338</item>
<item>u+1f337</item>
<item>u+1f340</item>
<item>u+1f339</item>
<item>u+1f33b</item>
<item>u+1f33a</item>
<item>u+1f341</item>
<item>u+1f343</item>
<item>u+1f342</item>
<item>u+1f33f</item>
<item>u+1f33e</item>
<item>u+1f344</item>
<item>u+1f335</item>
<item>u+1f334</item>
<item>u+1f332</item>
<item>u+1f333</item>
<item>u+1f330</item>
<item>u+1f331</item>
<item>u+1f33c</item>
<item>u+1f310</item>
<item>u+1f31e</item>
<item>u+1f31d</item>
<item>u+1f31a</item>
<item>u+1f311</item>
<item>u+1f312</item>
<item>u+1f313</item>
<item>u+1f314</item>
<item>u+1f315</item>
<item>u+1f316</item>
<item>u+1f317</item>
<item>u+1f318</item>
<item>u+1f31c</item>
<item>u+1f31b</item>
<item>u+1f319</item>
<item>u+1f30d</item>
<item>u+1f30e</item>
<item>u+1f30f</item>
<item>u+1f30b</item>
<item>u+1f30c</item>
<item>u+1f320</item>
<item>u+2b50</item>
<item>u+2600</item>
<item>u+26c5</item>
<item>u+2601</item>
<item>u+26a1</item>
<item>u+2614</item>
<item>u+2744</item>
<item>u+26c4</item>
<item>u+1f300</item>
<item>u+1f301</item>
<item>u+1f308</item>
<item>u+1f30a</item>
<!-- Places -->
<item>u+1f3e0</item>
<item>u+1f3e1</item>
<item>u+1f3eb</item>
<item>u+1f3e2</item>
<item>u+1f3e3</item>
<item>u+1f3e5</item>
<item>u+1f3e6</item>
<item>u+1f3ea</item>
<item>u+1f3e9</item>
<item>u+1f3e8</item>
<item>u+1f492</item>
<item>u+26ea</item>
<item>u+1f3ec</item>
<item>u+1f3e4</item>
<item>u+1f307</item>
<item>u+1f306</item>
<item>u+1f3ef</item>
<item>u+1f3f0</item>
<item>u+26fa</item>
<item>u+1f3ed</item>
<item>u+1f5fc</item>
<item>u+1f5fe</item>
<item>u+1f5fb</item>
<item>u+1f304</item>
<item>u+1f305</item>
<item>u+1f303</item>
<item>u+1f5fd</item>
<item>u+1f309</item>
<item>u+1f3a0</item>
<item>u+1f3a1</item>
<item>u+26f2</item>
<item>u+1f3a2</item>
<item>u+1f6a2</item>
<item>u+26f5</item>
<item>u+1f6a4</item>
<item>u+1f6a3</item>
<item>u+2693</item>
<item>u+1f680</item>
<item>u+2708</item>
<item>u+1f4ba</item>
<item>u+1f681</item>
<item>u+1f682</item>
<item>u+1f68a</item>
<item>u+1f689</item>
<item>u+1f69e</item>
<item>u+1f686</item>
<item>u+1f684</item>
<item>u+1f685</item>
<item>u+1f688</item>
<item>u+1f687</item>
<item>u+1f69d</item>
<item>u+1f68b</item>
<item>u+1f683</item>
<item>u+1f68e</item>
<item>u+1f68c</item>
<item>u+1f68d</item>
<item>u+1f699</item>
<item>u+1f698</item>
<item>u+1f697</item>
<item>u+1f695</item>
<item>u+1f696</item>
<item>u+1f69b</item>
<item>u+1f69a</item>
<item>u+1f6a8</item>
<item>u+1f693</item>
<item>u+1f694</item>
<item>u+1f692</item>
<item>u+1f691</item>
<item>u+1f690</item>
<item>u+1f6b2</item>
<item>u+1f6a1</item>
<item>u+1f69f</item>
<item>u+1f6a0</item>
<item>u+1f69c</item>
<item>u+1f488</item>
<item>u+1f68f</item>
<item>u+1f3ab</item>
<item>u+1f6a6</item>
<item>u+1f6a5</item>
<item>u+26a0</item>
<item>u+1f6a7</item>
<item>u+1f530</item>
<item>u+26fd</item>
<item>u+1f3ee</item>
<item>u+1f3b0</item>
<item>u+2668</item>
<item>u+1f5ff</item>
<item>u+1f3aa</item>
<item>u+1f3ad</item>
<item>u+1f4cd</item>
<item>u+1f6a9</item>
</string-array>
</resources>

@ -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"
}

@ -0,0 +1,3 @@
<manifest package="com.theartofdev.edmodo.cropper">
</manifest>

@ -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<Void, Void, BitmapCroppingWorkerTask.Result> {
// region: Fields and Consts
/**
* Use a WeakReference to ensure the ImageView can be garbage collected
*/
private final WeakReference<CropImageView> mCropImageViewReference;
/**
* the bitmap to crop
*/
private final Bitmap mBitmap;
/**
* The Android URI of the image to load
*/
private final Uri mUri;
/**
* The context of the crop image view widget used for loading of bitmap by Android URI
*/
private final Context mContext;
/**
* Required cropping 4 points (x0,y0,x1,y1,x2,y2,x3,y3)
*/
private final float[] mCropPoints;
/**
* Degrees the image was rotated after loading
*/
private final int mDegreesRotated;
/**
* the original width of the image to be cropped (for image loaded from URI)
*/
private final int mOrgWidth;
/**
* the original height of the image to be cropped (for image loaded from URI)
*/
private final int mOrgHeight;
/**
* is there is fixed aspect ratio for the crop rectangle
*/
private final boolean mFixAspectRatio;
/**
* the X aspect ration of the crop rectangle
*/
private final int mAspectRatioX;
/**
* the Y aspect ration of the crop rectangle
*/
private final int mAspectRatioY;
/**
* required width of the cropping image
*/
private final int mReqWidth;
/**
* required height of the cropping image
*/
private final int mReqHeight;
/**
* is the image flipped horizontally
*/
private final boolean mFlipHorizontally;
/**
* is the image flipped vertically
*/
private final boolean mFlipVertically;
/**
* The option to handle requested width/height
*/
private final CropImageView.RequestSizeOptions mReqSizeOptions;
/**
* the Android Uri to save the cropped image to
*/
private final Uri mSaveUri;
/**
* the compression format to use when writing the image
*/
private final Bitmap.CompressFormat mSaveCompressFormat;
/**
* the quality (if applicable) to use when writing the image (0 - 100)
*/
private final int mSaveCompressQuality;
// endregion
BitmapCroppingWorkerTask(
CropImageView cropImageView,
Bitmap bitmap,
float[] cropPoints,
int degreesRotated,
boolean fixAspectRatio,
int aspectRatioX,
int aspectRatioY,
int reqWidth,
int reqHeight,
boolean flipHorizontally,
boolean flipVertically,
CropImageView.RequestSizeOptions options,
Uri saveUri,
Bitmap.CompressFormat saveCompressFormat,
int saveCompressQuality) {
mCropImageViewReference = new WeakReference<>(cropImageView);
mContext = cropImageView.getContext();
mBitmap = bitmap;
mCropPoints = cropPoints;
mUri = null;
mDegreesRotated = degreesRotated;
mFixAspectRatio = fixAspectRatio;
mAspectRatioX = aspectRatioX;
mAspectRatioY = aspectRatioY;
mReqWidth = reqWidth;
mReqHeight = reqHeight;
mFlipHorizontally = flipHorizontally;
mFlipVertically = flipVertically;
mReqSizeOptions = options;
mSaveUri = saveUri;
mSaveCompressFormat = saveCompressFormat;
mSaveCompressQuality = saveCompressQuality;
mOrgWidth = 0;
mOrgHeight = 0;
}
BitmapCroppingWorkerTask(
CropImageView cropImageView,
Uri uri,
float[] cropPoints,
int degreesRotated,
int orgWidth,
int orgHeight,
boolean fixAspectRatio,
int aspectRatioX,
int aspectRatioY,
int reqWidth,
int reqHeight,
boolean flipHorizontally,
boolean flipVertically,
CropImageView.RequestSizeOptions options,
Uri saveUri,
Bitmap.CompressFormat saveCompressFormat,
int saveCompressQuality) {
mCropImageViewReference = new WeakReference<>(cropImageView);
mContext = cropImageView.getContext();
mUri = uri;
mCropPoints = cropPoints;
mDegreesRotated = degreesRotated;
mFixAspectRatio = fixAspectRatio;
mAspectRatioX = aspectRatioX;
mAspectRatioY = aspectRatioY;
mOrgWidth = orgWidth;
mOrgHeight = orgHeight;
mReqWidth = reqWidth;
mReqHeight = reqHeight;
mFlipHorizontally = flipHorizontally;
mFlipVertically = flipVertically;
mReqSizeOptions = options;
mSaveUri = saveUri;
mSaveCompressFormat = saveCompressFormat;
mSaveCompressQuality = saveCompressQuality;
mBitmap = null;
}
/**
* The Android URI that this task is currently loading.
*/
public Uri getUri() {
return mUri;
}
/**
* Crop image in background.
*
* @param params ignored
* @return the decoded bitmap data
*/
@Override
protected BitmapCroppingWorkerTask.Result doInBackground(Void... params) {
try {
if (!isCancelled()) {
BitmapUtils.BitmapSampled bitmapSampled;
if (mUri != null) {
bitmapSampled =
BitmapUtils.cropBitmap(
mContext,
mUri,
mCropPoints,
mDegreesRotated,
mOrgWidth,
mOrgHeight,
mFixAspectRatio,
mAspectRatioX,
mAspectRatioY,
mReqWidth,
mReqHeight,
mFlipHorizontally,
mFlipVertically);
} else if (mBitmap != null) {
bitmapSampled =
BitmapUtils.cropBitmapObjectHandleOOM(
mBitmap,
mCropPoints,
mDegreesRotated,
mFixAspectRatio,
mAspectRatioX,
mAspectRatioY,
mFlipHorizontally,
mFlipVertically);
} else {
return new Result((Bitmap) null, 1);
}
Bitmap bitmap =
BitmapUtils.resizeBitmap(bitmapSampled.bitmap, mReqWidth, mReqHeight, mReqSizeOptions);
if (mSaveUri == null) {
return new Result(bitmap, bitmapSampled.sampleSize);
} else {
BitmapUtils.writeBitmapToUri(
mContext, bitmap, mSaveUri, mSaveCompressFormat, mSaveCompressQuality);
if (bitmap != null) {
bitmap.recycle();
}
return new Result(mSaveUri, bitmapSampled.sampleSize);
}
}
return null;
} catch (Exception e) {
return new Result(e, mSaveUri != null);
}
}
/**
* Once complete, see if ImageView is still around and set bitmap.
*
* @param result the result of bitmap cropping
*/
@Override
protected void onPostExecute(Result result) {
if (result != null) {
boolean completeCalled = false;
if (!isCancelled()) {
CropImageView cropImageView = mCropImageViewReference.get();
if (cropImageView != null) {
completeCalled = true;
cropImageView.onImageCroppingAsyncComplete(result);
}
}
if (!completeCalled && result.bitmap != null) {
// fast release of unused bitmap
result.bitmap.recycle();
}
}
}
// region: Inner class: Result
/**
* The result of BitmapCroppingWorkerTask async loading.
*/
static final class Result {
/**
* The cropped bitmap
*/
public final Bitmap bitmap;
/**
* The saved cropped bitmap uri
*/
public final Uri uri;
/**
* The error that occurred during async bitmap cropping.
*/
final Exception error;
/**
* is the cropping request was to get a bitmap or to save it to uri
*/
final boolean isSave;
/**
* sample size used creating the crop bitmap to lower its size
*/
final int sampleSize;
Result(Bitmap bitmap, int sampleSize) {
this.bitmap = bitmap;
this.uri = null;
this.error = null;
this.isSave = false;
this.sampleSize = sampleSize;
}
Result(Uri uri, int sampleSize) {
this.bitmap = null;
this.uri = uri;
this.error = null;
this.isSave = true;
this.sampleSize = sampleSize;
}
Result(Exception error, boolean isSave) {
this.bitmap = null;
this.uri = null;
this.error = error;
this.isSave = isSave;
this.sampleSize = 1;
}
}
// endregion
}

@ -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<Void, Void, BitmapLoadingWorkerTask.Result> {
// region: Fields and Consts
/**
* Use a WeakReference to ensure the ImageView can be garbage collected
*/
private final WeakReference<CropImageView> mCropImageViewReference;
/**
* The Android URI of the image to load
*/
private final Uri mUri;
/**
* The context of the crop image view widget used for loading of bitmap by Android URI
*/
private final Context mContext;
/**
* required width of the cropping image after density adjustment
*/
private final int mWidth;
/**
* required height of the cropping image after density adjustment
*/
private final int mHeight;
// endregion
public BitmapLoadingWorkerTask(CropImageView cropImageView, Uri uri) {
mUri = uri;
mCropImageViewReference = new WeakReference<>(cropImageView);
mContext = cropImageView.getContext();
DisplayMetrics metrics = cropImageView.getResources().getDisplayMetrics();
double densityAdj = metrics.density > 1 ? 1 / metrics.density : 1;
mWidth = (int) (metrics.widthPixels * densityAdj);
mHeight = (int) (metrics.heightPixels * densityAdj);
}
/**
* The Android URI that this task is currently loading.
*/
public Uri getUri() {
return mUri;
}
/**
* Decode image in background.
*
* @param params ignored
* @return the decoded bitmap data
*/
@Override
protected Result doInBackground(Void... params) {
try {
if (!isCancelled()) {
BitmapUtils.BitmapSampled decodeResult =
BitmapUtils.decodeSampledBitmap(mContext, mUri, mWidth, mHeight);
if (!isCancelled()) {
BitmapUtils.RotateBitmapResult rotateResult =
BitmapUtils.rotateBitmapByExif(decodeResult.bitmap, mContext, mUri);
return new Result(
mUri, rotateResult.bitmap, decodeResult.sampleSize, rotateResult.degrees);
}
}
return null;
} catch (Exception e) {
return new Result(mUri, e);
}
}
/**
* Once complete, see if ImageView is still around and set bitmap.
*
* @param result the result of bitmap loading
*/
@Override
protected void onPostExecute(Result result) {
if (result != null) {
boolean completeCalled = false;
if (!isCancelled()) {
CropImageView cropImageView = mCropImageViewReference.get();
if (cropImageView != null) {
completeCalled = true;
cropImageView.onSetImageUriAsyncComplete(result);
}
}
if (!completeCalled && result.bitmap != null) {
// fast release of unused bitmap
result.bitmap.recycle();
}
}
}
// region: Inner class: Result
/**
* The result of BitmapLoadingWorkerTask async loading.
*/
public static final class Result {
/**
* The Android URI of the image to load
*/
public final Uri uri;
/**
* The loaded bitmap
*/
public final Bitmap bitmap;
/**
* The sample size used to load the given bitmap
*/
public final int loadSampleSize;
/**
* The degrees the image was rotated
*/
public final int degreesRotated;
/**
* The error that occurred during async bitmap loading.
*/
public final Exception error;
Result(Uri uri, Bitmap bitmap, int loadSampleSize, int degreesRotated) {
this.uri = uri;
this.bitmap = bitmap;
this.loadSampleSize = loadSampleSize;
this.degreesRotated = degreesRotated;
this.error = null;
}
Result(Uri uri, Exception error) {
this.uri = uri;
this.bitmap = null;
this.loadSampleSize = 0;
this.degreesRotated = 0;
this.error = error;
}
}
// endregion
}

@ -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<String, WeakReference<Bitmap>> mStateBitmap;
/**
* Used to know the max texture size allowed to be rendered
*/
private static int mMaxTextureSize;
/**
* Rotate the given image by reading the Exif value of the image (uri).<br>
* If no rotation is required the image will not be rotated.<br>
* New bitmap is created and the old one is recycled.
*/
static RotateBitmapResult rotateBitmapByExif(Bitmap bitmap, Context context, Uri uri) {
ExifInterface ei = null;
try {
InputStream is = context.getContentResolver().openInputStream(uri);
if (is != null) {
ei = new ExifInterface(is);
is.close();
}
} catch (Exception ignored) {
}
return ei != null ? rotateBitmapByExif(bitmap, ei) : new RotateBitmapResult(bitmap, 0);
}
/**
* Rotate the given image by given Exif value.<br>
* If no rotation is required the image will not be rotated.<br>
* New bitmap is created and the old one is recycled.
*/
static RotateBitmapResult rotateBitmapByExif(Bitmap bitmap, ExifInterface exif) {
int degrees;
int orientation =
exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degrees = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degrees = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degrees = 270;
break;
default:
degrees = 0;
break;
}
return new RotateBitmapResult(bitmap, degrees);
}
/**
* Decode bitmap from stream using sampling to get bitmap with the requested limit.
*/
static BitmapSampled decodeSampledBitmap(Context context, Uri uri, int reqWidth, int reqHeight) {
try {
ContentResolver resolver = context.getContentResolver();
// First decode with inJustDecodeBounds=true to check dimensions
BitmapFactory.Options options = decodeImageForOption(resolver, uri);
if (options.outWidth == -1 && options.outHeight == -1)
throw new RuntimeException("File is not a picture");
// Calculate inSampleSize
options.inSampleSize =
Math.max(
calculateInSampleSizeByReqestedSize(
options.outWidth, options.outHeight, reqWidth, reqHeight),
calculateInSampleSizeByMaxTextureSize(options.outWidth, options.outHeight));
// Decode bitmap with inSampleSize set
Bitmap bitmap = decodeImage(resolver, uri, options);
return new BitmapSampled(bitmap, options.inSampleSize);
} catch (Exception e) {
throw new RuntimeException(
"Failed to load sampled bitmap: " + uri + "\r\n" + e.getMessage(), e);
}
}
/**
* Crop image bitmap from given bitmap using the given points in the original bitmap and the given
* rotation.<br>
* if the rotation is not 0,90,180 or 270 degrees then we must first crop a larger area of the
* image that contains the requires rectangle, rotate and then crop again a sub rectangle.<br>
* If crop fails due to OOM we scale the cropping image by 0.5 every time it fails until it is
* small enough.
*/
static BitmapSampled cropBitmapObjectHandleOOM(
Bitmap bitmap,
float[] points,
int degreesRotated,
boolean fixAspectRatio,
int aspectRatioX,
int aspectRatioY,
boolean flipHorizontally,
boolean flipVertically) {
int scale = 1;
while (true) {
try {
Bitmap cropBitmap =
cropBitmapObjectWithScale(
bitmap,
points,
degreesRotated,
fixAspectRatio,
aspectRatioX,
aspectRatioY,
1 / (float) scale,
flipHorizontally,
flipVertically);
return new BitmapSampled(cropBitmap, scale);
} catch (OutOfMemoryError e) {
scale *= 2;
if (scale > 8) {
throw e;
}
}
}
}
/**
* Crop image bitmap from given bitmap using the given points in the original bitmap and the given
* rotation.<br>
* if the rotation is not 0,90,180 or 270 degrees then we must first crop a larger area of the
* image that contains the requires rectangle, rotate and then crop again a sub rectangle.
*
* @param scale how much to scale the cropped image part, use 0.5 to lower the image by half (OOM
* handling)
*/
private static Bitmap cropBitmapObjectWithScale(
Bitmap bitmap,
float[] points,
int degreesRotated,
boolean fixAspectRatio,
int aspectRatioX,
int aspectRatioY,
float scale,
boolean flipHorizontally,
boolean flipVertically) {
// get the rectangle in original image that contains the required cropped area (larger for non
// rectangular crop)
Rect rect =
getRectFromPoints(
points,
bitmap.getWidth(),
bitmap.getHeight(),
fixAspectRatio,
aspectRatioX,
aspectRatioY);
// crop and rotate the cropped image in one operation
Matrix matrix = new Matrix();
matrix.setRotate(degreesRotated, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
matrix.postScale(flipHorizontally ? -scale : scale, flipVertically ? -scale : scale);
Bitmap result =
Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height(), matrix, true);
if (result == bitmap) {
// corner case when all bitmap is selected, no worth optimizing for it
result = bitmap.copy(bitmap.getConfig(), false);
}
// rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping
if (degreesRotated % 90 != 0) {
// extra crop because non rectangular crop cannot be done directly on the image without
// rotating first
result =
cropForRotatedImage(
result, points, rect, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY);
}
return result;
}
/**
* Crop image bitmap from URI by decoding it with specific width and height to down-sample if
* required.<br>
* Additionally if OOM is thrown try to increase the sampling (2,4,8).
*/
static BitmapSampled cropBitmap(
Context context,
Uri loadedImageUri,
float[] points,
int degreesRotated,
int orgWidth,
int orgHeight,
boolean fixAspectRatio,
int aspectRatioX,
int aspectRatioY,
int reqWidth,
int reqHeight,
boolean flipHorizontally,
boolean flipVertically) {
int sampleMulti = 1;
while (true) {
try {
// if successful, just return the resulting bitmap
return cropBitmap(
context,
loadedImageUri,
points,
degreesRotated,
orgWidth,
orgHeight,
fixAspectRatio,
aspectRatioX,
aspectRatioY,
reqWidth,
reqHeight,
flipHorizontally,
flipVertically,
sampleMulti);
} catch (OutOfMemoryError e) {
// if OOM try to increase the sampling to lower the memory usage
sampleMulti *= 2;
if (sampleMulti > 16) {
throw new RuntimeException(
"Failed to handle OOM by sampling ("
+ sampleMulti
+ "): "
+ loadedImageUri
+ "\r\n"
+ e.getMessage(),
e);
}
}
}
}
/**
* Get left value of the bounding rectangle of the given points.
*/
static float getRectLeft(float[] points) {
return Math.min(Math.min(Math.min(points[0], points[2]), points[4]), points[6]);
}
/**
* Get top value of the bounding rectangle of the given points.
*/
static float getRectTop(float[] points) {
return Math.min(Math.min(Math.min(points[1], points[3]), points[5]), points[7]);
}
/**
* Get right value of the bounding rectangle of the given points.
*/
static float getRectRight(float[] points) {
return Math.max(Math.max(Math.max(points[0], points[2]), points[4]), points[6]);
}
/**
* Get bottom value of the bounding rectangle of the given points.
*/
static float getRectBottom(float[] points) {
return Math.max(Math.max(Math.max(points[1], points[3]), points[5]), points[7]);
}
/**
* Get width of the bounding rectangle of the given points.
*/
static float getRectWidth(float[] points) {
return getRectRight(points) - getRectLeft(points);
}
/**
* Get height of the bounding rectangle of the given points.
*/
static float getRectHeight(float[] points) {
return getRectBottom(points) - getRectTop(points);
}
/**
* Get horizontal center value of the bounding rectangle of the given points.
*/
static float getRectCenterX(float[] points) {
return (getRectRight(points) + getRectLeft(points)) / 2f;
}
/**
* Get vertical center value of the bounding rectangle of the given points.
*/
static float getRectCenterY(float[] points) {
return (getRectBottom(points) + getRectTop(points)) / 2f;
}
/**
* Get a rectangle for the given 4 points (x0,y0,x1,y1,x2,y2,x3,y3) by finding the min/max 2
* points that contains the given 4 points and is a straight rectangle.
*/
static Rect getRectFromPoints(
float[] points,
int imageWidth,
int imageHeight,
boolean fixAspectRatio,
int aspectRatioX,
int aspectRatioY) {
int left = Math.round(Math.max(0, getRectLeft(points)));
int top = Math.round(Math.max(0, getRectTop(points)));
int right = Math.round(Math.min(imageWidth, getRectRight(points)));
int bottom = Math.round(Math.min(imageHeight, getRectBottom(points)));
Rect rect = new Rect(left, top, right, bottom);
if (fixAspectRatio) {
fixRectForAspectRatio(rect, aspectRatioX, aspectRatioY);
}
return rect;
}
/**
* Fix the given rectangle if it doesn't confirm to aspect ration rule.<br>
* Make sure that width and height are equal if 1:1 fixed aspect ratio is requested.
*/
private static void fixRectForAspectRatio(Rect rect, int aspectRatioX, int aspectRatioY) {
if (aspectRatioX == aspectRatioY && rect.width() != rect.height()) {
if (rect.height() > rect.width()) {
rect.bottom -= rect.height() - rect.width();
} else {
rect.right -= rect.width() - rect.height();
}
}
}
/**
* Write given bitmap to a temp file. If file already exists no-op as we already saved the file in
* this session. Uses JPEG 95% compression.
*
* @param uri the uri to write the bitmap to, if null
* @return the uri where the image was saved in, either the given uri or new pointing to temp
* file.
*/
static Uri writeTempStateStoreBitmap(Context context, Bitmap bitmap, Uri uri) {
try {
boolean needSave = true;
if (uri == null) {
uri =
Uri.fromFile(
File.createTempFile("aic_state_store_temp", ".jpg", context.getCacheDir()));
} else if (new File(uri.getPath()).exists()) {
needSave = false;
}
if (needSave) {
writeBitmapToUri(context, bitmap, uri, Bitmap.CompressFormat.JPEG, 95);
}
return uri;
} catch (Exception e) {
Log.w("AIC", "Failed to write bitmap to temp file for image-cropper save instance state", e);
return null;
}
}
/**
* Write the given bitmap to the given uri using the given compression.
*/
static void writeBitmapToUri(
Context context,
Bitmap bitmap,
Uri uri,
Bitmap.CompressFormat compressFormat,
int compressQuality)
throws FileNotFoundException {
OutputStream outputStream = null;
try {
outputStream = context.getContentResolver().openOutputStream(uri);
bitmap.compress(compressFormat, compressQuality, outputStream);
} finally {
closeSafe(outputStream);
}
}
/**
* Resize the given bitmap to the given width/height by the given option.<br>
*/
static Bitmap resizeBitmap(
Bitmap bitmap, int reqWidth, int reqHeight, CropImageView.RequestSizeOptions options) {
try {
if (reqWidth > 0
&& reqHeight > 0
&& (options == CropImageView.RequestSizeOptions.RESIZE_FIT
|| options == CropImageView.RequestSizeOptions.RESIZE_INSIDE
|| options == CropImageView.RequestSizeOptions.RESIZE_EXACT)) {
Bitmap resized = null;
if (options == CropImageView.RequestSizeOptions.RESIZE_EXACT) {
resized = Bitmap.createScaledBitmap(bitmap, reqWidth, reqHeight, false);
} else {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
float scale = Math.max(width / (float) reqWidth, height / (float) reqHeight);
if (scale > 1 || options == CropImageView.RequestSizeOptions.RESIZE_FIT) {
resized =
Bitmap.createScaledBitmap(
bitmap, (int) (width / scale), (int) (height / scale), false);
}
}
if (resized != null) {
if (resized != bitmap) {
bitmap.recycle();
}
return resized;
}
}
} catch (Exception e) {
Log.w("AIC", "Failed to resize cropped image, return bitmap before resize", e);
}
return bitmap;
}
// region: Private methods
/**
* Crop image bitmap from URI by decoding it with specific width and height to down-sample if
* required.
*
* @param orgWidth used to get rectangle from points (handle edge cases to limit rectangle)
* @param orgHeight used to get rectangle from points (handle edge cases to limit rectangle)
* @param sampleMulti used to increase the sampling of the image to handle memory issues.
*/
private static BitmapSampled cropBitmap(
Context context,
Uri loadedImageUri,
float[] points,
int degreesRotated,
int orgWidth,
int orgHeight,
boolean fixAspectRatio,
int aspectRatioX,
int aspectRatioY,
int reqWidth,
int reqHeight,
boolean flipHorizontally,
boolean flipVertically,
int sampleMulti) {
// get the rectangle in original image that contains the required cropped area (larger for non
// rectangular crop)
Rect rect =
getRectFromPoints(points, orgWidth, orgHeight, fixAspectRatio, aspectRatioX, aspectRatioY);
int width = reqWidth > 0 ? reqWidth : rect.width();
int height = reqHeight > 0 ? reqHeight : rect.height();
Bitmap result = null;
int sampleSize = 1;
try {
// decode only the required image from URI, optionally sub-sampling if reqWidth/reqHeight is
// given.
BitmapSampled bitmapSampled =
decodeSampledBitmapRegion(context, loadedImageUri, rect, width, height, sampleMulti);
result = bitmapSampled.bitmap;
sampleSize = bitmapSampled.sampleSize;
} catch (Exception ignored) {
}
if (result != null) {
try {
// rotate the decoded region by the required amount
result = rotateAndFlipBitmapInt(result, degreesRotated, flipHorizontally, flipVertically);
// rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping
if (degreesRotated % 90 != 0) {
// extra crop because non rectangular crop cannot be done directly on the image without
// rotating first
result =
cropForRotatedImage(
result, points, rect, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY);
}
} catch (OutOfMemoryError e) {
if (result != null) {
result.recycle();
}
throw e;
}
return new BitmapSampled(result, sampleSize);
} else {
// failed to decode region, may be skia issue, try full decode and then crop
return cropBitmap(
context,
loadedImageUri,
points,
degreesRotated,
fixAspectRatio,
aspectRatioX,
aspectRatioY,
sampleMulti,
rect,
width,
height,
flipHorizontally,
flipVertically);
}
}
/**
* Crop bitmap by fully loading the original and then cropping it, fallback in case cropping
* region failed.
*/
private static BitmapSampled cropBitmap(
Context context,
Uri loadedImageUri,
float[] points,
int degreesRotated,
boolean fixAspectRatio,
int aspectRatioX,
int aspectRatioY,
int sampleMulti,
Rect rect,
int width,
int height,
boolean flipHorizontally,
boolean flipVertically) {
Bitmap result = null;
int sampleSize;
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize =
sampleSize =
sampleMulti
* calculateInSampleSizeByReqestedSize(rect.width(), rect.height(), width, height);
Bitmap fullBitmap = decodeImage(context.getContentResolver(), loadedImageUri, options);
if (fullBitmap != null) {
try {
// adjust crop points by the sampling because the image is smaller
float[] points2 = new float[points.length];
System.arraycopy(points, 0, points2, 0, points.length);
for (int i = 0; i < points2.length; i++) {
points2[i] = points2[i] / options.inSampleSize;
}
result =
cropBitmapObjectWithScale(
fullBitmap,
points2,
degreesRotated,
fixAspectRatio,
aspectRatioX,
aspectRatioY,
1,
flipHorizontally,
flipVertically);
} finally {
if (result != fullBitmap) {
fullBitmap.recycle();
}
}
}
} catch (OutOfMemoryError e) {
if (result != null) {
result.recycle();
}
throw e;
} catch (Exception e) {
throw new RuntimeException(
"Failed to load sampled bitmap: " + loadedImageUri + "\r\n" + e.getMessage(), e);
}
return new BitmapSampled(result, sampleSize);
}
/**
* Decode image from uri using "inJustDecodeBounds" to get the image dimensions.
*/
private static BitmapFactory.Options decodeImageForOption(ContentResolver resolver, Uri uri)
throws FileNotFoundException {
InputStream stream = null;
try {
stream = resolver.openInputStream(uri);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(stream, EMPTY_RECT, options);
options.inJustDecodeBounds = false;
return options;
} finally {
closeSafe(stream);
}
}
/**
* Decode image from uri using given "inSampleSize", but if failed due to out-of-memory then raise
* the inSampleSize until success.
*/
private static Bitmap decodeImage(
ContentResolver resolver, Uri uri, BitmapFactory.Options options)
throws FileNotFoundException {
do {
InputStream stream = null;
try {
stream = resolver.openInputStream(uri);
return BitmapFactory.decodeStream(stream, EMPTY_RECT, options);
} catch (OutOfMemoryError e) {
options.inSampleSize *= 2;
} finally {
closeSafe(stream);
}
} while (options.inSampleSize <= 512);
throw new RuntimeException("Failed to decode image: " + uri);
}
/**
* Decode specific rectangle bitmap from stream using sampling to get bitmap with the requested
* limit.
*
* @param sampleMulti used to increase the sampling of the image to handle memory issues.
*/
private static BitmapSampled decodeSampledBitmapRegion(
Context context, Uri uri, Rect rect, int reqWidth, int reqHeight, int sampleMulti) {
InputStream stream = null;
BitmapRegionDecoder decoder = null;
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize =
sampleMulti
* calculateInSampleSizeByReqestedSize(
rect.width(), rect.height(), reqWidth, reqHeight);
stream = context.getContentResolver().openInputStream(uri);
decoder = BitmapRegionDecoder.newInstance(stream, false);
do {
try {
return new BitmapSampled(decoder.decodeRegion(rect, options), options.inSampleSize);
} catch (OutOfMemoryError e) {
options.inSampleSize *= 2;
}
} while (options.inSampleSize <= 512);
} catch (Exception e) {
throw new RuntimeException(
"Failed to load sampled bitmap: " + uri + "\r\n" + e.getMessage(), e);
} finally {
closeSafe(stream);
if (decoder != null) {
decoder.recycle();
}
}
return new BitmapSampled(null, 1);
}
/**
* Special crop of bitmap rotated by not stright angle, in this case the original crop bitmap
* contains parts beyond the required crop area, this method crops the already cropped and rotated
* bitmap to the final rectangle.<br>
* Note: rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping.
*/
private static Bitmap cropForRotatedImage(
Bitmap bitmap,
float[] points,
Rect rect,
int degreesRotated,
boolean fixAspectRatio,
int aspectRatioX,
int aspectRatioY) {
if (degreesRotated % 90 != 0) {
int adjLeft = 0, adjTop = 0, width = 0, height = 0;
double rads = Math.toRadians(degreesRotated);
int compareTo =
degreesRotated < 90 || (degreesRotated > 180 && degreesRotated < 270)
? rect.left
: rect.right;
for (int i = 0; i < points.length; i += 2) {
if (points[i] >= compareTo - 1 && points[i] <= compareTo + 1) {
adjLeft = (int) Math.abs(Math.sin(rads) * (rect.bottom - points[i + 1]));
adjTop = (int) Math.abs(Math.cos(rads) * (points[i + 1] - rect.top));
width = (int) Math.abs((points[i + 1] - rect.top) / Math.sin(rads));
height = (int) Math.abs((rect.bottom - points[i + 1]) / Math.cos(rads));
break;
}
}
rect.set(adjLeft, adjTop, adjLeft + width, adjTop + height);
if (fixAspectRatio) {
fixRectForAspectRatio(rect, aspectRatioX, aspectRatioY);
}
Bitmap bitmapTmp = bitmap;
bitmap = Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height());
if (bitmapTmp != bitmap) {
bitmapTmp.recycle();
}
}
return bitmap;
}
/**
* Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width
* larger than the requested height and width.
*/
private static int calculateInSampleSizeByReqestedSize(
int width, int height, int reqWidth, int reqHeight) {
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
while ((height / 2 / inSampleSize) > reqHeight && (width / 2 / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
/**
* Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width
* smaller than max texture size allowed for the device.
*/
private static int calculateInSampleSizeByMaxTextureSize(int width, int height) {
int inSampleSize = 1;
if (mMaxTextureSize == 0) {
mMaxTextureSize = getMaxTextureSize();
}
if (mMaxTextureSize > 0) {
while ((height / inSampleSize) > mMaxTextureSize
|| (width / inSampleSize) > mMaxTextureSize) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
/**
* Rotate the given bitmap by the given degrees.<br>
* New bitmap is created and the old one is recycled.
*/
private static Bitmap rotateAndFlipBitmapInt(
Bitmap bitmap, int degrees, boolean flipHorizontally, boolean flipVertically) {
if (degrees > 0 || flipHorizontally || flipVertically) {
Matrix matrix = new Matrix();
matrix.setRotate(degrees);
matrix.postScale(flipHorizontally ? -1 : 1, flipVertically ? -1 : 1);
Bitmap newBitmap =
Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
if (newBitmap != bitmap) {
bitmap.recycle();
}
return newBitmap;
} else {
return bitmap;
}
}
/**
* Get the max size of bitmap allowed to be rendered on the device.<br>
* http://stackoverflow.com/questions/7428996/hw-accelerated-activity-how-to-get-opengl-texture-size-limit.
*/
private static int getMaxTextureSize() {
// Safe minimum default size
final int IMAGE_MAX_BITMAP_DIMENSION = 2048;
try {
// Get EGL Display
EGL10 egl = (EGL10) EGLContext.getEGL();
EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
// Initialise
int[] version = new int[2];
egl.eglInitialize(display, version);
// Query total number of configurations
int[] totalConfigurations = new int[1];
egl.eglGetConfigs(display, null, 0, totalConfigurations);
// Query actual list configurations
EGLConfig[] configurationsList = new EGLConfig[totalConfigurations[0]];
egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations);
int[] textureSize = new int[1];
int maximumTextureSize = 0;
// Iterate through all the configurations to located the maximum texture size
for (int i = 0; i < totalConfigurations[0]; i++) {
// Only need to check for width since opengl textures are always squared
egl.eglGetConfigAttrib(
display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize);
// Keep track of the maximum texture size
if (maximumTextureSize < textureSize[0]) {
maximumTextureSize = textureSize[0];
}
}
// Release
egl.eglTerminate(display);
// Return largest texture size found, or default
return Math.max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION);
} catch (Exception e) {
return IMAGE_MAX_BITMAP_DIMENSION;
}
}
/**
* Close the given closeable object (Stream) in a safe way: check if it is null and catch-log
* exception thrown.
*
* @param closeable the closable object to close
*/
private static void closeSafe(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException ignored) {
}
}
}
// endregion
// region: Inner class: BitmapSampled
/**
* Holds bitmap instance and the sample size that the bitmap was loaded/cropped with.
*/
static final class BitmapSampled {
/**
* The bitmap instance
*/
public final Bitmap bitmap;
/**
* The sample size used to lower the size of the bitmap (1,2,4,8,...)
*/
final int sampleSize;
BitmapSampled(Bitmap bitmap, int sampleSize) {
this.bitmap = bitmap;
this.sampleSize = sampleSize;
}
}
// endregion
// region: Inner class: RotateBitmapResult
/**
* The result of {@link #rotateBitmapByExif(android.graphics.Bitmap, ExifInterface)}.
*/
static final class RotateBitmapResult {
/**
* The loaded bitmap
*/
public final Bitmap bitmap;
/**
* The degrees the image was rotated
*/
final int degrees;
RotateBitmapResult(Bitmap bitmap, int degrees) {
this.bitmap = bitmap;
this.degrees = degrees;
}
}
// endregion
}

@ -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.<br>
* Use {@link CropImage#activity(Uri)} to create a builder to start this activity.
*/
public class CropImageActivity extends AppCompatActivity
implements CropImageView.OnSetImageUriCompleteListener,
CropImageView.OnCropImageCompleteListener {
/**
* The crop image view library widget used in the activity
*/
private CropImageView mCropImageView;
/**
* Persist URI image to crop URI if specific permissions are required
*/
private Uri mCropImageUri;
/**
* the options that were set for the crop image
*/
private CropImageOptions mOptions;
@Override
@SuppressLint("NewApi")
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.crop_image_activity);
mCropImageView = findViewById(R.id.cropImageView);
Bundle bundle = getIntent().getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE);
mCropImageUri = bundle.getParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE);
mOptions = bundle.getParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS);
if (savedInstanceState == null) {
if (mCropImageUri == null || mCropImageUri.equals(Uri.EMPTY)) {
if (CropImage.isExplicitCameraPermissionRequired(this)) {
// request permissions and handle the result in onRequestPermissionsResult()
requestPermissions(
new String[]{Manifest.permission.CAMERA},
CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE);
} else {
CropImage.startPickImageActivity(this);
}
} else if (CropImage.isReadExternalStoragePermissionsRequired(this, mCropImageUri)) {
// request permissions and handle the result in onRequestPermissionsResult()
requestPermissions(
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE);
} else {
// no permissions required or already grunted, can start crop image activity
mCropImageView.setImageUriAsync(mCropImageUri);
}
}
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
CharSequence title = mOptions != null &&
mOptions.activityTitle != null && mOptions.activityTitle.length() > 0
? mOptions.activityTitle
: getResources().getString(R.string.crop_image_activity_title);
actionBar.setTitle(title);
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
@Override
protected void onStart() {
super.onStart();
mCropImageView.setOnSetImageUriCompleteListener(this);
mCropImageView.setOnCropImageCompleteListener(this);
}
@Override
protected void onStop() {
super.onStop();
mCropImageView.setOnSetImageUriCompleteListener(null);
mCropImageView.setOnCropImageCompleteListener(null);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.crop_image_menu, menu);
if (!mOptions.allowRotation) {
menu.removeItem(R.id.crop_image_menu_rotate_left);
menu.removeItem(R.id.crop_image_menu_rotate_right);
} else if (mOptions.allowCounterRotation) {
menu.findItem(R.id.crop_image_menu_rotate_left).setVisible(true);
}
if (!mOptions.allowFlipping) {
menu.removeItem(R.id.crop_image_menu_flip);
}
if (mOptions.cropMenuCropButtonTitle != null) {
menu.findItem(R.id.crop_image_menu_crop).setTitle(mOptions.cropMenuCropButtonTitle);
}
Drawable cropIcon = null;
try {
if (mOptions.cropMenuCropButtonIcon != 0) {
cropIcon = ContextCompat.getDrawable(this, mOptions.cropMenuCropButtonIcon);
menu.findItem(R.id.crop_image_menu_crop).setIcon(cropIcon);
}
} catch (Exception e) {
e.printStackTrace();
}
if (mOptions.activityMenuIconColor != 0) {
updateMenuItemIconColor(
menu, R.id.crop_image_menu_rotate_left, mOptions.activityMenuIconColor);
updateMenuItemIconColor(
menu, R.id.crop_image_menu_rotate_right, mOptions.activityMenuIconColor);
updateMenuItemIconColor(menu, R.id.crop_image_menu_flip, mOptions.activityMenuIconColor);
if (cropIcon != null) {
updateMenuItemIconColor(menu, R.id.crop_image_menu_crop, mOptions.activityMenuIconColor);
}
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.crop_image_menu_crop) {
cropImage();
return true;
}
if (item.getItemId() == R.id.crop_image_menu_rotate_left) {
rotateImage(-mOptions.rotationDegrees);
return true;
}
if (item.getItemId() == R.id.crop_image_menu_rotate_right) {
rotateImage(mOptions.rotationDegrees);
return true;
}
if (item.getItemId() == R.id.crop_image_menu_flip_horizontally) {
mCropImageView.flipImageHorizontally();
return true;
}
if (item.getItemId() == R.id.crop_image_menu_flip_vertically) {
mCropImageView.flipImageVertically();
return true;
}
if (item.getItemId() == android.R.id.home) {
setResultCancel();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
super.onBackPressed();
setResultCancel();
}
@Override
@SuppressLint("NewApi")
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// handle result of pick image chooser
if (requestCode == CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE) {
if (resultCode == Activity.RESULT_CANCELED) {
// User cancelled the picker. We don't have anything to crop
setResultCancel();
}
if (resultCode == Activity.RESULT_OK) {
mCropImageUri = CropImage.getPickImageResultUri(this, data);
// For API >= 23 we need to check specifically that we have permissions to read external
// storage.
if (CropImage.isReadExternalStoragePermissionsRequired(this, mCropImageUri)) {
// request permissions and handle the result in onRequestPermissionsResult()
requestPermissions(
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE);
} else {
// no permissions required or already grunted, can start crop image activity
mCropImageView.setImageUriAsync(mCropImageUri);
}
}
}
}
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE) {
if (mCropImageUri != null
&& grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// required permissions granted, start crop image activity
mCropImageView.setImageUriAsync(mCropImageUri);
} else {
Toast.makeText(this, R.string.crop_image_activity_no_permissions, Toast.LENGTH_LONG).show();
setResultCancel();
}
}
if (requestCode == CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE) {
// Irrespective of whether camera permission was given or not, we show the picker
// The picker will not add the camera intent if permission is not available
CropImage.startPickImageActivity(this);
}
}
@Override
public void onSetImageUriComplete(CropImageView view, Uri uri, Exception error) {
if (error == null) {
if (mOptions.initialCropWindowRectangle != null) {
mCropImageView.setCropRect(mOptions.initialCropWindowRectangle);
}
if (mOptions.initialRotation > -1) {
mCropImageView.setRotatedDegrees(mOptions.initialRotation);
}
} else {
setResult(null, error, 1);
}
}
@Override
public void onCropImageComplete(CropImageView view, CropImageView.CropResult result) {
setResult(result.getUri(), result.getError(), result.getSampleSize());
}
// region: Private methods
/**
* Execute crop image and save the result tou output uri.
*/
protected void cropImage() {
if (mOptions.noOutputImage) {
setResult(null, null, 1);
} else {
Uri outputUri = getOutputUri();
mCropImageView.saveCroppedImageAsync(
outputUri,
mOptions.outputCompressFormat,
mOptions.outputCompressQuality,
mOptions.outputRequestWidth,
mOptions.outputRequestHeight,
mOptions.outputRequestSizeOptions);
}
}
/**
* Rotate the image in the crop image view.
*/
protected void rotateImage(int degrees) {
mCropImageView.rotateImage(degrees);
}
/**
* Get Android uri to save the cropped image into.<br>
* Use the given in options or create a temp file.
*/
protected Uri getOutputUri() {
Uri outputUri = mOptions.outputUri;
if (outputUri == null || outputUri.equals(Uri.EMPTY)) {
try {
String ext =
mOptions.outputCompressFormat == Bitmap.CompressFormat.JPEG
? ".jpg"
: mOptions.outputCompressFormat == Bitmap.CompressFormat.PNG ? ".png" : ".webp";
outputUri = Uri.fromFile(File.createTempFile("cropped", ext, getCacheDir()));
} catch (IOException e) {
throw new RuntimeException("Failed to create temp file for output image", e);
}
}
return outputUri;
}
/**
* Result with cropped image data or error if failed.
*/
protected void setResult(Uri uri, Exception error, int sampleSize) {
int resultCode = error == null ? RESULT_OK : CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE;
setResult(resultCode, getResultIntent(uri, error, sampleSize));
finish();
}
/**
* Cancel of cropping activity.
*/
protected void setResultCancel() {
setResult(RESULT_CANCELED);
finish();
}
/**
* Get intent instance to be used for the result of this activity.
*/
protected Intent getResultIntent(Uri uri, Exception error, int sampleSize) {
CropImage.ActivityResult result =
new CropImage.ActivityResult(
mCropImageView.getImageUri(),
uri,
error,
mCropImageView.getCropPoints(),
mCropImageView.getCropRect(),
mCropImageView.getRotatedDegrees(),
mCropImageView.getWholeImageRect(),
sampleSize);
Intent intent = new Intent();
intent.putExtras(getIntent());
intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, result);
return intent;
}
/**
* Update the color of a specific menu item to the given color.
*/
private void updateMenuItemIconColor(Menu menu, int itemId, int color) {
MenuItem menuItem = menu.findItem(itemId);
if (menuItem != null) {
Drawable menuItemIcon = menuItem.getIcon();
if (menuItemIcon != null) {
try {
menuItemIcon.mutate();
menuItemIcon.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
menuItem.setIcon(menuItemIcon);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}

@ -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) {
}
}

@ -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.<br>
* Initialized with default values.
*/
public class CropImageOptions implements Parcelable {
public static final Creator<CropImageOptions> CREATOR =
new Creator<CropImageOptions>() {
@Override
public CropImageOptions createFromParcel(Parcel in) {
return new CropImageOptions(in);
}
@Override
public CropImageOptions[] newArray(int size) {
return new CropImageOptions[size];
}
};
/**
* The shape of the cropping window.
*/
public CropImageView.CropShape cropShape;
/**
* An edge of the crop window will snap to the corresponding edge of a specified bounding box when
* the crop window edge is less than or equal to this distance (in pixels) away from the bounding
* box edge. (in pixels)
*/
public float snapRadius;
/**
* The radius of the touchable area around the handle. (in pixels)<br>
* We are basing this value off of the recommended 48dp Rhythm.<br>
* See: http://developer.android.com/design/style/metrics-grids.html#48dp-rhythm
*/
public float touchRadius;
/**
* whether the guidelines should be on, off, or only showing when resizing.
*/
public CropImageView.Guidelines guidelines;
/**
* The initial scale type of the image in the crop image view
*/
public CropImageView.ScaleType scaleType;
/**
* if to show crop overlay UI what contains the crop window UI surrounded by background over the
* cropping image.<br>
* default: true, may disable for animation or frame transition.
*/
public boolean showCropOverlay;
/**
* if to show progress bar when image async loading/cropping is in progress.<br>
* default: true, disable to provide custom progress bar UI.
*/
public boolean showProgressBar;
/**
* if auto-zoom functionality is enabled.<br>
* default: true.
*/
public boolean autoZoomEnabled;
/**
* if multi-touch should be enabled on the crop box default: false
*/
public boolean multiTouchEnabled;
/**
* The max zoom allowed during cropping.
*/
public int maxZoom;
/**
* The initial crop window padding from image borders in percentage of the cropping image
* dimensions.
*/
public float initialCropWindowPaddingRatio;
/**
* whether the width to height aspect ratio should be maintained or free to change.
*/
public boolean fixAspectRatio;
/**
* the X value of the aspect ratio.
*/
public int aspectRatioX;
/**
* the Y value of the aspect ratio.
*/
public int aspectRatioY;
/**
* the thickness of the guidelines lines in pixels. (in pixels)
*/
public float borderLineThickness;
/**
* the color of the guidelines lines
*/
public int borderLineColor;
/**
* thickness of the corner line. (in pixels)
*/
public float borderCornerThickness;
/**
* the offset of corner line from crop window border. (in pixels)
*/
public float borderCornerOffset;
/**
* the length of the corner line away from the corner. (in pixels)
*/
public float borderCornerLength;
/**
* the color of the corner line
*/
public int borderCornerColor;
/**
* the thickness of the guidelines lines. (in pixels)
*/
public float guidelinesThickness;
/**
* the color of the guidelines lines
*/
public int guidelinesColor;
/**
* the color of the overlay background around the crop window cover the image parts not in the
* crop window.
*/
public int backgroundColor;
/**
* the min width the crop window is allowed to be. (in pixels)
*/
public int minCropWindowWidth;
/**
* the min height the crop window is allowed to be. (in pixels)
*/
public int minCropWindowHeight;
/**
* the min width the resulting cropping image is allowed to be, affects the cropping window
* limits. (in pixels)
*/
public int minCropResultWidth;
/**
* the min height the resulting cropping image is allowed to be, affects the cropping window
* limits. (in pixels)
*/
public int minCropResultHeight;
/**
* the max width the resulting cropping image is allowed to be, affects the cropping window
* limits. (in pixels)
*/
public int maxCropResultWidth;
/**
* the max height the resulting cropping image is allowed to be, affects the cropping window
* limits. (in pixels)
*/
public int maxCropResultHeight;
/**
* the title of the {@link CropImageActivity}
*/
public CharSequence activityTitle;
/**
* the color to use for action bar items icons
*/
public int activityMenuIconColor;
/**
* the Android Uri to save the cropped image to
*/
public Uri outputUri;
/**
* the compression format to use when writing the image
*/
public Bitmap.CompressFormat outputCompressFormat;
/**
* the quality (if applicable) to use when writing the image (0 - 100)
*/
public int outputCompressQuality;
/**
* the width to resize the cropped image to (see options)
*/
public int outputRequestWidth;
/**
* the height to resize the cropped image to (see options)
*/
public int outputRequestHeight;
/**
* the resize method to use on the cropped bitmap (see options documentation)
*/
public CropImageView.RequestSizeOptions outputRequestSizeOptions;
/**
* if the result of crop image activity should not save the cropped image bitmap
*/
public boolean noOutputImage;
/**
* the initial rectangle to set on the cropping image after loading
*/
public Rect initialCropWindowRectangle;
/**
* the initial rotation to set on the cropping image after loading (0-360 degrees clockwise)
*/
public int initialRotation;
/**
* if to allow (all) rotation during cropping (activity)
*/
public boolean allowRotation;
/**
* if to allow (all) flipping during cropping (activity)
*/
public boolean allowFlipping;
/**
* if to allow counter-clockwise rotation during cropping (activity)
*/
public boolean allowCounterRotation;
/**
* the amount of degrees to rotate clockwise or counter-clockwise
*/
public int rotationDegrees;
/**
* whether the image should be flipped horizontally
*/
public boolean flipHorizontally;
/**
* whether the image should be flipped vertically
*/
public boolean flipVertically;
/**
* optional, the text of the crop menu crop button
*/
public CharSequence cropMenuCropButtonTitle;
/**
* optional image resource to be used for crop menu crop icon instead of text
*/
public int cropMenuCropButtonIcon;
/**
* Init options with defaults.
*/
public CropImageOptions() {
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
cropShape = CropImageView.CropShape.RECTANGLE;
snapRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm);
touchRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24, dm);
guidelines = CropImageView.Guidelines.ON_TOUCH;
scaleType = CropImageView.ScaleType.FIT_CENTER;
showCropOverlay = true;
showProgressBar = true;
autoZoomEnabled = true;
multiTouchEnabled = false;
maxZoom = 4;
initialCropWindowPaddingRatio = 0.1f;
fixAspectRatio = false;
aspectRatioX = 1;
aspectRatioY = 1;
borderLineThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm);
borderLineColor = Color.argb(170, 255, 255, 255);
borderCornerThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, dm);
borderCornerOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, dm);
borderCornerLength = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14, dm);
borderCornerColor = Color.WHITE;
guidelinesThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, dm);
guidelinesColor = Color.argb(170, 255, 255, 255);
backgroundColor = Color.argb(119, 0, 0, 0);
minCropWindowWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm);
minCropWindowHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm);
minCropResultWidth = 40;
minCropResultHeight = 40;
maxCropResultWidth = 99999;
maxCropResultHeight = 99999;
activityTitle = "";
activityMenuIconColor = 0;
outputUri = Uri.EMPTY;
outputCompressFormat = Bitmap.CompressFormat.JPEG;
outputCompressQuality = 90;
outputRequestWidth = 0;
outputRequestHeight = 0;
outputRequestSizeOptions = CropImageView.RequestSizeOptions.NONE;
noOutputImage = false;
initialCropWindowRectangle = null;
initialRotation = -1;
allowRotation = true;
allowFlipping = true;
allowCounterRotation = false;
rotationDegrees = 90;
flipHorizontally = false;
flipVertically = false;
cropMenuCropButtonTitle = null;
cropMenuCropButtonIcon = 0;
}
/**
* Create object from parcel.
*/
protected CropImageOptions(Parcel in) {
cropShape = CropImageView.CropShape.values()[in.readInt()];
snapRadius = in.readFloat();
touchRadius = in.readFloat();
guidelines = CropImageView.Guidelines.values()[in.readInt()];
scaleType = CropImageView.ScaleType.values()[in.readInt()];
showCropOverlay = in.readByte() != 0;
showProgressBar = in.readByte() != 0;
autoZoomEnabled = in.readByte() != 0;
multiTouchEnabled = in.readByte() != 0;
maxZoom = in.readInt();
initialCropWindowPaddingRatio = in.readFloat();
fixAspectRatio = in.readByte() != 0;
aspectRatioX = in.readInt();
aspectRatioY = in.readInt();
borderLineThickness = in.readFloat();
borderLineColor = in.readInt();
borderCornerThickness = in.readFloat();
borderCornerOffset = in.readFloat();
borderCornerLength = in.readFloat();
borderCornerColor = in.readInt();
guidelinesThickness = in.readFloat();
guidelinesColor = in.readInt();
backgroundColor = in.readInt();
minCropWindowWidth = in.readInt();
minCropWindowHeight = in.readInt();
minCropResultWidth = in.readInt();
minCropResultHeight = in.readInt();
maxCropResultWidth = in.readInt();
maxCropResultHeight = in.readInt();
activityTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
activityMenuIconColor = in.readInt();
outputUri = in.readParcelable(Uri.class.getClassLoader());
outputCompressFormat = Bitmap.CompressFormat.valueOf(in.readString());
outputCompressQuality = in.readInt();
outputRequestWidth = in.readInt();
outputRequestHeight = in.readInt();
outputRequestSizeOptions = CropImageView.RequestSizeOptions.values()[in.readInt()];
noOutputImage = in.readByte() != 0;
initialCropWindowRectangle = in.readParcelable(Rect.class.getClassLoader());
initialRotation = in.readInt();
allowRotation = in.readByte() != 0;
allowFlipping = in.readByte() != 0;
allowCounterRotation = in.readByte() != 0;
rotationDegrees = in.readInt();
flipHorizontally = in.readByte() != 0;
flipVertically = in.readByte() != 0;
cropMenuCropButtonTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
cropMenuCropButtonIcon = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(cropShape.ordinal());
dest.writeFloat(snapRadius);
dest.writeFloat(touchRadius);
dest.writeInt(guidelines.ordinal());
dest.writeInt(scaleType.ordinal());
dest.writeByte((byte) (showCropOverlay ? 1 : 0));
dest.writeByte((byte) (showProgressBar ? 1 : 0));
dest.writeByte((byte) (autoZoomEnabled ? 1 : 0));
dest.writeByte((byte) (multiTouchEnabled ? 1 : 0));
dest.writeInt(maxZoom);
dest.writeFloat(initialCropWindowPaddingRatio);
dest.writeByte((byte) (fixAspectRatio ? 1 : 0));
dest.writeInt(aspectRatioX);
dest.writeInt(aspectRatioY);
dest.writeFloat(borderLineThickness);
dest.writeInt(borderLineColor);
dest.writeFloat(borderCornerThickness);
dest.writeFloat(borderCornerOffset);
dest.writeFloat(borderCornerLength);
dest.writeInt(borderCornerColor);
dest.writeFloat(guidelinesThickness);
dest.writeInt(guidelinesColor);
dest.writeInt(backgroundColor);
dest.writeInt(minCropWindowWidth);
dest.writeInt(minCropWindowHeight);
dest.writeInt(minCropResultWidth);
dest.writeInt(minCropResultHeight);
dest.writeInt(maxCropResultWidth);
dest.writeInt(maxCropResultHeight);
TextUtils.writeToParcel(activityTitle, dest, flags);
dest.writeInt(activityMenuIconColor);
dest.writeParcelable(outputUri, flags);
dest.writeString(outputCompressFormat.name());
dest.writeInt(outputCompressQuality);
dest.writeInt(outputRequestWidth);
dest.writeInt(outputRequestHeight);
dest.writeInt(outputRequestSizeOptions.ordinal());
dest.writeInt(noOutputImage ? 1 : 0);
dest.writeParcelable(initialCropWindowRectangle, flags);
dest.writeInt(initialRotation);
dest.writeByte((byte) (allowRotation ? 1 : 0));
dest.writeByte((byte) (allowFlipping ? 1 : 0));
dest.writeByte((byte) (allowCounterRotation ? 1 : 0));
dest.writeInt(rotationDegrees);
dest.writeByte((byte) (flipHorizontally ? 1 : 0));
dest.writeByte((byte) (flipVertically ? 1 : 0));
TextUtils.writeToParcel(cropMenuCropButtonTitle, dest, flags);
dest.writeInt(cropMenuCropButtonIcon);
}
@Override
public int describeContents() {
return 0;
}
/**
* Validate all the options are withing valid range.
*
* @throws IllegalArgumentException if any of the options is not valid
*/
public void validate() {
if (maxZoom < 0) {
throw new IllegalArgumentException("Cannot set max zoom to a number < 1");
}
if (touchRadius < 0) {
throw new IllegalArgumentException("Cannot set touch radius value to a number <= 0 ");
}
if (initialCropWindowPaddingRatio < 0 || initialCropWindowPaddingRatio >= 0.5) {
throw new IllegalArgumentException(
"Cannot set initial crop window padding value to a number < 0 or >= 0.5");
}
if (aspectRatioX <= 0) {
throw new IllegalArgumentException(
"Cannot set aspect ratio value to a number less than or equal to 0.");
}
if (aspectRatioY <= 0) {
throw new IllegalArgumentException(
"Cannot set aspect ratio value to a number less than or equal to 0.");
}
if (borderLineThickness < 0) {
throw new IllegalArgumentException(
"Cannot set line thickness value to a number less than 0.");
}
if (borderCornerThickness < 0) {
throw new IllegalArgumentException(
"Cannot set corner thickness value to a number less than 0.");
}
if (guidelinesThickness < 0) {
throw new IllegalArgumentException(
"Cannot set guidelines thickness value to a number less than 0.");
}
if (minCropWindowHeight < 0) {
throw new IllegalArgumentException(
"Cannot set min crop window height value to a number < 0 ");
}
if (minCropResultWidth < 0) {
throw new IllegalArgumentException("Cannot set min crop result width value to a number < 0 ");
}
if (minCropResultHeight < 0) {
throw new IllegalArgumentException(
"Cannot set min crop result height value to a number < 0 ");
}
if (maxCropResultWidth < minCropResultWidth) {
throw new IllegalArgumentException(
"Cannot set max crop result width to smaller value than min crop result width");
}
if (maxCropResultHeight < minCropResultHeight) {
throw new IllegalArgumentException(
"Cannot set max crop result height to smaller value than min crop result height");
}
if (outputRequestWidth < 0) {
throw new IllegalArgumentException("Cannot set request width value to a number < 0 ");
}
if (outputRequestHeight < 0) {
throw new IllegalArgumentException("Cannot set request height value to a number < 0 ");
}
if (rotationDegrees < 0 || rotationDegrees > 360) {
throw new IllegalArgumentException(
"Cannot set rotation degrees value to a number < 0 or > 360");
}
}
}

@ -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).<br>
*/
public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
mMinCropResultWidth = minCropResultWidth;
mMinCropResultHeight = minCropResultHeight;
}
/**
* the max size the resulting cropping image is allowed to be, affects the cropping window limits
* (in pixels).<br>
*/
public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
mMaxCropResultWidth = maxCropResultWidth;
mMaxCropResultHeight = maxCropResultHeight;
}
// region: Private methods
/**
* set the max width/height and scale factor of the showen image to original image to scale the
* limits appropriately.
*/
public void setCropWindowLimits(
float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) {
mMaxCropWindowWidth = maxWidth;
mMaxCropWindowHeight = maxHeight;
mScaleFactorWidth = scaleFactorWidth;
mScaleFactorHeight = scaleFactorHeight;
}
/**
* Set the variables to be used during crop window handling.
*/
public void setInitialAttributeValues(CropImageOptions options) {
mMinCropWindowWidth = options.minCropWindowWidth;
mMinCropWindowHeight = options.minCropWindowHeight;
mMinCropResultWidth = options.minCropResultWidth;
mMinCropResultHeight = options.minCropResultHeight;
mMaxCropResultWidth = options.maxCropResultWidth;
mMaxCropResultHeight = options.maxCropResultHeight;
}
/**
* Indicates whether the crop window is small enough that the guidelines should be shown. Public
* because this function is also used to determine if the center handle should be focused.
*
* @return boolean Whether the guidelines should be shown or not
*/
public boolean showGuidelines() {
return !(mEdges.width() < 100 || mEdges.height() < 100);
}
/**
* Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
* box, and the touch radius.
*
* @param x the x-coordinate of the touch point
* @param y the y-coordinate of the touch point
* @param targetRadius the target radius in pixels
* @return the Handle that was pressed; null if no Handle was pressed
*/
public CropWindowMoveHandler getMoveHandler(
float x, float y, float targetRadius, CropImageView.CropShape cropShape) {
CropWindowMoveHandler.Type type =
cropShape == CropImageView.CropShape.OVAL
? getOvalPressedMoveType(x, y)
: getRectanglePressedMoveType(x, y, targetRadius);
return type != null ? new CropWindowMoveHandler(type, this, x, y) : null;
}
/**
* Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
* box, and the touch radius.
*
* @param x the x-coordinate of the touch point
* @param y the y-coordinate of the touch point
* @param targetRadius the target radius in pixels
* @return the Handle that was pressed; null if no Handle was pressed
*/
private CropWindowMoveHandler.Type getRectanglePressedMoveType(
float x, float y, float targetRadius) {
CropWindowMoveHandler.Type moveType = null;
// Note: corner-handles take precedence, then side-handles, then center.
if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.left, mEdges.top, targetRadius)) {
moveType = CropWindowMoveHandler.Type.TOP_LEFT;
} else if (CropWindowHandler.isInCornerTargetZone(
x, y, mEdges.right, mEdges.top, targetRadius)) {
moveType = CropWindowMoveHandler.Type.TOP_RIGHT;
} else if (CropWindowHandler.isInCornerTargetZone(
x, y, mEdges.left, mEdges.bottom, targetRadius)) {
moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT;
} else if (CropWindowHandler.isInCornerTargetZone(
x, y, mEdges.right, mEdges.bottom, targetRadius)) {
moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT;
} else if (CropWindowHandler.isInCenterTargetZone(
x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom)
&& focusCenter()) {
moveType = CropWindowMoveHandler.Type.CENTER;
} else if (CropWindowHandler.isInHorizontalTargetZone(
x, y, mEdges.left, mEdges.right, mEdges.top, targetRadius)) {
moveType = CropWindowMoveHandler.Type.TOP;
} else if (CropWindowHandler.isInHorizontalTargetZone(
x, y, mEdges.left, mEdges.right, mEdges.bottom, targetRadius)) {
moveType = CropWindowMoveHandler.Type.BOTTOM;
} else if (CropWindowHandler.isInVerticalTargetZone(
x, y, mEdges.left, mEdges.top, mEdges.bottom, targetRadius)) {
moveType = CropWindowMoveHandler.Type.LEFT;
} else if (CropWindowHandler.isInVerticalTargetZone(
x, y, mEdges.right, mEdges.top, mEdges.bottom, targetRadius)) {
moveType = CropWindowMoveHandler.Type.RIGHT;
} else if (CropWindowHandler.isInCenterTargetZone(
x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom)
&& !focusCenter()) {
moveType = CropWindowMoveHandler.Type.CENTER;
}
return moveType;
}
/**
* Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
* box/oval, and the touch radius.
*
* @param x the x-coordinate of the touch point
* @param y the y-coordinate of the touch point
* @return the Handle that was pressed; null if no Handle was pressed
*/
private CropWindowMoveHandler.Type getOvalPressedMoveType(float x, float y) {
/*
Use a 6x6 grid system divided into 9 "handles", with the center the biggest region. While
this is not perfect, it's a good quick-to-ship approach.
TL T T T T TR
L C C C C R
L C C C C R
L C C C C R
L C C C C R
BL B B B B BR
*/
float cellLength = mEdges.width() / 6;
float leftCenter = mEdges.left + cellLength;
float rightCenter = mEdges.left + (5 * cellLength);
float cellHeight = mEdges.height() / 6;
float topCenter = mEdges.top + cellHeight;
float bottomCenter = mEdges.top + 5 * cellHeight;
CropWindowMoveHandler.Type moveType;
if (x < leftCenter) {
if (y < topCenter) {
moveType = CropWindowMoveHandler.Type.TOP_LEFT;
} else if (y < bottomCenter) {
moveType = CropWindowMoveHandler.Type.LEFT;
} else {
moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT;
}
} else if (x < rightCenter) {
if (y < topCenter) {
moveType = CropWindowMoveHandler.Type.TOP;
} else if (y < bottomCenter) {
moveType = CropWindowMoveHandler.Type.CENTER;
} else {
moveType = CropWindowMoveHandler.Type.BOTTOM;
}
} else {
if (y < topCenter) {
moveType = CropWindowMoveHandler.Type.TOP_RIGHT;
} else if (y < bottomCenter) {
moveType = CropWindowMoveHandler.Type.RIGHT;
} else {
moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT;
}
}
return moveType;
}
/**
* Determines if the cropper should focus on the center handle or the side handles. If it is a
* small image, focus on the center handle so the user can move it. If it is a large image, focus
* on the side handles so user can grab them. Corresponds to the appearance of the
* RuleOfThirdsGuidelines.
*
* @return true if it is small enough such that it should focus on the center; less than
* show_guidelines limit
*/
private boolean focusCenter() {
return !showGuidelines();
}
// endregion
}

@ -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.
* <br>
*/
final class CropWindowMoveHandler {
// region: Fields and Consts
/**
* Matrix used for rectangle rotation handling
*/
private static final Matrix MATRIX = new Matrix();
/**
* Minimum width in pixels that the crop window can get.
*/
private final float mMinCropWidth;
/**
* Minimum width in pixels that the crop window can get.
*/
private final float mMinCropHeight;
/**
* Maximum height in pixels that the crop window can get.
*/
private final float mMaxCropWidth;
/**
* Maximum height in pixels that the crop window can get.
*/
private final float mMaxCropHeight;
/**
* The type of crop window move that is handled.
*/
private final Type mType;
/**
* Holds the x and y offset between the exact touch location and the exact handle location that is
* activated. There may be an offset because we allow for some leeway (specified by mHandleRadius)
* in activating a handle. However, we want to maintain these offset values while the handle is
* being dragged so that the handle doesn't jump.
*/
private final PointF mTouchOffset = new PointF();
// endregion
/**
* @param edgeMoveType the type of move this handler is executing
* @param horizontalEdge the primary edge associated with this handle; may be null
* @param verticalEdge the secondary edge associated with this handle; may be null
* @param cropWindowHandler main crop window handle to get and update the crop window edges
* @param touchX the location of the initial toch possition to measure move distance
* @param touchY the location of the initial toch possition to measure move distance
*/
public CropWindowMoveHandler(
Type type, CropWindowHandler cropWindowHandler, float touchX, float touchY) {
mType = type;
mMinCropWidth = cropWindowHandler.getMinCropWidth();
mMinCropHeight = cropWindowHandler.getMinCropHeight();
mMaxCropWidth = cropWindowHandler.getMaxCropWidth();
mMaxCropHeight = cropWindowHandler.getMaxCropHeight();
calculateTouchOffset(cropWindowHandler.getRect(), touchX, touchY);
}
/**
* Calculates the aspect ratio given a rectangle.
*/
private static float calculateAspectRatio(float left, float top, float right, float bottom) {
return (right - left) / (bottom - top);
}
// region: Private methods
/**
* Updates the crop window by change in the toch location.<br>
* Move type handled by this instance, as initialized in creation, affects how the change in toch
* location changes the crop window position and size.<br>
* After the crop window position/size is changed by toch move it may result in values that
* vialate contraints: outside the bounds of the shown bitmap, smaller/larger than min/max size or
* missmatch in aspect ratio. So a series of fixes is executed on "secondary" edges to adjust it
* by the "primary" edge movement.<br>
* Primary is the edge directly affected by move type, secondary is the other edge.<br>
* The crop window is changed by directly setting the Edge coordinates.
*
* @param x the new x-coordinate of this handle
* @param y the new y-coordinate of this handle
* @param bounds the bounding rectangle of the image
* @param viewWidth The bounding image view width used to know the crop overlay is at view edges.
* @param viewHeight The bounding image view height used to know the crop overlay is at view
* edges.
* @param parentView the parent View containing the image
* @param snapMargin the maximum distance (in pixels) at which the crop window should snap to the
* image
* @param fixedAspectRatio is the aspect ration fixed and 'targetAspectRatio' should be used
* @param aspectRatio the aspect ratio to maintain
*/
public void move(
RectF rect,
float x,
float y,
RectF bounds,
int viewWidth,
int viewHeight,
float snapMargin,
boolean fixedAspectRatio,
float aspectRatio) {
// Adjust the coordinates for the finger position's offset (i.e. the
// distance from the initial touch to the precise handle location).
// We want to maintain the initial touch's distance to the pressed
// handle so that the crop window size does not "jump".
float adjX = x + mTouchOffset.x;
float adjY = y + mTouchOffset.y;
if (mType == Type.CENTER) {
moveCenter(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin);
} else {
if (fixedAspectRatio) {
moveSizeWithFixedAspectRatio(
rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin, aspectRatio);
} else {
moveSizeWithFreeAspectRatio(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin);
}
}
}
/**
* Calculates the offset of the touch point from the precise location of the specified handle.<br>
* Save these values in a member variable since we want to maintain this offset as we drag the
* handle.
*/
private void calculateTouchOffset(RectF rect, float touchX, float touchY) {
float touchOffsetX = 0;
float touchOffsetY = 0;
// Calculate the offset from the appropriate handle.
switch (mType) {
case TOP_LEFT:
touchOffsetX = rect.left - touchX;
touchOffsetY = rect.top - touchY;
break;
case TOP_RIGHT:
touchOffsetX = rect.right - touchX;
touchOffsetY = rect.top - touchY;
break;
case BOTTOM_LEFT:
touchOffsetX = rect.left - touchX;
touchOffsetY = rect.bottom - touchY;
break;
case BOTTOM_RIGHT:
touchOffsetX = rect.right - touchX;
touchOffsetY = rect.bottom - touchY;
break;
case LEFT:
touchOffsetX = rect.left - touchX;
touchOffsetY = 0;
break;
case TOP:
touchOffsetX = 0;
touchOffsetY = rect.top - touchY;
break;
case RIGHT:
touchOffsetX = rect.right - touchX;
touchOffsetY = 0;
break;
case BOTTOM:
touchOffsetX = 0;
touchOffsetY = rect.bottom - touchY;
break;
case CENTER:
touchOffsetX = rect.centerX() - touchX;
touchOffsetY = rect.centerY() - touchY;
break;
default:
break;
}
mTouchOffset.x = touchOffsetX;
mTouchOffset.y = touchOffsetY;
}
/**
* Center move only changes the position of the crop window without changing the size.
*/
private void moveCenter(
RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapRadius) {
float dx = x - rect.centerX();
float dy = y - rect.centerY();
if (rect.left + dx < 0
|| rect.right + dx > viewWidth
|| rect.left + dx < bounds.left
|| rect.right + dx > bounds.right) {
dx /= 1.05f;
mTouchOffset.x -= dx / 2;
}
if (rect.top + dy < 0
|| rect.bottom + dy > viewHeight
|| rect.top + dy < bounds.top
|| rect.bottom + dy > bounds.bottom) {
dy /= 1.05f;
mTouchOffset.y -= dy / 2;
}
rect.offset(dx, dy);
snapEdgesToBounds(rect, bounds, snapRadius);
}
/**
* Change the size of the crop window on the required edge (or edges for corner size move) without
* affecting "secondary" edges.<br>
* Only the primary edge(s) are fixed to stay within limits.
*/
private void moveSizeWithFreeAspectRatio(
RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapMargin) {
switch (mType) {
case TOP_LEFT:
adjustTop(rect, y, bounds, snapMargin, 0, false, false);
adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
break;
case TOP_RIGHT:
adjustTop(rect, y, bounds, snapMargin, 0, false, false);
adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
break;
case BOTTOM_LEFT:
adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
break;
case BOTTOM_RIGHT:
adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
break;
case LEFT:
adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
break;
case TOP:
adjustTop(rect, y, bounds, snapMargin, 0, false, false);
break;
case RIGHT:
adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
break;
case BOTTOM:
adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
break;
default:
break;
}
}
/**
* Change the size of the crop window on the required "primary" edge WITH affect to relevant
* "secondary" edge via aspect ratio.<br>
* Example: change in the left edge (primary) will affect top and bottom edges (secondary) to
* preserve the given aspect ratio.
*/
private void moveSizeWithFixedAspectRatio(
RectF rect,
float x,
float y,
RectF bounds,
int viewWidth,
int viewHeight,
float snapMargin,
float aspectRatio) {
switch (mType) {
case TOP_LEFT:
if (calculateAspectRatio(x, y, rect.right, rect.bottom) < aspectRatio) {
adjustTop(rect, y, bounds, snapMargin, aspectRatio, true, false);
adjustLeftByAspectRatio(rect, aspectRatio);
} else {
adjustLeft(rect, x, bounds, snapMargin, aspectRatio, true, false);
adjustTopByAspectRatio(rect, aspectRatio);
}
break;
case TOP_RIGHT:
if (calculateAspectRatio(rect.left, y, x, rect.bottom) < aspectRatio) {
adjustTop(rect, y, bounds, snapMargin, aspectRatio, false, true);
adjustRightByAspectRatio(rect, aspectRatio);
} else {
adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, true, false);
adjustTopByAspectRatio(rect, aspectRatio);
}
break;
case BOTTOM_LEFT:
if (calculateAspectRatio(x, rect.top, rect.right, y) < aspectRatio) {
adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, true, false);
adjustLeftByAspectRatio(rect, aspectRatio);
} else {
adjustLeft(rect, x, bounds, snapMargin, aspectRatio, false, true);
adjustBottomByAspectRatio(rect, aspectRatio);
}
break;
case BOTTOM_RIGHT:
if (calculateAspectRatio(rect.left, rect.top, x, y) < aspectRatio) {
adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, false, true);
adjustRightByAspectRatio(rect, aspectRatio);
} else {
adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, false, true);
adjustBottomByAspectRatio(rect, aspectRatio);
}
break;
case LEFT:
adjustLeft(rect, x, bounds, snapMargin, aspectRatio, true, true);
adjustTopBottomByAspectRatio(rect, bounds, aspectRatio);
break;
case TOP:
adjustTop(rect, y, bounds, snapMargin, aspectRatio, true, true);
adjustLeftRightByAspectRatio(rect, bounds, aspectRatio);
break;
case RIGHT:
adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, true, true);
adjustTopBottomByAspectRatio(rect, bounds, aspectRatio);
break;
case BOTTOM:
adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, true, true);
adjustLeftRightByAspectRatio(rect, bounds, aspectRatio);
break;
default:
break;
}
}
/**
* Check if edges have gone out of bounds (including snap margin), and fix if needed.
*/
private void snapEdgesToBounds(RectF edges, RectF bounds, float margin) {
if (edges.left < bounds.left + margin) {
edges.offset(bounds.left - edges.left, 0);
}
if (edges.top < bounds.top + margin) {
edges.offset(0, bounds.top - edges.top);
}
if (edges.right > bounds.right - margin) {
edges.offset(bounds.right - edges.right, 0);
}
if (edges.bottom > bounds.bottom - margin) {
edges.offset(0, bounds.bottom - edges.bottom);
}
}
/**
* Get the resulting x-position of the left edge of the crop window given the handle's position
* and the image's bounding box and snap radius.
*
* @param left the position that the left edge is dragged to
* @param bounds the bounding box of the image that is being cropped
* @param snapMargin the snap distance to the image edge (in pixels)
*/
private void adjustLeft(
RectF rect,
float left,
RectF bounds,
float snapMargin,
float aspectRatio,
boolean topMoves,
boolean bottomMoves) {
float newLeft = left;
if (newLeft < 0) {
newLeft /= 1.05f;
mTouchOffset.x -= newLeft / 1.1f;
}
if (newLeft < bounds.left) {
mTouchOffset.x -= (newLeft - bounds.left) / 2f;
}
if (newLeft - bounds.left < snapMargin) {
newLeft = bounds.left;
}
// Checks if the window is too small horizontally
if (rect.right - newLeft < mMinCropWidth) {
newLeft = rect.right - mMinCropWidth;
}
// Checks if the window is too large horizontally
if (rect.right - newLeft > mMaxCropWidth) {
newLeft = rect.right - mMaxCropWidth;
}
if (newLeft - bounds.left < snapMargin) {
newLeft = bounds.left;
}
// check vertical bounds if aspect ratio is in play
if (aspectRatio > 0) {
float newHeight = (rect.right - newLeft) / aspectRatio;
// Checks if the window is too small vertically
if (newHeight < mMinCropHeight) {
newLeft = Math.max(bounds.left, rect.right - mMinCropHeight * aspectRatio);
newHeight = (rect.right - newLeft) / aspectRatio;
}
// Checks if the window is too large vertically
if (newHeight > mMaxCropHeight) {
newLeft = Math.max(bounds.left, rect.right - mMaxCropHeight * aspectRatio);
newHeight = (rect.right - newLeft) / aspectRatio;
}
// if top AND bottom edge moves by aspect ratio check that it is within full height bounds
if (topMoves && bottomMoves) {
newLeft =
Math.max(newLeft, Math.max(bounds.left, rect.right - bounds.height() * aspectRatio));
} else {
// if top edge moves by aspect ratio check that it is within bounds
if (topMoves && rect.bottom - newHeight < bounds.top) {
newLeft = Math.max(bounds.left, rect.right - (rect.bottom - bounds.top) * aspectRatio);
newHeight = (rect.right - newLeft) / aspectRatio;
}
// if bottom edge moves by aspect ratio check that it is within bounds
if (bottomMoves && rect.top + newHeight > bounds.bottom) {
newLeft =
Math.max(
newLeft,
Math.max(bounds.left, rect.right - (bounds.bottom - rect.top) * aspectRatio));
}
}
}
rect.left = newLeft;
}
/**
* Get the resulting x-position of the right edge of the crop window given the handle's position
* and the image's bounding box and snap radius.
*
* @param right the position that the right edge is dragged to
* @param bounds the bounding box of the image that is being cropped
* @param viewWidth
* @param snapMargin the snap distance to the image edge (in pixels)
*/
private void adjustRight(
RectF rect,
float right,
RectF bounds,
int viewWidth,
float snapMargin,
float aspectRatio,
boolean topMoves,
boolean bottomMoves) {
float newRight = right;
if (newRight > viewWidth) {
newRight = viewWidth + (newRight - viewWidth) / 1.05f;
mTouchOffset.x -= (newRight - viewWidth) / 1.1f;
}
if (newRight > bounds.right) {
mTouchOffset.x -= (newRight - bounds.right) / 2f;
}
// If close to the edge
if (bounds.right - newRight < snapMargin) {
newRight = bounds.right;
}
// Checks if the window is too small horizontally
if (newRight - rect.left < mMinCropWidth) {
newRight = rect.left + mMinCropWidth;
}
// Checks if the window is too large horizontally
if (newRight - rect.left > mMaxCropWidth) {
newRight = rect.left + mMaxCropWidth;
}
// If close to the edge
if (bounds.right - newRight < snapMargin) {
newRight = bounds.right;
}
// check vertical bounds if aspect ratio is in play
if (aspectRatio > 0) {
float newHeight = (newRight - rect.left) / aspectRatio;
// Checks if the window is too small vertically
if (newHeight < mMinCropHeight) {
newRight = Math.min(bounds.right, rect.left + mMinCropHeight * aspectRatio);
newHeight = (newRight - rect.left) / aspectRatio;
}
// Checks if the window is too large vertically
if (newHeight > mMaxCropHeight) {
newRight = Math.min(bounds.right, rect.left + mMaxCropHeight * aspectRatio);
newHeight = (newRight - rect.left) / aspectRatio;
}
// if top AND bottom edge moves by aspect ratio check that it is within full height bounds
if (topMoves && bottomMoves) {
newRight =
Math.min(newRight, Math.min(bounds.right, rect.left + bounds.height() * aspectRatio));
} else {
// if top edge moves by aspect ratio check that it is within bounds
if (topMoves && rect.bottom - newHeight < bounds.top) {
newRight = Math.min(bounds.right, rect.left + (rect.bottom - bounds.top) * aspectRatio);
newHeight = (newRight - rect.left) / aspectRatio;
}
// if bottom edge moves by aspect ratio check that it is within bounds
if (bottomMoves && rect.top + newHeight > bounds.bottom) {
newRight =
Math.min(
newRight,
Math.min(bounds.right, rect.left + (bounds.bottom - rect.top) * aspectRatio));
}
}
}
rect.right = newRight;
}
/**
* Get the resulting y-position of the top edge of the crop window given the handle's position and
* the image's bounding box and snap radius.
*
* @param top the x-position that the top edge is dragged to
* @param bounds the bounding box of the image that is being cropped
* @param snapMargin the snap distance to the image edge (in pixels)
*/
private void adjustTop(
RectF rect,
float top,
RectF bounds,
float snapMargin,
float aspectRatio,
boolean leftMoves,
boolean rightMoves) {
float newTop = top;
if (newTop < 0) {
newTop /= 1.05f;
mTouchOffset.y -= newTop / 1.1f;
}
if (newTop < bounds.top) {
mTouchOffset.y -= (newTop - bounds.top) / 2f;
}
if (newTop - bounds.top < snapMargin) {
newTop = bounds.top;
}
// Checks if the window is too small vertically
if (rect.bottom - newTop < mMinCropHeight) {
newTop = rect.bottom - mMinCropHeight;
}
// Checks if the window is too large vertically
if (rect.bottom - newTop > mMaxCropHeight) {
newTop = rect.bottom - mMaxCropHeight;
}
if (newTop - bounds.top < snapMargin) {
newTop = bounds.top;
}
// check horizontal bounds if aspect ratio is in play
if (aspectRatio > 0) {
float newWidth = (rect.bottom - newTop) * aspectRatio;
// Checks if the crop window is too small horizontally due to aspect ratio adjustment
if (newWidth < mMinCropWidth) {
newTop = Math.max(bounds.top, rect.bottom - (mMinCropWidth / aspectRatio));
newWidth = (rect.bottom - newTop) * aspectRatio;
}
// Checks if the crop window is too large horizontally due to aspect ratio adjustment
if (newWidth > mMaxCropWidth) {
newTop = Math.max(bounds.top, rect.bottom - (mMaxCropWidth / aspectRatio));
newWidth = (rect.bottom - newTop) * aspectRatio;
}
// if left AND right edge moves by aspect ratio check that it is within full width bounds
if (leftMoves && rightMoves) {
newTop = Math.max(newTop, Math.max(bounds.top, rect.bottom - bounds.width() / aspectRatio));
} else {
// if left edge moves by aspect ratio check that it is within bounds
if (leftMoves && rect.right - newWidth < bounds.left) {
newTop = Math.max(bounds.top, rect.bottom - (rect.right - bounds.left) / aspectRatio);
newWidth = (rect.bottom - newTop) * aspectRatio;
}
// if right edge moves by aspect ratio check that it is within bounds
if (rightMoves && rect.left + newWidth > bounds.right) {
newTop =
Math.max(
newTop,
Math.max(bounds.top, rect.bottom - (bounds.right - rect.left) / aspectRatio));
}
}
}
rect.top = newTop;
}
/**
* Get the resulting y-position of the bottom edge of the crop window given the handle's position
* and the image's bounding box and snap radius.
*
* @param bottom the position that the bottom edge is dragged to
* @param bounds the bounding box of the image that is being cropped
* @param viewHeight
* @param snapMargin the snap distance to the image edge (in pixels)
*/
private void adjustBottom(
RectF rect,
float bottom,
RectF bounds,
int viewHeight,
float snapMargin,
float aspectRatio,
boolean leftMoves,
boolean rightMoves) {
float newBottom = bottom;
if (newBottom > viewHeight) {
newBottom = viewHeight + (newBottom - viewHeight) / 1.05f;
mTouchOffset.y -= (newBottom - viewHeight) / 1.1f;
}
if (newBottom > bounds.bottom) {
mTouchOffset.y -= (newBottom - bounds.bottom) / 2f;
}
if (bounds.bottom - newBottom < snapMargin) {
newBottom = bounds.bottom;
}
// Checks if the window is too small vertically
if (newBottom - rect.top < mMinCropHeight) {
newBottom = rect.top + mMinCropHeight;
}
// Checks if the window is too small vertically
if (newBottom - rect.top > mMaxCropHeight) {
newBottom = rect.top + mMaxCropHeight;
}
if (bounds.bottom - newBottom < snapMargin) {
newBottom = bounds.bottom;
}
// check horizontal bounds if aspect ratio is in play
if (aspectRatio > 0) {
float newWidth = (newBottom - rect.top) * aspectRatio;
// Checks if the window is too small horizontally
if (newWidth < mMinCropWidth) {
newBottom = Math.min(bounds.bottom, rect.top + mMinCropWidth / aspectRatio);
newWidth = (newBottom - rect.top) * aspectRatio;
}
// Checks if the window is too large horizontally
if (newWidth > mMaxCropWidth) {
newBottom = Math.min(bounds.bottom, rect.top + mMaxCropWidth / aspectRatio);
newWidth = (newBottom - rect.top) * aspectRatio;
}
// if left AND right edge moves by aspect ratio check that it is within full width bounds
if (leftMoves && rightMoves) {
newBottom =
Math.min(newBottom, Math.min(bounds.bottom, rect.top + bounds.width() / aspectRatio));
} else {
// if left edge moves by aspect ratio check that it is within bounds
if (leftMoves && rect.right - newWidth < bounds.left) {
newBottom = Math.min(bounds.bottom, rect.top + (rect.right - bounds.left) / aspectRatio);
newWidth = (newBottom - rect.top) * aspectRatio;
}
// if right edge moves by aspect ratio check that it is within bounds
if (rightMoves && rect.left + newWidth > bounds.right) {
newBottom =
Math.min(
newBottom,
Math.min(bounds.bottom, rect.top + (bounds.right - rect.left) / aspectRatio));
}
}
}
rect.bottom = newBottom;
}
/**
* Adjust left edge by current crop window height and the given aspect ratio, the right edge
* remains in possition while the left adjusts to keep aspect ratio to the height.
*/
private void adjustLeftByAspectRatio(RectF rect, float aspectRatio) {
rect.left = rect.right - rect.height() * aspectRatio;
}
/**
* Adjust top edge by current crop window width and the given aspect ratio, the bottom edge
* remains in possition while the top adjusts to keep aspect ratio to the width.
*/
private void adjustTopByAspectRatio(RectF rect, float aspectRatio) {
rect.top = rect.bottom - rect.width() / aspectRatio;
}
/**
* Adjust right edge by current crop window height and the given aspect ratio, the left edge
* remains in possition while the left adjusts to keep aspect ratio to the height.
*/
private void adjustRightByAspectRatio(RectF rect, float aspectRatio) {
rect.right = rect.left + rect.height() * aspectRatio;
}
/**
* Adjust bottom edge by current crop window width and the given aspect ratio, the top edge
* remains in possition while the top adjusts to keep aspect ratio to the width.
*/
private void adjustBottomByAspectRatio(RectF rect, float aspectRatio) {
rect.bottom = rect.top + rect.width() / aspectRatio;
}
/**
* Adjust left and right edges by current crop window height and the given aspect ratio, both
* right and left edges adjusts equally relative to center to keep aspect ratio to the height.
*/
private void adjustLeftRightByAspectRatio(RectF rect, RectF bounds, float aspectRatio) {
rect.inset((rect.width() - rect.height() * aspectRatio) / 2, 0);
if (rect.left < bounds.left) {
rect.offset(bounds.left - rect.left, 0);
}
if (rect.right > bounds.right) {
rect.offset(bounds.right - rect.right, 0);
}
}
/**
* Adjust top and bottom edges by current crop window width and the given aspect ratio, both top
* and bottom edges adjusts equally relative to center to keep aspect ratio to the width.
*/
private void adjustTopBottomByAspectRatio(RectF rect, RectF bounds, float aspectRatio) {
rect.inset(0, (rect.height() - rect.width() / aspectRatio) / 2);
if (rect.top < bounds.top) {
rect.offset(0, bounds.top - rect.top);
}
if (rect.bottom > bounds.bottom) {
rect.offset(0, bounds.bottom - rect.bottom);
}
}
// endregion
// region: Inner class: Type
/**
* The type of crop window move that is handled.
*/
public enum Type {
TOP_LEFT,
TOP_RIGHT,
BOTTOM_LEFT,
BOTTOM_RIGHT,
LEFT,
TOP,
RIGHT,
BOTTOM,
CENTER
}
// endregion
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save