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