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.wallpaperpicker;
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 
192     /**
193      * Offsets wallpaper preview according to the state it will be displayed in upon returning home.
194      * @param offset Ranges from 0 to 1, where 0 is the leftmost parallax and 1 is the rightmost.
195      */
setParallaxOffset(float offset, RectF crop)196     public void setParallaxOffset(float offset, RectF crop) {
197         offset = Math.max(0, Math.min(offset, 1)); // Make sure the offset is in the correct range.
198         float screenWidth = getWidth() / mRenderer.scale;
199         mCenterX = screenWidth / 2 + offset * (crop.width() - screenWidth) + crop.left;
200         updateCenter();
201     }
202 
moveToLeft()203     public void moveToLeft() {
204         if (getWidth() == 0 || getHeight() == 0) {
205             final ViewTreeObserver observer = getViewTreeObserver();
206             observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
207                     public void onGlobalLayout() {
208                         moveToLeft();
209                         getViewTreeObserver().removeOnGlobalLayoutListener(this);
210                     }
211                 });
212         }
213         final RectF edges = mTempEdges;
214         getEdgesHelper(edges);
215         final float scale = mRenderer.scale;
216         mCenterX += Math.ceil(edges.left / scale);
217         updateCenter();
218     }
219 
updateCenter()220     private void updateCenter() {
221         mRenderer.centerX = Math.round(mCenterX);
222         mRenderer.centerY = Math.round(mCenterY);
223     }
224 
setTouchEnabled(boolean enabled)225     public void setTouchEnabled(boolean enabled) {
226         mTouchEnabled = enabled;
227     }
228 
setTouchCallback(TouchCallback cb)229     public void setTouchCallback(TouchCallback cb) {
230         mTouchCallback = cb;
231     }
232 
233     @Override
onTouchEvent(MotionEvent event)234     public boolean onTouchEvent(MotionEvent event) {
235         int action = event.getActionMasked();
236         final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
237         final int skipIndex = pointerUp ? event.getActionIndex() : -1;
238 
239         // Determine focal point
240         float sumX = 0, sumY = 0;
241         final int count = event.getPointerCount();
242         for (int i = 0; i < count; i++) {
243             if (skipIndex == i)
244                 continue;
245             sumX += event.getX(i);
246             sumY += event.getY(i);
247         }
248         final int div = pointerUp ? count - 1 : count;
249         float x = sumX / div;
250         float y = sumY / div;
251 
252         if (action == MotionEvent.ACTION_DOWN) {
253             mFirstX = x;
254             mFirstY = y;
255             mTouchDownTime = System.currentTimeMillis();
256             if (mTouchCallback != null) {
257                 mTouchCallback.onTouchDown();
258             }
259         } else if (action == MotionEvent.ACTION_UP) {
260             ViewConfiguration config = ViewConfiguration.get(getContext());
261 
262             float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y);
263             float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop();
264             long now = System.currentTimeMillis();
265             if (mTouchCallback != null) {
266                 // only do this if it's a small movement
267                 if (squaredDist < slop &&
268                         now < mTouchDownTime + ViewConfiguration.getTapTimeout()) {
269                     mTouchCallback.onTap();
270                 }
271                 mTouchCallback.onTouchUp();
272             }
273         }
274 
275         if (!mTouchEnabled) {
276             return true;
277         }
278 
279         synchronized (mLock) {
280             mScaleGestureDetector.onTouchEvent(event);
281             switch (action) {
282                 case MotionEvent.ACTION_MOVE:
283                     float[] point = mTempPoint;
284                     point[0] = (mLastX - x) / mRenderer.scale;
285                     point[1] = (mLastY - y) / mRenderer.scale;
286                     mInverseRotateMatrix.mapPoints(point);
287                     mCenterX += point[0];
288                     mCenterY += point[1];
289                     updateCenter();
290                     invalidate();
291                     break;
292             }
293             if (mRenderer.source != null) {
294                 // Adjust position so that the wallpaper covers the entire area
295                 // of the screen
296                 final RectF edges = mTempEdges;
297                 getEdgesHelper(edges);
298                 final float scale = mRenderer.scale;
299 
300                 float[] coef = mTempCoef;
301                 coef[0] = 1;
302                 coef[1] = 1;
303                 mRotateMatrix.mapPoints(coef);
304                 float[] adjustment = mTempAdjustment;
305                 mTempAdjustment[0] = 0;
306                 mTempAdjustment[1] = 0;
307                 if (edges.left > 0) {
308                     adjustment[0] = edges.left / scale;
309                 } else if (edges.right < getWidth()) {
310                     adjustment[0] = (edges.right - getWidth()) / scale;
311                 }
312                 if (edges.top > 0) {
313                     adjustment[1] = (float) Math.ceil(edges.top / scale);
314                 } else if (edges.bottom < getHeight()) {
315                     adjustment[1] = (edges.bottom - getHeight()) / scale;
316                 }
317                 for (int dim = 0; dim <= 1; dim++) {
318                     if (coef[dim] > 0) adjustment[dim] = (float) Math.ceil(adjustment[dim]);
319                 }
320 
321                 mInverseRotateMatrix.mapPoints(adjustment);
322                 mCenterX += adjustment[0];
323                 mCenterY += adjustment[1];
324                 updateCenter();
325             }
326         }
327 
328         mLastX = x;
329         mLastY = y;
330         return true;
331     }
332 }
333