blob: 4405207274387e8cf4b09afcc3df7ba9256288de [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;
24import android.view.MotionEvent;
25import android.view.ScaleGestureDetector;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020026import android.view.ScaleGestureDetector.OnScaleGestureListener;
Michael Jurkae72aa7f2013-10-07 17:03:30 -070027import android.view.ViewConfiguration;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020028import android.view.ViewTreeObserver;
29import android.view.ViewTreeObserver.OnGlobalLayoutListener;
30
31import com.android.photos.views.TiledImageRenderer.TileSource;
32import com.android.photos.views.TiledImageView;
33
34public 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;
Michael Jurka69784062013-10-14 14:42:50 -070040 private float mCenterX, mCenterY;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020041 private float mMinScale;
42 private boolean mTouchEnabled = true;
43 private RectF mTempEdges = new RectF();
Michael Jurka69784062013-10-14 14:42:50 -070044 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 };
Michael Jurkae8d1bf72013-09-09 15:58:54 +020049 TouchCallback mTouchCallback;
Michael Jurka69784062013-10-14 14:42:50 -070050 Matrix mRotateMatrix;
51 Matrix mInverseRotateMatrix;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020052
53 public interface TouchCallback {
54 void onTouchDown();
55 void onTap();
Michael Jurkae72aa7f2013-10-07 17:03:30 -070056 void onTouchUp();
Michael Jurkae8d1bf72013-09-09 15:58:54 +020057 }
58
59 public CropView(Context context) {
60 this(context, null);
61 }
62
63 public CropView(Context context, AttributeSet attrs) {
64 super(context, attrs);
65 mScaleGestureDetector = new ScaleGestureDetector(context, this);
Michael Jurka69784062013-10-14 14:42:50 -070066 mRotateMatrix = new Matrix();
67 mInverseRotateMatrix = new Matrix();
68 }
69
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;
Michael Jurkae8d1bf72013-09-09 15:58:54 +020080 }
81
82 private void getEdgesHelper(RectF edgesOut) {
83 final float width = getWidth();
84 final float height = getHeight();
Michael Jurka69784062013-10-14 14:42:50 -070085 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
Michael Jurkae8d1bf72013-09-09 15:58:54 +020099 final float scale = mRenderer.scale;
Michael Jurka69784062013-10-14 14:42:50 -0700100 float centerX = (width / 2f - rendererCenter[0] + (imageWidth - width) / 2f)
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200101 * scale + width / 2f;
Michael Jurka69784062013-10-14 14:42:50 -0700102 float centerY = (height / 2f - rendererCenter[1] + (imageHeight - height) / 2f)
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200103 * 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
Michael Jurka69784062013-10-14 14:42:50 -0700115 public int getImageRotation() {
116 return mRenderer.rotation;
117 }
118
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200119 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
132 public Point getSourceDimensions() {
133 return new Point(mRenderer.source.getImageWidth(), mRenderer.source.getImageHeight());
134 }
135
136 public void setTileSource(TileSource source, Runnable isReadyCallback) {
137 super.setTileSource(source, isReadyCallback);
Michael Jurka69784062013-10-14 14:42:50 -0700138 mCenterX = mRenderer.centerX;
139 mCenterY = mRenderer.centerY;
140 mRotateMatrix.reset();
141 mRotateMatrix.setRotate(mRenderer.rotation);
142 mInverseRotateMatrix.reset();
143 mInverseRotateMatrix.setRotate(-mRenderer.rotation);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200144 updateMinScale(getWidth(), getHeight(), source, true);
145 }
146
147 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
148 updateMinScale(w, h, mRenderer.source, false);
149 }
150
151 public void setScale(float scale) {
152 synchronized (mLock) {
153 mRenderer.scale = scale;
154 }
155 }
156
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) {
Michael Jurka69784062013-10-14 14:42:50 -0700163 final float[] imageDims = getImageDims();
164 final float imageWidth = imageDims[0];
165 final float imageHeight = imageDims[1];
166 mMinScale = Math.max(w / imageWidth, h / imageHeight);
Michael Jurkac82618c2013-12-02 15:05:44 -0800167 mRenderer.scale =
168 Math.max(mMinScale, resetScale ? Float.MIN_VALUE : mRenderer.scale);
Michael Jurkae8d1bf72013-09-09 15:58:54 +0200169 }
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) {
Neil Fuller33253a42014-10-01 11:55:10 +0100302 adjustment[1] = (float) Math.ceil(edges.top / scale);
Michael Jurka69784062013-10-14 14:42:50 -0700303 } 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++) {
Neil Fuller33253a42014-10-01 11:55:10 +0100307 if (coef[dim] > 0) adjustment[dim] = (float) Math.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}