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.photoeditor.actions; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Canvas; 22 import android.graphics.Color; 23 import android.graphics.Paint; 24 import android.graphics.Rect; 25 import android.graphics.RectF; 26 import android.graphics.Region; 27 import android.graphics.RegionIterator; 28 import android.graphics.drawable.Drawable; 29 import android.util.AttributeSet; 30 import android.view.MotionEvent; 31 import android.view.View; 32 33 import com.android.photoeditor.R; 34 35 import java.util.Vector; 36 37 /** 38 * A view that track touch motions and adjust crop bounds accordingly. 39 */ 40 class CropView extends View { 41 42 /** 43 * Listener of crop bounds. 44 */ 45 public interface OnCropChangeListener { 46 47 void onCropChanged(RectF bounds, boolean fromUser); 48 } 49 50 private static final int TOUCH_AREA_NONE = 0; 51 private static final int TOUCH_AREA_LEFT = 1; 52 private static final int TOUCH_AREA_TOP = 2; 53 private static final int TOUCH_AREA_RIGHT = 4; 54 private static final int TOUCH_AREA_BOTTOM = 8; 55 private static final int TOUCH_AREA_INSIDE = 15; 56 private static final int TOUCH_AREA_OUTSIDE = 16; 57 private static final int TOUCH_AREA_TOP_LEFT = 3; 58 private static final int TOUCH_AREA_TOP_RIGHT = 6; 59 private static final int TOUCH_AREA_BOTTOM_LEFT = 9; 60 private static final int TOUCH_AREA_BOTTOM_RIGHT = 12; 61 62 private static final int BORDER_COLOR = 0xFF008AFF; 63 private static final int OUTER_COLOR = 0xA0000000; 64 private static final int INDICATION_COLOR = 0xFFCC9900; 65 private static final int TOUCH_AREA_SPAN = 20; 66 private static final int TOUCH_AREA_SPAN2 = TOUCH_AREA_SPAN * 2; 67 private static final float BORDER_WIDTH = 2.0f; 68 69 private final Paint outerAreaPaint; 70 private final Paint borderPaint; 71 private final Paint highlightPaint; 72 73 private final Drawable heightIndicator; 74 private final Drawable widthIndicator; 75 private final int indicatorSize; 76 77 private RectF cropBounds; 78 private RectF photoBounds; 79 80 private OnCropChangeListener listener; 81 82 private float lastX; 83 private float lastY; 84 private int currentTouchArea; 85 86 public CropView(Context context, AttributeSet attrs) { 87 super(context, attrs); 88 89 Resources resources = context.getResources(); 90 heightIndicator = resources.getDrawable(R.drawable.crop_height_holo); 91 widthIndicator = resources.getDrawable(R.drawable.crop_width_holo); 92 indicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size); 93 94 outerAreaPaint = new Paint(); 95 outerAreaPaint.setStyle(Paint.Style.FILL); 96 outerAreaPaint.setColor(OUTER_COLOR); 97 98 borderPaint = new Paint(); 99 borderPaint.setStyle(Paint.Style.STROKE); 100 borderPaint.setColor(BORDER_COLOR); 101 borderPaint.setStrokeWidth(BORDER_WIDTH); 102 103 highlightPaint = new Paint(); 104 highlightPaint.setStyle(Paint.Style.STROKE); 105 highlightPaint.setColor(INDICATION_COLOR); 106 highlightPaint.setStrokeWidth(BORDER_WIDTH); 107 108 currentTouchArea = TOUCH_AREA_NONE; 109 } 110 111 public void setOnCropChangeListener(OnCropChangeListener listener) { 112 this.listener = listener; 113 } 114 115 private void notifyCropChange(boolean fromUser) { 116 if (listener != null) { 117 listener.onCropChanged(cropBounds, fromUser); 118 } 119 } 120 121 public void setCropBounds(RectF bounds) { 122 bounds.intersect(photoBounds); 123 cropBounds = bounds; 124 if (photoBounds.width() <= TOUCH_AREA_SPAN2) { 125 cropBounds.left = photoBounds.left; 126 cropBounds.right = photoBounds.right; 127 } 128 if (photoBounds.height() <= TOUCH_AREA_SPAN2) { 129 cropBounds.top = photoBounds.top; 130 cropBounds.bottom = photoBounds.bottom; 131 } 132 notifyCropChange(false); 133 invalidate(); 134 } 135 136 /** 137 * Sets bounds to crop within. 138 */ 139 public void setPhotoBounds(RectF bounds) { 140 photoBounds = bounds; 141 } 142 143 public boolean fullPhotoCropped() { 144 return cropBounds.contains(photoBounds); 145 } 146 147 private int detectTouchArea(float x, float y) { 148 RectF area = new RectF(); 149 area.set(cropBounds); 150 area.inset(-TOUCH_AREA_SPAN, -TOUCH_AREA_SPAN); 151 if (!area.contains(x, y)) { 152 return TOUCH_AREA_OUTSIDE; 153 } 154 155 // left 156 area.set(cropBounds.left - TOUCH_AREA_SPAN, cropBounds.top + TOUCH_AREA_SPAN, 157 cropBounds.left + TOUCH_AREA_SPAN, cropBounds.bottom - TOUCH_AREA_SPAN); 158 if (area.contains(x, y)) { 159 return TOUCH_AREA_LEFT; 160 } 161 // right 162 area.offset(cropBounds.width(), 0f); 163 if (area.contains(x, y)) { 164 return TOUCH_AREA_RIGHT; 165 } 166 // top 167 area.set(cropBounds.left + TOUCH_AREA_SPAN, cropBounds.top - TOUCH_AREA_SPAN, 168 cropBounds.right - TOUCH_AREA_SPAN, cropBounds.top + TOUCH_AREA_SPAN); 169 if (area.contains(x, y)) { 170 return TOUCH_AREA_TOP; 171 } 172 // bottom 173 area.offset(0f, cropBounds.height()); 174 if (area.contains(x, y)) { 175 return TOUCH_AREA_BOTTOM; 176 } 177 // top left 178 area.set(cropBounds.left - TOUCH_AREA_SPAN, cropBounds.top - TOUCH_AREA_SPAN, 179 cropBounds.left + TOUCH_AREA_SPAN, cropBounds.top + TOUCH_AREA_SPAN); 180 if (area.contains(x, y)) { 181 return TOUCH_AREA_TOP_LEFT; 182 } 183 // top right 184 area.offset(cropBounds.width(), 0f); 185 if (area.contains(x, y)) { 186 return TOUCH_AREA_TOP_RIGHT; 187 } 188 // bottom right 189 area.offset(0f, cropBounds.height()); 190 if (area.contains(x, y)) { 191 return TOUCH_AREA_BOTTOM_RIGHT; 192 } 193 // bottom left 194 area.offset(-cropBounds.width(), 0f); 195 if (area.contains(x, y)) { 196 return TOUCH_AREA_BOTTOM_LEFT; 197 } 198 return TOUCH_AREA_INSIDE; 199 } 200 201 private void performMove(float deltaX, float deltaY) { 202 if (currentTouchArea == TOUCH_AREA_INSIDE){ // moving the rect. 203 cropBounds.offset(deltaX, deltaY); 204 if (cropBounds.left < photoBounds.left) { 205 cropBounds.offset(photoBounds.left - cropBounds.left, 0f); 206 } else if (cropBounds.right > photoBounds.right) { 207 cropBounds.offset(photoBounds.right - cropBounds.right, 0f); 208 } 209 if (cropBounds.top < photoBounds.top) { 210 cropBounds.offset(0f, photoBounds.top - cropBounds.top); 211 } else if (cropBounds.bottom > photoBounds.bottom) { 212 cropBounds.offset(0f, photoBounds.bottom - cropBounds.bottom); 213 } 214 } else { // adjusting bounds. 215 if ((currentTouchArea & TOUCH_AREA_LEFT) != 0) { 216 cropBounds.left = Math.min(cropBounds.left + deltaX, 217 cropBounds.right - TOUCH_AREA_SPAN2); 218 } 219 if ((currentTouchArea & TOUCH_AREA_TOP) != 0) { 220 cropBounds.top = Math.min(cropBounds.top + deltaY, 221 cropBounds.bottom - TOUCH_AREA_SPAN2); 222 } 223 if ((currentTouchArea & TOUCH_AREA_RIGHT) != 0) { 224 cropBounds.right = Math.max(cropBounds.right + deltaX, 225 cropBounds.left + TOUCH_AREA_SPAN2); 226 } 227 if ((currentTouchArea & TOUCH_AREA_BOTTOM) != 0) { 228 cropBounds.bottom = Math.max(cropBounds.bottom + deltaY, 229 cropBounds.top + TOUCH_AREA_SPAN2); 230 } 231 cropBounds.intersect(photoBounds); 232 } 233 } 234 235 @Override 236 public boolean onTouchEvent(MotionEvent event) { 237 super.onTouchEvent(event); 238 239 if (!isEnabled()) { 240 return true; 241 } 242 float x = event.getX(); 243 float y = event.getY(); 244 245 switch (event.getAction()) { 246 case MotionEvent.ACTION_DOWN: 247 currentTouchArea = detectTouchArea(x, y); 248 lastX = x; 249 lastY = y; 250 invalidate(); 251 break; 252 253 case MotionEvent.ACTION_MOVE: 254 performMove(x - lastX, y - lastY); 255 256 lastX = x; 257 lastY = y; 258 notifyCropChange(true); 259 invalidate(); 260 break; 261 262 case MotionEvent.ACTION_CANCEL: 263 case MotionEvent.ACTION_UP: 264 currentTouchArea = TOUCH_AREA_NONE; 265 invalidate(); 266 break; 267 } 268 return true; 269 } 270 271 private void drawIndicator(Canvas canvas, Drawable indicator, float centerX, float centerY) { 272 int left = (int) centerX - indicatorSize / 2; 273 int top = (int) centerY - indicatorSize / 2; 274 int right = left + indicatorSize; 275 int bottom = top + indicatorSize; 276 indicator.setBounds(left, top, right, bottom); 277 indicator.draw(canvas); 278 } 279 280 private void drawIndicators(Canvas canvas) { 281 drawIndicator(canvas, heightIndicator, cropBounds.centerX(), cropBounds.top); 282 drawIndicator(canvas, heightIndicator, cropBounds.centerX(), cropBounds.bottom); 283 drawIndicator(canvas, widthIndicator, cropBounds.left, cropBounds.centerY()); 284 drawIndicator(canvas, widthIndicator, cropBounds.right, cropBounds.centerY()); 285 } 286 287 private void drawTouchHighlights(Canvas canvas) { 288 if ((currentTouchArea & TOUCH_AREA_TOP) != 0) { 289 canvas.drawLine(cropBounds.left, cropBounds.top, cropBounds.right, cropBounds.top, 290 highlightPaint); 291 } 292 if ((currentTouchArea & TOUCH_AREA_BOTTOM) != 0) { 293 canvas.drawLine(cropBounds.left, cropBounds.bottom, cropBounds.right, 294 cropBounds.bottom, highlightPaint); 295 } 296 if ((currentTouchArea & TOUCH_AREA_LEFT) != 0) { 297 canvas.drawLine(cropBounds.left, cropBounds.top, cropBounds.left, cropBounds.bottom, 298 highlightPaint); 299 } 300 if ((currentTouchArea & TOUCH_AREA_RIGHT) != 0) { 301 canvas.drawLine(cropBounds.right, cropBounds.top, cropBounds.right, cropBounds.bottom, 302 highlightPaint); 303 } 304 } 305 306 private void drawBounds(Canvas canvas) { 307 Rect r = new Rect(); 308 photoBounds.roundOut(r); 309 Region drawRegion = new Region(r); 310 cropBounds.roundOut(r); 311 drawRegion.op(r, Region.Op.DIFFERENCE); 312 RegionIterator iter = new RegionIterator(drawRegion); 313 while (iter.next(r)) { 314 canvas.drawRect(r, outerAreaPaint); 315 } 316 317 canvas.drawRect(cropBounds, borderPaint); 318 } 319 320 @Override 321 protected void onDraw(Canvas canvas) { 322 super.onDraw(canvas); 323 324 drawBounds(canvas); 325 if (currentTouchArea != TOUCH_AREA_NONE) { 326 drawTouchHighlights(canvas); 327 drawIndicators(canvas); 328 } 329 } 330 } 331