1 /* 2 * Copyright (C) 2010 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.ui; 18 19 import android.graphics.Bitmap; 20 import android.graphics.Bitmap.Config; 21 import android.graphics.Canvas; 22 import android.graphics.Color; 23 import android.graphics.Paint; 24 import android.graphics.PointF; 25 import android.graphics.RectF; 26 import android.media.FaceDetector; 27 import android.os.Handler; 28 import android.os.Message; 29 import android.util.FloatMath; 30 import android.view.MotionEvent; 31 import android.view.animation.DecelerateInterpolator; 32 import android.widget.Toast; 33 34 import com.android.gallery3d.R; 35 import com.android.gallery3d.anim.Animation; 36 import com.android.gallery3d.app.GalleryActivity; 37 import com.android.gallery3d.common.Utils; 38 39 import java.util.ArrayList; 40 41 import javax.microedition.khronos.opengles.GL11; 42 43 /** 44 * The activity can crop specific region of interest from an image. 45 */ 46 public class CropView extends GLView { 47 private static final String TAG = "CropView"; 48 49 private static final int FACE_PIXEL_COUNT = 120000; // around 400x300 50 51 private static final int COLOR_OUTLINE = 0xFF008AFF; 52 private static final int COLOR_FACE_OUTLINE = 0xFF000000; 53 54 private static final float OUTLINE_WIDTH = 3f; 55 56 private static final int SIZE_UNKNOWN = -1; 57 private static final int TOUCH_TOLERANCE = 30; 58 59 private static final float MIN_SELECTION_LENGTH = 16f; 60 public static final float UNSPECIFIED = -1f; 61 62 private static final int MAX_FACE_COUNT = 3; 63 private static final float FACE_EYE_RATIO = 2f; 64 65 private static final int ANIMATION_DURATION = 1250; 66 67 private static final int MOVE_LEFT = 1; 68 private static final int MOVE_TOP = 2; 69 private static final int MOVE_RIGHT = 4; 70 private static final int MOVE_BOTTOM = 8; 71 private static final int MOVE_BLOCK = 16; 72 73 private static final float MAX_SELECTION_RATIO = 0.8f; 74 private static final float MIN_SELECTION_RATIO = 0.4f; 75 private static final float SELECTION_RATIO = 0.60f; 76 private static final int ANIMATION_TRIGGER = 64; 77 78 private static final int MSG_UPDATE_FACES = 1; 79 80 private float mAspectRatio = UNSPECIFIED; 81 private float mSpotlightRatioX = 0; 82 private float mSpotlightRatioY = 0; 83 84 private Handler mMainHandler; 85 86 private FaceHighlightView mFaceDetectionView; 87 private HighlightRectangle mHighlightRectangle; 88 private TileImageView mImageView; 89 private AnimationController mAnimation = new AnimationController(); 90 91 private int mImageWidth = SIZE_UNKNOWN; 92 private int mImageHeight = SIZE_UNKNOWN; 93 94 private GalleryActivity mActivity; 95 96 private GLPaint mPaint = new GLPaint(); 97 private GLPaint mFacePaint = new GLPaint(); 98 99 private int mImageRotation; 100 101 public CropView(GalleryActivity activity) { 102 mActivity = activity; 103 mImageView = new TileImageView(activity); 104 mFaceDetectionView = new FaceHighlightView(); 105 mHighlightRectangle = new HighlightRectangle(); 106 107 addComponent(mImageView); 108 addComponent(mFaceDetectionView); 109 addComponent(mHighlightRectangle); 110 111 mHighlightRectangle.setVisibility(GLView.INVISIBLE); 112 113 mPaint.setColor(COLOR_OUTLINE); 114 mPaint.setLineWidth(OUTLINE_WIDTH); 115 116 mFacePaint.setColor(COLOR_FACE_OUTLINE); 117 mFacePaint.setLineWidth(OUTLINE_WIDTH); 118 119 mMainHandler = new SynchronizedHandler(activity.getGLRoot()) { 120 @Override 121 public void handleMessage(Message message) { 122 Utils.assertTrue(message.what == MSG_UPDATE_FACES); 123 ((DetectFaceTask) message.obj).updateFaces(); 124 } 125 }; 126 } 127 128 public void setAspectRatio(float ratio) { 129 mAspectRatio = ratio; 130 } 131 132 public void setSpotlightRatio(float ratioX, float ratioY) { 133 mSpotlightRatioX = ratioX; 134 mSpotlightRatioY = ratioY; 135 } 136 137 @Override 138 public void onLayout(boolean changed, int l, int t, int r, int b) { 139 int width = r - l; 140 int height = b - t; 141 142 mFaceDetectionView.layout(0, 0, width, height); 143 mHighlightRectangle.layout(0, 0, width, height); 144 mImageView.layout(0, 0, width, height); 145 if (mImageHeight != SIZE_UNKNOWN) { 146 mAnimation.initialize(); 147 if (mHighlightRectangle.getVisibility() == GLView.VISIBLE) { 148 mAnimation.parkNow( 149 mHighlightRectangle.mHighlightRect); 150 } 151 } 152 } 153 154 private boolean setImageViewPosition(int centerX, int centerY, float scale) { 155 int inverseX = mImageWidth - centerX; 156 int inverseY = mImageHeight - centerY; 157 TileImageView t = mImageView; 158 int rotation = mImageRotation; 159 switch (rotation) { 160 case 0: return t.setPosition(centerX, centerY, scale, 0); 161 case 90: return t.setPosition(centerY, inverseX, scale, 90); 162 case 180: return t.setPosition(inverseX, inverseY, scale, 180); 163 case 270: return t.setPosition(inverseY, centerX, scale, 270); 164 default: throw new IllegalArgumentException(String.valueOf(rotation)); 165 } 166 } 167 168 @Override 169 public void render(GLCanvas canvas) { 170 AnimationController a = mAnimation; 171 if (a.calculate(AnimationTime.get())) invalidate(); 172 setImageViewPosition(a.getCenterX(), a.getCenterY(), a.getScale()); 173 super.render(canvas); 174 } 175 176 @Override 177 public void renderBackground(GLCanvas canvas) { 178 canvas.clearBuffer(); 179 } 180 181 public RectF getCropRectangle() { 182 if (mHighlightRectangle.getVisibility() == GLView.INVISIBLE) return null; 183 RectF rect = mHighlightRectangle.mHighlightRect; 184 RectF result = new RectF(rect.left * mImageWidth, rect.top * mImageHeight, 185 rect.right * mImageWidth, rect.bottom * mImageHeight); 186 return result; 187 } 188 189 public int getImageWidth() { 190 return mImageWidth; 191 } 192 193 public int getImageHeight() { 194 return mImageHeight; 195 } 196 197 private class FaceHighlightView extends GLView { 198 private static final int INDEX_NONE = -1; 199 private ArrayList<RectF> mFaces = new ArrayList<RectF>(); 200 private RectF mRect = new RectF(); 201 private int mPressedFaceIndex = INDEX_NONE; 202 203 public void addFace(RectF faceRect) { 204 mFaces.add(faceRect); 205 invalidate(); 206 } 207 208 private void renderFace(GLCanvas canvas, RectF face, boolean pressed) { 209 GL11 gl = canvas.getGLInstance(); 210 if (pressed) { 211 gl.glEnable(GL11.GL_STENCIL_TEST); 212 gl.glClear(GL11.GL_STENCIL_BUFFER_BIT); 213 gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE); 214 gl.glStencilFunc(GL11.GL_ALWAYS, 1, 1); 215 } 216 217 RectF r = mAnimation.mapRect(face, mRect); 218 canvas.fillRect(r.left, r.top, r.width(), r.height(), Color.TRANSPARENT); 219 canvas.drawRect(r.left, r.top, r.width(), r.height(), mFacePaint); 220 221 if (pressed) { 222 gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP); 223 } 224 } 225 226 @Override 227 protected void renderBackground(GLCanvas canvas) { 228 ArrayList<RectF> faces = mFaces; 229 for (int i = 0, n = faces.size(); i < n; ++i) { 230 renderFace(canvas, faces.get(i), i == mPressedFaceIndex); 231 } 232 233 GL11 gl = canvas.getGLInstance(); 234 if (mPressedFaceIndex != INDEX_NONE) { 235 gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1); 236 canvas.fillRect(0, 0, getWidth(), getHeight(), 0x66000000); 237 gl.glDisable(GL11.GL_STENCIL_TEST); 238 } 239 } 240 241 private void setPressedFace(int index) { 242 if (mPressedFaceIndex == index) return; 243 mPressedFaceIndex = index; 244 invalidate(); 245 } 246 247 private int getFaceIndexByPosition(float x, float y) { 248 ArrayList<RectF> faces = mFaces; 249 for (int i = 0, n = faces.size(); i < n; ++i) { 250 RectF r = mAnimation.mapRect(faces.get(i), mRect); 251 if (r.contains(x, y)) return i; 252 } 253 return INDEX_NONE; 254 } 255 256 @Override 257 protected boolean onTouch(MotionEvent event) { 258 float x = event.getX(); 259 float y = event.getY(); 260 switch (event.getAction()) { 261 case MotionEvent.ACTION_DOWN: 262 case MotionEvent.ACTION_MOVE: { 263 setPressedFace(getFaceIndexByPosition(x, y)); 264 break; 265 } 266 case MotionEvent.ACTION_CANCEL: 267 case MotionEvent.ACTION_UP: { 268 int index = mPressedFaceIndex; 269 setPressedFace(INDEX_NONE); 270 if (index != INDEX_NONE) { 271 mHighlightRectangle.setRectangle(mFaces.get(index)); 272 mHighlightRectangle.setVisibility(GLView.VISIBLE); 273 setVisibility(GLView.INVISIBLE); 274 } 275 } 276 } 277 return true; 278 } 279 } 280 281 private class AnimationController extends Animation { 282 private int mCurrentX; 283 private int mCurrentY; 284 private float mCurrentScale; 285 private int mStartX; 286 private int mStartY; 287 private float mStartScale; 288 private int mTargetX; 289 private int mTargetY; 290 private float mTargetScale; 291 292 public AnimationController() { 293 setDuration(ANIMATION_DURATION); 294 setInterpolator(new DecelerateInterpolator(4)); 295 } 296 297 public void initialize() { 298 mCurrentX = mImageWidth / 2; 299 mCurrentY = mImageHeight / 2; 300 mCurrentScale = Math.min(2, Math.min( 301 (float) getWidth() / mImageWidth, 302 (float) getHeight() / mImageHeight)); 303 } 304 305 public void startParkingAnimation(RectF highlight) { 306 RectF r = mAnimation.mapRect(highlight, new RectF()); 307 int width = getWidth(); 308 int height = getHeight(); 309 310 float wr = r.width() / width; 311 float hr = r.height() / height; 312 final int d = ANIMATION_TRIGGER; 313 if (wr >= MIN_SELECTION_RATIO && wr < MAX_SELECTION_RATIO 314 && hr >= MIN_SELECTION_RATIO && hr < MAX_SELECTION_RATIO 315 && r.left >= d && r.right < width - d 316 && r.top >= d && r.bottom < height - d) return; 317 318 mStartX = mCurrentX; 319 mStartY = mCurrentY; 320 mStartScale = mCurrentScale; 321 calculateTarget(highlight); 322 start(); 323 } 324 325 public void parkNow(RectF highlight) { 326 calculateTarget(highlight); 327 forceStop(); 328 mStartX = mCurrentX = mTargetX; 329 mStartY = mCurrentY = mTargetY; 330 mStartScale = mCurrentScale = mTargetScale; 331 } 332 333 public void inverseMapPoint(PointF point) { 334 float s = mCurrentScale; 335 point.x = Utils.clamp(((point.x - getWidth() * 0.5f) / s 336 + mCurrentX) / mImageWidth, 0, 1); 337 point.y = Utils.clamp(((point.y - getHeight() * 0.5f) / s 338 + mCurrentY) / mImageHeight, 0, 1); 339 } 340 341 public RectF mapRect(RectF input, RectF output) { 342 float offsetX = getWidth() * 0.5f; 343 float offsetY = getHeight() * 0.5f; 344 int x = mCurrentX; 345 int y = mCurrentY; 346 float s = mCurrentScale; 347 output.set( 348 offsetX + (input.left * mImageWidth - x) * s, 349 offsetY + (input.top * mImageHeight - y) * s, 350 offsetX + (input.right * mImageWidth - x) * s, 351 offsetY + (input.bottom * mImageHeight - y) * s); 352 return output; 353 } 354 355 @Override 356 protected void onCalculate(float progress) { 357 mCurrentX = Math.round(mStartX + (mTargetX - mStartX) * progress); 358 mCurrentY = Math.round(mStartY + (mTargetY - mStartY) * progress); 359 mCurrentScale = mStartScale + (mTargetScale - mStartScale) * progress; 360 361 if (mCurrentX == mTargetX && mCurrentY == mTargetY 362 && mCurrentScale == mTargetScale) forceStop(); 363 } 364 365 public int getCenterX() { 366 return mCurrentX; 367 } 368 369 public int getCenterY() { 370 return mCurrentY; 371 } 372 373 public float getScale() { 374 return mCurrentScale; 375 } 376 377 private void calculateTarget(RectF highlight) { 378 float width = getWidth(); 379 float height = getHeight(); 380 381 if (mImageWidth != SIZE_UNKNOWN) { 382 float minScale = Math.min(width / mImageWidth, height / mImageHeight); 383 float scale = Utils.clamp(SELECTION_RATIO * Math.min( 384 width / (highlight.width() * mImageWidth), 385 height / (highlight.height() * mImageHeight)), minScale, 2f); 386 int centerX = Math.round( 387 mImageWidth * (highlight.left + highlight.right) * 0.5f); 388 int centerY = Math.round( 389 mImageHeight * (highlight.top + highlight.bottom) * 0.5f); 390 391 if (Math.round(mImageWidth * scale) > width) { 392 int limitX = Math.round(width * 0.5f / scale); 393 centerX = Math.round( 394 (highlight.left + highlight.right) * mImageWidth / 2); 395 centerX = Utils.clamp(centerX, limitX, mImageWidth - limitX); 396 } else { 397 centerX = mImageWidth / 2; 398 } 399 if (Math.round(mImageHeight * scale) > height) { 400 int limitY = Math.round(height * 0.5f / scale); 401 centerY = Math.round( 402 (highlight.top + highlight.bottom) * mImageHeight / 2); 403 centerY = Utils.clamp(centerY, limitY, mImageHeight - limitY); 404 } else { 405 centerY = mImageHeight / 2; 406 } 407 mTargetX = centerX; 408 mTargetY = centerY; 409 mTargetScale = scale; 410 } 411 } 412 413 } 414 415 private class HighlightRectangle extends GLView { 416 private RectF mHighlightRect = new RectF(0.25f, 0.25f, 0.75f, 0.75f); 417 private RectF mTempRect = new RectF(); 418 private PointF mTempPoint = new PointF(); 419 420 private ResourceTexture mArrow; 421 422 private int mMovingEdges = 0; 423 private float mReferenceX; 424 private float mReferenceY; 425 426 public HighlightRectangle() { 427 mArrow = new ResourceTexture(mActivity.getAndroidContext(), 428 R.drawable.camera_crop_holo); 429 } 430 431 public void setInitRectangle() { 432 float targetRatio = mAspectRatio == UNSPECIFIED 433 ? 1f 434 : mAspectRatio * mImageHeight / mImageWidth; 435 float w = SELECTION_RATIO / 2f; 436 float h = SELECTION_RATIO / 2f; 437 if (targetRatio > 1) { 438 h = w / targetRatio; 439 } else { 440 w = h * targetRatio; 441 } 442 mHighlightRect.set(0.5f - w, 0.5f - h, 0.5f + w, 0.5f + h); 443 } 444 445 public void setRectangle(RectF faceRect) { 446 mHighlightRect.set(faceRect); 447 mAnimation.startParkingAnimation(faceRect); 448 invalidate(); 449 } 450 451 private void moveEdges(MotionEvent event) { 452 float scale = mAnimation.getScale(); 453 float dx = (event.getX() - mReferenceX) / scale / mImageWidth; 454 float dy = (event.getY() - mReferenceY) / scale / mImageHeight; 455 mReferenceX = event.getX(); 456 mReferenceY = event.getY(); 457 RectF r = mHighlightRect; 458 459 if ((mMovingEdges & MOVE_BLOCK) != 0) { 460 dx = Utils.clamp(dx, -r.left, 1 - r.right); 461 dy = Utils.clamp(dy, -r.top , 1 - r.bottom); 462 r.top += dy; 463 r.bottom += dy; 464 r.left += dx; 465 r.right += dx; 466 } else { 467 PointF point = mTempPoint; 468 point.set(mReferenceX, mReferenceY); 469 mAnimation.inverseMapPoint(point); 470 float left = r.left + MIN_SELECTION_LENGTH / mImageWidth; 471 float right = r.right - MIN_SELECTION_LENGTH / mImageWidth; 472 float top = r.top + MIN_SELECTION_LENGTH / mImageHeight; 473 float bottom = r.bottom - MIN_SELECTION_LENGTH / mImageHeight; 474 if ((mMovingEdges & MOVE_RIGHT) != 0) { 475 r.right = Utils.clamp(point.x, left, 1f); 476 } 477 if ((mMovingEdges & MOVE_LEFT) != 0) { 478 r.left = Utils.clamp(point.x, 0, right); 479 } 480 if ((mMovingEdges & MOVE_TOP) != 0) { 481 r.top = Utils.clamp(point.y, 0, bottom); 482 } 483 if ((mMovingEdges & MOVE_BOTTOM) != 0) { 484 r.bottom = Utils.clamp(point.y, top, 1f); 485 } 486 if (mAspectRatio != UNSPECIFIED) { 487 float targetRatio = mAspectRatio * mImageHeight / mImageWidth; 488 if (r.width() / r.height() > targetRatio) { 489 float height = r.width() / targetRatio; 490 if ((mMovingEdges & MOVE_BOTTOM) != 0) { 491 r.bottom = Utils.clamp(r.top + height, top, 1f); 492 } else { 493 r.top = Utils.clamp(r.bottom - height, 0, bottom); 494 } 495 } else { 496 float width = r.height() * targetRatio; 497 if ((mMovingEdges & MOVE_LEFT) != 0) { 498 r.left = Utils.clamp(r.right - width, 0, right); 499 } else { 500 r.right = Utils.clamp(r.left + width, left, 1f); 501 } 502 } 503 if (r.width() / r.height() > targetRatio) { 504 float width = r.height() * targetRatio; 505 if ((mMovingEdges & MOVE_LEFT) != 0) { 506 r.left = Utils.clamp(r.right - width, 0, right); 507 } else { 508 r.right = Utils.clamp(r.left + width, left, 1f); 509 } 510 } else { 511 float height = r.width() / targetRatio; 512 if ((mMovingEdges & MOVE_BOTTOM) != 0) { 513 r.bottom = Utils.clamp(r.top + height, top, 1f); 514 } else { 515 r.top = Utils.clamp(r.bottom - height, 0, bottom); 516 } 517 } 518 } 519 } 520 invalidate(); 521 } 522 523 private void setMovingEdges(MotionEvent event) { 524 RectF r = mAnimation.mapRect(mHighlightRect, mTempRect); 525 float x = event.getX(); 526 float y = event.getY(); 527 528 if (x > r.left + TOUCH_TOLERANCE && x < r.right - TOUCH_TOLERANCE 529 && y > r.top + TOUCH_TOLERANCE && y < r.bottom - TOUCH_TOLERANCE) { 530 mMovingEdges = MOVE_BLOCK; 531 return; 532 } 533 534 boolean inVerticalRange = (r.top - TOUCH_TOLERANCE) <= y 535 && y <= (r.bottom + TOUCH_TOLERANCE); 536 boolean inHorizontalRange = (r.left - TOUCH_TOLERANCE) <= x 537 && x <= (r.right + TOUCH_TOLERANCE); 538 539 if (inVerticalRange) { 540 boolean left = Math.abs(x - r.left) <= TOUCH_TOLERANCE; 541 boolean right = Math.abs(x - r.right) <= TOUCH_TOLERANCE; 542 if (left && right) { 543 left = Math.abs(x - r.left) < Math.abs(x - r.right); 544 right = !left; 545 } 546 if (left) mMovingEdges |= MOVE_LEFT; 547 if (right) mMovingEdges |= MOVE_RIGHT; 548 if (mAspectRatio != UNSPECIFIED && inHorizontalRange) { 549 mMovingEdges |= (y > 550 (r.top + r.bottom) / 2) ? MOVE_BOTTOM : MOVE_TOP; 551 } 552 } 553 if (inHorizontalRange) { 554 boolean top = Math.abs(y - r.top) <= TOUCH_TOLERANCE; 555 boolean bottom = Math.abs(y - r.bottom) <= TOUCH_TOLERANCE; 556 if (top && bottom) { 557 top = Math.abs(y - r.top) < Math.abs(y - r.bottom); 558 bottom = !top; 559 } 560 if (top) mMovingEdges |= MOVE_TOP; 561 if (bottom) mMovingEdges |= MOVE_BOTTOM; 562 if (mAspectRatio != UNSPECIFIED && inVerticalRange) { 563 mMovingEdges |= (x > 564 (r.left + r.right) / 2) ? MOVE_RIGHT : MOVE_LEFT; 565 } 566 } 567 } 568 569 @Override 570 protected boolean onTouch(MotionEvent event) { 571 switch (event.getAction()) { 572 case MotionEvent.ACTION_DOWN: { 573 mReferenceX = event.getX(); 574 mReferenceY = event.getY(); 575 setMovingEdges(event); 576 invalidate(); 577 return true; 578 } 579 case MotionEvent.ACTION_MOVE: 580 moveEdges(event); 581 break; 582 case MotionEvent.ACTION_CANCEL: 583 case MotionEvent.ACTION_UP: { 584 mMovingEdges = 0; 585 mAnimation.startParkingAnimation(mHighlightRect); 586 invalidate(); 587 return true; 588 } 589 } 590 return true; 591 } 592 593 @Override 594 protected void renderBackground(GLCanvas canvas) { 595 RectF r = mAnimation.mapRect(mHighlightRect, mTempRect); 596 drawHighlightRectangle(canvas, r); 597 598 float centerY = (r.top + r.bottom) / 2; 599 float centerX = (r.left + r.right) / 2; 600 boolean notMoving = mMovingEdges == 0; 601 if ((mMovingEdges & MOVE_RIGHT) != 0 || notMoving) { 602 mArrow.draw(canvas, 603 Math.round(r.right - mArrow.getWidth() / 2), 604 Math.round(centerY - mArrow.getHeight() / 2)); 605 } 606 if ((mMovingEdges & MOVE_LEFT) != 0 || notMoving) { 607 mArrow.draw(canvas, 608 Math.round(r.left - mArrow.getWidth() / 2), 609 Math.round(centerY - mArrow.getHeight() / 2)); 610 } 611 if ((mMovingEdges & MOVE_TOP) != 0 || notMoving) { 612 mArrow.draw(canvas, 613 Math.round(centerX - mArrow.getWidth() / 2), 614 Math.round(r.top - mArrow.getHeight() / 2)); 615 } 616 if ((mMovingEdges & MOVE_BOTTOM) != 0 || notMoving) { 617 mArrow.draw(canvas, 618 Math.round(centerX - mArrow.getWidth() / 2), 619 Math.round(r.bottom - mArrow.getHeight() / 2)); 620 } 621 } 622 623 private void drawHighlightRectangle(GLCanvas canvas, RectF r) { 624 GL11 gl = canvas.getGLInstance(); 625 gl.glLineWidth(3.0f); 626 gl.glEnable(GL11.GL_LINE_SMOOTH); 627 628 gl.glEnable(GL11.GL_STENCIL_TEST); 629 gl.glClear(GL11.GL_STENCIL_BUFFER_BIT); 630 gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE); 631 gl.glStencilFunc(GL11.GL_ALWAYS, 1, 1); 632 633 if (mSpotlightRatioX == 0 || mSpotlightRatioY == 0) { 634 canvas.fillRect(r.left, r.top, r.width(), r.height(), Color.TRANSPARENT); 635 canvas.drawRect(r.left, r.top, r.width(), r.height(), mPaint); 636 } else { 637 float sx = r.width() * mSpotlightRatioX; 638 float sy = r.height() * mSpotlightRatioY; 639 float cx = r.centerX(); 640 float cy = r.centerY(); 641 642 canvas.fillRect(cx - sx / 2, cy - sy / 2, sx, sy, Color.TRANSPARENT); 643 canvas.drawRect(cx - sx / 2, cy - sy / 2, sx, sy, mPaint); 644 canvas.drawRect(r.left, r.top, r.width(), r.height(), mPaint); 645 646 gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1); 647 gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE); 648 649 canvas.drawRect(cx - sy / 2, cy - sx / 2, sy, sx, mPaint); 650 canvas.fillRect(cx - sy / 2, cy - sx / 2, sy, sx, Color.TRANSPARENT); 651 canvas.fillRect(r.left, r.top, r.width(), r.height(), 0x80000000); 652 } 653 654 gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1); 655 gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP); 656 657 canvas.fillRect(0, 0, getWidth(), getHeight(), 0xA0000000); 658 659 gl.glDisable(GL11.GL_STENCIL_TEST); 660 } 661 } 662 663 private class DetectFaceTask extends Thread { 664 private final FaceDetector.Face[] mFaces = new FaceDetector.Face[MAX_FACE_COUNT]; 665 private final Bitmap mFaceBitmap; 666 private int mFaceCount; 667 668 public DetectFaceTask(Bitmap bitmap) { 669 mFaceBitmap = bitmap; 670 setName("face-detect"); 671 } 672 673 @Override 674 public void run() { 675 Bitmap bitmap = mFaceBitmap; 676 FaceDetector detector = new FaceDetector( 677 bitmap.getWidth(), bitmap.getHeight(), MAX_FACE_COUNT); 678 mFaceCount = detector.findFaces(bitmap, mFaces); 679 mMainHandler.sendMessage( 680 mMainHandler.obtainMessage(MSG_UPDATE_FACES, this)); 681 } 682 683 private RectF getFaceRect(FaceDetector.Face face) { 684 PointF point = new PointF(); 685 face.getMidPoint(point); 686 687 int width = mFaceBitmap.getWidth(); 688 int height = mFaceBitmap.getHeight(); 689 float rx = face.eyesDistance() * FACE_EYE_RATIO; 690 float ry = rx; 691 float aspect = mAspectRatio; 692 if (aspect != UNSPECIFIED) { 693 if (aspect > 1) { 694 rx = ry * aspect; 695 } else { 696 ry = rx / aspect; 697 } 698 } 699 700 RectF r = new RectF( 701 point.x - rx, point.y - ry, point.x + rx, point.y + ry); 702 r.intersect(0, 0, width, height); 703 704 if (aspect != UNSPECIFIED) { 705 if (r.width() / r.height() > aspect) { 706 float w = r.height() * aspect; 707 r.left = (r.left + r.right - w) * 0.5f; 708 r.right = r.left + w; 709 } else { 710 float h = r.width() / aspect; 711 r.top = (r.top + r.bottom - h) * 0.5f; 712 r.bottom = r.top + h; 713 } 714 } 715 716 r.left /= width; 717 r.right /= width; 718 r.top /= height; 719 r.bottom /= height; 720 return r; 721 } 722 723 public void updateFaces() { 724 if (mFaceCount > 1) { 725 for (int i = 0, n = mFaceCount; i < n; ++i) { 726 mFaceDetectionView.addFace(getFaceRect(mFaces[i])); 727 } 728 mFaceDetectionView.setVisibility(GLView.VISIBLE); 729 Toast.makeText(mActivity.getAndroidContext(), 730 R.string.multiface_crop_help, Toast.LENGTH_SHORT).show(); 731 } else if (mFaceCount == 1) { 732 mFaceDetectionView.setVisibility(GLView.INVISIBLE); 733 mHighlightRectangle.setRectangle(getFaceRect(mFaces[0])); 734 mHighlightRectangle.setVisibility(GLView.VISIBLE); 735 } else /*mFaceCount == 0*/ { 736 mHighlightRectangle.setInitRectangle(); 737 mHighlightRectangle.setVisibility(GLView.VISIBLE); 738 } 739 } 740 } 741 742 public void setDataModel(TileImageView.Model dataModel, int rotation) { 743 if (((rotation / 90) & 0x01) != 0) { 744 mImageWidth = dataModel.getImageHeight(); 745 mImageHeight = dataModel.getImageWidth(); 746 } else { 747 mImageWidth = dataModel.getImageWidth(); 748 mImageHeight = dataModel.getImageHeight(); 749 } 750 751 mImageRotation = rotation; 752 753 mImageView.setModel(dataModel); 754 mAnimation.initialize(); 755 } 756 757 public void detectFaces(Bitmap bitmap) { 758 int rotation = mImageRotation; 759 int width = bitmap.getWidth(); 760 int height = bitmap.getHeight(); 761 float scale = FloatMath.sqrt((float) FACE_PIXEL_COUNT / (width * height)); 762 763 // faceBitmap is a correctly rotated bitmap, as viewed by a user. 764 Bitmap faceBitmap; 765 if (((rotation / 90) & 1) == 0) { 766 int w = (Math.round(width * scale) & ~1); // must be even 767 int h = Math.round(height * scale); 768 faceBitmap = Bitmap.createBitmap(w, h, Config.RGB_565); 769 Canvas canvas = new Canvas(faceBitmap); 770 canvas.rotate(rotation, w / 2, h / 2); 771 canvas.scale((float) w / width, (float) h / height); 772 canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG)); 773 } else { 774 int w = (Math.round(height * scale) & ~1); // must be even 775 int h = Math.round(width * scale); 776 faceBitmap = Bitmap.createBitmap(w, h, Config.RGB_565); 777 Canvas canvas = new Canvas(faceBitmap); 778 canvas.translate(w / 2, h / 2); 779 canvas.rotate(rotation); 780 canvas.translate(-h / 2, -w / 2); 781 canvas.scale((float) w / height, (float) h / width); 782 canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG)); 783 } 784 new DetectFaceTask(faceBitmap).start(); 785 } 786 787 public void initializeHighlightRectangle() { 788 mHighlightRectangle.setInitRectangle(); 789 mHighlightRectangle.setVisibility(GLView.VISIBLE); 790 } 791 792 public void resume() { 793 mImageView.prepareTextures(); 794 } 795 796 public void pause() { 797 mImageView.freeTextures(); 798 } 799 } 800 801