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.launcher3; 18 19 import android.content.Context; 20 import android.graphics.Matrix; 21 import android.graphics.Point; 22 import android.graphics.PointF; 23 import android.graphics.RectF; 24 import android.util.AttributeSet; 25 import android.view.MotionEvent; 26 import android.view.ScaleGestureDetector; 27 import android.view.ScaleGestureDetector.OnScaleGestureListener; 28 import android.view.ViewConfiguration; 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; 41 private float mCenterX, mCenterY; 42 private float mMinScale; 43 private boolean mTouchEnabled = true; 44 private RectF mTempEdges = new RectF(); 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 }; 50 TouchCallback mTouchCallback; 51 Matrix mRotateMatrix; 52 Matrix mInverseRotateMatrix; 53 54 public interface TouchCallback { onTouchDown()55 void onTouchDown(); onTap()56 void onTap(); onTouchUp()57 void onTouchUp(); 58 } 59 CropView(Context context)60 public CropView(Context context) { 61 this(context, null); 62 } 63 CropView(Context context, AttributeSet attrs)64 public CropView(Context context, AttributeSet attrs) { 65 super(context, attrs); 66 mScaleGestureDetector = new ScaleGestureDetector(context, this); 67 mRotateMatrix = new Matrix(); 68 mInverseRotateMatrix = new Matrix(); 69 } 70 getImageDims()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; 81 } 82 getEdgesHelper(RectF edgesOut)83 private void getEdgesHelper(RectF edgesOut) { 84 final float width = getWidth(); 85 final float height = getHeight(); 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 100 final float scale = mRenderer.scale; 101 float centerX = (width / 2f - rendererCenter[0] + (imageWidth - width) / 2f) 102 * scale + width / 2f; 103 float centerY = (height / 2f - rendererCenter[1] + (imageHeight - height) / 2f) 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 getImageRotation()116 public int getImageRotation() { 117 return mRenderer.rotation; 118 } 119 getCrop()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 getSourceDimensions()133 public Point getSourceDimensions() { 134 return new Point(mRenderer.source.getImageWidth(), mRenderer.source.getImageHeight()); 135 } 136 setTileSource(TileSource source, Runnable isReadyCallback)137 public void setTileSource(TileSource source, Runnable isReadyCallback) { 138 super.setTileSource(source, isReadyCallback); 139 mCenterX = mRenderer.centerX; 140 mCenterY = mRenderer.centerY; 141 mRotateMatrix.reset(); 142 mRotateMatrix.setRotate(mRenderer.rotation); 143 mInverseRotateMatrix.reset(); 144 mInverseRotateMatrix.setRotate(-mRenderer.rotation); 145 updateMinScale(getWidth(), getHeight(), source, true); 146 } 147 onSizeChanged(int w, int h, int oldw, int oldh)148 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 149 updateMinScale(w, h, mRenderer.source, false); 150 } 151 setScaleAndCenter(float scale, float x, float y)152 public void setScaleAndCenter(float scale, float x, float y) { 153 synchronized (mLock) { 154 mRenderer.scale = scale; 155 mCenterX = x; 156 mCenterY = y; 157 updateCenter(); 158 } 159 } 160 getScale()161 public float getScale() { 162 return mRenderer.scale; 163 } 164 updateMinScale(int w, int h, TileSource source, boolean resetScale)165 private void updateMinScale(int w, int h, TileSource source, boolean resetScale) { 166 synchronized (mLock) { 167 if (resetScale) { 168 mRenderer.scale = 1; 169 } 170 if (source != null) { 171 final float[] imageDims = getImageDims(); 172 final float imageWidth = imageDims[0]; 173 final float imageHeight = imageDims[1]; 174 mMinScale = Math.max(w / imageWidth, h / imageHeight); 175 mRenderer.scale = 176 Math.max(mMinScale, resetScale ? Float.MIN_VALUE : mRenderer.scale); 177 } 178 } 179 } 180 181 @Override onScaleBegin(ScaleGestureDetector detector)182 public boolean onScaleBegin(ScaleGestureDetector detector) { 183 return true; 184 } 185 186 @Override onScale(ScaleGestureDetector detector)187 public boolean onScale(ScaleGestureDetector detector) { 188 // Don't need the lock because this will only fire inside of 189 // onTouchEvent 190 mRenderer.scale *= detector.getScaleFactor(); 191 mRenderer.scale = Math.max(mMinScale, mRenderer.scale); 192 invalidate(); 193 return true; 194 } 195 196 @Override onScaleEnd(ScaleGestureDetector detector)197 public void onScaleEnd(ScaleGestureDetector detector) { 198 } 199 moveToLeft()200 public void moveToLeft() { 201 if (getWidth() == 0 || getHeight() == 0) { 202 final ViewTreeObserver observer = getViewTreeObserver(); 203 observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 204 public void onGlobalLayout() { 205 moveToLeft(); 206 getViewTreeObserver().removeOnGlobalLayoutListener(this); 207 } 208 }); 209 } 210 final RectF edges = mTempEdges; 211 getEdgesHelper(edges); 212 final float scale = mRenderer.scale; 213 mCenterX += Math.ceil(edges.left / scale); 214 updateCenter(); 215 } 216 updateCenter()217 private void updateCenter() { 218 mRenderer.centerX = Math.round(mCenterX); 219 mRenderer.centerY = Math.round(mCenterY); 220 } 221 getCenter()222 public PointF getCenter() { 223 return new PointF(mCenterX, mCenterY); 224 } 225 setTouchEnabled(boolean enabled)226 public void setTouchEnabled(boolean enabled) { 227 mTouchEnabled = enabled; 228 } 229 setTouchCallback(TouchCallback cb)230 public void setTouchCallback(TouchCallback cb) { 231 mTouchCallback = cb; 232 } 233 234 @Override onTouchEvent(MotionEvent event)235 public boolean onTouchEvent(MotionEvent event) { 236 int action = event.getActionMasked(); 237 final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP; 238 final int skipIndex = pointerUp ? event.getActionIndex() : -1; 239 240 // Determine focal point 241 float sumX = 0, sumY = 0; 242 final int count = event.getPointerCount(); 243 for (int i = 0; i < count; i++) { 244 if (skipIndex == i) 245 continue; 246 sumX += event.getX(i); 247 sumY += event.getY(i); 248 } 249 final int div = pointerUp ? count - 1 : count; 250 float x = sumX / div; 251 float y = sumY / div; 252 253 if (action == MotionEvent.ACTION_DOWN) { 254 mFirstX = x; 255 mFirstY = y; 256 mTouchDownTime = System.currentTimeMillis(); 257 if (mTouchCallback != null) { 258 mTouchCallback.onTouchDown(); 259 } 260 } else if (action == MotionEvent.ACTION_UP) { 261 ViewConfiguration config = ViewConfiguration.get(getContext()); 262 263 float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y); 264 float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop(); 265 long now = System.currentTimeMillis(); 266 if (mTouchCallback != null) { 267 // only do this if it's a small movement 268 if (squaredDist < slop && 269 now < mTouchDownTime + ViewConfiguration.getTapTimeout()) { 270 mTouchCallback.onTap(); 271 } 272 mTouchCallback.onTouchUp(); 273 } 274 } 275 276 if (!mTouchEnabled) { 277 return true; 278 } 279 280 synchronized (mLock) { 281 mScaleGestureDetector.onTouchEvent(event); 282 switch (action) { 283 case MotionEvent.ACTION_MOVE: 284 float[] point = mTempPoint; 285 point[0] = (mLastX - x) / mRenderer.scale; 286 point[1] = (mLastY - y) / mRenderer.scale; 287 mInverseRotateMatrix.mapPoints(point); 288 mCenterX += point[0]; 289 mCenterY += point[1]; 290 updateCenter(); 291 invalidate(); 292 break; 293 } 294 if (mRenderer.source != null) { 295 // Adjust position so that the wallpaper covers the entire area 296 // of the screen 297 final RectF edges = mTempEdges; 298 getEdgesHelper(edges); 299 final float scale = mRenderer.scale; 300 301 float[] coef = mTempCoef; 302 coef[0] = 1; 303 coef[1] = 1; 304 mRotateMatrix.mapPoints(coef); 305 float[] adjustment = mTempAdjustment; 306 mTempAdjustment[0] = 0; 307 mTempAdjustment[1] = 0; 308 if (edges.left > 0) { 309 adjustment[0] = edges.left / scale; 310 } else if (edges.right < getWidth()) { 311 adjustment[0] = (edges.right - getWidth()) / scale; 312 } 313 if (edges.top > 0) { 314 adjustment[1] = (float) Math.ceil(edges.top / scale); 315 } else if (edges.bottom < getHeight()) { 316 adjustment[1] = (edges.bottom - getHeight()) / scale; 317 } 318 for (int dim = 0; dim <= 1; dim++) { 319 if (coef[dim] > 0) adjustment[dim] = (float) Math.ceil(adjustment[dim]); 320 } 321 322 mInverseRotateMatrix.mapPoints(adjustment); 323 mCenterX += adjustment[0]; 324 mCenterY += adjustment[1]; 325 updateCenter(); 326 } 327 } 328 329 mLastX = x; 330 mLastY = y; 331 return true; 332 } 333 } 334