/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.gallery3d.photoeditor.actions; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.MotionEvent; import com.android.gallery3d.R; /** * A view that tracks touch motions and adjusts crop bounds accordingly. */ class CropView extends FullscreenToolView { /** * Listener of crop bounds. */ public interface OnCropChangeListener { void onCropChanged(RectF cropBounds, boolean fromUser); } private static final int MOVE_LEFT = 1; private static final int MOVE_TOP = 2; private static final int MOVE_RIGHT = 4; private static final int MOVE_BOTTOM = 8; private static final int MOVE_BLOCK = 16; private static final int MIN_CROP_WIDTH_HEIGHT = 2; private static final int TOUCH_TOLERANCE = 25; private static final int SHADOW_ALPHA = 160; private final Paint borderPaint; private final Drawable cropIndicator; private final int indicatorSize; private final RectF cropBounds = new RectF(0, 0, 1, 1); private float lastX; private float lastY; private int movingEdges; private OnCropChangeListener listener; public CropView(Context context, AttributeSet attrs) { super(context, attrs); Resources resources = context.getResources(); cropIndicator = resources.getDrawable(R.drawable.camera_crop_holo); indicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size); int borderColor = resources.getColor(R.color.opaque_cyan); borderPaint = new Paint(); borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setColor(borderColor); borderPaint.setStrokeWidth(2f); } public void setOnCropChangeListener(OnCropChangeListener listener) { this.listener = listener; } private void refreshByCropChange(boolean fromUser) { if (listener != null) { listener.onCropChanged(new RectF(cropBounds), fromUser); } invalidate(); } /** * Sets cropped bounds; modifies the bounds if it's smaller than the allowed dimensions. */ public void setCropBounds(RectF bounds) { // Avoid cropping smaller than minimum width or height. if (bounds.width() * getPhotoWidth() < MIN_CROP_WIDTH_HEIGHT) { bounds.set(0, bounds.top, 1, bounds.bottom); } if (bounds.height() * getPhotoHeight() < MIN_CROP_WIDTH_HEIGHT) { bounds.set(bounds.left, 0, bounds.right, 1); } cropBounds.set(bounds); refreshByCropChange(false); } private RectF getCropBoundsDisplayed() { float width = displayBounds.width(); float height = displayBounds.height(); RectF cropped = new RectF(cropBounds.left * width, cropBounds.top * height, cropBounds.right * width, cropBounds.bottom * height); cropped.offset(displayBounds.left, displayBounds.top); return cropped; } private void detectMovingEdges(float x, float y) { RectF cropped = getCropBoundsDisplayed(); movingEdges = 0; // Check left or right. float left = Math.abs(x - cropped.left); float right = Math.abs(x - cropped.right); if ((left <= TOUCH_TOLERANCE) && (left < right)) { movingEdges |= MOVE_LEFT; } else if (right <= TOUCH_TOLERANCE) { movingEdges |= MOVE_RIGHT; } // Check top or bottom. float top = Math.abs(y - cropped.top); float bottom = Math.abs(y - cropped.bottom); if ((top <= TOUCH_TOLERANCE) & (top < bottom)) { movingEdges |= MOVE_TOP; } else if (bottom <= TOUCH_TOLERANCE) { movingEdges |= MOVE_BOTTOM; } // Check inside block. if (cropped.contains(x, y) && (movingEdges == 0)) { movingEdges = MOVE_BLOCK; } invalidate(); } private void moveEdges(float deltaX, float deltaY) { RectF cropped = getCropBoundsDisplayed(); if (movingEdges == MOVE_BLOCK) { // Move the whole cropped bounds within the photo display bounds. deltaX = (deltaX > 0) ? Math.min(displayBounds.right - cropped.right, deltaX) : Math.max(displayBounds.left - cropped.left, deltaX); deltaY = (deltaY > 0) ? Math.min(displayBounds.bottom - cropped.bottom, deltaY) : Math.max(displayBounds.top - cropped.top, deltaY); cropped.offset(deltaX, deltaY); } else { // Adjust cropped bound dimensions within the photo display bounds. float minWidth = MIN_CROP_WIDTH_HEIGHT * displayBounds.width() / getPhotoWidth(); float minHeight = MIN_CROP_WIDTH_HEIGHT * displayBounds.height() / getPhotoHeight(); if ((movingEdges & MOVE_LEFT) != 0) { cropped.left = Math.min(cropped.left + deltaX, cropped.right - minWidth); } if ((movingEdges & MOVE_TOP) != 0) { cropped.top = Math.min(cropped.top + deltaY, cropped.bottom - minHeight); } if ((movingEdges & MOVE_RIGHT) != 0) { cropped.right = Math.max(cropped.right + deltaX, cropped.left + minWidth); } if ((movingEdges & MOVE_BOTTOM) != 0) { cropped.bottom = Math.max(cropped.bottom + deltaY, cropped.top + minHeight); } cropped.intersect(displayBounds); } mapPhotoRect(cropped, cropBounds); refreshByCropChange(true); } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); if (isEnabled()) { float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: detectMovingEdges(x, y); lastX = x; lastY = y; break; case MotionEvent.ACTION_MOVE: if (movingEdges != 0) { moveEdges(x - lastX, y - lastY); } lastX = x; lastY = y; break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: movingEdges = 0; invalidate(); break; } } return true; } private void drawIndicator(Canvas canvas, Drawable indicator, float centerX, float centerY) { int left = (int) centerX - indicatorSize / 2; int top = (int) centerY - indicatorSize / 2; indicator.setBounds(left, top, left + indicatorSize, top + indicatorSize); indicator.draw(canvas); } private void drawShadow(Canvas canvas, float left, float top, float right, float bottom) { canvas.save(); canvas.clipRect(left, top, right, bottom); canvas.drawARGB(SHADOW_ALPHA, 0, 0, 0); canvas.restore(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Draw shadow on non-cropped bounds and the border around cropped bounds. RectF cropped = getCropBoundsDisplayed(); drawShadow(canvas, displayBounds.left, displayBounds.top, displayBounds.right, cropped.top); drawShadow(canvas, displayBounds.left, cropped.top, cropped.left, displayBounds.bottom); drawShadow(canvas, cropped.right, cropped.top, displayBounds.right, displayBounds.bottom); drawShadow(canvas, cropped.left, cropped.bottom, cropped.right, displayBounds.bottom); canvas.drawRect(cropped, borderPaint); boolean notMoving = movingEdges == 0; if (((movingEdges & MOVE_TOP) != 0) || notMoving) { drawIndicator(canvas, cropIndicator, cropped.centerX(), cropped.top); } if (((movingEdges & MOVE_BOTTOM) != 0) || notMoving) { drawIndicator(canvas, cropIndicator, cropped.centerX(), cropped.bottom); } if (((movingEdges & MOVE_LEFT) != 0) || notMoving) { drawIndicator(canvas, cropIndicator, cropped.left, cropped.centerY()); } if (((movingEdges & MOVE_RIGHT) != 0) || notMoving) { drawIndicator(canvas, cropIndicator, cropped.right, cropped.centerY()); } } }