Feat[cropper]: asynchronously load the image and change output resolution

This commit is contained in:
artdeell 2024-01-27 11:52:22 +03:00 committed by Maksim Belov
parent 11ec17b410
commit bc1fdaf3ec
5 changed files with 93 additions and 67 deletions

View File

@ -5,6 +5,7 @@ import android.graphics.Bitmap;
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;
@ -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;
@ -131,9 +132,7 @@ public class ProfileEditorFragment extends Fragment implements CropperUtils.Crop
}));
// Set up the icon change click listener
mProfileIcon.setOnClickListener(v ->{
CropperUtils.startCropper(mCropperLauncher, v.getContext());
});
mProfileIcon.setOnClickListener(v -> CropperUtils.startCropper(mCropperLauncher));
@ -235,6 +234,7 @@ public class ProfileEditorFragment extends Fragment implements CropperUtils.Crop
@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);

View File

@ -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);
@ -74,7 +74,7 @@ public class CropperView extends View {
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;
@ -106,7 +106,7 @@ public class CropperView extends View {
}
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);
@ -121,7 +121,7 @@ public class CropperView extends View {
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
cropperBehaviour.drawPreHighlight(canvas);
mCropperBehaviour.drawPreHighlight(canvas);
canvas.restore();
canvas.drawRect(mSelectionHighlight, mSelectionPaint);
}
@ -150,7 +150,7 @@ public class CropperView extends View {
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
@ -169,7 +169,7 @@ public class CropperView extends View {
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(
@ -191,12 +191,21 @@ public class CropperView extends View {
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);
}
}

View File

@ -8,6 +8,7 @@ import android.graphics.Matrix;
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;
@ -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());

View File

@ -15,7 +15,9 @@ import androidx.activity.result.contract.ActivityResultContracts;
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;
@ -43,48 +45,62 @@ public class CropperUtils {
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();
});
}
@ -102,12 +118,12 @@ public class CropperUtils {
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/*"});

View File

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