1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.gallery3d.photoeditor.actions; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Canvas; 22 import android.graphics.Paint; 23 import android.graphics.RectF; 24 import android.graphics.drawable.Drawable; 25 import android.util.AttributeSet; 26 import android.view.MotionEvent; 27 28 import com.android.gallery3d.R; 29 30 /** 31 * A view that tracks touch motions and adjusts crop bounds accordingly. 32 */ 33 class CropView extends FullscreenToolView { 34 35 /** 36 * Listener of crop bounds. 37 */ 38 public interface OnCropChangeListener { 39 onCropChanged(RectF cropBounds, boolean fromUser)40 void onCropChanged(RectF cropBounds, boolean fromUser); 41 } 42 43 private static final int MOVE_LEFT = 1; 44 private static final int MOVE_TOP = 2; 45 private static final int MOVE_RIGHT = 4; 46 private static final int MOVE_BOTTOM = 8; 47 private static final int MOVE_BLOCK = 16; 48 49 private static final int MIN_CROP_WIDTH_HEIGHT = 2; 50 private static final int TOUCH_TOLERANCE = 25; 51 private static final int SHADOW_ALPHA = 160; 52 53 private final Paint borderPaint; 54 private final Drawable cropIndicator; 55 private final int indicatorSize; 56 private final RectF cropBounds = new RectF(0, 0, 1, 1); 57 58 private float lastX; 59 private float lastY; 60 private int movingEdges; 61 private OnCropChangeListener listener; 62 CropView(Context context, AttributeSet attrs)63 public CropView(Context context, AttributeSet attrs) { 64 super(context, attrs); 65 66 Resources resources = context.getResources(); 67 cropIndicator = resources.getDrawable(R.drawable.camera_crop_holo); 68 indicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size); 69 int borderColor = resources.getColor(R.color.opaque_cyan); 70 71 borderPaint = new Paint(); 72 borderPaint.setStyle(Paint.Style.STROKE); 73 borderPaint.setColor(borderColor); 74 borderPaint.setStrokeWidth(2f); 75 } 76 setOnCropChangeListener(OnCropChangeListener listener)77 public void setOnCropChangeListener(OnCropChangeListener listener) { 78 this.listener = listener; 79 } 80 refreshByCropChange(boolean fromUser)81 private void refreshByCropChange(boolean fromUser) { 82 if (listener != null) { 83 listener.onCropChanged(new RectF(cropBounds), fromUser); 84 } 85 invalidate(); 86 } 87 88 /** 89 * Sets cropped bounds; modifies the bounds if it's smaller than the allowed dimensions. 90 */ setCropBounds(RectF bounds)91 public void setCropBounds(RectF bounds) { 92 // Avoid cropping smaller than minimum width or height. 93 if (bounds.width() * getPhotoWidth() < MIN_CROP_WIDTH_HEIGHT) { 94 bounds.set(0, bounds.top, 1, bounds.bottom); 95 } 96 if (bounds.height() * getPhotoHeight() < MIN_CROP_WIDTH_HEIGHT) { 97 bounds.set(bounds.left, 0, bounds.right, 1); 98 } 99 cropBounds.set(bounds); 100 refreshByCropChange(false); 101 } 102 getCropBoundsDisplayed()103 private RectF getCropBoundsDisplayed() { 104 float width = displayBounds.width(); 105 float height = displayBounds.height(); 106 RectF cropped = new RectF(cropBounds.left * width, cropBounds.top * height, 107 cropBounds.right * width, cropBounds.bottom * height); 108 cropped.offset(displayBounds.left, displayBounds.top); 109 return cropped; 110 } 111 detectMovingEdges(float x, float y)112 private void detectMovingEdges(float x, float y) { 113 RectF cropped = getCropBoundsDisplayed(); 114 movingEdges = 0; 115 116 // Check left or right. 117 float left = Math.abs(x - cropped.left); 118 float right = Math.abs(x - cropped.right); 119 if ((left <= TOUCH_TOLERANCE) && (left < right)) { 120 movingEdges |= MOVE_LEFT; 121 } 122 else if (right <= TOUCH_TOLERANCE) { 123 movingEdges |= MOVE_RIGHT; 124 } 125 126 // Check top or bottom. 127 float top = Math.abs(y - cropped.top); 128 float bottom = Math.abs(y - cropped.bottom); 129 if ((top <= TOUCH_TOLERANCE) & (top < bottom)) { 130 movingEdges |= MOVE_TOP; 131 } 132 else if (bottom <= TOUCH_TOLERANCE) { 133 movingEdges |= MOVE_BOTTOM; 134 } 135 136 // Check inside block. 137 if (cropped.contains(x, y) && (movingEdges == 0)) { 138 movingEdges = MOVE_BLOCK; 139 } 140 invalidate(); 141 } 142 moveEdges(float deltaX, float deltaY)143 private void moveEdges(float deltaX, float deltaY) { 144 RectF cropped = getCropBoundsDisplayed(); 145 if (movingEdges == MOVE_BLOCK) { 146 // Move the whole cropped bounds within the photo display bounds. 147 deltaX = (deltaX > 0) ? Math.min(displayBounds.right - cropped.right, deltaX) 148 : Math.max(displayBounds.left - cropped.left, deltaX); 149 deltaY = (deltaY > 0) ? Math.min(displayBounds.bottom - cropped.bottom, deltaY) 150 : Math.max(displayBounds.top - cropped.top, deltaY); 151 cropped.offset(deltaX, deltaY); 152 } else { 153 // Adjust cropped bound dimensions within the photo display bounds. 154 float minWidth = MIN_CROP_WIDTH_HEIGHT * displayBounds.width() / getPhotoWidth(); 155 float minHeight = MIN_CROP_WIDTH_HEIGHT * displayBounds.height() / getPhotoHeight(); 156 if ((movingEdges & MOVE_LEFT) != 0) { 157 cropped.left = Math.min(cropped.left + deltaX, cropped.right - minWidth); 158 } 159 if ((movingEdges & MOVE_TOP) != 0) { 160 cropped.top = Math.min(cropped.top + deltaY, cropped.bottom - minHeight); 161 } 162 if ((movingEdges & MOVE_RIGHT) != 0) { 163 cropped.right = Math.max(cropped.right + deltaX, cropped.left + minWidth); 164 } 165 if ((movingEdges & MOVE_BOTTOM) != 0) { 166 cropped.bottom = Math.max(cropped.bottom + deltaY, cropped.top + minHeight); 167 } 168 cropped.intersect(displayBounds); 169 } 170 mapPhotoRect(cropped, cropBounds); 171 refreshByCropChange(true); 172 } 173 174 @Override onTouchEvent(MotionEvent event)175 public boolean onTouchEvent(MotionEvent event) { 176 super.onTouchEvent(event); 177 178 if (isEnabled()) { 179 float x = event.getX(); 180 float y = event.getY(); 181 182 switch (event.getAction()) { 183 case MotionEvent.ACTION_DOWN: 184 detectMovingEdges(x, y); 185 lastX = x; 186 lastY = y; 187 break; 188 189 case MotionEvent.ACTION_MOVE: 190 if (movingEdges != 0) { 191 moveEdges(x - lastX, y - lastY); 192 } 193 lastX = x; 194 lastY = y; 195 break; 196 197 case MotionEvent.ACTION_CANCEL: 198 case MotionEvent.ACTION_UP: 199 movingEdges = 0; 200 invalidate(); 201 break; 202 } 203 } 204 return true; 205 } 206 drawIndicator(Canvas canvas, Drawable indicator, float centerX, float centerY)207 private void drawIndicator(Canvas canvas, Drawable indicator, float centerX, float centerY) { 208 int left = (int) centerX - indicatorSize / 2; 209 int top = (int) centerY - indicatorSize / 2; 210 indicator.setBounds(left, top, left + indicatorSize, top + indicatorSize); 211 indicator.draw(canvas); 212 } 213 drawShadow(Canvas canvas, float left, float top, float right, float bottom)214 private void drawShadow(Canvas canvas, float left, float top, float right, float bottom) { 215 canvas.save(); 216 canvas.clipRect(left, top, right, bottom); 217 canvas.drawARGB(SHADOW_ALPHA, 0, 0, 0); 218 canvas.restore(); 219 } 220 221 @Override onDraw(Canvas canvas)222 protected void onDraw(Canvas canvas) { 223 super.onDraw(canvas); 224 225 // Draw shadow on non-cropped bounds and the border around cropped bounds. 226 RectF cropped = getCropBoundsDisplayed(); 227 drawShadow(canvas, displayBounds.left, displayBounds.top, displayBounds.right, cropped.top); 228 drawShadow(canvas, displayBounds.left, cropped.top, cropped.left, displayBounds.bottom); 229 drawShadow(canvas, cropped.right, cropped.top, displayBounds.right, displayBounds.bottom); 230 drawShadow(canvas, cropped.left, cropped.bottom, cropped.right, displayBounds.bottom); 231 canvas.drawRect(cropped, borderPaint); 232 233 boolean notMoving = movingEdges == 0; 234 if (((movingEdges & MOVE_TOP) != 0) || notMoving) { 235 drawIndicator(canvas, cropIndicator, cropped.centerX(), cropped.top); 236 } 237 if (((movingEdges & MOVE_BOTTOM) != 0) || notMoving) { 238 drawIndicator(canvas, cropIndicator, cropped.centerX(), cropped.bottom); 239 } 240 if (((movingEdges & MOVE_LEFT) != 0) || notMoving) { 241 drawIndicator(canvas, cropIndicator, cropped.left, cropped.centerY()); 242 } 243 if (((movingEdges & MOVE_RIGHT) != 0) || notMoving) { 244 drawIndicator(canvas, cropIndicator, cropped.right, cropped.centerY()); 245 } 246 } 247 } 248