1 /* 2 * Copyright (C) 2012 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.graphics.Bitmap; 21 import android.graphics.Canvas; 22 import android.graphics.Color; 23 import android.graphics.Matrix; 24 import android.graphics.Paint; 25 import android.graphics.Paint.Style; 26 import android.graphics.Path; 27 import android.graphics.RectF; 28 import android.util.AttributeSet; 29 import android.view.MotionEvent; 30 import android.view.View; 31 32 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata.FLIP; 33 import com.android.gallery3d.filtershow.presets.ImagePreset; 34 35 public abstract class ImageGeometry extends ImageShow { 36 protected boolean mVisibilityGained = false; 37 private boolean mHasDrawn = false; 38 39 protected static final float MAX_STRAIGHTEN_ANGLE = 45; 40 protected static final float MIN_STRAIGHTEN_ANGLE = -45; 41 42 protected float mCenterX; 43 protected float mCenterY; 44 45 protected float mCurrentX; 46 protected float mCurrentY; 47 protected float mTouchCenterX; 48 protected float mTouchCenterY; 49 50 // Local geometry data 51 private GeometryMetadata mLocalGeometry = null; 52 private RectF mLocalDisplayBounds = null; 53 protected float mXOffset = 0; 54 protected float mYOffset = 0; 55 56 protected enum MODES { 57 NONE, DOWN, UP, MOVE 58 } 59 60 protected MODES mMode = MODES.NONE; 61 62 private static final String LOGTAG = "ImageGeometry"; 63 64 public ImageGeometry(Context context, AttributeSet attrs) { 65 super(context, attrs); 66 } 67 68 public ImageGeometry(Context context) { 69 super(context); 70 } 71 72 private void setupLocalDisplayBounds(RectF b) { 73 mLocalDisplayBounds = b; 74 calculateLocalScalingFactorAndOffset(); 75 } 76 77 protected static float angleFor(float dx, float dy) { 78 return (float) (Math.atan2(dx, dy) * 180 / Math.PI); 79 } 80 81 protected static int snappedAngle(float angle) { 82 float remainder = angle % 90; 83 int current = (int) (angle / 90); // truncates 84 if (remainder < -45) { 85 --current; 86 } else if (remainder > 45) { 87 ++current; 88 } 89 return current * 90; 90 } 91 92 protected float getCurrentTouchAngle() { 93 if (mCurrentX == mTouchCenterX && mCurrentY == mTouchCenterY) { 94 return 0; 95 } 96 float dX1 = mTouchCenterX - mCenterX; 97 float dY1 = mTouchCenterY - mCenterY; 98 float dX2 = mCurrentX - mCenterX; 99 float dY2 = mCurrentY - mCenterY; 100 101 float angleA = angleFor(dX1, dY1); 102 float angleB = angleFor(dX2, dY2); 103 return (angleB - angleA) % 360; 104 } 105 106 protected float computeScale(float width, float height) { 107 float imageWidth = mLocalGeometry.getPhotoBounds().width(); 108 float imageHeight = mLocalGeometry.getPhotoBounds().height(); 109 return GeometryMath.scale(imageWidth, imageHeight, width, height); 110 } 111 112 private void calculateLocalScalingFactorAndOffset() { 113 if (mLocalGeometry == null || mLocalDisplayBounds == null) 114 return; 115 RectF imageBounds = mLocalGeometry.getPhotoBounds(); 116 float imageWidth = imageBounds.width(); 117 float imageHeight = imageBounds.height(); 118 float displayWidth = mLocalDisplayBounds.width(); 119 float displayHeight = mLocalDisplayBounds.height(); 120 121 mCenterX = displayWidth / 2; 122 mCenterY = displayHeight / 2; 123 mYOffset = (displayHeight - imageHeight) / 2.0f; 124 mXOffset = (displayWidth - imageWidth) / 2.0f; 125 updateScale(); 126 } 127 128 @Override 129 public void resetParameter() { 130 super.resetParameter(); 131 setLocalRotation(0); 132 setLocalStraighten(0); 133 setLocalCropBounds(getLocalPhotoBounds()); 134 setLocalFlip(FLIP.NONE); 135 saveAndSetPreset(); 136 invalidate(); 137 } 138 139 // Overwrites local with master 140 public void syncLocalToMasterGeometry() { 141 mLocalGeometry = getGeometry(); 142 calculateLocalScalingFactorAndOffset(); 143 } 144 145 protected RectF getLocalPhotoBounds() { 146 return mLocalGeometry.getPhotoBounds(); 147 } 148 149 protected RectF getLocalCropBounds() { 150 return mLocalGeometry.getPreviewCropBounds(); 151 } 152 153 protected RectF getLocalDisplayBounds() { 154 return new RectF(mLocalDisplayBounds); 155 } 156 157 protected float getLocalScale() { 158 return mLocalGeometry.getScaleFactor(); 159 } 160 161 protected float getLocalRotation() { 162 return mLocalGeometry.getRotation(); 163 } 164 165 protected float getLocalStraighten() { 166 return mLocalGeometry.getStraightenRotation(); 167 } 168 169 protected void setLocalScale(float s) { 170 mLocalGeometry.setScaleFactor(s); 171 } 172 173 protected void updateScale() { 174 RectF bounds = getUntranslatedStraightenCropBounds(mLocalGeometry.getPhotoBounds(), 175 getLocalStraighten()); 176 float zoom = computeScale(bounds.width(), bounds.height()); 177 setLocalScale(zoom); 178 } 179 180 protected void setLocalRotation(float r) { 181 mLocalGeometry.setRotation(r); 182 updateScale(); 183 } 184 185 /** 186 * Constrains rotation to be in [0, 90, 180, 270]. 187 */ 188 protected int constrainedRotation(float rotation) { 189 int r = (int) ((rotation % 360) / 90); 190 r = (r < 0) ? (r + 4) : r; 191 return r * 90; 192 } 193 194 protected boolean isHeightWidthSwapped() { 195 return ((int) (getLocalRotation() / 90)) % 2 != 0; 196 } 197 198 protected void setLocalStraighten(float r) { 199 mLocalGeometry.setStraightenRotation(r); 200 updateScale(); 201 } 202 203 protected void setLocalCropBounds(RectF c) { 204 mLocalGeometry.setCropBounds(c); 205 updateScale(); 206 } 207 208 protected FLIP getLocalFlip() { 209 return mLocalGeometry.getFlipType(); 210 } 211 212 protected void setLocalFlip(FLIP flip) { 213 mLocalGeometry.setFlipType(flip); 214 } 215 216 protected float getTotalLocalRotation() { 217 return getLocalRotation() + getLocalStraighten(); 218 } 219 220 protected static Path drawClosedPath(Canvas canvas, Paint paint, float[] points) { 221 Path crop = new Path(); 222 crop.moveTo(points[0], points[1]); 223 crop.lineTo(points[2], points[3]); 224 crop.lineTo(points[4], points[5]); 225 crop.lineTo(points[6], points[7]); 226 crop.close(); 227 canvas.drawPath(crop, paint); 228 return crop; 229 } 230 231 protected static float getNewHeightForWidthAspect(float width, float w, float h) { 232 return width * h / w; 233 } 234 235 protected static float getNewWidthForHeightAspect(float height, float w, float h) { 236 return height * w / h; 237 } 238 239 @Override 240 protected void onVisibilityChanged(View changedView, int visibility) { 241 super.onVisibilityChanged(changedView, visibility); 242 if (visibility == View.VISIBLE) { 243 mVisibilityGained = true; 244 MasterImage.getImage().invalidateFiltersOnly(); 245 syncLocalToMasterGeometry(); 246 updateScale(); 247 gainedVisibility(); 248 } else { 249 if (mVisibilityGained == true && mHasDrawn == true) { 250 lostVisibility(); 251 } 252 mVisibilityGained = false; 253 mHasDrawn = false; 254 } 255 } 256 257 protected void gainedVisibility() { 258 // Override this stub. 259 } 260 261 protected void lostVisibility() { 262 // Override this stub. 263 } 264 265 @Override 266 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 267 super.onSizeChanged(w, h, oldw, oldh); 268 setupLocalDisplayBounds(new RectF(0, 0, w, h)); 269 } 270 271 @Override 272 public boolean onTouchEvent(MotionEvent event) { 273 switch (event.getActionMasked()) { 274 case (MotionEvent.ACTION_DOWN): 275 setActionDown(event.getX(), event.getY()); 276 break; 277 case (MotionEvent.ACTION_UP): 278 setActionUp(); 279 saveAndSetPreset(); 280 break; 281 case (MotionEvent.ACTION_MOVE): 282 setActionMove(event.getX(), event.getY()); 283 break; 284 default: 285 setNoAction(); 286 } 287 invalidate(); 288 return true; 289 } 290 291 protected int getLocalValue() { 292 return 0; // Override this 293 } 294 295 protected void setActionDown(float x, float y) { 296 mTouchCenterX = x; 297 mTouchCenterY = y; 298 mCurrentX = x; 299 mCurrentY = y; 300 mMode = MODES.DOWN; 301 } 302 303 protected void setActionMove(float x, float y) { 304 mCurrentX = x; 305 mCurrentY = y; 306 mMode = MODES.MOVE; 307 } 308 309 protected void setActionUp() { 310 mMode = MODES.UP; 311 } 312 313 protected void setNoAction() { 314 mMode = MODES.NONE; 315 } 316 317 @Override 318 public boolean showTitle() { 319 return false; 320 } 321 322 public String getName() { 323 return "Geometry"; 324 } 325 326 public void saveAndSetPreset() { 327 ImagePreset lastHistoryItem = MasterImage.getImage().getHistory().getLast(); 328 if (lastHistoryItem != null && lastHistoryItem.historyName().equalsIgnoreCase(getName())) { 329 getImagePreset().setGeometry(mLocalGeometry); 330 resetImageCaches(this); 331 } else { 332 if (mLocalGeometry.hasModifications()) { 333 ImagePreset copy = new ImagePreset(getImagePreset()); 334 copy.setGeometry(mLocalGeometry); 335 copy.setHistoryName(getName()); 336 copy.setIsFx(false); 337 MasterImage.getImage().setPreset(copy, true); 338 } 339 } 340 invalidate(); 341 } 342 343 public static RectF getUntranslatedStraightenCropBounds(RectF imageRect, float straightenAngle) { 344 float deg = straightenAngle; 345 if (deg < 0) { 346 deg = -deg; 347 } 348 double a = Math.toRadians(deg); 349 double sina = Math.sin(a); 350 double cosa = Math.cos(a); 351 352 double rw = imageRect.width(); 353 double rh = imageRect.height(); 354 double h1 = rh * rh / (rw * sina + rh * cosa); 355 double h2 = rh * rw / (rw * cosa + rh * sina); 356 double hh = Math.min(h1, h2); 357 double ww = hh * rw / rh; 358 359 float left = (float) ((rw - ww) * 0.5f); 360 float top = (float) ((rh - hh) * 0.5f); 361 float right = (float) (left + ww); 362 float bottom = (float) (top + hh); 363 364 return new RectF(left, top, right, bottom); 365 } 366 367 protected RectF straightenBounds() { 368 RectF bounds = getUntranslatedStraightenCropBounds(getLocalPhotoBounds(), 369 getLocalStraighten()); 370 float scale = computeScale(getWidth(), getHeight()); 371 bounds = GeometryMath.scaleRect(bounds, scale); 372 float dx = (getWidth() / 2) - bounds.centerX(); 373 float dy = (getHeight() / 2) - bounds.centerY(); 374 bounds.offset(dx, dy); 375 return bounds; 376 } 377 378 protected static void drawRotatedShadows(Canvas canvas, Paint p, RectF innerBounds, 379 RectF outerBounds, 380 float rotation, float centerX, float centerY) { 381 canvas.save(); 382 canvas.rotate(rotation, centerX, centerY); 383 384 float x = (outerBounds.left - outerBounds.right); 385 float y = (outerBounds.top - outerBounds.bottom); 386 float longest = (float) Math.sqrt(x * x + y * y) / 2; 387 float minX = centerX - longest; 388 float maxX = centerX + longest; 389 float minY = centerY - longest; 390 float maxY = centerY + longest; 391 canvas.drawRect(minX, minY, innerBounds.right, innerBounds.top, p); 392 canvas.drawRect(minX, innerBounds.top, innerBounds.left, maxY, p); 393 canvas.drawRect(innerBounds.left, innerBounds.bottom, maxX, maxY, 394 p); 395 canvas.drawRect(innerBounds.right, minY, maxX, 396 innerBounds.bottom, p); 397 canvas.rotate(-rotation, centerX, centerY); 398 canvas.restore(); 399 } 400 401 protected void drawShadows(Canvas canvas, Paint p, RectF innerBounds) { 402 float w = getWidth(); 403 float h = getHeight(); 404 canvas.drawRect(0f, 0f, w, innerBounds.top, p); 405 canvas.drawRect(0f, innerBounds.top, innerBounds.left, innerBounds.bottom, p); 406 canvas.drawRect(innerBounds.right, innerBounds.top, w, innerBounds.bottom, p); 407 canvas.drawRect(0f, innerBounds.bottom, w, h, p); 408 } 409 410 @Override 411 public void onDraw(Canvas canvas) { 412 if (getDirtyGeometryFlag()) { 413 syncLocalToMasterGeometry(); 414 clearDirtyGeometryFlag(); 415 } 416 Bitmap image = getFiltersOnlyImage(); 417 if (image == null) { 418 invalidate(); 419 return; 420 } 421 mHasDrawn = true; 422 423 drawShape(canvas, image); 424 } 425 426 protected void drawShape(Canvas canvas, Bitmap image) { 427 // TODO: Override this stub. 428 } 429 430 /** 431 * Sets up inputs for buildCenteredPhotoMatrix and buildWanderingCropMatrix 432 * and returns the scale factor. 433 */ 434 protected float getTransformState(RectF photo, RectF crop, float[] displayCenter) { 435 RectF photoBounds = getLocalPhotoBounds(); 436 RectF cropBounds = getLocalCropBounds(); 437 float scale = computeScale(getWidth(), getHeight()); 438 // checks if local rotation is an odd multiple of 90. 439 if (isHeightWidthSwapped()) { 440 scale = computeScale(getHeight(), getWidth()); 441 } 442 // put in screen coordinates 443 if (crop != null) { 444 crop.set(GeometryMath.scaleRect(cropBounds, scale)); 445 } 446 if (photo != null) { 447 photo.set(GeometryMath.scaleRect(photoBounds, scale)); 448 } 449 if (displayCenter != null && displayCenter.length >= 2) { 450 displayCenter[0] = getWidth() / 2f; 451 displayCenter[1] = getHeight() / 2f; 452 } 453 return scale; 454 } 455 456 protected RectF drawTransformed(Canvas canvas, Bitmap photo, Paint p, float[] offset) { 457 p.setARGB(255, 0, 0, 0); 458 float[] displayCenter = new float[2]; 459 RectF scaledCrop = new RectF(); 460 RectF scaledPhoto = new RectF(); 461 float scale = getTransformState(scaledPhoto, scaledCrop, displayCenter); 462 Matrix m = GeometryMetadata.buildCenteredPhotoMatrix(scaledPhoto, scaledCrop, 463 getLocalRotation(), getLocalStraighten(), getLocalFlip(), displayCenter); 464 465 Matrix m1 = GeometryMetadata.buildWanderingCropMatrix(scaledPhoto, scaledCrop, 466 getLocalRotation(), getLocalStraighten(), getLocalFlip(), displayCenter); 467 m1.mapRect(scaledCrop); 468 Path path = new Path(); 469 scaledCrop.offset(-offset[0], -offset[1]); 470 path.addRect(scaledCrop, Path.Direction.CCW); 471 472 m.preScale(scale, scale); 473 m.postTranslate(-offset[0], -offset[1]); 474 canvas.save(); 475 canvas.drawBitmap(photo, m, p); 476 canvas.restore(); 477 478 p.setColor(Color.WHITE); 479 p.setStyle(Style.STROKE); 480 p.setStrokeWidth(2); 481 canvas.drawPath(path, p); 482 483 p.setColor(getDefaultBackgroundColor()); 484 p.setAlpha(128); 485 p.setStyle(Paint.Style.FILL); 486 drawShadows(canvas, p, scaledCrop); 487 return scaledCrop; 488 } 489 490 protected void drawTransformedCropped(Canvas canvas, Bitmap photo, Paint p) { 491 RectF photoBounds = getLocalPhotoBounds(); 492 RectF cropBounds = getLocalCropBounds(); 493 float imageWidth = cropBounds.width(); 494 float imageHeight = cropBounds.height(); 495 float scale = GeometryMath.scale(imageWidth, imageHeight, getWidth(), getHeight()); 496 // checks if local rotation is an odd multiple of 90. 497 if (isHeightWidthSwapped()) { 498 scale = GeometryMath.scale(imageWidth, imageHeight, getHeight(), getWidth()); 499 } 500 // put in screen coordinates 501 RectF scaledCrop = GeometryMath.scaleRect(cropBounds, scale); 502 RectF scaledPhoto = GeometryMath.scaleRect(photoBounds, scale); 503 float[] displayCenter = { 504 getWidth() / 2f, getHeight() / 2f 505 }; 506 Matrix m1 = GeometryMetadata.buildWanderingCropMatrix(scaledPhoto, scaledCrop, 507 getLocalRotation(), getLocalStraighten(), getLocalFlip(), displayCenter); 508 float[] cropCenter = { 509 scaledCrop.centerX(), scaledCrop.centerY() 510 }; 511 m1.mapPoints(cropCenter); 512 GeometryMetadata.concatRecenterMatrix(m1, cropCenter, displayCenter); 513 m1.preRotate(getLocalStraighten(), scaledPhoto.centerX(), scaledPhoto.centerY()); 514 m1.preScale(scale, scale); 515 516 p.setARGB(255, 0, 0, 0); 517 canvas.save(); 518 canvas.drawBitmap(photo, m1, p); 519 canvas.restore(); 520 521 p.setColor(getDefaultBackgroundColor()); 522 p.setStyle(Paint.Style.FILL); 523 scaledCrop.offset(displayCenter[0] - scaledCrop.centerX(), displayCenter[1] 524 - scaledCrop.centerY()); 525 RectF display = new RectF(0, 0, getWidth(), getHeight()); 526 drawRotatedShadows(canvas, p, scaledCrop, display, getLocalRotation(), getWidth() / 2, 527 getHeight() / 2); 528 } 529 } 530