PreviewOverlay.java revision 56688f7019fbe4dee110b7600349f48da9fff601
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.util.AttributeSet;
25import android.view.GestureDetector;
26import android.view.MotionEvent;
27import android.view.ScaleGestureDetector;
28import android.view.View;
29
30import com.android.camera2.R;
31
32import java.util.List;
33import java.util.ArrayList;
34
35/**
36 * PreviewOverlay is a view that sits on top of the preview. It serves to disambiguate
37 * touch events, as {@link com.android.camera.app.CameraAppUI} has a touch listener
38 * set on it. As a result, touch events that happen on preview will first go through
39 * the touch listener in AppUI, which filters out swipes that should be handled on
40 * the app level. The rest of the touch events will be handled here in
41 * {@link #onTouchEvent(android.view.MotionEvent)}.
42 * <p/>
43 * For scale gestures, if an {@link OnZoomChangedListener} is set, the listener
44 * will receive callbacks as the scaling happens, and a zoom UI will be hosted in
45 * this class.
46 */
47public class PreviewOverlay extends View
48    implements PreviewStatusListener.PreviewAreaSizeChangedListener {
49
50    private static final String TAG = "PreviewOverlay";
51
52    public static final int ZOOM_MIN_FACTOR = 100;
53
54    private final ZoomGestureDetector mScaleDetector;
55    private final ZoomProcessor mZoomProcessor = new ZoomProcessor();
56    private GestureDetector mGestureDetector = null;
57    private OnZoomChangedListener mZoomListener = null;
58    private OnPreviewTouchedListener mOnPreviewTouchedListener;
59
60    public interface OnZoomChangedListener {
61        /**
62         * This gets called when a zoom is detected and started.
63         */
64        void onZoomStart();
65
66        /**
67         * This gets called when zoom gesture has ended.
68         */
69        void onZoomEnd();
70
71        /**
72         * This gets called when scale gesture changes the zoom value.
73         *
74         * @param index index of the list of supported zoom ratios
75         */
76        void onZoomValueChanged(int index);  // only for immediate zoom
77    }
78
79    public interface OnPreviewTouchedListener {
80        /**
81         * This gets called on any preview touch event.
82         */
83        public void onPreviewTouched(MotionEvent ev);
84    }
85
86    public PreviewOverlay(Context context, AttributeSet attrs) {
87        super(context, attrs);
88        mScaleDetector = new ZoomGestureDetector();
89    }
90
91    /**
92     * This sets up the zoom listener and zoom related parameters.
93     *
94     * @param zoomMax max zoom index
95     * @param zoom current zoom index
96     * @param zoomRatios a list of zoom ratios
97     * @param zoomChangeListener a listener that receives callbacks when zoom changes
98     */
99    public void setupZoom(int zoomMax, int zoom, List<Integer> zoomRatios,
100                          OnZoomChangedListener zoomChangeListener) {
101        mZoomListener = zoomChangeListener;
102        mZoomProcessor.setupZoom(zoomMax, zoom, zoomRatios);
103    }
104
105    /**
106     * This sets up the zoom listener and zoom related parameters when
107     * the range of zoom ratios is continuous.
108     *
109     * @param zoomMaxRatio max zoom ratio
110     * @param zoom current zoom index
111     * @param zoomChangeListener a listener that receives callbacks when zoom changes
112     */
113    public void setupZoom(float zoomMaxRatio, int zoom, OnZoomChangedListener zoomChangeListener) {
114        mZoomListener = zoomChangeListener;
115        int zoomMax = ((int) zoomMaxRatio * 100) - ZOOM_MIN_FACTOR;
116        mZoomProcessor.setupZoom(zoomMax, zoom, null);
117    }
118
119    @Override
120    public boolean onTouchEvent(MotionEvent m) {
121        // Pass the touch events to scale detector and gesture detector
122        if (mGestureDetector != null) {
123            mGestureDetector.onTouchEvent(m);
124        }
125        mScaleDetector.onTouchEvent(m);
126        if (mOnPreviewTouchedListener != null) {
127            mOnPreviewTouchedListener.onPreviewTouched(m);
128        }
129        return true;
130    }
131
132    /**
133     * Set an {@link OnPreviewTouchedListener} to be executed on any preview
134     * touch event.
135     */
136    public void setOnPreviewTouchedListener(OnPreviewTouchedListener listener) {
137        mOnPreviewTouchedListener = listener;
138    }
139
140    @Override
141    public void onPreviewAreaSizeChanged(float previewWidth, float previewHeight) {
142        mZoomProcessor.layout(0, 0, (int) previewWidth, (int) previewHeight);
143    }
144
145    @Override
146    public void onDraw(Canvas canvas) {
147        super.onDraw(canvas);
148        mZoomProcessor.draw(canvas);
149    }
150
151    /**
152     * Each module can pass in their own gesture listener through App UI. When a gesture
153     * is detected, the {#link GestureDetector.OnGestureListener} will be notified of
154     * the gesture.
155     *
156     * @param gestureListener a listener from a module that defines how to handle gestures
157     */
158    public void setGestureListener(GestureDetector.OnGestureListener gestureListener) {
159        if (gestureListener != null) {
160            mGestureDetector = new GestureDetector(getContext(), gestureListener);
161        }
162    }
163
164    /**
165     * During module switch, connections to the previous module should be cleared.
166     */
167    public void reset() {
168        mZoomListener = null;
169        mGestureDetector = null;
170    }
171
172    /**
173     * Custom scale gesture detector that ignores touch events when no
174     * {@link OnZoomChangedListener} is set. Otherwise, it calculates the real-time
175     * angle between two fingers in a scale gesture.
176     */
177    private class ZoomGestureDetector extends ScaleGestureDetector {
178        private float mDeltaX;
179        private float mDeltaY;
180
181        public ZoomGestureDetector() {
182            super(getContext(), mZoomProcessor);
183        }
184
185        @Override
186        public boolean onTouchEvent(MotionEvent ev) {
187            if (mZoomListener == null) {
188                return false;
189            } else {
190                boolean handled = super.onTouchEvent(ev);
191                if (ev.getPointerCount() > 1) {
192                    mDeltaX = ev.getX(1) - ev.getX(0);
193                    mDeltaY = ev.getY(1) - ev.getY(0);
194                }
195                return handled;
196            }
197        }
198
199        /**
200         * Calculate the angle between two fingers. Range: [-pi, pi]
201         */
202        public float getAngle() {
203            return (float) Math.atan2(-mDeltaY, mDeltaX);
204        }
205    }
206
207    /**
208     * This class processes recognized scale gestures, notifies {@link OnZoomChangedListener}
209     * of any change in scale, and draw the zoom UI on screen.
210     */
211    private class ZoomProcessor implements ScaleGestureDetector.OnScaleGestureListener {
212        private static final String TAG = "ZoomProcessor";
213
214        // Diameter of Zoom UI as fraction of maximum possible without clipping.
215        private static final float ZOOM_UI_SIZE = 0.8f;
216        // Diameter of Zoom UI donut hole as fraction of Zoom UI diameter.
217        private static final float ZOOM_UI_DONUT = 0.25f;
218
219        final private int mMinIndex = 0;
220        private int mMaxIndex;
221        // Discrete Zoom level [mMinIndex,mMaxIndex].
222        private int mCurrentIndex;
223        // Continuous Zoom level [0,1].
224        private float mCurrentFraction;
225        private double mFingerAngle;  // in radians.
226        private final Paint mPaint;
227        private int mCenterX;
228        private int mCenterY;
229        private float mOuterRadius;
230        private float mInnerRadius;
231        private final int mZoomStroke;
232        private boolean mVisible = false;
233        private List<Integer> mZoomRatios;
234
235        public ZoomProcessor() {
236            Resources res = getResources();
237            mZoomStroke = res.getDimensionPixelSize(R.dimen.zoom_stroke);
238            mPaint = new Paint();
239            mPaint.setAntiAlias(true);
240            mPaint.setColor(Color.WHITE);
241            mPaint.setStyle(Paint.Style.STROKE);
242            mPaint.setStrokeWidth(mZoomStroke);
243            mPaint.setStrokeCap(Paint.Cap.ROUND);
244        }
245
246        // Set maximum Zoom Index from Module.
247        public void setZoomMax(int zoomMaxIndex) {
248            mMaxIndex = zoomMaxIndex;
249        }
250
251        // Set current Zoom Index from Module.
252        public void setZoom(int index) {
253            mCurrentIndex = index;
254            mCurrentFraction = (float) index / (mMaxIndex - mMinIndex);
255        }
256
257        public void setZoomValue(int value) {
258            // Do nothing because we are not display text value in current UI.
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 zoomRadius = mInnerRadius + mCurrentFraction * (mOuterRadius - mInnerRadius);
288            canvas.drawLine(mCenterX + mInnerRadius * (float) Math.cos(mFingerAngle),
289                    mCenterY - mInnerRadius * (float) Math.sin(mFingerAngle),
290                    mCenterX + zoomRadius * (float) Math.cos(mFingerAngle),
291                    mCenterY - zoomRadius * (float) Math.sin(mFingerAngle), mPaint);
292            canvas.drawLine(mCenterX - mInnerRadius * (float) Math.cos(mFingerAngle),
293                    mCenterY + mInnerRadius * (float) Math.sin(mFingerAngle),
294                    mCenterX - zoomRadius * (float) Math.cos(mFingerAngle),
295                    mCenterY + zoomRadius * (float) Math.sin(mFingerAngle), mPaint);
296        }
297
298        @Override
299        public boolean onScale(ScaleGestureDetector detector) {
300            final float sf = detector.getScaleFactor();
301            mCurrentFraction = (0.33f + mCurrentFraction) * sf * sf - 0.33f;
302            if (mCurrentFraction < 0.0f) mCurrentFraction = 0.0f;
303            if (mCurrentFraction > 1.0f) mCurrentFraction = 1.0f;
304            int newIndex = mMinIndex + (int) (mCurrentFraction * (mMaxIndex - mMinIndex));
305            if (mZoomListener != null && newIndex != mCurrentIndex) {
306                mZoomListener.onZoomValueChanged(newIndex);
307                mCurrentIndex = newIndex;
308            }
309            mFingerAngle = mScaleDetector.getAngle();
310            invalidate();
311            return true;
312        }
313
314        @Override
315        public boolean onScaleBegin(ScaleGestureDetector detector) {
316            if (mZoomListener == null) {
317                return false;
318            }
319            mVisible = true;
320            if (mZoomListener != null) {
321                mZoomListener.onZoomStart();
322            }
323            mFingerAngle = mScaleDetector.getAngle();
324            invalidate();
325            return true;
326        }
327
328        @Override
329        public void onScaleEnd(ScaleGestureDetector detector) {
330            mVisible = false;
331            if (mZoomListener != null) {
332                mZoomListener.onZoomEnd();
333            }
334            invalidate();
335        }
336
337        private void setupZoom(int zoomMax, int zoom, List<Integer> zoomRatios) {
338            mZoomRatios = zoomRatios;
339            setZoomMax(zoomMax);
340            setZoom(zoom);
341        }
342    };
343
344}
345