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