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