blob: 14f7c1d1f7cf5c08f37e1c05f968a92173c038d9 [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 Jurkae8d1bf72013-09-09 15:58:54 +0200168 mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
169 }
170 }
171 }
172
173 @Override
174 public boolean onScaleBegin(ScaleGestureDetector detector) {
175 return true;
176 }
177
178 @Override
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
189 public void onScaleEnd(ScaleGestureDetector detector) {
190 }
191
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700192 public void moveToLeft() {
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200193 if (getWidth() == 0 || getHeight() == 0) {
194 final ViewTreeObserver observer = getViewTreeObserver();
195 observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
196 public void onGlobalLayout() {
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700197 moveToLeft();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200198 getViewTreeObserver().removeOnGlobalLayoutListener(this);
199 }
200 });
201 }
202 final RectF edges = mTempEdges;
203 getEdgesHelper(edges);
204 final float scale = mRenderer.scale;
Michael Jurka69784062013-10-14 14:42:50 -0700205 mCenterX += Math.ceil(edges.left / scale);
206 updateCenter();
207 }
208
209 private void updateCenter() {
210 mRenderer.centerX = Math.round(mCenterX);
211 mRenderer.centerY = Math.round(mCenterY);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200212 }
213
214 public void setTouchEnabled(boolean enabled) {
215 mTouchEnabled = enabled;
216 }
217
218 public void setTouchCallback(TouchCallback cb) {
219 mTouchCallback = cb;
220 }
221
222 @Override
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();
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700254 if (mTouchCallback != null) {
255 // only do this if it's a small movement
256 if (squaredDist < slop &&
Michael Jurka69784062013-10-14 14:42:50 -0700257 now < mTouchDownTime + ViewConfiguration.getTapTimeout()) {
Michael Jurkae72aa7f2013-10-07 17:03:30 -0700258 mTouchCallback.onTap();
259 }
260 mTouchCallback.onTouchUp();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200261 }
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:
Michael Jurka69784062013-10-14 14:42:50 -0700272 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();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200279 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;
Michael Jurka69784062013-10-14 14:42:50 -0700288
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;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200296 if (edges.left > 0) {
Michael Jurka69784062013-10-14 14:42:50 -0700297 adjustment[0] = edges.left / scale;
298 } else if (edges.right < getWidth()) {
299 adjustment[0] = (edges.right - getWidth()) / scale;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200300 }
301 if (edges.top > 0) {
Michael Jurka69784062013-10-14 14:42:50 -0700302 adjustment[1] = FloatMath.ceil(edges.top / scale);
303 } else if (edges.bottom < getHeight()) {
304 adjustment[1] = (edges.bottom - getHeight()) / scale;
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200305 }
Michael Jurka69784062013-10-14 14:42:50 -0700306 for (int dim = 0; dim <= 1; dim++) {
307 if (coef[dim] > 0) adjustment[dim] = FloatMath.ceil(adjustment[dim]);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200308 }
Michael Jurka69784062013-10-14 14:42:50 -0700309
310 mInverseRotateMatrix.mapPoints(adjustment);
311 mCenterX += adjustment[0];
312 mCenterY += adjustment[1];
313 updateCenter();
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200314 }
315 }
316
317 mLastX = x;
318 mLastY = y;
319 return true;
320 }
321}