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 
17 package com.android.gallery3d.filtershow.crop;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Bitmap;
22 import android.graphics.Canvas;
23 import android.graphics.DashPathEffect;
24 import android.graphics.Matrix;
25 import android.graphics.Paint;
26 import android.graphics.Rect;
27 import android.graphics.RectF;
28 import android.graphics.drawable.Drawable;
29 import android.graphics.drawable.NinePatchDrawable;
30 import android.util.AttributeSet;
31 import android.util.Log;
32 import android.view.MotionEvent;
33 import android.view.View;
34 
35 import com.android.gallery3d.R;
36 
37 
38 public 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 
CropView(Context context)82     public CropView(Context context) {
83         super(context);
84         setup(context);
85     }
86 
CropView(Context context, AttributeSet attrs)87     public CropView(Context context, AttributeSet attrs) {
88         super(context, attrs);
89         setup(context);
90     }
91 
CropView(Context context, AttributeSet attrs, int defStyle)92     public CropView(Context context, AttributeSet attrs, int defStyle) {
93         super(context, attrs, defStyle);
94         setup(context);
95     }
96 
setup(Context context)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 
initialize(Bitmap image, RectF newCropBounds, RectF newPhotoBounds, int rotation)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 
getCrop()131     public RectF getCrop() {
132         return mCropObj.getInnerBounds();
133     }
134 
getPhoto()135     public RectF getPhoto() {
136         return mCropObj.getOuterBounds();
137     }
138 
139     @Override
onTouchEvent(MotionEvent event)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 
reset()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 
clearDisplay()197     private void clearDisplay() {
198         mDisplayMatrix = null;
199         mDisplayMatrixInverse = null;
200         invalidate();
201     }
202 
configChanged()203     protected void configChanged() {
204         mDirty = true;
205     }
206 
applyFreeAspect()207     public void applyFreeAspect() {
208         mCropObj.unsetAspectRatio();
209         invalidate();
210     }
211 
applyOriginalAspect()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 
applySquareAspect()224     public void applySquareAspect() {
225         applyAspect(1, 1);
226     }
227 
applyAspect(float x, float y)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 
setWallpaperSpotlight(float spotlightX, float spotlightY)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 
unsetWallpaperSpotlight()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      */
bitCycleLeft(int x, int times, int d)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      */
decode(int movingEdges, float rotation)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
onDraw(Canvas canvas)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 primary
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(
314                         mDisplayMatrix, mImageBounds, mScreenBounds, 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(),
375                     decode(mCropObj.getSelectState(), mRotation));
376         }
377 
378     }
379 }
380