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
17package com.android.gallery3d.photoeditor.actions;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.graphics.RectF;
24import android.graphics.drawable.Drawable;
25import android.util.AttributeSet;
26import android.view.MotionEvent;
27
28import com.android.gallery3d.R;
29
30/**
31 * A view that tracks touch motions and adjusts crop bounds accordingly.
32 */
33class CropView extends FullscreenToolView {
34
35    /**
36     * Listener of crop bounds.
37     */
38    public interface OnCropChangeListener {
39
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
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
77    public void setOnCropChangeListener(OnCropChangeListener listener) {
78        this.listener = listener;
79    }
80
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     */
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
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
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
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
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
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
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
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