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 com.android.camera.debug.Log;
33 import com.android.camera2.R;
34 
35 import 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  */
49 public 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          */
onZoomStart()72         void onZoomStart();
73 
74         /**
75          * This gets called when zoom gesture has ended.
76          */
onZoomEnd()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          */
onZoomValueChanged(float ratio)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          */
onPreviewTouched(MotionEvent ev)91         public void onPreviewTouched(MotionEvent ev);
92     }
93 
PreviewOverlay(Context context, AttributeSet attrs)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      */
setupZoom(float zoomMaxRatio, float zoom, OnZoomChangedListener zoomChangeListener)107     public void setupZoom(float zoomMaxRatio, float zoom,
108                           OnZoomChangedListener zoomChangeListener) {
109         mZoomListener = zoomChangeListener;
110         mZoomProcessor.setupZoom(zoomMaxRatio, zoom);
111     }
112 
113     @Override
onTouchEvent(MotionEvent m)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      */
setOnPreviewTouchedListener(OnPreviewTouchedListener listener)133     public void setOnPreviewTouchedListener(OnPreviewTouchedListener listener) {
134         mOnPreviewTouchedListener = listener;
135     }
136 
137     @Override
onPreviewAreaChanged(RectF previewArea)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
onDraw(Canvas canvas)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      */
setGestureListener(GestureDetector.OnGestureListener gestureListener)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      */
setTouchListener(View.OnTouchListener touchListener)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      */
reset()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 
ZoomGestureDetector()188         public ZoomGestureDetector() {
189             super(getContext(), mZoomProcessor);
190         }
191 
192         @Override
onTouchEvent(MotionEvent ev)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          */
getAngle()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 
ZoomProcessor()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.
setZoomMax(float zoomMaxRatio)252         public void setZoomMax(float zoomMaxRatio) {
253             mMaxRatio = zoomMaxRatio;
254         }
255 
256         // Set current zoom ratio from Module.
setZoom(float ratio)257         public void setZoom(float ratio) {
258             mCurrentRatio = ratio;
259         }
260 
layout(int l, int t, int r, int b)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 
draw(Canvas canvas)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
onScale(ScaleGestureDetector detector)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
onScaleBegin(ScaleGestureDetector detector)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
onScaleEnd(ScaleGestureDetector detector)342         public void onScaleEnd(ScaleGestureDetector detector) {
343             mZoomProcessor.hideZoomUI();
344             if (mZoomListener != null) {
345                 mZoomListener.onZoomEnd();
346             }
347             invalidate();
348         }
349 
isVisible()350         public boolean isVisible() {
351             return mVisible;
352         }
353 
showZoomUI()354         public void showZoomUI() {
355             if (mZoomListener == null) {
356                 return;
357             }
358             mVisible = true;
359             mFingerAngle = mScaleDetector.getAngle();
360             invalidate();
361         }
362 
hideZoomUI()363         public void hideZoomUI() {
364             if (mZoomListener == null) {
365                 return;
366             }
367             mVisible = false;
368             invalidate();
369         }
370 
setupZoom(float zoomMax, float zoom)371         private void setupZoom(float zoomMax, float zoom) {
372             setZoomMax(zoomMax);
373             setZoom(zoom);
374         }
375     };
376 
377 }
378