OSDN Git Service

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