1 /*
2  * Copyright (C) 2007 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.camera;
18 
19 import com.android.gallery.R;
20 
21 import android.graphics.Canvas;
22 import android.graphics.Matrix;
23 import android.graphics.Paint;
24 import android.graphics.Path;
25 import android.graphics.Rect;
26 import android.graphics.RectF;
27 import android.graphics.Region;
28 import android.graphics.drawable.Drawable;
29 import android.view.View;
30 
31 // This class is used by CropImage to display a highlighted cropping rectangle
32 // overlayed with the image. There are two coordinate spaces in use. One is
33 // image, another is screen. computeLayout() uses mMatrix to map from image
34 // space to screen space.
35 class HighlightView {
36 
37     @SuppressWarnings("unused")
38     private static final String TAG = "HighlightView";
39     View mContext;  // The View displaying the image.
40 
41     public static final int GROW_NONE        = (1 << 0);
42     public static final int GROW_LEFT_EDGE   = (1 << 1);
43     public static final int GROW_RIGHT_EDGE  = (1 << 2);
44     public static final int GROW_TOP_EDGE    = (1 << 3);
45     public static final int GROW_BOTTOM_EDGE = (1 << 4);
46     public static final int MOVE             = (1 << 5);
47 
HighlightView(View ctx)48     public HighlightView(View ctx) {
49         mContext = ctx;
50     }
51 
init()52     private void init() {
53         android.content.res.Resources resources = mContext.getResources();
54         mResizeDrawableWidth =
55                 resources.getDrawable(R.drawable.camera_crop_width);
56         mResizeDrawableHeight =
57                 resources.getDrawable(R.drawable.camera_crop_height);
58         mResizeDrawableDiagonal =
59                 resources.getDrawable(R.drawable.indicator_autocrop);
60     }
61 
62     boolean mIsFocused;
63     boolean mHidden;
64 
hasFocus()65     public boolean hasFocus() {
66         return mIsFocused;
67     }
68 
setFocus(boolean f)69     public void setFocus(boolean f) {
70         mIsFocused = f;
71     }
72 
setHidden(boolean hidden)73     public void setHidden(boolean hidden) {
74         mHidden = hidden;
75     }
76 
draw(Canvas canvas)77     protected void draw(Canvas canvas) {
78         if (mHidden) {
79             return;
80         }
81         canvas.save();
82         Path path = new Path();
83         if (!hasFocus()) {
84             mOutlinePaint.setColor(0xFF000000);
85             canvas.drawRect(mDrawRect, mOutlinePaint);
86         } else {
87             Rect viewDrawingRect = new Rect();
88             mContext.getDrawingRect(viewDrawingRect);
89             if (mCircle) {
90                 float width  = mDrawRect.width();
91                 float height = mDrawRect.height();
92                 path.addCircle(mDrawRect.left + (width  / 2),
93                                mDrawRect.top + (height / 2),
94                                width / 2,
95                                Path.Direction.CW);
96                 mOutlinePaint.setColor(0xFFEF04D6);
97             } else {
98                 path.addRect(new RectF(mDrawRect), Path.Direction.CW);
99                 mOutlinePaint.setColor(0xFFFF8A00);
100             }
101             canvas.clipPath(path, Region.Op.DIFFERENCE);
102             canvas.drawRect(viewDrawingRect,
103                     hasFocus() ? mFocusPaint : mNoFocusPaint);
104 
105             canvas.restore();
106             canvas.drawPath(path, mOutlinePaint);
107 
108             if (mMode == ModifyMode.Grow) {
109                 if (mCircle) {
110                     int width  = mResizeDrawableDiagonal.getIntrinsicWidth();
111                     int height = mResizeDrawableDiagonal.getIntrinsicHeight();
112 
113                     int d  = (int) Math.round(Math.cos(/*45deg*/Math.PI / 4D)
114                             * (mDrawRect.width() / 2D));
115                     int x  = mDrawRect.left
116                             + (mDrawRect.width() / 2) + d - width / 2;
117                     int y  = mDrawRect.top
118                             + (mDrawRect.height() / 2) - d - height / 2;
119                     mResizeDrawableDiagonal.setBounds(x, y,
120                             x + mResizeDrawableDiagonal.getIntrinsicWidth(),
121                             y + mResizeDrawableDiagonal.getIntrinsicHeight());
122                     mResizeDrawableDiagonal.draw(canvas);
123                 } else {
124                     int left    = mDrawRect.left   + 1;
125                     int right   = mDrawRect.right  + 1;
126                     int top     = mDrawRect.top    + 4;
127                     int bottom  = mDrawRect.bottom + 3;
128 
129                     int widthWidth   =
130                             mResizeDrawableWidth.getIntrinsicWidth() / 2;
131                     int widthHeight  =
132                             mResizeDrawableWidth.getIntrinsicHeight() / 2;
133                     int heightHeight =
134                             mResizeDrawableHeight.getIntrinsicHeight() / 2;
135                     int heightWidth  =
136                             mResizeDrawableHeight.getIntrinsicWidth() / 2;
137 
138                     int xMiddle = mDrawRect.left
139                             + ((mDrawRect.right  - mDrawRect.left) / 2);
140                     int yMiddle = mDrawRect.top
141                             + ((mDrawRect.bottom - mDrawRect.top) / 2);
142 
143                     mResizeDrawableWidth.setBounds(left - widthWidth,
144                                                    yMiddle - widthHeight,
145                                                    left + widthWidth,
146                                                    yMiddle + widthHeight);
147                     mResizeDrawableWidth.draw(canvas);
148 
149                     mResizeDrawableWidth.setBounds(right - widthWidth,
150                                                    yMiddle - widthHeight,
151                                                    right + widthWidth,
152                                                    yMiddle + widthHeight);
153                     mResizeDrawableWidth.draw(canvas);
154 
155                     mResizeDrawableHeight.setBounds(xMiddle - heightWidth,
156                                                     top - heightHeight,
157                                                     xMiddle + heightWidth,
158                                                     top + heightHeight);
159                     mResizeDrawableHeight.draw(canvas);
160 
161                     mResizeDrawableHeight.setBounds(xMiddle - heightWidth,
162                                                     bottom - heightHeight,
163                                                     xMiddle + heightWidth,
164                                                     bottom + heightHeight);
165                     mResizeDrawableHeight.draw(canvas);
166                 }
167             }
168         }
169     }
170 
setMode(ModifyMode mode)171     public void setMode(ModifyMode mode) {
172         if (mode != mMode) {
173             mMode = mode;
174             mContext.invalidate();
175         }
176     }
177 
178     // Determines which edges are hit by touching at (x, y).
getHit(float x, float y)179     public int getHit(float x, float y) {
180         Rect r = computeLayout();
181         final float hysteresis = 20F;
182         int retval = GROW_NONE;
183 
184         if (mCircle) {
185             float distX = x - r.centerX();
186             float distY = y - r.centerY();
187             int distanceFromCenter =
188                     (int) Math.sqrt(distX * distX + distY * distY);
189             int radius  = mDrawRect.width() / 2;
190             int delta = distanceFromCenter - radius;
191             if (Math.abs(delta) <= hysteresis) {
192                 if (Math.abs(distY) > Math.abs(distX)) {
193                     if (distY < 0) {
194                         retval = GROW_TOP_EDGE;
195                     } else {
196                         retval = GROW_BOTTOM_EDGE;
197                     }
198                 } else {
199                     if (distX < 0) {
200                         retval = GROW_LEFT_EDGE;
201                     } else {
202                         retval = GROW_RIGHT_EDGE;
203                     }
204                 }
205             } else if (distanceFromCenter < radius) {
206                 retval = MOVE;
207             } else {
208                 retval = GROW_NONE;
209             }
210         } else {
211             // verticalCheck makes sure the position is between the top and
212             // the bottom edge (with some tolerance). Similar for horizCheck.
213             boolean verticalCheck = (y >= r.top - hysteresis)
214                     && (y < r.bottom + hysteresis);
215             boolean horizCheck = (x >= r.left - hysteresis)
216                     && (x < r.right + hysteresis);
217 
218             // Check whether the position is near some edge(s).
219             if ((Math.abs(r.left - x)     < hysteresis)  &&  verticalCheck) {
220                 retval |= GROW_LEFT_EDGE;
221             }
222             if ((Math.abs(r.right - x)    < hysteresis)  &&  verticalCheck) {
223                 retval |= GROW_RIGHT_EDGE;
224             }
225             if ((Math.abs(r.top - y)      < hysteresis)  &&  horizCheck) {
226                 retval |= GROW_TOP_EDGE;
227             }
228             if ((Math.abs(r.bottom - y)   < hysteresis)  &&  horizCheck) {
229                 retval |= GROW_BOTTOM_EDGE;
230             }
231 
232             // Not near any edge but inside the rectangle: move.
233             if (retval == GROW_NONE && r.contains((int) x, (int) y)) {
234                 retval = MOVE;
235             }
236         }
237         return retval;
238     }
239 
240     // Handles motion (dx, dy) in screen space.
241     // The "edge" parameter specifies which edges the user is dragging.
handleMotion(int edge, float dx, float dy)242     void handleMotion(int edge, float dx, float dy) {
243         Rect r = computeLayout();
244         if (edge == GROW_NONE) {
245             return;
246         } else if (edge == MOVE) {
247             // Convert to image space before sending to moveBy().
248             moveBy(dx * (mCropRect.width() / r.width()),
249                    dy * (mCropRect.height() / r.height()));
250         } else {
251             if (((GROW_LEFT_EDGE | GROW_RIGHT_EDGE) & edge) == 0) {
252                 dx = 0;
253             }
254 
255             if (((GROW_TOP_EDGE | GROW_BOTTOM_EDGE) & edge) == 0) {
256                 dy = 0;
257             }
258 
259             // Convert to image space before sending to growBy().
260             float xDelta = dx * (mCropRect.width() / r.width());
261             float yDelta = dy * (mCropRect.height() / r.height());
262             growBy((((edge & GROW_LEFT_EDGE) != 0) ? -1 : 1) * xDelta,
263                     (((edge & GROW_TOP_EDGE) != 0) ? -1 : 1) * yDelta);
264         }
265     }
266 
267     // Grows the cropping rectange by (dx, dy) in image space.
moveBy(float dx, float dy)268     void moveBy(float dx, float dy) {
269         Rect invalRect = new Rect(mDrawRect);
270 
271         mCropRect.offset(dx, dy);
272 
273         // Put the cropping rectangle inside image rectangle.
274         mCropRect.offset(
275                 Math.max(0, mImageRect.left - mCropRect.left),
276                 Math.max(0, mImageRect.top  - mCropRect.top));
277 
278         mCropRect.offset(
279                 Math.min(0, mImageRect.right  - mCropRect.right),
280                 Math.min(0, mImageRect.bottom - mCropRect.bottom));
281 
282         mDrawRect = computeLayout();
283         invalRect.union(mDrawRect);
284         invalRect.inset(-10, -10);
285         mContext.invalidate(invalRect);
286     }
287 
288     // Grows the cropping rectange by (dx, dy) in image space.
growBy(float dx, float dy)289     void growBy(float dx, float dy) {
290         if (mMaintainAspectRatio) {
291             if (dx != 0) {
292                 dy = dx / mInitialAspectRatio;
293             } else if (dy != 0) {
294                 dx = dy * mInitialAspectRatio;
295             }
296         }
297 
298         // Don't let the cropping rectangle grow too fast.
299         // Grow at most half of the difference between the image rectangle and
300         // the cropping rectangle.
301         RectF r = new RectF(mCropRect);
302         if (dx > 0F && r.width() + 2 * dx > mImageRect.width()) {
303             float adjustment = (mImageRect.width() - r.width()) / 2F;
304             dx = adjustment;
305             if (mMaintainAspectRatio) {
306                 dy = dx / mInitialAspectRatio;
307             }
308         }
309         if (dy > 0F && r.height() + 2 * dy > mImageRect.height()) {
310             float adjustment = (mImageRect.height() - r.height()) / 2F;
311             dy = adjustment;
312             if (mMaintainAspectRatio) {
313                 dx = dy * mInitialAspectRatio;
314             }
315         }
316 
317         r.inset(-dx, -dy);
318 
319         // Don't let the cropping rectangle shrink too fast.
320         final float widthCap = 25F;
321         if (r.width() < widthCap) {
322             r.inset(-(widthCap - r.width()) / 2F, 0F);
323         }
324         float heightCap = mMaintainAspectRatio
325                 ? (widthCap / mInitialAspectRatio)
326                 : widthCap;
327         if (r.height() < heightCap) {
328             r.inset(0F, -(heightCap - r.height()) / 2F);
329         }
330 
331         // Put the cropping rectangle inside the image rectangle.
332         if (r.left < mImageRect.left) {
333             r.offset(mImageRect.left - r.left, 0F);
334         } else if (r.right > mImageRect.right) {
335             r.offset(-(r.right - mImageRect.right), 0);
336         }
337         if (r.top < mImageRect.top) {
338             r.offset(0F, mImageRect.top - r.top);
339         } else if (r.bottom > mImageRect.bottom) {
340             r.offset(0F, -(r.bottom - mImageRect.bottom));
341         }
342 
343         mCropRect.set(r);
344         mDrawRect = computeLayout();
345         mContext.invalidate();
346     }
347 
348     // Returns the cropping rectangle in image space.
getCropRect()349     public Rect getCropRect() {
350         return new Rect((int) mCropRect.left, (int) mCropRect.top,
351                         (int) mCropRect.right, (int) mCropRect.bottom);
352     }
353 
354     // Maps the cropping rectangle from image space to screen space.
computeLayout()355     private Rect computeLayout() {
356         RectF r = new RectF(mCropRect.left, mCropRect.top,
357                             mCropRect.right, mCropRect.bottom);
358         mMatrix.mapRect(r);
359         return new Rect(Math.round(r.left), Math.round(r.top),
360                         Math.round(r.right), Math.round(r.bottom));
361     }
362 
invalidate()363     public void invalidate() {
364         mDrawRect = computeLayout();
365     }
366 
setup(Matrix m, Rect imageRect, RectF cropRect, boolean circle, boolean maintainAspectRatio)367     public void setup(Matrix m, Rect imageRect, RectF cropRect, boolean circle,
368                       boolean maintainAspectRatio) {
369         if (circle) {
370             maintainAspectRatio = true;
371         }
372         mMatrix = new Matrix(m);
373 
374         mCropRect = cropRect;
375         mImageRect = new RectF(imageRect);
376         mMaintainAspectRatio = maintainAspectRatio;
377         mCircle = circle;
378 
379         mInitialAspectRatio = mCropRect.width() / mCropRect.height();
380         mDrawRect = computeLayout();
381 
382         mFocusPaint.setARGB(125, 50, 50, 50);
383         mNoFocusPaint.setARGB(125, 50, 50, 50);
384         mOutlinePaint.setStrokeWidth(3F);
385         mOutlinePaint.setStyle(Paint.Style.STROKE);
386         mOutlinePaint.setAntiAlias(true);
387 
388         mMode = ModifyMode.None;
389         init();
390     }
391 
392     enum ModifyMode { None, Move, Grow }
393 
394     private ModifyMode mMode = ModifyMode.None;
395 
396     Rect mDrawRect;  // in screen space
397     private RectF mImageRect;  // in image space
398     RectF mCropRect;  // in image space
399     Matrix mMatrix;
400 
401     private boolean mMaintainAspectRatio = false;
402     private float mInitialAspectRatio;
403     private boolean mCircle = false;
404 
405     private Drawable mResizeDrawableWidth;
406     private Drawable mResizeDrawableHeight;
407     private Drawable mResizeDrawableDiagonal;
408 
409     private final Paint mFocusPaint = new Paint();
410     private final Paint mNoFocusPaint = new Paint();
411     private final Paint mOutlinePaint = new Paint();
412 }
413