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 
17 package com.android.camera.ui;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Canvas;
22 import android.graphics.Color;
23 import android.graphics.Paint;
24 import android.graphics.RectF;
25 import android.os.SystemClock;
26 import android.util.AttributeSet;
27 import android.view.GestureDetector;
28 import android.view.MotionEvent;
29 import android.view.ScaleGestureDetector;
30 import android.view.View;
31 
32 import android.widget.Button;
33 import com.android.camera.debug.Log;
34 import com.android.camera2.R;
35 
36 import 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  */
50 public 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          */
onZoomStart()88         void onZoomStart();
89 
90         /**
91          * This gets called when zoom gesture has ended.
92          */
onZoomEnd()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          */
onZoomValueChanged(float ratio)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          */
onPreviewTouched(MotionEvent ev)107         public void onPreviewTouched(MotionEvent ev);
108     }
109 
PreviewOverlay(Context context, AttributeSet attrs)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      */
setupZoom(float zoomMaxRatio, float zoom, OnZoomChangedListener zoomChangeListener)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      */
zoomIn(View view, float maxZoom)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      */
zoomOut(View view, float maxZoom)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      */
getZoomAtLevel(int level)172     private float getZoomAtLevel(int level) {
173         return (MIN_ZOOM + ((level - 1) * ((mMaxZoom - MIN_ZOOM) / (NUM_ZOOM_LEVELS - 1))));
174     }
175 
176     @Override
onTouchEvent(MotionEvent m)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      */
setOnPreviewTouchedListener(OnPreviewTouchedListener listener)196     public void setOnPreviewTouchedListener(OnPreviewTouchedListener listener) {
197         mOnPreviewTouchedListener = listener;
198     }
199 
200     @Override
onPreviewAreaChanged(RectF previewArea)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
onDraw(Canvas canvas)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      */
setGestureListener(GestureDetector.OnGestureListener gestureListener)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      */
setTouchListener(View.OnTouchListener touchListener)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      */
reset()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 
ZoomGestureDetector()253         public ZoomGestureDetector() {
254             super(getContext(), mZoomProcessor);
255         }
256 
257         @Override
onTouchEvent(MotionEvent ev)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          */
getAngle()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 
ZoomProcessor()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.
setZoomMax(float zoomMaxRatio)317         public void setZoomMax(float zoomMaxRatio) {
318             mMaxRatio = zoomMaxRatio;
319         }
320 
321         // Set current zoom ratio from Module.
setZoom(float ratio)322         public void setZoom(float ratio) {
323             mCurrentRatio = ratio;
324         }
325 
layout(int l, int t, int r, int b)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 
draw(Canvas canvas)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
onScale(ScaleGestureDetector detector)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
onScaleBegin(ScaleGestureDetector detector)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
onScaleEnd(ScaleGestureDetector detector)406         public void onScaleEnd(ScaleGestureDetector detector) {
407             mZoomProcessor.hideZoomUI();
408             if (mZoomListener != null) {
409                 mZoomListener.onZoomEnd();
410             }
411             invalidate();
412         }
413 
isVisible()414         public boolean isVisible() {
415             return mVisible;
416         }
417 
showZoomUI()418         public void showZoomUI() {
419             if (mZoomListener == null) {
420                 return;
421             }
422             mVisible = true;
423             mFingerAngle = mScaleDetector.getAngle();
424             invalidate();
425         }
426 
hideZoomUI()427         public void hideZoomUI() {
428             if (mZoomListener == null) {
429                 return;
430             }
431             mVisible = false;
432             invalidate();
433         }
434 
setupZoom(float zoomMax, float zoom)435         private void setupZoom(float zoomMax, float zoom) {
436             setZoomMax(zoomMax);
437             setZoom(zoom);
438         }
439     };
440 
441 }
442