Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 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 | /* Copied from Launcher3 */ |
| 17 | package com.android.wallpapercropper; |
| 18 | |
| 19 | import android.content.Context; |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 20 | import android.graphics.Matrix; |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 21 | import android.graphics.Point; |
| 22 | import android.graphics.RectF; |
| 23 | import android.util.AttributeSet; |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 24 | import android.util.FloatMath; |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 25 | import android.view.MotionEvent; |
| 26 | import android.view.ScaleGestureDetector; |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 27 | import android.view.ScaleGestureDetector.OnScaleGestureListener; |
Michael Jurka | e72aa7f | 2013-10-07 17:03:30 -0700 | [diff] [blame] | 28 | import android.view.ViewConfiguration; |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 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; |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 41 | private float mCenterX, mCenterY; |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 42 | private float mMinScale; |
| 43 | private boolean mTouchEnabled = true; |
| 44 | private RectF mTempEdges = new RectF(); |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 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 }; |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 50 | TouchCallback mTouchCallback; |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 51 | Matrix mRotateMatrix; |
| 52 | Matrix mInverseRotateMatrix; |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 53 | |
| 54 | public interface TouchCallback { |
| 55 | void onTouchDown(); |
| 56 | void onTap(); |
Michael Jurka | e72aa7f | 2013-10-07 17:03:30 -0700 | [diff] [blame] | 57 | void onTouchUp(); |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 58 | } |
| 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 Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 67 | 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 Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 81 | } |
| 82 | |
| 83 | private void getEdgesHelper(RectF edgesOut) { |
| 84 | final float width = getWidth(); |
| 85 | final float height = getHeight(); |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 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 | |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 100 | final float scale = mRenderer.scale; |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 101 | float centerX = (width / 2f - rendererCenter[0] + (imageWidth - width) / 2f) |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 102 | * scale + width / 2f; |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 103 | float centerY = (height / 2f - rendererCenter[1] + (imageHeight - height) / 2f) |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 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 | |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 116 | public int getImageRotation() { |
| 117 | return mRenderer.rotation; |
| 118 | } |
| 119 | |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 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 | |
| 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 Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 139 | mCenterX = mRenderer.centerX; |
| 140 | mCenterY = mRenderer.centerY; |
| 141 | mRotateMatrix.reset(); |
| 142 | mRotateMatrix.setRotate(mRenderer.rotation); |
| 143 | mInverseRotateMatrix.reset(); |
| 144 | mInverseRotateMatrix.setRotate(-mRenderer.rotation); |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 145 | 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 Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 164 | 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 Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 168 | 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 Jurka | e72aa7f | 2013-10-07 17:03:30 -0700 | [diff] [blame] | 192 | public void moveToLeft() { |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 193 | if (getWidth() == 0 || getHeight() == 0) { |
| 194 | final ViewTreeObserver observer = getViewTreeObserver(); |
| 195 | observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { |
| 196 | public void onGlobalLayout() { |
Michael Jurka | e72aa7f | 2013-10-07 17:03:30 -0700 | [diff] [blame] | 197 | moveToLeft(); |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 198 | getViewTreeObserver().removeOnGlobalLayoutListener(this); |
| 199 | } |
| 200 | }); |
| 201 | } |
| 202 | final RectF edges = mTempEdges; |
| 203 | getEdgesHelper(edges); |
| 204 | final float scale = mRenderer.scale; |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 205 | 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 Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 212 | } |
| 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 Jurka | e72aa7f | 2013-10-07 17:03:30 -0700 | [diff] [blame] | 254 | if (mTouchCallback != null) { |
| 255 | // only do this if it's a small movement |
| 256 | if (squaredDist < slop && |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 257 | now < mTouchDownTime + ViewConfiguration.getTapTimeout()) { |
Michael Jurka | e72aa7f | 2013-10-07 17:03:30 -0700 | [diff] [blame] | 258 | mTouchCallback.onTap(); |
| 259 | } |
| 260 | mTouchCallback.onTouchUp(); |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 261 | } |
| 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 Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 272 | 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 Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 279 | 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 Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 288 | |
| 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 Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 296 | if (edges.left > 0) { |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 297 | adjustment[0] = edges.left / scale; |
| 298 | } else if (edges.right < getWidth()) { |
| 299 | adjustment[0] = (edges.right - getWidth()) / scale; |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 300 | } |
| 301 | if (edges.top > 0) { |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 302 | adjustment[1] = FloatMath.ceil(edges.top / scale); |
| 303 | } else if (edges.bottom < getHeight()) { |
| 304 | adjustment[1] = (edges.bottom - getHeight()) / scale; |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 305 | } |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 306 | for (int dim = 0; dim <= 1; dim++) { |
| 307 | if (coef[dim] > 0) adjustment[dim] = FloatMath.ceil(adjustment[dim]); |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 308 | } |
Michael Jurka | 6978406 | 2013-10-14 14:42:50 -0700 | [diff] [blame] | 309 | |
| 310 | mInverseRotateMatrix.mapPoints(adjustment); |
| 311 | mCenterX += adjustment[0]; |
| 312 | mCenterY += adjustment[1]; |
| 313 | updateCenter(); |
Michael Jurka | e8d1bf7 | 2013-09-09 15:58:54 +0200 | [diff] [blame] | 314 | } |
| 315 | } |
| 316 | |
| 317 | mLastX = x; |
| 318 | mLastY = y; |
| 319 | return true; |
| 320 | } |
| 321 | } |