After Width: | Height: | Size: 156 KiB |
After Width: | Height: | Size: 153 KiB |
After Width: | Height: | Size: 136 KiB |
After Width: | Height: | Size: 158 KiB |
After Width: | Height: | Size: 152 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 148 KiB |
After Width: | Height: | Size: 162 KiB |
After Width: | Height: | Size: 156 KiB |
After Width: | Height: | Size: 156 KiB |
After Width: | Height: | Size: 155 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 150 KiB |
After Width: | Height: | Size: 156 KiB |
After Width: | Height: | Size: 99 KiB |
After Width: | Height: | Size: 102 KiB |
After Width: | Height: | Size: 156 KiB |
After Width: | Height: | Size: 157 KiB |
After Width: | Height: | Size: 114 KiB |
After Width: | Height: | Size: 156 KiB |
After Width: | Height: | Size: 159 KiB |
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 154 KiB |
@ -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
|
||||||
|
}
|
After Width: | Height: | Size: 164 KiB |
After Width: | Height: | Size: 169 KiB |
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>
|
After Width: | Height: | Size: 99 KiB |
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>
|
@ -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
|
||||||
|
}
|
After Width: | Height: | Size: 262 B |
After Width: | Height: | Size: 634 B |