diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/CropActivity.java b/gallery/src/main/java/cc/winboll/studio/gallery/CropActivity.java new file mode 100644 index 0000000..0027525 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/CropActivity.java @@ -0,0 +1,172 @@ +package cc.winboll.studio.gallery; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.RectF; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; +import cc.winboll.studio.libappbase.LogUtils; +import java.io.File; +import java.io.FileOutputStream; + +public class CropActivity extends AppCompatActivity { + public static final String TAG = "CropActivity"; + public static final String EXTRA_IMAGE_URI = "image_uri"; + public static final String EXTRA_IMAGE_PATH = "image_path"; + public static final String EXTRA_ALBUM_PATH = "album_path"; + public static final String EXTRA_CROP_WIDTH = "crop_width"; + public static final String EXTRA_CROP_HEIGHT = "crop_height"; + + private CropCanvasView cropCanvasView; + private CropOverlayView cropOverlay; + private Bitmap originalBitmap; + private String imagePath; + private String albumPath; + private int cropWidth = 240; + private int cropHeight = 120; + private float cropRatio = 2.0f; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_crop); + + imagePath = getIntent().getStringExtra(EXTRA_IMAGE_PATH); + albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH); + cropWidth = getIntent().getIntExtra(EXTRA_CROP_WIDTH, 240); + cropHeight = getIntent().getIntExtra(EXTRA_CROP_HEIGHT, 120); + + Preferences prefs = new Preferences(this); + if (cropWidth > 0 && cropHeight > 0) { + cropRatio = (float) cropWidth / cropHeight; + } else { + cropRatio = prefs.getCoverRatio(); + cropWidth = (int) (120 * cropRatio); + cropHeight = 120; + } + + findViewById(R.id.btn_close).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + + findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + saveCroppedCover(); + } + }); + + cropCanvasView = findViewById(R.id.crop_canvas_view); + cropOverlay = findViewById(R.id.crop_overlay); + loadImage(); + } + + private void loadImage() { + try { + if (imagePath != null && new File(imagePath).exists()) { + originalBitmap = BitmapFactory.decodeFile(imagePath); + } else { + Uri imageUri = getIntent().getParcelableExtra(EXTRA_IMAGE_URI); + if (imageUri != null) { + originalBitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri); + } + } + + if (originalBitmap != null) { + cropCanvasView.setImageBitmap(originalBitmap); + cropCanvasView.post(new Runnable() { + @Override + public void run() { + initCropOverlay(); + } + }); + } else { + Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show(); + finish(); + } + } catch (Exception e) { + LogUtils.e(TAG, "loadImage error: " + e.getMessage()); + Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show(); + finish(); + } + } + + private void initCropOverlay() { + cropCanvasView.initCanvas(originalBitmap.getWidth(), originalBitmap.getHeight(), cropRatio); + + int canvasW = cropCanvasView.getCanvasWidth(); + int canvasH = cropCanvasView.getCanvasHeight(); + + cropOverlay.setTargetRatio(cropRatio); + cropOverlay.initCanvas(canvasW, canvasH); + } + + private void saveCroppedCover() { + if (originalBitmap == null) { + Toast.makeText(this, "Failed to get image", Toast.LENGTH_SHORT).show(); + return; + } + + try { + RectF cropRect = cropOverlay.getCropRect(); + RectF canvasBounds = cropCanvasView.getCanvasBounds(); + RectF imageBounds = cropCanvasView.getImageBounds(); + + int imgW = originalBitmap.getWidth(); + int imgH = originalBitmap.getHeight(); + + float scaleX = (float) imgW / canvasBounds.width(); + float scaleY = (float) imgH / canvasBounds.height(); + + float canvasLeft = cropRect.left - canvasBounds.left; + float canvasTop = cropRect.top - canvasBounds.top; + + int cropX = (int) ((canvasLeft - imageBounds.left) * scaleX); + int cropY = (int) ((canvasTop - imageBounds.top) * scaleY); + int cropW = (int) (cropRect.width() * scaleX); + int cropH = (int) (cropRect.height() * scaleY); + + cropX = Math.max(0, Math.min(cropX, imgW - 1)); + cropY = Math.max(0, Math.min(cropY, imgH - 1)); + cropW = Math.max(1, Math.min(cropW, imgW - cropX)); + cropH = Math.max(1, Math.min(cropH, imgH - cropY)); + + Bitmap cropped = Bitmap.createBitmap(originalBitmap, cropX, cropY, cropW, cropH); + + File cacheDir = getCacheDir(); + File coverFile = new File(cacheDir, "cover_" + System.currentTimeMillis() + ".png"); + FileOutputStream fos = new FileOutputStream(coverFile); + cropped.compress(Bitmap.CompressFormat.PNG, 100, fos); + fos.close(); + + cropped.recycle(); + + AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this); + coverDbHelper.setCoverWithCrop(albumPath, imagePath, coverFile.getAbsolutePath()); + + Toast.makeText(this, "封面已保存", Toast.LENGTH_SHORT).show(); + setResult(RESULT_OK); + finish(); + } catch (Exception e) { + LogUtils.e(TAG, "saveCroppedCover error: " + e.getMessage()); + Toast.makeText(this, "Failed to save cover", Toast.LENGTH_SHORT).show(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (originalBitmap != null) { + originalBitmap.recycle(); + originalBitmap = null; + } + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/CropCanvasView.java b/gallery/src/main/java/cc/winboll/studio/gallery/CropCanvasView.java new file mode 100644 index 0000000..7f44b65 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/CropCanvasView.java @@ -0,0 +1,140 @@ +package cc.winboll.studio.gallery; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; + +public class CropCanvasView extends View { + private Paint imagePaint; + + private int imageWidth; + private int imageHeight; + private float coverRatio = 2.0f; + + private int extendHeight; + private int extendWidth; + private int canvasWidth; + private int canvasHeight; + + private RectF imageBounds = new RectF(); + private RectF canvasBounds = new RectF(); + private Bitmap originalBitmap; + + public CropCanvasView(Context context) { + super(context); + init(); + } + + public CropCanvasView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public CropCanvasView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + imagePaint = new Paint(); + imagePaint.setFilterBitmap(true); + } + + public void setImageBitmap(Bitmap bitmap) { + this.originalBitmap = bitmap; + } + + public void initCanvas(int imgWidth, int imgHeight, float ratio) { + this.imageWidth = imgWidth; + this.imageHeight = imgHeight; + this.coverRatio = ratio; + + float imgRatio = (float) imgWidth / imgHeight; + + if (coverRatio >= imgRatio) { + extendHeight = imgHeight; + extendWidth = (int) (imgHeight * coverRatio); + } else { + extendWidth = imgWidth; + extendHeight = (int) (imgWidth / coverRatio); + } + + canvasHeight = Math.max(imageHeight, extendHeight); + canvasWidth = Math.max(imageWidth, extendWidth); + + float left = (canvasWidth - imageWidth) / 2f; + float top = (canvasHeight - imageHeight) / 2f; + imageBounds.set(left, top, left + imageWidth, top + imageHeight); + canvasBounds.set(0, 0, canvasWidth, canvasHeight); + + requestLayout(); + invalidate(); + } + + public void scaleToView(int viewWidth, int viewHeight) { + if (viewWidth > 0 && viewHeight > 0 && canvasWidth > 0 && canvasHeight > 0) { + float scale = Math.max((float) viewWidth / canvasWidth, (float) viewHeight / canvasHeight); + canvasWidth = (int) (canvasWidth * scale); + canvasHeight = (int) (canvasHeight * scale); + + float left = (canvasWidth - imageWidth) / 2f; + float top = (canvasHeight - imageHeight) / 2f; + imageBounds.set(left, top, left + imageWidth, top + imageHeight); + canvasBounds.set(0, 0, canvasWidth, canvasHeight); + + requestLayout(); + invalidate(); + } + } + + public int getCanvasWidth() { + return canvasWidth; + } + + public int getCanvasHeight() { + return canvasHeight; + } + + public int getExtendWidth() { + return extendWidth; + } + + public int getExtendHeight() { + return extendHeight; + } + + public RectF getCanvasBounds() { + return new RectF(canvasBounds); + } + + public RectF getImageBounds() { + return new RectF(imageBounds); + } + + public float getCoverRatio() { + return coverRatio; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int w = canvasWidth > 0 ? canvasWidth : 0; + int h = canvasHeight > 0 ? canvasHeight : 0; + setMeasuredDimension(w, h); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + canvas.drawColor(Color.GREEN); + + if (originalBitmap != null && !originalBitmap.isRecycled()) { + canvas.drawBitmap(originalBitmap, imageBounds.left, imageBounds.top, imagePaint); + } + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/CropOverlayView.java b/gallery/src/main/java/cc/winboll/studio/gallery/CropOverlayView.java new file mode 100644 index 0000000..9d01dc3 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/CropOverlayView.java @@ -0,0 +1,224 @@ +package cc.winboll.studio.gallery; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +public class CropOverlayView extends View { + public interface OnCanvasSizeChangedListener { + void onCanvasSizeChanged(RectF canvasBounds); + } + + private OnCanvasSizeChangedListener canvasSizeChangedListener; + + private Paint borderPaint; + private Paint cornerPaint; + private RectF cropRect; + private int touchArea = 50; + + private float lastX, lastY; + private int activeCorner = -1; + private static final int CORNER_TOP_LEFT = 0; + private static final int CORNER_TOP_RIGHT = 1; + private static final int CORNER_BOTTOM_LEFT = 2; + private static final int CORNER_BOTTOM_RIGHT = 3; + private static final int CORNER_CENTER = 4; + + private float targetRatio = 2.0f; + private RectF canvasBounds = new RectF(); + private float minSize = 50; + + public CropOverlayView(Context context) { + super(context); + initPaints(); + } + + public CropOverlayView(Context context, AttributeSet attrs) { + super(context, attrs); + initPaints(); + } + + public CropOverlayView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initPaints(); + } + + private void initPaints() { + borderPaint = new Paint(); + borderPaint.setColor(Color.parseColor("#CCAA00")); + borderPaint.setStyle(Paint.Style.STROKE); + borderPaint.setStrokeWidth(3); + + cornerPaint = new Paint(); + cornerPaint.setColor(Color.WHITE); + cornerPaint.setStyle(Paint.Style.FILL); + } + + public void setTargetRatio(float ratio) { + this.targetRatio = ratio; + } + + public float getTargetRatio() { + return targetRatio; + } + + public void setOnCanvasSizeChangedListener(OnCanvasSizeChangedListener listener) { + this.canvasSizeChangedListener = listener; + } + + public void initCanvas(int canvasW, int canvasH) { + canvasBounds.set(0, 0, canvasW, canvasH); + + cropRect = new RectF(0, 0, canvasW, canvasH); + + if (canvasSizeChangedListener != null) { + canvasSizeChangedListener.onCanvasSizeChanged(new RectF(canvasBounds)); + } + invalidate(); + } + + public RectF getCropRect() { + return new RectF(cropRect); + } + + public RectF getCanvasBounds() { + return new RectF(canvasBounds); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + canvas.drawColor(Color.TRANSPARENT); + + if (cropRect != null) { + canvas.drawRect(cropRect, borderPaint); + + canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint); + canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint); + canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint); + canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (cropRect == null || canvasBounds.isEmpty()) return false; + + float x = event.getX(); + float y = event.getY(); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + lastX = x; + lastY = y; + activeCorner = getActiveCorner(x, y); + return true; + + case MotionEvent.ACTION_MOVE: + float dx = x - lastX; + float dy = y - lastY; + + if (activeCorner == CORNER_CENTER) { + float newLeft = cropRect.left + dx; + float newTop = cropRect.top + dy; + float newRight = cropRect.right + dx; + float newBottom = cropRect.bottom + dy; + + if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right) { + cropRect.left = newLeft; + cropRect.right = newRight; + } + if (newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) { + cropRect.top = newTop; + cropRect.bottom = newBottom; + } + } else if (activeCorner == CORNER_TOP_LEFT) { + adjustCorner(cropRect.left + dx, cropRect.top + dy, true, true); + } else if (activeCorner == CORNER_TOP_RIGHT) { + adjustCorner(cropRect.right + dx, cropRect.top + dy, false, true); + } else if (activeCorner == CORNER_BOTTOM_LEFT) { + adjustCorner(cropRect.left + dx, cropRect.bottom + dy, true, false); + } else if (activeCorner == CORNER_BOTTOM_RIGHT) { + adjustCorner(cropRect.right + dx, cropRect.bottom + dy, false, false); + } + + lastX = x; + lastY = y; + invalidate(); + return true; + + case MotionEvent.ACTION_UP: + activeCorner = -1; + return true; + } + return super.onTouchEvent(event); + } + + private void adjustCorner(float nx, float ny, boolean left, boolean top) { + float newWidth; + float newLeft = cropRect.left; + float newTop = cropRect.top; + float newRight = cropRect.right; + float newBottom = cropRect.bottom; + + if (left) { + newWidth = cropRect.width() - (nx - cropRect.left); + newLeft = Math.max(canvasBounds.left, Math.min(nx, cropRect.right - minSize)); + } else { + newWidth = nx - cropRect.left; + newRight = Math.min(canvasBounds.right, Math.max(nx, cropRect.left + minSize)); + newLeft = cropRect.left; + } + + float newHeight = newWidth / targetRatio; + + if (top) { + newTop = Math.max(canvasBounds.top, Math.min(cropRect.bottom - minSize, cropRect.bottom - newHeight)); + newBottom = newTop + newHeight; + } else { + newBottom = Math.min(canvasBounds.bottom, Math.max(cropRect.top + minSize, cropRect.top + newHeight)); + newTop = newBottom - newHeight; + } + + if (left) { + cropRect.left = newLeft; + cropRect.right = newLeft + newWidth; + } else { + cropRect.right = newRight; + cropRect.left = newRight - newWidth; + } + + if (top) { + cropRect.top = newTop; + cropRect.bottom = newTop + newHeight; + } else { + cropRect.bottom = newBottom; + cropRect.top = newBottom - newHeight; + } + } + + private int getActiveCorner(float x, float y) { + if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) { + return CORNER_TOP_LEFT; + } + if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) { + return CORNER_TOP_RIGHT; + } + if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) { + return CORNER_BOTTOM_LEFT; + } + if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) { + return CORNER_BOTTOM_RIGHT; + } + if (cropRect.contains(x, y)) { + return CORNER_CENTER; + } + return -1; + } +} \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_close.xml b/gallery/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..2789d9c --- /dev/null +++ b/gallery/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/gallery/src/main/res/layout/activity_crop.xml b/gallery/src/main/res/layout/activity_crop.xml new file mode 100644 index 0000000..73e87c3 --- /dev/null +++ b/gallery/src/main/res/layout/activity_crop.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/src/main/res/menu/menu_crop.xml b/gallery/src/main/res/menu/menu_crop.xml new file mode 100644 index 0000000..674a000 --- /dev/null +++ b/gallery/src/main/res/menu/menu_crop.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file