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