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.gallery3d.filtershow.crop; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Bitmap; 22 import android.graphics.Canvas; 23 import android.graphics.DashPathEffect; 24 import android.graphics.Matrix; 25 import android.graphics.Paint; 26 import android.graphics.Rect; 27 import android.graphics.RectF; 28 import android.graphics.drawable.Drawable; 29 import android.graphics.drawable.NinePatchDrawable; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.view.MotionEvent; 33 import android.view.View; 34 35 import com.android.gallery3d.R; 36 37 38 public class CropView extends View { 39 private static final String LOGTAG = "CropView"; 40 41 private RectF mImageBounds = new RectF(); 42 private RectF mScreenBounds = new RectF(); 43 private RectF mScreenImageBounds = new RectF(); 44 private RectF mScreenCropBounds = new RectF(); 45 private Rect mShadowBounds = new Rect(); 46 47 private Bitmap mBitmap; 48 private Paint mPaint = new Paint(); 49 50 private NinePatchDrawable mShadow; 51 private CropObject mCropObj = null; 52 private Drawable mCropIndicator; 53 private int mIndicatorSize; 54 private int mRotation = 0; 55 private boolean mMovingBlock = false; 56 private Matrix mDisplayMatrix = null; 57 private Matrix mDisplayMatrixInverse = null; 58 private boolean mDirty = false; 59 60 private float mPrevX = 0; 61 private float mPrevY = 0; 62 private float mSpotX = 0; 63 private float mSpotY = 0; 64 private boolean mDoSpot = false; 65 66 private int mShadowMargin = 15; 67 private int mMargin = 32; 68 private int mOverlayShadowColor = 0xCF000000; 69 private int mOverlayWPShadowColor = 0x5F000000; 70 private int mWPMarkerColor = 0x7FFFFFFF; 71 private int mMinSideSize = 90; 72 private int mTouchTolerance = 40; 73 private float mDashOnLength = 20; 74 private float mDashOffLength = 10; 75 76 private enum Mode { 77 NONE, MOVE 78 } 79 80 private Mode mState = Mode.NONE; 81 CropView(Context context)82 public CropView(Context context) { 83 super(context); 84 setup(context); 85 } 86 CropView(Context context, AttributeSet attrs)87 public CropView(Context context, AttributeSet attrs) { 88 super(context, attrs); 89 setup(context); 90 } 91 CropView(Context context, AttributeSet attrs, int defStyle)92 public CropView(Context context, AttributeSet attrs, int defStyle) { 93 super(context, attrs, defStyle); 94 setup(context); 95 } 96 setup(Context context)97 private void setup(Context context) { 98 Resources rsc = context.getResources(); 99 mShadow = (NinePatchDrawable) rsc.getDrawable(R.drawable.geometry_shadow); 100 mCropIndicator = rsc.getDrawable(R.drawable.camera_crop); 101 mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size); 102 mShadowMargin = (int) rsc.getDimension(R.dimen.shadow_margin); 103 mMargin = (int) rsc.getDimension(R.dimen.preview_margin); 104 mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side); 105 mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance); 106 mOverlayShadowColor = (int) rsc.getColor(R.color.crop_shadow_color); 107 mOverlayWPShadowColor = (int) rsc.getColor(R.color.crop_shadow_wp_color); 108 mWPMarkerColor = (int) rsc.getColor(R.color.crop_wp_markers); 109 mDashOnLength = rsc.getDimension(R.dimen.wp_selector_dash_length); 110 mDashOffLength = rsc.getDimension(R.dimen.wp_selector_off_length); 111 } 112 initialize(Bitmap image, RectF newCropBounds, RectF newPhotoBounds, int rotation)113 public void initialize(Bitmap image, RectF newCropBounds, RectF newPhotoBounds, int rotation) { 114 mBitmap = image; 115 if (mCropObj != null) { 116 RectF crop = mCropObj.getInnerBounds(); 117 RectF containing = mCropObj.getOuterBounds(); 118 if (crop != newCropBounds || containing != newPhotoBounds 119 || mRotation != rotation) { 120 mRotation = rotation; 121 mCropObj.resetBoundsTo(newCropBounds, newPhotoBounds); 122 clearDisplay(); 123 } 124 } else { 125 mRotation = rotation; 126 mCropObj = new CropObject(newPhotoBounds, newCropBounds, 0); 127 clearDisplay(); 128 } 129 } 130 getCrop()131 public RectF getCrop() { 132 return mCropObj.getInnerBounds(); 133 } 134 getPhoto()135 public RectF getPhoto() { 136 return mCropObj.getOuterBounds(); 137 } 138 139 @Override onTouchEvent(MotionEvent event)140 public boolean onTouchEvent(MotionEvent event) { 141 float x = event.getX(); 142 float y = event.getY(); 143 if (mDisplayMatrix == null || mDisplayMatrixInverse == null) { 144 return true; 145 } 146 float[] touchPoint = { 147 x, y 148 }; 149 mDisplayMatrixInverse.mapPoints(touchPoint); 150 x = touchPoint[0]; 151 y = touchPoint[1]; 152 switch (event.getActionMasked()) { 153 case (MotionEvent.ACTION_DOWN): 154 if (mState == Mode.NONE) { 155 if (!mCropObj.selectEdge(x, y)) { 156 mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK); 157 } 158 mPrevX = x; 159 mPrevY = y; 160 mState = Mode.MOVE; 161 } 162 break; 163 case (MotionEvent.ACTION_UP): 164 if (mState == Mode.MOVE) { 165 mCropObj.selectEdge(CropObject.MOVE_NONE); 166 mMovingBlock = false; 167 mPrevX = x; 168 mPrevY = y; 169 mState = Mode.NONE; 170 } 171 break; 172 case (MotionEvent.ACTION_MOVE): 173 if (mState == Mode.MOVE) { 174 float dx = x - mPrevX; 175 float dy = y - mPrevY; 176 mCropObj.moveCurrentSelection(dx, dy); 177 mPrevX = x; 178 mPrevY = y; 179 } 180 break; 181 default: 182 break; 183 } 184 invalidate(); 185 return true; 186 } 187 reset()188 private void reset() { 189 Log.w(LOGTAG, "crop reset called"); 190 mState = Mode.NONE; 191 mCropObj = null; 192 mRotation = 0; 193 mMovingBlock = false; 194 clearDisplay(); 195 } 196 clearDisplay()197 private void clearDisplay() { 198 mDisplayMatrix = null; 199 mDisplayMatrixInverse = null; 200 invalidate(); 201 } 202 configChanged()203 protected void configChanged() { 204 mDirty = true; 205 } 206 applyFreeAspect()207 public void applyFreeAspect() { 208 mCropObj.unsetAspectRatio(); 209 invalidate(); 210 } 211 applyOriginalAspect()212 public void applyOriginalAspect() { 213 RectF outer = mCropObj.getOuterBounds(); 214 float w = outer.width(); 215 float h = outer.height(); 216 if (w > 0 && h > 0) { 217 applyAspect(w, h); 218 mCropObj.resetBoundsTo(outer, outer); 219 } else { 220 Log.w(LOGTAG, "failed to set aspect ratio original"); 221 } 222 } 223 applySquareAspect()224 public void applySquareAspect() { 225 applyAspect(1, 1); 226 } 227 applyAspect(float x, float y)228 public void applyAspect(float x, float y) { 229 if (x <= 0 || y <= 0) { 230 throw new IllegalArgumentException("Bad arguments to applyAspect"); 231 } 232 // If we are rotated by 90 degrees from horizontal, swap x and y 233 if (((mRotation < 0) ? -mRotation : mRotation) % 180 == 90) { 234 float tmp = x; 235 x = y; 236 y = tmp; 237 } 238 if (!mCropObj.setInnerAspectRatio(x, y)) { 239 Log.w(LOGTAG, "failed to set aspect ratio"); 240 } 241 invalidate(); 242 } 243 setWallpaperSpotlight(float spotlightX, float spotlightY)244 public void setWallpaperSpotlight(float spotlightX, float spotlightY) { 245 mSpotX = spotlightX; 246 mSpotY = spotlightY; 247 if (mSpotX > 0 && mSpotY > 0) { 248 mDoSpot = true; 249 } 250 } 251 unsetWallpaperSpotlight()252 public void unsetWallpaperSpotlight() { 253 mDoSpot = false; 254 } 255 256 /** 257 * Rotates first d bits in integer x to the left some number of times. 258 */ bitCycleLeft(int x, int times, int d)259 private int bitCycleLeft(int x, int times, int d) { 260 int mask = (1 << d) - 1; 261 int mout = x & mask; 262 times %= d; 263 int hi = mout >> (d - times); 264 int low = (mout << times) & mask; 265 int ret = x & ~mask; 266 ret |= low; 267 ret |= hi; 268 return ret; 269 } 270 271 /** 272 * Find the selected edge or corner in screen coordinates. 273 */ decode(int movingEdges, float rotation)274 private int decode(int movingEdges, float rotation) { 275 int rot = CropMath.constrainedRotation(rotation); 276 switch (rot) { 277 case 90: 278 return bitCycleLeft(movingEdges, 1, 4); 279 case 180: 280 return bitCycleLeft(movingEdges, 2, 4); 281 case 270: 282 return bitCycleLeft(movingEdges, 3, 4); 283 default: 284 return movingEdges; 285 } 286 } 287 288 @Override onDraw(Canvas canvas)289 public void onDraw(Canvas canvas) { 290 if (mBitmap == null) { 291 return; 292 } 293 if (mDirty) { 294 mDirty = false; 295 clearDisplay(); 296 } 297 298 mImageBounds = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 299 mScreenBounds = new RectF(0, 0, canvas.getWidth(), canvas.getHeight()); 300 mScreenBounds.inset(mMargin, mMargin); 301 302 // If crop object doesn't exist, create it and update it from master 303 // state 304 if (mCropObj == null) { 305 reset(); 306 mCropObj = new CropObject(mImageBounds, mImageBounds, 0); 307 } 308 309 // If display matrix doesn't exist, create it and its dependencies 310 if (mDisplayMatrix == null || mDisplayMatrixInverse == null) { 311 mDisplayMatrix = new Matrix(); 312 mDisplayMatrix.reset(); 313 if (!CropDrawingUtils.setImageToScreenMatrix(mDisplayMatrix, mImageBounds, mScreenBounds, 314 mRotation)) { 315 Log.w(LOGTAG, "failed to get screen matrix"); 316 mDisplayMatrix = null; 317 return; 318 } 319 mDisplayMatrixInverse = new Matrix(); 320 mDisplayMatrixInverse.reset(); 321 if (!mDisplayMatrix.invert(mDisplayMatrixInverse)) { 322 Log.w(LOGTAG, "could not invert display matrix"); 323 mDisplayMatrixInverse = null; 324 return; 325 } 326 // Scale min side and tolerance by display matrix scale factor 327 mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize)); 328 mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance)); 329 } 330 331 mScreenImageBounds.set(mImageBounds); 332 333 // Draw background shadow 334 if (mDisplayMatrix.mapRect(mScreenImageBounds)) { 335 int margin = (int) mDisplayMatrix.mapRadius(mShadowMargin); 336 mScreenImageBounds.roundOut(mShadowBounds); 337 mShadowBounds.set(mShadowBounds.left - margin, mShadowBounds.top - 338 margin, mShadowBounds.right + margin, mShadowBounds.bottom + margin); 339 mShadow.setBounds(mShadowBounds); 340 mShadow.draw(canvas); 341 } 342 343 mPaint.setAntiAlias(true); 344 mPaint.setFilterBitmap(true); 345 // Draw actual bitmap 346 canvas.drawBitmap(mBitmap, mDisplayMatrix, mPaint); 347 348 mCropObj.getInnerBounds(mScreenCropBounds); 349 350 if (mDisplayMatrix.mapRect(mScreenCropBounds)) { 351 352 // Draw overlay shadows 353 Paint p = new Paint(); 354 p.setColor(mOverlayShadowColor); 355 p.setStyle(Paint.Style.FILL); 356 CropDrawingUtils.drawShadows(canvas, p, mScreenCropBounds, mScreenImageBounds); 357 358 // Draw crop rect and markers 359 CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds); 360 if (!mDoSpot) { 361 CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds); 362 } else { 363 Paint wpPaint = new Paint(); 364 wpPaint.setColor(mWPMarkerColor); 365 wpPaint.setStrokeWidth(3); 366 wpPaint.setStyle(Paint.Style.STROKE); 367 wpPaint.setPathEffect(new DashPathEffect(new float[] 368 {mDashOnLength, mDashOnLength + mDashOffLength}, 0)); 369 p.setColor(mOverlayWPShadowColor); 370 CropDrawingUtils.drawWallpaperSelectionFrame(canvas, mScreenCropBounds, 371 mSpotX, mSpotY, wpPaint, p); 372 } 373 CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize, 374 mScreenCropBounds, mCropObj.isFixedAspect(), decode(mCropObj.getSelectState(), mRotation)); 375 } 376 377 } 378 } 379