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