Skip to content

Commit

Permalink
Feat[cropper]: asynchronously load the image and change output resolu…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
artdeell committed Jan 27, 2024
1 parent a7f036a commit bb16bff
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import android.os.Bundle;
import android.util.Base64;
import android.util.Base64OutputStream;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
Expand Down Expand Up @@ -53,7 +54,7 @@ public class ProfileEditorFragment extends Fragment implements CropperUtils.Crop
private EditText mDefaultName, mDefaultJvmArgument;
private TextView mDefaultPath, mDefaultVersion, mDefaultControl;
private ImageView mProfileIcon;
private ActivityResultLauncher<?> mCropperLauncher = CropperUtils.registerCropper(this, this);
private final ActivityResultLauncher<?> mCropperLauncher = CropperUtils.registerCropper(this, this);

private List<String> mRenderNames;

Expand Down Expand Up @@ -131,9 +132,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
}));

// Set up the icon change click listener
mProfileIcon.setOnClickListener(v ->{
CropperUtils.startCropper(mCropperLauncher, v.getContext());
});
mProfileIcon.setOnClickListener(v -> CropperUtils.startCropper(mCropperLauncher));



Expand Down Expand Up @@ -235,6 +234,7 @@ private void save(){
@Override
public void onCropped(Bitmap contentBitmap) {
mProfileIcon.setImageBitmap(contentBitmap);
Log.i("bitmap", "w="+contentBitmap.getWidth() +" h="+contentBitmap.getHeight());
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (Base64OutputStream base64OutputStream = new Base64OutputStream(byteArrayOutputStream, Base64.NO_WRAP)) {
contentBitmap.compress(Bitmap.CompressFormat.PNG, 60, base64OutputStream);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class CropperView extends View {
private float mSelectionPadding;
private int mLastTrackedPointer;
private Paint mSelectionPaint;
public CropperBehaviour cropperBehaviour = CropperBehaviour.DUMMY;
private CropperBehaviour mCropperBehaviour = CropperBehaviour.DUMMY;

public CropperView(Context context) {
super(context);
Expand Down Expand Up @@ -74,7 +74,7 @@ public boolean dispatchGenericMotionEvent(MotionEvent event) {
float multiplier = 0.005f;
float midpointX = (x1 + x2) / 2;
float midpointY = (y1 + y2) / 2;
cropperBehaviour.zoom(1 + distanceDelta * multiplier, midpointX, midpointY);
mCropperBehaviour.zoom(1 + distanceDelta * multiplier, midpointX, midpointY);
}
mLastDistance = distance;
return true;
Expand Down Expand Up @@ -106,7 +106,7 @@ public boolean dispatchGenericMotionEvent(MotionEvent event) {
}
if(trackedIndex != -1) {
// If we still track out current pointer, pan the image by the movement delta
cropperBehaviour.pan(x1 - mLastTouchX, y1 - mLastTouchY);
mCropperBehaviour.pan(x1 - mLastTouchX, y1 - mLastTouchY);
} else {
// Otherwise, mark the new tracked pointer without panning.
mLastTrackedPointer = event.getPointerId(0);
Expand All @@ -121,7 +121,7 @@ public boolean dispatchGenericMotionEvent(MotionEvent event) {
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
cropperBehaviour.drawPreHighlight(canvas);
mCropperBehaviour.drawPreHighlight(canvas);
canvas.restore();
canvas.drawRect(mSelectionHighlight, mSelectionPaint);
}
Expand Down Expand Up @@ -150,7 +150,7 @@ protected void onSizeChanged(int w, int h, int oldW, int oldH) {
mSelectionRect.top = centerShiftY;
mSelectionRect.right = centerShiftX + lesserDimension;
mSelectionRect.bottom = centerShiftY + lesserDimension;
cropperBehaviour.onSelectionRectUpdated();
mCropperBehaviour.onSelectionRectUpdated();
// Adjust the selection highlight rectangle to be bigger than the selection area
// by the highlight thickness, to make sure that the entire inside of the selection highlight
// will fit into the image
Expand All @@ -169,7 +169,7 @@ protected void onMeasure(int widthSpec, int heightSpec) {
setMeasuredDimension(widthSize, heightSize);
return;
}
int biggestAllowedDimension = cropperBehaviour.getLargestImageSide();
int biggestAllowedDimension = mCropperBehaviour.getLargestImageSide();
if(widthMode == MeasureSpec.EXACTLY) biggestAllowedDimension = widthSize;
if(heightMode == MeasureSpec.EXACTLY) biggestAllowedDimension = heightSize;
setMeasuredDimension(
Expand All @@ -191,12 +191,21 @@ private int pickDesiredDimension(int mode, int size, int desired) {
return desired;
}

public void setCropperBehaviour(CropperBehaviour cropperBehaviour) {
this.mCropperBehaviour = cropperBehaviour;
cropperBehaviour.onSelectionRectUpdated();
}

public void resetTransforms() {
mCropperBehaviour.resetTransforms();
}


@CallSuper
protected void reset() {
mLastDistance = -1;
}
public Bitmap crop(int targetMaxSide) {
return cropperBehaviour.crop(targetMaxSide);
return mCropperBehaviour.crop(targetMaxSide);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Looper;

import net.kdt.pojavlaunch.PojavApplication;
import net.kdt.pojavlaunch.modloaders.modpacks.SelfReferencingFuture;
Expand All @@ -21,7 +22,7 @@ public class RegionDecoderCropBehaviour extends BitmapCropBehaviour {
private final RectF mOverlayDst = new RectF(0, 0, 0, 0);
private boolean mRequiresOverlayBitmap;
private final Matrix mDecoderPrescaleMatrix = new Matrix();
private final Handler mHiresLoadHandler = new Handler();
private final Handler mHiresLoadHandler = new Handler(Looper.getMainLooper());
private Future<?> mDecodeFuture;
private final Runnable mHiresLoadRunnable = ()->{
RectF subsectionRect = new RectF(0,0, mHostView.getWidth(), mHostView.getHeight());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;

import net.kdt.pojavlaunch.PojavApplication;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.imgcropper.BitmapCropBehaviour;
import net.kdt.pojavlaunch.imgcropper.CropperBehaviour;
import net.kdt.pojavlaunch.imgcropper.CropperView;
Expand Down Expand Up @@ -43,48 +45,62 @@ private static void openCropperDialog(Context context, Uri selectedUri,
builder.setNegativeButton(android.R.string.cancel, null);
AlertDialog dialog = builder.show();
CropperView cropImageView = dialog.findViewById(R.id.crop_dialog_view);
View finishProgressBar = dialog.findViewById(R.id.crop_dialog_progressbar);
assert cropImageView != null;
try {
try (InputStream inputStream = contentResolver.openInputStream(selectedUri)) {
if(inputStream == null) return; // The provider has crashed, there is no point in trying again.
try {
BitmapRegionDecoder regionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
RegionDecoderCropBehaviour cropBehaviour = new RegionDecoderCropBehaviour(cropImageView);
cropBehaviour.loadRegionDecoder(regionDecoder);
finishViewSetup(dialog, cropImageView, cropBehaviour, cropperListener);
return;
}catch (IOException e) {
// Catch IOE here to detect the case when BitmapRegionDecoder does not support this image format.
// If it does not, we will just have to load the bitmap in full resolution using BitmapFactory.
Log.w("CropperUtils", "Failed to load image into BitmapRegionDecoder", e);
}
}
// We can safely re-open the stream here as ACTION_OPEN_DOCUMENT grants us long-term access
// to the file that we have picked.
try (InputStream inputStream = contentResolver.openInputStream(selectedUri)) {
if(inputStream == null) return;
Bitmap originalBitmap = BitmapFactory.decodeStream(inputStream);
BitmapCropBehaviour cropBehaviour = new BitmapCropBehaviour(cropImageView);
cropBehaviour.loadBitmap(originalBitmap);
finishViewSetup(dialog, cropImageView, cropBehaviour, cropperListener);
}
}catch (Exception e){
cropperListener.onFailed(e);
assert finishProgressBar != null;
bindViews(dialog, cropImageView);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v->{
dialog.dismiss();
// I chose 70 dp here because it resolves to 192x192 on my device
// (which has a typical screen density of 395 dpi)
cropperListener.onCropped(cropImageView.crop((int) Tools.dpToPx(70)));
});
PojavApplication.sExecutorService.execute(()->{
try {
loadBehaviour(cropImageView, contentResolver, selectedUri);
Tools.runOnUiThread(()->finishProgressBar.setVisibility(View.GONE));
}catch (Exception e){ Tools.runOnUiThread(()->{
cropperListener.onFailed(e);
dialog.dismiss();
});}
});

}


private static void loadBehaviour(CropperView cropImageView,
ContentResolver contentResolver,
Uri selectedUri) throws Exception {
try (InputStream inputStream = contentResolver.openInputStream(selectedUri)) {
if(inputStream == null) return; // The provider has crashed, there is no point in trying again.
try {
BitmapRegionDecoder regionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
RegionDecoderCropBehaviour cropBehaviour = new RegionDecoderCropBehaviour(cropImageView);
cropBehaviour.loadRegionDecoder(regionDecoder);
finishViewSetup(cropImageView, cropBehaviour);
return;
}catch (IOException e) {
// Catch IOE here to detect the case when BitmapRegionDecoder does not support this image format.
// If it does not, we will just have to load the bitmap in full resolution using BitmapFactory.
Log.w("CropperUtils", "Failed to load image into BitmapRegionDecoder", e);
}
}
// We can safely re-open the stream here as ACTION_OPEN_DOCUMENT grants us long-term access
// to the file that we have picked.
try (InputStream inputStream = contentResolver.openInputStream(selectedUri)) {
if(inputStream == null) return;
Bitmap originalBitmap = BitmapFactory.decodeStream(inputStream);
BitmapCropBehaviour cropBehaviour = new BitmapCropBehaviour(cropImageView);
cropBehaviour.loadBitmap(originalBitmap);
finishViewSetup(cropImageView,cropBehaviour);
}
}


private static void finishViewSetup(AlertDialog dialog,
CropperView cropImageView,
CropperBehaviour cropBehaviour,
CropperListener cropperListener) {
cropImageView.cropperBehaviour = cropBehaviour;
cropImageView.requestLayout();
bindViews(dialog, cropImageView);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v->{
dialog.dismiss();
cropperListener.onCropped(cropImageView.crop(256));
private static void finishViewSetup(CropperView cropImageView, CropperBehaviour cropBehaviour) {
Tools.runOnUiThread(()->{
cropImageView.setCropperBehaviour(cropBehaviour);
cropImageView.requestLayout();
});
}

Expand All @@ -102,12 +118,12 @@ private static void bindViews(AlertDialog alertDialog, CropperView imageCropperV
imageCropperView.verticalLock = verticalLock.isChecked()
);
reset.setOnClickListener(v->
imageCropperView.cropperBehaviour.resetTransforms()
imageCropperView.resetTransforms()
);
}

@SuppressWarnings("unchecked")
public static void startCropper(ActivityResultLauncher<?> resultLauncher, Context context) {
public static void startCropper(ActivityResultLauncher<?> resultLauncher) {
ActivityResultLauncher<String[]> realResultLauncher =
(ActivityResultLauncher<String[]>) resultLauncher;
realResultLauncher.launch(new String[]{"image/*"});
Expand Down
34 changes: 17 additions & 17 deletions app_pojavlauncher/src/main/res/layout/dialog_cropper.xml
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
<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:layout_height="wrap_content"
android:orientation="vertical">
<net.kdt.pojavlaunch.imgcropper.CropperView
android:id="@+id/crop_dialog_view"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/crop_dialog_button_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:layout_marginBottom="8dp" />
<LinearLayout
android:id="@+id/crop_dialog_button_layout"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@+id/crop_dialog_view"
app:layout_constraintStart_toStartOf="@+id/crop_dialog_view"
app:layout_constraintEnd_toEndOf="@+id/crop_dialog_view">
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp">
<ToggleButton
android:id="@+id/crop_dialog_hlock"
android:layout_weight="1"
Expand All @@ -46,5 +41,10 @@
android:layout_height="match_parent"
android:text="@string/cropper_reset"/>
</LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:id="@+id/crop_dialog_progressbar"
android:indeterminate="true"/>
</LinearLayout>

0 comments on commit bb16bff

Please sign in to comment.