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