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 /* Copied from Launcher3 */
17 package com.android.wallpapercropper;
18 
19 import android.content.Context;
20 import android.graphics.Matrix;
21 import android.graphics.Point;
22 import android.graphics.RectF;
23 import android.util.AttributeSet;
24 import android.view.MotionEvent;
25 import android.view.ScaleGestureDetector;
26 import android.view.ScaleGestureDetector.OnScaleGestureListener;
27 import android.view.ViewConfiguration;
28 import android.view.ViewTreeObserver;
29 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
30 
31 import com.android.photos.views.TiledImageRenderer.TileSource;
32 import com.android.photos.views.TiledImageView;
33 
34 public class CropView extends TiledImageView implements OnScaleGestureListener {
35 
36     private ScaleGestureDetector mScaleGestureDetector;
37     private long mTouchDownTime;
38     private float mFirstX, mFirstY;
39     private float mLastX, mLastY;
40     private float mCenterX, mCenterY;
41     private float mMinScale;
42     private boolean mTouchEnabled = true;
43     private RectF mTempEdges = new RectF();
44     private float[] mTempPoint = new float[] { 0, 0 };
45     private float[] mTempCoef = new float[] { 0, 0 };
46     private float[] mTempAdjustment = new float[] { 0, 0 };
47     private float[] mTempImageDims = new float[] { 0, 0 };
48     private float[] mTempRendererCenter = new float[] { 0, 0 };
49     TouchCallback mTouchCallback;
50     Matrix mRotateMatrix;
51     Matrix mInverseRotateMatrix;
52 
53     public interface TouchCallback {
onTouchDown()54         void onTouchDown();
onTap()55         void onTap();
onTouchUp()56         void onTouchUp();
57     }
58 
CropView(Context context)59     public CropView(Context context) {
60         this(context, null);
61     }
62 
CropView(Context context, AttributeSet attrs)63     public CropView(Context context, AttributeSet attrs) {
64         super(context, attrs);
65         mScaleGestureDetector = new ScaleGestureDetector(context, this);
66         mRotateMatrix = new Matrix();
67         mInverseRotateMatrix = new Matrix();
68     }
69 
getImageDims()70     private float[] getImageDims() {
71         final float imageWidth = mRenderer.source.getImageWidth();
72         final float imageHeight = mRenderer.source.getImageHeight();
73         float[] imageDims = mTempImageDims;
74         imageDims[0] = imageWidth;
75         imageDims[1] = imageHeight;
76         mRotateMatrix.mapPoints(imageDims);
77         imageDims[0] = Math.abs(imageDims[0]);
78         imageDims[1] = Math.abs(imageDims[1]);
79         return imageDims;
80     }
81 
getEdgesHelper(RectF edgesOut)82     private void getEdgesHelper(RectF edgesOut) {
83         final float width = getWidth();
84         final float height = getHeight();
85         final float[] imageDims = getImageDims();
86         final float imageWidth = imageDims[0];
87         final float imageHeight = imageDims[1];
88 
89         float initialCenterX = mRenderer.source.getImageWidth() / 2f;
90         float initialCenterY = mRenderer.source.getImageHeight() / 2f;
91 
92         float[] rendererCenter = mTempRendererCenter;
93         rendererCenter[0] = mCenterX - initialCenterX;
94         rendererCenter[1] = mCenterY - initialCenterY;
95         mRotateMatrix.mapPoints(rendererCenter);
96         rendererCenter[0] += imageWidth / 2;
97         rendererCenter[1] += imageHeight / 2;
98 
99         final float scale = mRenderer.scale;
100         float centerX = (width / 2f - rendererCenter[0] + (imageWidth - width) / 2f)
101                 * scale + width / 2f;
102         float centerY = (height / 2f - rendererCenter[1] + (imageHeight - height) / 2f)
103                 * scale + height / 2f;
104         float leftEdge = centerX - imageWidth / 2f * scale;
105         float rightEdge = centerX + imageWidth / 2f * scale;
106         float topEdge = centerY - imageHeight / 2f * scale;
107         float bottomEdge = centerY + imageHeight / 2f * scale;
108 
109         edgesOut.left = leftEdge;
110         edgesOut.right = rightEdge;
111         edgesOut.top = topEdge;
112         edgesOut.bottom = bottomEdge;
113     }
114 
getImageRotation()115     public int getImageRotation() {
116         return mRenderer.rotation;
117     }
118 
getCrop()119     public RectF getCrop() {
120         final RectF edges = mTempEdges;
121         getEdgesHelper(edges);
122         final float scale = mRenderer.scale;
123 
124         float cropLeft = -edges.left / scale;
125         float cropTop = -edges.top / scale;
126         float cropRight = cropLeft + getWidth() / scale;
127         float cropBottom = cropTop + getHeight() / scale;
128 
129         return new RectF(cropLeft, cropTop, cropRight, cropBottom);
130     }
131 
getSourceDimensions()132     public Point getSourceDimensions() {
133         return new Point(mRenderer.source.getImageWidth(), mRenderer.source.getImageHeight());
134     }
135 
setTileSource(TileSource source, Runnable isReadyCallback)136     public void setTileSource(TileSource source, Runnable isReadyCallback) {
137         super.setTileSource(source, isReadyCallback);
138         mCenterX = mRenderer.centerX;
139         mCenterY = mRenderer.centerY;
140         mRotateMatrix.reset();
141         mRotateMatrix.setRotate(mRenderer.rotation);
142         mInverseRotateMatrix.reset();
143         mInverseRotateMatrix.setRotate(-mRenderer.rotation);
144         updateMinScale(getWidth(), getHeight(), source, true);
145     }
146 
onSizeChanged(int w, int h, int oldw, int oldh)147     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
148         updateMinScale(w, h, mRenderer.source, false);
149     }
150 
setScale(float scale)151     public void setScale(float scale) {
152         synchronized (mLock) {
153             mRenderer.scale = scale;
154         }
155     }
156 
updateMinScale(int w, int h, TileSource source, boolean resetScale)157     private void updateMinScale(int w, int h, TileSource source, boolean resetScale) {
158         synchronized (mLock) {
159             if (resetScale) {
160                 mRenderer.scale = 1;
161             }
162             if (source != null) {
163                 final float[] imageDims = getImageDims();
164                 final float imageWidth = imageDims[0];
165                 final float imageHeight = imageDims[1];
166                 mMinScale = Math.max(w / imageWidth, h / imageHeight);
167                 mRenderer.scale =
168                         Math.max(mMinScale, resetScale ? Float.MIN_VALUE : mRenderer.scale);
169             }
170         }
171     }
172 
173     @Override
onScaleBegin(ScaleGestureDetector detector)174     public boolean onScaleBegin(ScaleGestureDetector detector) {
175         return true;
176     }
177 
178     @Override
onScale(ScaleGestureDetector detector)179     public boolean onScale(ScaleGestureDetector detector) {
180         // Don't need the lock because this will only fire inside of
181         // onTouchEvent
182         mRenderer.scale *= detector.getScaleFactor();
183         mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
184         invalidate();
185         return true;
186     }
187 
188     @Override
onScaleEnd(ScaleGestureDetector detector)189     public void onScaleEnd(ScaleGestureDetector detector) {
190     }
191 
moveToLeft()192     public void moveToLeft() {
193         if (getWidth() == 0 || getHeight() == 0) {
194             final ViewTreeObserver observer = getViewTreeObserver();
195             observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
196                     public void onGlobalLayout() {
197                         moveToLeft();
198                         getViewTreeObserver().removeOnGlobalLayoutListener(this);
199                     }
200                 });
201         }
202         final RectF edges = mTempEdges;
203         getEdgesHelper(edges);
204         final float scale = mRenderer.scale;
205         mCenterX += Math.ceil(edges.left / scale);
206         updateCenter();
207     }
208 
updateCenter()209     private void updateCenter() {
210         mRenderer.centerX = Math.round(mCenterX);
211         mRenderer.centerY = Math.round(mCenterY);
212     }
213 
setTouchEnabled(boolean enabled)214     public void setTouchEnabled(boolean enabled) {
215         mTouchEnabled = enabled;
216     }
217 
setTouchCallback(TouchCallback cb)218     public void setTouchCallback(TouchCallback cb) {
219         mTouchCallback = cb;
220     }
221 
222     @Override
onTouchEvent(MotionEvent event)223     public boolean onTouchEvent(MotionEvent event) {
224         int action = event.getActionMasked();
225         final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
226         final int skipIndex = pointerUp ? event.getActionIndex() : -1;
227 
228         // Determine focal point
229         float sumX = 0, sumY = 0;
230         final int count = event.getPointerCount();
231         for (int i = 0; i < count; i++) {
232             if (skipIndex == i)
233                 continue;
234             sumX += event.getX(i);
235             sumY += event.getY(i);
236         }
237         final int div = pointerUp ? count - 1 : count;
238         float x = sumX / div;
239         float y = sumY / div;
240 
241         if (action == MotionEvent.ACTION_DOWN) {
242             mFirstX = x;
243             mFirstY = y;
244             mTouchDownTime = System.currentTimeMillis();
245             if (mTouchCallback != null) {
246                 mTouchCallback.onTouchDown();
247             }
248         } else if (action == MotionEvent.ACTION_UP) {
249             ViewConfiguration config = ViewConfiguration.get(getContext());
250 
251             float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y);
252             float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop();
253             long now = System.currentTimeMillis();
254             if (mTouchCallback != null) {
255                 // only do this if it's a small movement
256                 if (squaredDist < slop &&
257                         now < mTouchDownTime + ViewConfiguration.getTapTimeout()) {
258                     mTouchCallback.onTap();
259                 }
260                 mTouchCallback.onTouchUp();
261             }
262         }
263 
264         if (!mTouchEnabled) {
265             return true;
266         }
267 
268         synchronized (mLock) {
269             mScaleGestureDetector.onTouchEvent(event);
270             switch (action) {
271                 case MotionEvent.ACTION_MOVE:
272                     float[] point = mTempPoint;
273                     point[0] = (mLastX - x) / mRenderer.scale;
274                     point[1] = (mLastY - y) / mRenderer.scale;
275                     mInverseRotateMatrix.mapPoints(point);
276                     mCenterX += point[0];
277                     mCenterY += point[1];
278                     updateCenter();
279                     invalidate();
280                     break;
281             }
282             if (mRenderer.source != null) {
283                 // Adjust position so that the wallpaper covers the entire area
284                 // of the screen
285                 final RectF edges = mTempEdges;
286                 getEdgesHelper(edges);
287                 final float scale = mRenderer.scale;
288 
289                 float[] coef = mTempCoef;
290                 coef[0] = 1;
291                 coef[1] = 1;
292                 mRotateMatrix.mapPoints(coef);
293                 float[] adjustment = mTempAdjustment;
294                 mTempAdjustment[0] = 0;
295                 mTempAdjustment[1] = 0;
296                 if (edges.left > 0) {
297                     adjustment[0] = edges.left / scale;
298                 } else if (edges.right < getWidth()) {
299                     adjustment[0] = (edges.right - getWidth()) / scale;
300                 }
301                 if (edges.top > 0) {
302                     adjustment[1] = (float) Math.ceil(edges.top / scale);
303                 } else if (edges.bottom < getHeight()) {
304                     adjustment[1] = (edges.bottom - getHeight()) / scale;
305                 }
306                 for (int dim = 0; dim <= 1; dim++) {
307                     if (coef[dim] > 0) adjustment[dim] = (float) Math.ceil(adjustment[dim]);
308                 }
309 
310                 mInverseRotateMatrix.mapPoints(adjustment);
311                 mCenterX += adjustment[0];
312                 mCenterY += adjustment[1];
313                 updateCenter();
314             }
315         }
316 
317         mLastX = x;
318         mLastY = y;
319         return true;
320     }
321 }
322