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.os.Bundle;
import android.util.Base64; import android.util.Base64;
import android.util.Base64OutputStream; import android.util.Base64OutputStream;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -53,7 +54,7 @@ public class ProfileEditorFragment extends Fragment implements CropperUtils.Crop
private EditText mDefaultName, mDefaultJvmArgument; private EditText mDefaultName, mDefaultJvmArgument;
private TextView mDefaultPath, mDefaultVersion, mDefaultControl; private TextView mDefaultPath, mDefaultVersion, mDefaultControl;
private ImageView mProfileIcon; private ImageView mProfileIcon;
private ActivityResultLauncher<?> mCropperLauncher = CropperUtils.registerCropper(this, this); private final ActivityResultLauncher<?> mCropperLauncher = CropperUtils.registerCropper(this, this);
private List<String> mRenderNames; private List<String> mRenderNames;
@ -131,9 +132,7 @@ public class ProfileEditorFragment extends Fragment implements CropperUtils.Crop
})); }));
// Set up the icon change click listener // Set up the icon change click listener
mProfileIcon.setOnClickListener(v ->{ mProfileIcon.setOnClickListener(v -> CropperUtils.startCropper(mCropperLauncher));
CropperUtils.startCropper(mCropperLauncher, v.getContext());
});
@ -235,6 +234,7 @@ public class ProfileEditorFragment extends Fragment implements CropperUtils.Crop
@Override @Override
public void onCropped(Bitmap contentBitmap) { public void onCropped(Bitmap contentBitmap) {
mProfileIcon.setImageBitmap(contentBitmap); mProfileIcon.setImageBitmap(contentBitmap);
Log.i("bitmap", "w="+contentBitmap.getWidth() +" h="+contentBitmap.getHeight());
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (Base64OutputStream base64OutputStream = new Base64OutputStream(byteArrayOutputStream, Base64.NO_WRAP)) { try (Base64OutputStream base64OutputStream = new Base64OutputStream(byteArrayOutputStream, Base64.NO_WRAP)) {
contentBitmap.compress(Bitmap.CompressFormat.PNG, 60, base64OutputStream); contentBitmap.compress(Bitmap.CompressFormat.PNG, 60, base64OutputStream);

View File

@ -28,7 +28,7 @@ public class CropperView extends View {
private float mSelectionPadding; private float mSelectionPadding;
private int mLastTrackedPointer; private int mLastTrackedPointer;
private Paint mSelectionPaint; private Paint mSelectionPaint;
public CropperBehaviour cropperBehaviour = CropperBehaviour.DUMMY; private CropperBehaviour mCropperBehaviour = CropperBehaviour.DUMMY;
public CropperView(Context context) { public CropperView(Context context) {
super(context); super(context);
@ -74,7 +74,7 @@ public class CropperView extends View {
float multiplier = 0.005f; float multiplier = 0.005f;
float midpointX = (x1 + x2) / 2; float midpointX = (x1 + x2) / 2;
float midpointY = (y1 + y2) / 2; float midpointY = (y1 + y2) / 2;
cropperBehaviour.zoom(1 + distanceDelta * multiplier, midpointX, midpointY); mCropperBehaviour.zoom(1 + distanceDelta * multiplier, midpointX, midpointY);
} }
mLastDistance = distance; mLastDistance = distance;
return true; return true;
@ -106,7 +106,7 @@ public class CropperView extends View {
} }
if(trackedIndex != -1) { if(trackedIndex != -1) {
// If we still track out current pointer, pan the image by the movement delta // 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 { } else {
// Otherwise, mark the new tracked pointer without panning. // Otherwise, mark the new tracked pointer without panning.
mLastTrackedPointer = event.getPointerId(0); mLastTrackedPointer = event.getPointerId(0);
@ -121,7 +121,7 @@ public class CropperView extends View {
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
canvas.save(); canvas.save();
cropperBehaviour.drawPreHighlight(canvas); mCropperBehaviour.drawPreHighlight(canvas);
canvas.restore(); canvas.restore();
canvas.drawRect(mSelectionHighlight, mSelectionPaint); canvas.drawRect(mSelectionHighlight, mSelectionPaint);
} }
@ -150,7 +150,7 @@ public class CropperView extends View {
mSelectionRect.top = centerShiftY; mSelectionRect.top = centerShiftY;
mSelectionRect.right = centerShiftX + lesserDimension; mSelectionRect.right = centerShiftX + lesserDimension;
mSelectionRect.bottom = centerShiftY + lesserDimension; mSelectionRect.bottom = centerShiftY + lesserDimension;
cropperBehaviour.onSelectionRectUpdated(); mCropperBehaviour.onSelectionRectUpdated();
// Adjust the selection highlight rectangle to be bigger than the selection area // 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 // by the highlight thickness, to make sure that the entire inside of the selection highlight
// will fit into the image // will fit into the image
@ -169,7 +169,7 @@ public class CropperView extends View {
setMeasuredDimension(widthSize, heightSize); setMeasuredDimension(widthSize, heightSize);
return; return;
} }
int biggestAllowedDimension = cropperBehaviour.getLargestImageSide(); int biggestAllowedDimension = mCropperBehaviour.getLargestImageSide();
if(widthMode == MeasureSpec.EXACTLY) biggestAllowedDimension = widthSize; if(widthMode == MeasureSpec.EXACTLY) biggestAllowedDimension = widthSize;
if(heightMode == MeasureSpec.EXACTLY) biggestAllowedDimension = heightSize; if(heightMode == MeasureSpec.EXACTLY) biggestAllowedDimension = heightSize;
setMeasuredDimension( setMeasuredDimension(
@ -191,12 +191,21 @@ public class CropperView extends View {
return desired; return desired;
} }
public void setCropperBehaviour(CropperBehaviour cropperBehaviour) {
this.mCropperBehaviour = cropperBehaviour;
cropperBehaviour.onSelectionRectUpdated();
}
public void resetTransforms() {
mCropperBehaviour.resetTransforms();
}
@CallSuper @CallSuper
protected void reset() { protected void reset() {
mLastDistance = -1; mLastDistance = -1;
} }
public Bitmap crop(int targetMaxSide) { 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.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.os.Handler; import android.os.Handler;
import android.os.Looper;
import net.kdt.pojavlaunch.PojavApplication; import net.kdt.pojavlaunch.PojavApplication;
import net.kdt.pojavlaunch.modloaders.modpacks.SelfReferencingFuture; 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 final RectF mOverlayDst = new RectF(0, 0, 0, 0);
private boolean mRequiresOverlayBitmap; private boolean mRequiresOverlayBitmap;
private final Matrix mDecoderPrescaleMatrix = new Matrix(); private final Matrix mDecoderPrescaleMatrix = new Matrix();
private final Handler mHiresLoadHandler = new Handler(); private final Handler mHiresLoadHandler = new Handler(Looper.getMainLooper());
private Future<?> mDecodeFuture; private Future<?> mDecodeFuture;
private final Runnable mHiresLoadRunnable = ()->{ private final Runnable mHiresLoadRunnable = ()->{
RectF subsectionRect = new RectF(0,0, mHostView.getWidth(), mHostView.getHeight()); 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.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import net.kdt.pojavlaunch.PojavApplication;
import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.imgcropper.BitmapCropBehaviour; import net.kdt.pojavlaunch.imgcropper.BitmapCropBehaviour;
import net.kdt.pojavlaunch.imgcropper.CropperBehaviour; import net.kdt.pojavlaunch.imgcropper.CropperBehaviour;
import net.kdt.pojavlaunch.imgcropper.CropperView; import net.kdt.pojavlaunch.imgcropper.CropperView;
@ -43,48 +45,62 @@ public class CropperUtils {
builder.setNegativeButton(android.R.string.cancel, null); builder.setNegativeButton(android.R.string.cancel, null);
AlertDialog dialog = builder.show(); AlertDialog dialog = builder.show();
CropperView cropImageView = dialog.findViewById(R.id.crop_dialog_view); CropperView cropImageView = dialog.findViewById(R.id.crop_dialog_view);
View finishProgressBar = dialog.findViewById(R.id.crop_dialog_progressbar);
assert cropImageView != null; assert cropImageView != null;
try { assert finishProgressBar != null;
try (InputStream inputStream = contentResolver.openInputStream(selectedUri)) { bindViews(dialog, cropImageView);
if(inputStream == null) return; // The provider has crashed, there is no point in trying again. dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v->{
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);
dialog.dismiss(); 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, private static void finishViewSetup(CropperView cropImageView, CropperBehaviour cropBehaviour) {
CropperView cropImageView, Tools.runOnUiThread(()->{
CropperBehaviour cropBehaviour, cropImageView.setCropperBehaviour(cropBehaviour);
CropperListener cropperListener) { cropImageView.requestLayout();
cropImageView.cropperBehaviour = cropBehaviour;
cropImageView.requestLayout();
bindViews(dialog, cropImageView);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v->{
dialog.dismiss();
cropperListener.onCropped(cropImageView.crop(256));
}); });
} }
@ -102,12 +118,12 @@ public class CropperUtils {
imageCropperView.verticalLock = verticalLock.isChecked() imageCropperView.verticalLock = verticalLock.isChecked()
); );
reset.setOnClickListener(v-> reset.setOnClickListener(v->
imageCropperView.cropperBehaviour.resetTransforms() imageCropperView.resetTransforms()
); );
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static void startCropper(ActivityResultLauncher<?> resultLauncher, Context context) { public static void startCropper(ActivityResultLauncher<?> resultLauncher) {
ActivityResultLauncher<String[]> realResultLauncher = ActivityResultLauncher<String[]> realResultLauncher =
(ActivityResultLauncher<String[]>) resultLauncher; (ActivityResultLauncher<String[]>) resultLauncher;
realResultLauncher.launch(new String[]{"image/*"}); realResultLauncher.launch(new String[]{"image/*"});

View File

@ -1,30 +1,25 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" 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_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:orientation="vertical">
<net.kdt.pojavlaunch.imgcropper.CropperView <net.kdt.pojavlaunch.imgcropper.CropperView
android:id="@+id/crop_dialog_view" android:id="@+id/crop_dialog_view"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="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" />
<LinearLayout <LinearLayout
android:id="@+id/crop_dialog_button_layout" android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@+id/crop_dialog_view" android:layout_marginStart="8dp"
app:layout_constraintStart_toStartOf="@+id/crop_dialog_view" android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="@+id/crop_dialog_view"> android:layout_marginBottom="8dp">
<ToggleButton <ToggleButton
android:id="@+id/crop_dialog_hlock" android:id="@+id/crop_dialog_hlock"
android:layout_weight="1" android:layout_weight="1"
@ -46,5 +41,10 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:text="@string/cropper_reset"/> android:text="@string/cropper_reset"/>
</LinearLayout> </LinearLayout>
<ProgressBar
</androidx.constraintlayout.widget.ConstraintLayout> 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>