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.imageshow; 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.Matrix; 24 import android.graphics.Paint; 25 import android.graphics.RectF; 26 import android.graphics.drawable.Drawable; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.view.MotionEvent; 30 31 import com.android.gallery3d.R; 32 import com.android.gallery3d.filtershow.crop.CropDrawingUtils; 33 import com.android.gallery3d.filtershow.crop.CropMath; 34 import com.android.gallery3d.filtershow.crop.CropObject; 35 import com.android.gallery3d.filtershow.editors.EditorCrop; 36 import com.android.gallery3d.filtershow.filters.FilterCropRepresentation; 37 import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder; 38 39 public class ImageCrop extends ImageShow { 40 private static final String TAG = ImageCrop.class.getSimpleName(); 41 private RectF mImageBounds = new RectF(); 42 private RectF mScreenCropBounds = new RectF(); 43 private Paint mPaint = new Paint(); 44 private CropObject mCropObj = null; 45 private GeometryHolder mGeometry = new GeometryHolder(); 46 private GeometryHolder mUpdateHolder = new GeometryHolder(); 47 private Drawable mCropIndicator; 48 private int mIndicatorSize; 49 private boolean mMovingBlock = false; 50 private Matrix mDisplayMatrix = null; 51 private Matrix mDisplayCropMatrix = null; 52 private Matrix mDisplayMatrixInverse = null; 53 private float mPrevX = 0; 54 private float mPrevY = 0; 55 private int mMinSideSize = 90; 56 private int mTouchTolerance = 40; 57 private enum Mode { 58 NONE, MOVE 59 } 60 private Mode mState = Mode.NONE; 61 private boolean mValidDraw = false; 62 FilterCropRepresentation mLocalRep = new FilterCropRepresentation(); 63 EditorCrop mEditorCrop; 64 65 public ImageCrop(Context context) { 66 super(context); 67 setup(context); 68 } 69 70 public ImageCrop(Context context, AttributeSet attrs) { 71 super(context, attrs); 72 setup(context); 73 } 74 75 public ImageCrop(Context context, AttributeSet attrs, int defStyle) { 76 super(context, attrs, defStyle); 77 setup(context); 78 } 79 80 private void setup(Context context) { 81 Resources rsc = context.getResources(); 82 mCropIndicator = rsc.getDrawable(R.drawable.camera_crop); 83 mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size); 84 mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side); 85 mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance); 86 } 87 88 public void setFilterCropRepresentation(FilterCropRepresentation crop) { 89 mLocalRep = (crop == null) ? new FilterCropRepresentation() : crop; 90 GeometryMathUtils.initializeHolder(mUpdateHolder, mLocalRep); 91 mValidDraw = true; 92 } 93 94 public FilterCropRepresentation getFinalRepresentation() { 95 return mLocalRep; 96 } 97 98 private void internallyUpdateLocalRep(RectF crop, RectF image) { 99 FilterCropRepresentation 100 .findNormalizedCrop(crop, (int) image.width(), (int) image.height()); 101 mGeometry.crop.set(crop); 102 mUpdateHolder.set(mGeometry); 103 mLocalRep.setCrop(crop); 104 } 105 106 @Override 107 public boolean onTouchEvent(MotionEvent event) { 108 float x = event.getX(); 109 float y = event.getY(); 110 if (mDisplayMatrix == null || mDisplayMatrixInverse == null) { 111 return true; 112 } 113 float[] touchPoint = { 114 x, y 115 }; 116 mDisplayMatrixInverse.mapPoints(touchPoint); 117 x = touchPoint[0]; 118 y = touchPoint[1]; 119 switch (event.getActionMasked()) { 120 case (MotionEvent.ACTION_DOWN): 121 if (mState == Mode.NONE) { 122 if (!mCropObj.selectEdge(x, y)) { 123 mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK); 124 } 125 mPrevX = x; 126 mPrevY = y; 127 mState = Mode.MOVE; 128 } 129 break; 130 case (MotionEvent.ACTION_UP): 131 if (mState == Mode.MOVE) { 132 mCropObj.selectEdge(CropObject.MOVE_NONE); 133 mMovingBlock = false; 134 mPrevX = x; 135 mPrevY = y; 136 mState = Mode.NONE; 137 internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds()); 138 } 139 break; 140 case (MotionEvent.ACTION_MOVE): 141 if (mState == Mode.MOVE) { 142 float dx = x - mPrevX; 143 float dy = y - mPrevY; 144 mCropObj.moveCurrentSelection(dx, dy); 145 mPrevX = x; 146 mPrevY = y; 147 } 148 break; 149 default: 150 break; 151 } 152 invalidate(); 153 return true; 154 } 155 156 private void clearDisplay() { 157 mDisplayMatrix = null; 158 mDisplayMatrixInverse = null; 159 invalidate(); 160 } 161 162 public void applyFreeAspect() { 163 mCropObj.unsetAspectRatio(); 164 invalidate(); 165 } 166 167 public void applyOriginalAspect() { 168 RectF outer = mCropObj.getOuterBounds(); 169 float w = outer.width(); 170 float h = outer.height(); 171 if (w > 0 && h > 0) { 172 applyAspect(w, h); 173 mCropObj.resetBoundsTo(outer, outer); 174 internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds()); 175 } else { 176 Log.w(TAG, "failed to set aspect ratio original"); 177 } 178 invalidate(); 179 } 180 181 public void applyAspect(float x, float y) { 182 if (x <= 0 || y <= 0) { 183 throw new IllegalArgumentException("Bad arguments to applyAspect"); 184 } 185 // If we are rotated by 90 degrees from horizontal, swap x and y 186 if (GeometryMathUtils.needsDimensionSwap(mGeometry.rotation)) { 187 float tmp = x; 188 x = y; 189 y = tmp; 190 } 191 if (!mCropObj.setInnerAspectRatio(x, y)) { 192 Log.w(TAG, "failed to set aspect ratio"); 193 } 194 internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds()); 195 invalidate(); 196 } 197 198 /** 199 * Rotates first d bits in integer x to the left some number of times. 200 */ 201 private int bitCycleLeft(int x, int times, int d) { 202 int mask = (1 << d) - 1; 203 int mout = x & mask; 204 times %= d; 205 int hi = mout >> (d - times); 206 int low = (mout << times) & mask; 207 int ret = x & ~mask; 208 ret |= low; 209 ret |= hi; 210 return ret; 211 } 212 213 /** 214 * Find the selected edge or corner in screen coordinates. 215 */ 216 private int decode(int movingEdges, float rotation) { 217 int rot = CropMath.constrainedRotation(rotation); 218 switch (rot) { 219 case 90: 220 return bitCycleLeft(movingEdges, 1, 4); 221 case 180: 222 return bitCycleLeft(movingEdges, 2, 4); 223 case 270: 224 return bitCycleLeft(movingEdges, 3, 4); 225 default: 226 return movingEdges; 227 } 228 } 229 230 private void forceStateConsistency() { 231 MasterImage master = MasterImage.getImage(); 232 Bitmap image = master.getFiltersOnlyImage(); 233 int width = image.getWidth(); 234 int height = image.getHeight(); 235 if (mCropObj == null || !mUpdateHolder.equals(mGeometry) 236 || mImageBounds.width() != width || mImageBounds.height() != height 237 || !mLocalRep.getCrop().equals(mUpdateHolder.crop)) { 238 mImageBounds.set(0, 0, width, height); 239 mGeometry.set(mUpdateHolder); 240 mLocalRep.setCrop(mUpdateHolder.crop); 241 RectF scaledCrop = new RectF(mUpdateHolder.crop); 242 FilterCropRepresentation.findScaledCrop(scaledCrop, width, height); 243 mCropObj = new CropObject(mImageBounds, scaledCrop, (int) mUpdateHolder.straighten); 244 mState = Mode.NONE; 245 clearDisplay(); 246 } 247 } 248 249 @Override 250 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 251 super.onSizeChanged(w, h, oldw, oldh); 252 clearDisplay(); 253 } 254 255 @Override 256 public void onDraw(Canvas canvas) { 257 Bitmap bitmap = MasterImage.getImage().getFiltersOnlyImage(); 258 if (bitmap == null) { 259 MasterImage.getImage().invalidateFiltersOnly(); 260 } 261 if (!mValidDraw || bitmap == null) { 262 return; 263 } 264 forceStateConsistency(); 265 mImageBounds.set(0, 0, bitmap.getWidth(), bitmap.getHeight()); 266 // If display matrix doesn't exist, create it and its dependencies 267 if (mDisplayCropMatrix == null || mDisplayMatrix == null || mDisplayMatrixInverse == null) { 268 mCropObj.unsetAspectRatio(); 269 mDisplayMatrix = GeometryMathUtils.getFullGeometryToScreenMatrix(mGeometry, 270 bitmap.getWidth(), bitmap.getHeight(), canvas.getWidth(), canvas.getHeight()); 271 float straighten = mGeometry.straighten; 272 mGeometry.straighten = 0; 273 mDisplayCropMatrix = GeometryMathUtils.getFullGeometryToScreenMatrix(mGeometry, 274 bitmap.getWidth(), bitmap.getHeight(), canvas.getWidth(), canvas.getHeight()); 275 mGeometry.straighten = straighten; 276 mDisplayMatrixInverse = new Matrix(); 277 mDisplayMatrixInverse.reset(); 278 if (!mDisplayCropMatrix.invert(mDisplayMatrixInverse)) { 279 Log.w(TAG, "could not invert display matrix"); 280 mDisplayMatrixInverse = null; 281 return; 282 } 283 // Scale min side and tolerance by display matrix scale factor 284 mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize)); 285 mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance)); 286 // drive Crop engine to clamp to crop bounds 287 int[] sides = {CropObject.MOVE_TOP, 288 CropObject.MOVE_BOTTOM, 289 CropObject.MOVE_LEFT, 290 CropObject.MOVE_RIGHT}; 291 int delta = Math.min(canvas.getWidth(), canvas.getHeight()) / 4; 292 int[] dy = {delta, -delta, 0, 0}; 293 int[] dx = {0, 0, delta, -delta}; 294 295 for (int i = 0; i < sides.length; i++) { 296 mCropObj.selectEdge(sides[i]); 297 298 mCropObj.moveCurrentSelection(dx[i], dy[i]); 299 mCropObj.moveCurrentSelection(-dx[i], -dy[i]); 300 } 301 mCropObj.selectEdge(CropObject.MOVE_NONE); 302 } 303 // Draw actual bitmap 304 mPaint.reset(); 305 mPaint.setAntiAlias(true); 306 mPaint.setFilterBitmap(true); 307 canvas.drawBitmap(bitmap, mDisplayMatrix, mPaint); 308 mCropObj.getInnerBounds(mScreenCropBounds); 309 RectF outer = mCropObj.getOuterBounds(); 310 FilterCropRepresentation.findNormalizedCrop(mScreenCropBounds, (int) outer.width(), 311 (int) outer.height()); 312 FilterCropRepresentation.findScaledCrop(mScreenCropBounds, bitmap.getWidth(), 313 bitmap.getHeight()); 314 if (mDisplayCropMatrix.mapRect(mScreenCropBounds)) { 315 // Draw crop rect and markers 316 CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds); 317 CropDrawingUtils.drawShade(canvas, mScreenCropBounds); 318 CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds); 319 CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize, 320 mScreenCropBounds, mCropObj.isFixedAspect(), 321 decode(mCropObj.getSelectState(), mGeometry.rotation.value())); 322 } 323 } 324 325 public void setEditor(EditorCrop editorCrop) { 326 mEditorCrop = editorCrop; 327 } 328 } 329