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 */ 88 void onZoomStart(); 89 90 /** 91 * This gets called when zoom gesture has ended. 92 */ 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 */ 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 */ 107 public void onPreviewTouched(MotionEvent ev); 108 } 109 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 */ 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 */ 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 */ 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 */ 172 private float getZoomAtLevel(int level) { 173 return (MIN_ZOOM + ((level - 1) * ((mMaxZoom - MIN_ZOOM) / (NUM_ZOOM_LEVELS - 1)))); 174 } 175 176 @Override 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 */ 196 public void setOnPreviewTouchedListener(OnPreviewTouchedListener listener) { 197 mOnPreviewTouchedListener = listener; 198 } 199 200 @Override 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 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 */ 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 */ 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 */ 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 253 public ZoomGestureDetector() { 254 super(getContext(), mZoomProcessor); 255 } 256 257 @Override 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 */ 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 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. 317 public void setZoomMax(float zoomMaxRatio) { 318 mMaxRatio = zoomMaxRatio; 319 } 320 321 // Set current zoom ratio from Module. 322 public void setZoom(float ratio) { 323 mCurrentRatio = ratio; 324 } 325 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 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 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 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 406 public void onScaleEnd(ScaleGestureDetector detector) { 407 mZoomProcessor.hideZoomUI(); 408 if (mZoomListener != null) { 409 mZoomListener.onZoomEnd(); 410 } 411 invalidate(); 412 } 413 414 public boolean isVisible() { 415 return mVisible; 416 } 417 418 public void showZoomUI() { 419 if (mZoomListener == null) { 420 return; 421 } 422 mVisible = true; 423 mFingerAngle = mScaleDetector.getAngle(); 424 invalidate(); 425 } 426 427 public void hideZoomUI() { 428 if (mZoomListener == null) { 429 return; 430 } 431 mVisible = false; 432 invalidate(); 433 } 434 435 private void setupZoom(float zoomMax, float zoom) { 436 setZoomMax(zoomMax); 437 setZoom(zoom); 438 } 439 }; 440 441 } 442