1/*
2 * Copyright (C) 2013 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.filtershow.crop;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
23import android.graphics.DashPathEffect;
24import android.graphics.Matrix;
25import android.graphics.Paint;
26import android.graphics.Rect;
27import android.graphics.RectF;
28import android.graphics.drawable.Drawable;
29import android.graphics.drawable.NinePatchDrawable;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.view.MotionEvent;
33import android.view.View;
34
35import com.android.gallery3d.R;
36
37
38public class CropView extends View {
39    private static final String LOGTAG = "CropView";
40
41    private RectF mImageBounds = new RectF();
42    private RectF mScreenBounds = new RectF();
43    private RectF mScreenImageBounds = new RectF();
44    private RectF mScreenCropBounds = new RectF();
45    private Rect mShadowBounds = new Rect();
46
47    private Bitmap mBitmap;
48    private Paint mPaint = new Paint();
49
50    private NinePatchDrawable mShadow;
51    private CropObject mCropObj = null;
52    private Drawable mCropIndicator;
53    private int mIndicatorSize;
54    private int mRotation = 0;
55    private boolean mMovingBlock = false;
56    private Matrix mDisplayMatrix = null;
57    private Matrix mDisplayMatrixInverse = null;
58    private boolean mDirty = false;
59
60    private float mPrevX = 0;
61    private float mPrevY = 0;
62    private float mSpotX = 0;
63    private float mSpotY = 0;
64    private boolean mDoSpot = false;
65
66    private int mShadowMargin = 15;
67    private int mMargin = 32;
68    private int mOverlayShadowColor = 0xCF000000;
69    private int mOverlayWPShadowColor = 0x5F000000;
70    private int mWPMarkerColor = 0x7FFFFFFF;
71    private int mMinSideSize = 90;
72    private int mTouchTolerance = 40;
73    private float mDashOnLength = 20;
74    private float mDashOffLength = 10;
75
76    private enum Mode {
77        NONE, MOVE
78    }
79
80    private Mode mState = Mode.NONE;
81
82    public CropView(Context context) {
83        super(context);
84        setup(context);
85    }
86
87    public CropView(Context context, AttributeSet attrs) {
88        super(context, attrs);
89        setup(context);
90    }
91
92    public CropView(Context context, AttributeSet attrs, int defStyle) {
93        super(context, attrs, defStyle);
94        setup(context);
95    }
96
97    private void setup(Context context) {
98        Resources rsc = context.getResources();
99        mShadow = (NinePatchDrawable) rsc.getDrawable(R.drawable.geometry_shadow);
100        mCropIndicator = rsc.getDrawable(R.drawable.camera_crop);
101        mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size);
102        mShadowMargin = (int) rsc.getDimension(R.dimen.shadow_margin);
103        mMargin = (int) rsc.getDimension(R.dimen.preview_margin);
104        mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side);
105        mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance);
106        mOverlayShadowColor = (int) rsc.getColor(R.color.crop_shadow_color);
107        mOverlayWPShadowColor = (int) rsc.getColor(R.color.crop_shadow_wp_color);
108        mWPMarkerColor = (int) rsc.getColor(R.color.crop_wp_markers);
109        mDashOnLength = rsc.getDimension(R.dimen.wp_selector_dash_length);
110        mDashOffLength = rsc.getDimension(R.dimen.wp_selector_off_length);
111    }
112
113    public void initialize(Bitmap image, RectF newCropBounds, RectF newPhotoBounds, int rotation) {
114        mBitmap = image;
115        if (mCropObj != null) {
116            RectF crop = mCropObj.getInnerBounds();
117            RectF containing = mCropObj.getOuterBounds();
118            if (crop != newCropBounds || containing != newPhotoBounds
119                    || mRotation != rotation) {
120                mRotation = rotation;
121                mCropObj.resetBoundsTo(newCropBounds, newPhotoBounds);
122                clearDisplay();
123            }
124        } else {
125            mRotation = rotation;
126            mCropObj = new CropObject(newPhotoBounds, newCropBounds, 0);
127            clearDisplay();
128        }
129    }
130
131    public RectF getCrop() {
132        return mCropObj.getInnerBounds();
133    }
134
135    public RectF getPhoto() {
136        return mCropObj.getOuterBounds();
137    }
138
139    @Override
140    public boolean onTouchEvent(MotionEvent event) {
141        float x = event.getX();
142        float y = event.getY();
143        if (mDisplayMatrix == null || mDisplayMatrixInverse == null) {
144            return true;
145        }
146        float[] touchPoint = {
147                x, y
148        };
149        mDisplayMatrixInverse.mapPoints(touchPoint);
150        x = touchPoint[0];
151        y = touchPoint[1];
152        switch (event.getActionMasked()) {
153            case (MotionEvent.ACTION_DOWN):
154                if (mState == Mode.NONE) {
155                    if (!mCropObj.selectEdge(x, y)) {
156                        mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK);
157                    }
158                    mPrevX = x;
159                    mPrevY = y;
160                    mState = Mode.MOVE;
161                }
162                break;
163            case (MotionEvent.ACTION_UP):
164                if (mState == Mode.MOVE) {
165                    mCropObj.selectEdge(CropObject.MOVE_NONE);
166                    mMovingBlock = false;
167                    mPrevX = x;
168                    mPrevY = y;
169                    mState = Mode.NONE;
170                }
171                break;
172            case (MotionEvent.ACTION_MOVE):
173                if (mState == Mode.MOVE) {
174                    float dx = x - mPrevX;
175                    float dy = y - mPrevY;
176                    mCropObj.moveCurrentSelection(dx, dy);
177                    mPrevX = x;
178                    mPrevY = y;
179                }
180                break;
181            default:
182                break;
183        }
184        invalidate();
185        return true;
186    }
187
188    private void reset() {
189        Log.w(LOGTAG, "crop reset called");
190        mState = Mode.NONE;
191        mCropObj = null;
192        mRotation = 0;
193        mMovingBlock = false;
194        clearDisplay();
195    }
196
197    private void clearDisplay() {
198        mDisplayMatrix = null;
199        mDisplayMatrixInverse = null;
200        invalidate();
201    }
202
203    protected void configChanged() {
204        mDirty = true;
205    }
206
207    public void applyFreeAspect() {
208        mCropObj.unsetAspectRatio();
209        invalidate();
210    }
211
212    public void applyOriginalAspect() {
213        RectF outer = mCropObj.getOuterBounds();
214        float w = outer.width();
215        float h = outer.height();
216        if (w > 0 && h > 0) {
217            applyAspect(w, h);
218            mCropObj.resetBoundsTo(outer, outer);
219        } else {
220            Log.w(LOGTAG, "failed to set aspect ratio original");
221        }
222    }
223
224    public void applySquareAspect() {
225        applyAspect(1, 1);
226    }
227
228    public void applyAspect(float x, float y) {
229        if (x <= 0 || y <= 0) {
230            throw new IllegalArgumentException("Bad arguments to applyAspect");
231        }
232        // If we are rotated by 90 degrees from horizontal, swap x and y
233        if (((mRotation < 0) ? -mRotation : mRotation) % 180 == 90) {
234            float tmp = x;
235            x = y;
236            y = tmp;
237        }
238        if (!mCropObj.setInnerAspectRatio(x, y)) {
239            Log.w(LOGTAG, "failed to set aspect ratio");
240        }
241        invalidate();
242    }
243
244    public void setWallpaperSpotlight(float spotlightX, float spotlightY) {
245        mSpotX = spotlightX;
246        mSpotY = spotlightY;
247        if (mSpotX > 0 && mSpotY > 0) {
248            mDoSpot = true;
249        }
250    }
251
252    public void unsetWallpaperSpotlight() {
253        mDoSpot = false;
254    }
255
256    /**
257     * Rotates first d bits in integer x to the left some number of times.
258     */
259    private int bitCycleLeft(int x, int times, int d) {
260        int mask = (1 << d) - 1;
261        int mout = x & mask;
262        times %= d;
263        int hi = mout >> (d - times);
264        int low = (mout << times) & mask;
265        int ret = x & ~mask;
266        ret |= low;
267        ret |= hi;
268        return ret;
269    }
270
271    /**
272     * Find the selected edge or corner in screen coordinates.
273     */
274    private int decode(int movingEdges, float rotation) {
275        int rot = CropMath.constrainedRotation(rotation);
276        switch (rot) {
277            case 90:
278                return bitCycleLeft(movingEdges, 1, 4);
279            case 180:
280                return bitCycleLeft(movingEdges, 2, 4);
281            case 270:
282                return bitCycleLeft(movingEdges, 3, 4);
283            default:
284                return movingEdges;
285        }
286    }
287
288    @Override
289    public void onDraw(Canvas canvas) {
290        if (mBitmap == null) {
291            return;
292        }
293        if (mDirty) {
294            mDirty = false;
295            clearDisplay();
296        }
297
298        mImageBounds = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
299        mScreenBounds = new RectF(0, 0, canvas.getWidth(), canvas.getHeight());
300        mScreenBounds.inset(mMargin, mMargin);
301
302        // If crop object doesn't exist, create it and update it from master
303        // state
304        if (mCropObj == null) {
305            reset();
306            mCropObj = new CropObject(mImageBounds, mImageBounds, 0);
307        }
308
309        // If display matrix doesn't exist, create it and its dependencies
310        if (mDisplayMatrix == null || mDisplayMatrixInverse == null) {
311            mDisplayMatrix = new Matrix();
312            mDisplayMatrix.reset();
313            if (!CropDrawingUtils.setImageToScreenMatrix(mDisplayMatrix, mImageBounds, mScreenBounds,
314                    mRotation)) {
315                Log.w(LOGTAG, "failed to get screen matrix");
316                mDisplayMatrix = null;
317                return;
318            }
319            mDisplayMatrixInverse = new Matrix();
320            mDisplayMatrixInverse.reset();
321            if (!mDisplayMatrix.invert(mDisplayMatrixInverse)) {
322                Log.w(LOGTAG, "could not invert display matrix");
323                mDisplayMatrixInverse = null;
324                return;
325            }
326            // Scale min side and tolerance by display matrix scale factor
327            mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize));
328            mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance));
329        }
330
331        mScreenImageBounds.set(mImageBounds);
332
333        // Draw background shadow
334        if (mDisplayMatrix.mapRect(mScreenImageBounds)) {
335            int margin = (int) mDisplayMatrix.mapRadius(mShadowMargin);
336            mScreenImageBounds.roundOut(mShadowBounds);
337            mShadowBounds.set(mShadowBounds.left - margin, mShadowBounds.top -
338                    margin, mShadowBounds.right + margin, mShadowBounds.bottom + margin);
339            mShadow.setBounds(mShadowBounds);
340            mShadow.draw(canvas);
341        }
342
343        mPaint.setAntiAlias(true);
344        mPaint.setFilterBitmap(true);
345        // Draw actual bitmap
346        canvas.drawBitmap(mBitmap, mDisplayMatrix, mPaint);
347
348        mCropObj.getInnerBounds(mScreenCropBounds);
349
350        if (mDisplayMatrix.mapRect(mScreenCropBounds)) {
351
352            // Draw overlay shadows
353            Paint p = new Paint();
354            p.setColor(mOverlayShadowColor);
355            p.setStyle(Paint.Style.FILL);
356            CropDrawingUtils.drawShadows(canvas, p, mScreenCropBounds, mScreenImageBounds);
357
358            // Draw crop rect and markers
359            CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds);
360            if (!mDoSpot) {
361                CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds);
362            } else {
363                Paint wpPaint = new Paint();
364                wpPaint.setColor(mWPMarkerColor);
365                wpPaint.setStrokeWidth(3);
366                wpPaint.setStyle(Paint.Style.STROKE);
367                wpPaint.setPathEffect(new DashPathEffect(new float[]
368                        {mDashOnLength, mDashOnLength + mDashOffLength}, 0));
369                p.setColor(mOverlayWPShadowColor);
370                CropDrawingUtils.drawWallpaperSelectionFrame(canvas, mScreenCropBounds,
371                        mSpotX, mSpotY, wpPaint, p);
372            }
373            CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize,
374                    mScreenCropBounds, mCropObj.isFixedAspect(), decode(mCropObj.getSelectState(), mRotation));
375        }
376
377    }
378}
379