1/*
2 * Copyright (C) 2013 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.camera.ui;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Paint;
24import android.graphics.RectF;
25import android.os.SystemClock;
26import android.util.AttributeSet;
27import android.view.GestureDetector;
28import android.view.MotionEvent;
29import android.view.ScaleGestureDetector;
30import android.view.View;
31
32import android.widget.Button;
33import com.android.camera.debug.Log;
34import com.android.camera2.R;
35
36import java.util.List;
37
38/**
39 * PreviewOverlay is a view that sits on top of the preview. It serves to disambiguate
40 * touch events, as {@link com.android.camera.app.CameraAppUI} has a touch listener
41 * set on it. As a result, touch events that happen on preview will first go through
42 * the touch listener in AppUI, which filters out swipes that should be handled on
43 * the app level. The rest of the touch events will be handled here in
44 * {@link #onTouchEvent(android.view.MotionEvent)}.
45 * <p/>
46 * For scale gestures, if an {@link OnZoomChangedListener} is set, the listener
47 * will receive callbacks as the scaling happens, and a zoom UI will be hosted in
48 * this class.
49 */
50public class PreviewOverlay extends View
51    implements PreviewStatusListener.PreviewAreaChangedListener {
52
53    public static final float ZOOM_MIN_RATIO = 1.0f;
54    private static final int NUM_ZOOM_LEVELS = 7;
55    private static final float MIN_ZOOM = 1f;
56
57    private static final Log.Tag TAG = new Log.Tag("PreviewOverlay");
58
59    /** Minimum time between calls to zoom listener. */
60    private static final long ZOOM_MINIMUM_WAIT_MILLIS = 33;
61
62    /** Next time zoom change should be sent to listener. */
63    private long mDelayZoomCallUntilMillis = 0;
64    private final ZoomGestureDetector mScaleDetector;
65    private final ZoomProcessor mZoomProcessor = new ZoomProcessor();
66    private GestureDetector mGestureDetector = null;
67    private View.OnTouchListener mTouchListener = null;
68    private OnZoomChangedListener mZoomListener = null;
69    private OnPreviewTouchedListener mOnPreviewTouchedListener;
70
71    /** Maximum zoom; intialize to 1.0 (disabled) */
72    private float mMaxZoom = MIN_ZOOM;
73    /**
74     * Current zoom value in accessibility mode, ranging from MIN_ZOOM to
75     * mMaxZoom.
76     */
77    private float mCurrA11yZoom = MIN_ZOOM;
78    /**
79     * Current zoom level ranging between 1 and NUM_ZOOM_LEVELS. Each level is
80     * associated with a discrete zoom value.
81     */
82    private int mCurrA11yZoomLevel = 1;
83
84    public interface OnZoomChangedListener {
85        /**
86         * This gets called when a zoom is detected and started.
87         */
88        void onZoomStart();
89
90        /**
91         * This gets called when zoom gesture has ended.
92         */
93        void onZoomEnd();
94
95        /**
96         * This gets called when scale gesture changes the zoom value.
97         *
98         * @param ratio zoom ratio, [1.0f,maximum]
99         */
100        void onZoomValueChanged(float ratio);  // only for immediate zoom
101    }
102
103    public interface OnPreviewTouchedListener {
104        /**
105         * This gets called on any preview touch event.
106         */
107        public void onPreviewTouched(MotionEvent ev);
108    }
109
110    public PreviewOverlay(Context context, AttributeSet attrs) {
111        super(context, attrs);
112        mScaleDetector = new ZoomGestureDetector();
113    }
114
115    /**
116     * This sets up the zoom listener and zoom related parameters when
117     * the range of zoom ratios is continuous.
118     *
119     * @param zoomMaxRatio max zoom ratio, [1.0f,+Inf)
120     * @param zoom current zoom ratio, [1.0f,zoomMaxRatio]
121     * @param zoomChangeListener a listener that receives callbacks when zoom changes
122     */
123    public void setupZoom(float zoomMaxRatio, float zoom,
124                          OnZoomChangedListener zoomChangeListener) {
125        mZoomListener = zoomChangeListener;
126        mZoomProcessor.setupZoom(zoomMaxRatio, zoom);
127    }
128
129    /**
130     * uZooms camera in when in accessibility mode.
131     *
132     * @param view is the current view
133     * @param maxZoom is the maximum zoom value on the given device
134     * @return float representing the current zoom value
135     */
136    public float zoomIn(View view, float maxZoom) {
137        mCurrA11yZoomLevel++;
138        mMaxZoom = maxZoom;
139        mCurrA11yZoom = getZoomAtLevel(mCurrA11yZoomLevel);
140        mZoomListener.onZoomValueChanged(mCurrA11yZoom);
141        view.announceForAccessibility(String.format(
142                view.getResources().
143                        getString(R.string.accessibility_zoom_announcement), mCurrA11yZoom));
144        return mCurrA11yZoom;
145    }
146
147    /**
148     * Zooms camera out when in accessibility mode.
149     *
150     * @param view is the current view
151     * @param maxZoom is the maximum zoom value on the given device
152     * @return float representing the current zoom value
153     */
154    public float zoomOut(View view, float maxZoom) {
155        mCurrA11yZoomLevel--;
156        mMaxZoom = maxZoom;
157        mCurrA11yZoom = getZoomAtLevel(mCurrA11yZoomLevel);
158        mZoomListener.onZoomValueChanged(mCurrA11yZoom);
159        view.announceForAccessibility(String.format(
160                view.getResources().
161                        getString(R.string.accessibility_zoom_announcement), mCurrA11yZoom));
162        return mCurrA11yZoom;
163    }
164
165    /**
166     * Method used in accessibility mode. Ensures that there are evenly spaced
167     * zoom values ranging from MIN_ZOOM to NUM_ZOOM_LEVELS
168     *
169     * @param level is the zoom level being computed in the range
170     * @return the zoom value at the given level
171     */
172    private float getZoomAtLevel(int level) {
173        return (MIN_ZOOM + ((level - 1) * ((mMaxZoom - MIN_ZOOM) / (NUM_ZOOM_LEVELS - 1))));
174    }
175
176    @Override
177    public boolean onTouchEvent(MotionEvent m) {
178        // Pass the touch events to scale detector and gesture detector
179        if (mGestureDetector != null) {
180            mGestureDetector.onTouchEvent(m);
181        }
182        if (mTouchListener != null) {
183            mTouchListener.onTouch(this, m);
184        }
185        mScaleDetector.onTouchEvent(m);
186        if (mOnPreviewTouchedListener != null) {
187            mOnPreviewTouchedListener.onPreviewTouched(m);
188        }
189        return true;
190    }
191
192    /**
193     * Set an {@link OnPreviewTouchedListener} to be executed on any preview
194     * touch event.
195     */
196    public void setOnPreviewTouchedListener(OnPreviewTouchedListener listener) {
197        mOnPreviewTouchedListener = listener;
198    }
199
200    @Override
201    public void onPreviewAreaChanged(RectF previewArea) {
202        mZoomProcessor.layout((int) previewArea.left, (int) previewArea.top,
203                (int) previewArea.right, (int) previewArea.bottom);
204    }
205
206    @Override
207    public void onDraw(Canvas canvas) {
208        super.onDraw(canvas);
209        mZoomProcessor.draw(canvas);
210    }
211
212    /**
213     * Each module can pass in their own gesture listener through App UI. When a gesture
214     * is detected, the {@link GestureDetector.OnGestureListener} will be notified of
215     * the gesture.
216     *
217     * @param gestureListener a listener from a module that defines how to handle gestures
218     */
219    public void setGestureListener(GestureDetector.OnGestureListener gestureListener) {
220        if (gestureListener != null) {
221            mGestureDetector = new GestureDetector(getContext(), gestureListener);
222        }
223    }
224
225    /**
226     * Set a touch listener on the preview overlay.  When a module doesn't support a
227     * {@link GestureDetector.OnGestureListener}, this can be used instead.
228     */
229    public void setTouchListener(View.OnTouchListener touchListener) {
230        mTouchListener = touchListener;
231    }
232
233    /**
234     * During module switch, connections to the previous module should be cleared.
235     */
236    public void reset() {
237        mZoomListener = null;
238        mGestureDetector = null;
239        mTouchListener = null;
240        mCurrA11yZoomLevel = 1;
241        mCurrA11yZoom = MIN_ZOOM;
242    }
243
244    /**
245     * Custom scale gesture detector that ignores touch events when no
246     * {@link OnZoomChangedListener} is set. Otherwise, it calculates the real-time
247     * angle between two fingers in a scale gesture.
248     */
249    private class ZoomGestureDetector extends ScaleGestureDetector {
250        private float mDeltaX;
251        private float mDeltaY;
252
253        public ZoomGestureDetector() {
254            super(getContext(), mZoomProcessor);
255        }
256
257        @Override
258        public boolean onTouchEvent(MotionEvent ev) {
259            if (mZoomListener == null) {
260                return false;
261            } else {
262                boolean handled = super.onTouchEvent(ev);
263                if (ev.getPointerCount() > 1) {
264                    mDeltaX = ev.getX(1) - ev.getX(0);
265                    mDeltaY = ev.getY(1) - ev.getY(0);
266                }
267                return handled;
268            }
269        }
270
271        /**
272         * Calculate the angle between two fingers. Range: [-pi, pi]
273         */
274        public float getAngle() {
275            return (float) Math.atan2(-mDeltaY, mDeltaX);
276        }
277    }
278
279    /**
280     * This class processes recognized scale gestures, notifies {@link OnZoomChangedListener}
281     * of any change in scale, and draw the zoom UI on screen.
282     */
283    private class ZoomProcessor implements ScaleGestureDetector.OnScaleGestureListener {
284        private final Log.Tag TAG = new Log.Tag("ZoomProcessor");
285
286        // Diameter of Zoom UI as fraction of maximum possible without clipping.
287        private static final float ZOOM_UI_SIZE = 0.8f;
288        // Diameter of Zoom UI donut hole as fraction of Zoom UI diameter.
289        private static final float ZOOM_UI_DONUT = 0.25f;
290
291        private final float mMinRatio = 1.0f;
292        private float mMaxRatio;
293        // Continuous Zoom level [0,1].
294        private float mCurrentRatio;
295        private double mFingerAngle;  // in radians.
296        private final Paint mPaint;
297        private int mCenterX;
298        private int mCenterY;
299        private float mOuterRadius;
300        private float mInnerRadius;
301        private final int mZoomStroke;
302        private boolean mVisible = false;
303        private List<Integer> mZoomRatios;
304
305        public ZoomProcessor() {
306            Resources res = getResources();
307            mZoomStroke = res.getDimensionPixelSize(R.dimen.zoom_stroke);
308            mPaint = new Paint();
309            mPaint.setAntiAlias(true);
310            mPaint.setColor(Color.WHITE);
311            mPaint.setStyle(Paint.Style.STROKE);
312            mPaint.setStrokeWidth(mZoomStroke);
313            mPaint.setStrokeCap(Paint.Cap.ROUND);
314        }
315
316        // Set maximum zoom ratio from Module.
317        public void setZoomMax(float zoomMaxRatio) {
318            mMaxRatio = zoomMaxRatio;
319        }
320
321        // Set current zoom ratio from Module.
322        public void setZoom(float ratio) {
323            mCurrentRatio = ratio;
324        }
325
326        public void layout(int l, int t, int r, int b) {
327            mCenterX = (r + l) / 2;
328            mCenterY = (b + t) / 2;
329            // UI will extend from 20% to 80% of maximum inset circle.
330            float insetCircleDiameter = Math.min(getWidth(), getHeight());
331            mOuterRadius = insetCircleDiameter * 0.5f * ZOOM_UI_SIZE;
332            mInnerRadius = mOuterRadius * ZOOM_UI_DONUT;
333        }
334
335        public void draw(Canvas canvas) {
336            if (!mVisible) {
337                return;
338            }
339            // Draw background.
340            mPaint.setAlpha(70);
341            canvas.drawLine(mCenterX + mInnerRadius * (float) Math.cos(mFingerAngle),
342                    mCenterY - mInnerRadius * (float) Math.sin(mFingerAngle),
343                    mCenterX + mOuterRadius * (float) Math.cos(mFingerAngle),
344                    mCenterY - mOuterRadius * (float) Math.sin(mFingerAngle), mPaint);
345            canvas.drawLine(mCenterX - mInnerRadius * (float) Math.cos(mFingerAngle),
346                    mCenterY + mInnerRadius * (float) Math.sin(mFingerAngle),
347                    mCenterX - mOuterRadius * (float) Math.cos(mFingerAngle),
348                    mCenterY + mOuterRadius * (float) Math.sin(mFingerAngle), mPaint);
349            // Draw Zoom progress.
350            mPaint.setAlpha(255);
351            float fillRatio = (mCurrentRatio - mMinRatio) / (mMaxRatio - mMinRatio);
352            float zoomRadius = mInnerRadius + fillRatio * (mOuterRadius - mInnerRadius);
353            canvas.drawLine(mCenterX + mInnerRadius * (float) Math.cos(mFingerAngle),
354                    mCenterY - mInnerRadius * (float) Math.sin(mFingerAngle),
355                    mCenterX + zoomRadius * (float) Math.cos(mFingerAngle),
356                    mCenterY - zoomRadius * (float) Math.sin(mFingerAngle), mPaint);
357            canvas.drawLine(mCenterX - mInnerRadius * (float) Math.cos(mFingerAngle),
358                    mCenterY + mInnerRadius * (float) Math.sin(mFingerAngle),
359                    mCenterX - zoomRadius * (float) Math.cos(mFingerAngle),
360                    mCenterY + zoomRadius * (float) Math.sin(mFingerAngle), mPaint);
361        }
362
363        @Override
364        public boolean onScale(ScaleGestureDetector detector) {
365            final float sf = detector.getScaleFactor();
366            mCurrentRatio = (0.33f + mCurrentRatio) * sf * sf - 0.33f;
367            if (mCurrentRatio < mMinRatio) {
368                mCurrentRatio = mMinRatio;
369            }
370            if (mCurrentRatio > mMaxRatio) {
371                mCurrentRatio = mMaxRatio;
372            }
373
374            // Only call the listener with a certain frequency. This is
375            // necessary because these listeners will make repeated
376            // applySettings() calls into the portability layer, and doing this
377            // too often can back up its handler and result in visible lag in
378            // updating the zoom level and other controls.
379            long now = SystemClock.uptimeMillis();
380            if (now > mDelayZoomCallUntilMillis) {
381                if (mZoomListener != null) {
382                    mZoomListener.onZoomValueChanged(mCurrentRatio);
383                }
384                mDelayZoomCallUntilMillis = now + ZOOM_MINIMUM_WAIT_MILLIS;
385            }
386            mFingerAngle = mScaleDetector.getAngle();
387            invalidate();
388            return true;
389        }
390
391        @Override
392        public boolean onScaleBegin(ScaleGestureDetector detector) {
393            mZoomProcessor.showZoomUI();
394            if (mZoomListener == null) {
395                return false;
396            }
397            if (mZoomListener != null) {
398                mZoomListener.onZoomStart();
399            }
400            mFingerAngle = mScaleDetector.getAngle();
401            invalidate();
402            return true;
403        }
404
405        @Override
406        public void onScaleEnd(ScaleGestureDetector detector) {
407            mZoomProcessor.hideZoomUI();
408            if (mZoomListener != null) {
409                mZoomListener.onZoomEnd();
410            }
411            invalidate();
412        }
413
414        public boolean isVisible() {
415            return mVisible;
416        }
417
418        public void showZoomUI() {
419            if (mZoomListener == null) {
420                return;
421            }
422            mVisible = true;
423            mFingerAngle = mScaleDetector.getAngle();
424            invalidate();
425        }
426
427        public void hideZoomUI() {
428            if (mZoomListener == null) {
429                return;
430            }
431            mVisible = false;
432            invalidate();
433        }
434
435        private void setupZoom(float zoomMax, float zoom) {
436            setZoomMax(zoomMax);
437            setZoom(zoom);
438        }
439    };
440
441}
442