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
17package com.android.camera;
18
19import com.android.gallery.R;
20
21import android.graphics.Canvas;
22import android.graphics.Matrix;
23import android.graphics.Paint;
24import android.graphics.Path;
25import android.graphics.Rect;
26import android.graphics.RectF;
27import android.graphics.Region;
28import android.graphics.drawable.Drawable;
29import 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.
35class 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
48    public HighlightView(View ctx) {
49        mContext = ctx;
50    }
51
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
65    public boolean hasFocus() {
66        return mIsFocused;
67    }
68
69    public void setFocus(boolean f) {
70        mIsFocused = f;
71    }
72
73    public void setHidden(boolean hidden) {
74        mHidden = hidden;
75    }
76
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
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).
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.
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.
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.
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.
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.
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
363    public void invalidate() {
364        mDrawRect = computeLayout();
365    }
366
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