blob: c3ef3024f4840658b613d0e46f4e852ee4707922 [file] [log] [blame]
Michael Jurkae8d1bf72013-09-09 15:58:54 +02001/*
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 */
17package com.android.wallpapercropper;
18
19import android.content.Context;
Michael Jurka69784062013-10-14 14:42:50 -070020import android.graphics.Matrix;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020021import android.graphics.Point;
22import android.graphics.RectF;
23import android.util.AttributeSet;
Michael Jurka69784062013-10-14 14:42:50 -070024import android.util.FloatMath;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020025import android.view.MotionEvent;
26import android.view.ScaleGestureDetector;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020027import android.view.ScaleGestureDetector.OnScaleGestureListener;
Michael Jurkae72aa7f2013-10-07 17:03:30 -070028import android.view.ViewConfiguration;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020029import android.view.ViewTreeObserver;
30import android.view.ViewTreeObserver.OnGlobalLayoutListener;
31
32import com.android.photos.views.TiledImageRenderer.TileSource;
33import com.android.photos.views.TiledImageView;
34
35public 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;
Michael Jurka69784062013-10-14 14:42:50 -070041 private float mCenterX, mCenterY;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020042 private float mMinScale;
43 private boolean mTouchEnabled = true;
44 private RectF mTempEdges = new RectF();
Michael Jurka69784062013-10-14 14:42:50 -070045 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 };
Michael Jurkae8d1bf72013-09-09 15:58:54 +020050 TouchCallback mTouchCallback;
Michael Jurka69784062013-10-14 14:42:50 -070051 Matrix mRotateMatrix;
52 Matrix mInverseRotateMatrix;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020053
54 public interface TouchCallback {
55 void onTouchDown();
56 void onTap();
Michael Jurkae72aa7f2013-10-07 17:03:30 -070057 void onTouchUp();
Michael Jurkae8d1bf72013-09-09 15:58:54 +020058 }
59
60 public CropView(Context context) {
61 this(context, null);
62 }
63
64 public CropView(Context context, AttributeSet attrs) {
65 super(context, attrs);
66 mScaleGestureDetector = new ScaleGestureDetector(context, this);
Michael Jurka69784062013-10-14 14:42:50 -070067 mRotateMatrix = new Matrix();
68 mInverseRotateMatrix = new Matrix();
69 }
70
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;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020081 }
82
83 private void getEdgesHelper(RectF edgesOut) {
84 final float width = getWidth();
85 final float height = getHeight();
Michael Jurka69784062013-10-14 14:42:50 -070086 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
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200100 final float scale = mRenderer.scale;
Michael Jurka69784062013-10-14 14:42:50 -0700101 float centerX = (width / 2f - rendererCenter[0] + (imageWidth - width) / 2f)
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200102 * scale + width / 2f;
Michael Jurka69784062013-10-14 14:42:50 -0700103 float centerY = (height / 2f - rendererCenter[1] + (imageHeight - height) / 2f)
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200104 * 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
Michael Jurka69784062013-10-14 14:42:50 -0700116 public int getImageRotation() {
117 return mRenderer.rotation;
118 }
119
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200120 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
133 public Point getSourceDimensions() {
134 return new Point(mRenderer.source.getImageWidth(), mRenderer.source.getImageHeight());
135 }
136
137 public void setTileSource(TileSource source, Runnable isReadyCallback) {
138 super.setTileSource(source, isReadyCallback);
Michael Jurka69784062013-10-14 14:42:50 -0700139 mCenterX = mRenderer.centerX;
140 mCenterY = mRenderer.centerY;
141 mRotateMatrix.reset();
142 mRotateMatrix.setRotate(mRenderer.rotation);
143 mInverseRotateMatrix.reset();
144 mInverseRotateMatrix.setRotate(-mRenderer.rotation);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200145 updateMinScale(getWidth(), getHeight(), source, true);
146 }
147
148 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
149 updateMinScale(w, h, mRenderer.source, false);
150 }
151
152 public void setScale(float scale) {
153 synchronized (mLock) {
154 mRenderer.scale = scale;
155 }
156 }
157
158 private void updateMinScale(int w, int h, TileSource source, boolean resetScale) {
159 synchronized (mLock) {
160 if (resetScale) {
161 mRenderer.scale = 1;
162 }
163 if (source != null) {
Michael Jurka69784062013-10-14 14:42:50 -0700164 final float[] imageDims = getImageDims();
165 final float imageWidth = imageDims[0];
166 final float imageHeight = imageDims[1];
167 mMinScale = Math.max(w / imageWidth, h / imageHeight);
Michael Jurkae39c9a92013-12-02 15:05:44 -0800168 mRenderer.scale =
169 Math.max(mMinScale, resetScale ? Float.MIN_VALUE : mRenderer.scale);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200170 }
171 }
172 }
173
174 @Override
175 public boolean onScaleBegin(ScaleGestureDetector detector) {
176 return true;
177 }
178
179 @Override
180 public boolean onScale(ScaleGestureDetector detector) {
181 // Don't need the lock because this will only fire inside of
182 // onTouchEvent
183 mRenderer.scale *= detector.getScaleFactor();
184 mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
185 invalidate();
186 return true;
187 }
188
189 @Override
190 public void onScaleEnd(ScaleGestureDetector detector) {
191 }
192
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700193 public void moveToLeft() {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200194 if (getWidth() == 0 || getHeight() == 0) {
195 final ViewTreeObserver observer = getViewTreeObserver();
196 observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
197 public void onGlobalLayout() {
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700198 moveToLeft();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200199 getViewTreeObserver().removeOnGlobalLayoutListener(this);
200 }
201 });
202 }
203 final RectF edges = mTempEdges;
204 getEdgesHelper(edges);
205 final float scale = mRenderer.scale;
Michael Jurka69784062013-10-14 14:42:50 -0700206 mCenterX += Math.ceil(edges.left / scale);
207 updateCenter();
208 }
209
210 private void updateCenter() {
211 mRenderer.centerX = Math.round(mCenterX);
212 mRenderer.centerY = Math.round(mCenterY);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200213 }
214
215 public void setTouchEnabled(boolean enabled) {
216 mTouchEnabled = enabled;
217 }
218
219 public void setTouchCallback(TouchCallback cb) {
220 mTouchCallback = cb;
221 }
222
223 @Override
224 public boolean onTouchEvent(MotionEvent event) {
225 int action = event.getActionMasked();
226 final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
227 final int skipIndex = pointerUp ? event.getActionIndex() : -1;
228
229 // Determine focal point
230 float sumX = 0, sumY = 0;
231 final int count = event.getPointerCount();
232 for (int i = 0; i < count; i++) {
233 if (skipIndex == i)
234 continue;
235 sumX += event.getX(i);
236 sumY += event.getY(i);
237 }
238 final int div = pointerUp ? count - 1 : count;
239 float x = sumX / div;
240 float y = sumY / div;
241
242 if (action == MotionEvent.ACTION_DOWN) {
243 mFirstX = x;
244 mFirstY = y;
245 mTouchDownTime = System.currentTimeMillis();
246 if (mTouchCallback != null) {
247 mTouchCallback.onTouchDown();
248 }
249 } else if (action == MotionEvent.ACTION_UP) {
250 ViewConfiguration config = ViewConfiguration.get(getContext());
251
252 float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y);
253 float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop();
254 long now = System.currentTimeMillis();
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700255 if (mTouchCallback != null) {
256 // only do this if it's a small movement
257 if (squaredDist < slop &&
Michael Jurka69784062013-10-14 14:42:50 -0700258 now < mTouchDownTime + ViewConfiguration.getTapTimeout()) {
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700259 mTouchCallback.onTap();
260 }
261 mTouchCallback.onTouchUp();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200262 }
263 }
264
265 if (!mTouchEnabled) {
266 return true;
267 }
268
269 synchronized (mLock) {
270 mScaleGestureDetector.onTouchEvent(event);
271 switch (action) {
272 case MotionEvent.ACTION_MOVE:
Michael Jurka69784062013-10-14 14:42:50 -0700273 float[] point = mTempPoint;
274 point[0] = (mLastX - x) / mRenderer.scale;
275 point[1] = (mLastY - y) / mRenderer.scale;
276 mInverseRotateMatrix.mapPoints(point);
277 mCenterX += point[0];
278 mCenterY += point[1];
279 updateCenter();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200280 invalidate();
281 break;
282 }
283 if (mRenderer.source != null) {
284 // Adjust position so that the wallpaper covers the entire area
285 // of the screen
286 final RectF edges = mTempEdges;
287 getEdgesHelper(edges);
288 final float scale = mRenderer.scale;
Michael Jurka69784062013-10-14 14:42:50 -0700289
290 float[] coef = mTempCoef;
291 coef[0] = 1;
292 coef[1] = 1;
293 mRotateMatrix.mapPoints(coef);
294 float[] adjustment = mTempAdjustment;
295 mTempAdjustment[0] = 0;
296 mTempAdjustment[1] = 0;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200297 if (edges.left > 0) {
Michael Jurka69784062013-10-14 14:42:50 -0700298 adjustment[0] = edges.left / scale;
299 } else if (edges.right < getWidth()) {
300 adjustment[0] = (edges.right - getWidth()) / scale;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200301 }
302 if (edges.top > 0) {
Michael Jurka69784062013-10-14 14:42:50 -0700303 adjustment[1] = FloatMath.ceil(edges.top / scale);
304 } else if (edges.bottom < getHeight()) {
305 adjustment[1] = (edges.bottom - getHeight()) / scale;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200306 }
Michael Jurka69784062013-10-14 14:42:50 -0700307 for (int dim = 0; dim <= 1; dim++) {
308 if (coef[dim] > 0) adjustment[dim] = FloatMath.ceil(adjustment[dim]);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200309 }
Michael Jurka69784062013-10-14 14:42:50 -0700310
311 mInverseRotateMatrix.mapPoints(adjustment);
312 mCenterX += adjustment[0];
313 mCenterY += adjustment[1];
314 updateCenter();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200315 }
316 }
317
318 mLastX = x;
319 mLastY = y;
320 return true;
321 }
322}