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