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