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 com.android.gallery3d.R;
20import com.android.gallery3d.anim.Animation;
21import com.android.gallery3d.app.GalleryActivity;
22import com.android.gallery3d.common.Utils;
23
24import android.graphics.Bitmap;
25import android.graphics.Bitmap.Config;
26import android.graphics.Canvas;
27import android.graphics.Color;
28import android.graphics.Paint;
29import android.graphics.PointF;
30import android.graphics.RectF;
31import android.media.FaceDetector;
32import android.os.Handler;
33import android.os.Message;
34import android.view.MotionEvent;
35import android.view.animation.DecelerateInterpolator;
36import android.widget.Toast;
37
38import java.util.ArrayList;
39import javax.microedition.khronos.opengles.GL11;
40
41/**
42 * The activity can crop specific region of interest from an image.
43 */
44public 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
800