/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.camera.ui; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.os.SystemClock; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import android.widget.Button; import com.android.camera.debug.Log; import com.android.camera2.R; import java.util.List; /** * PreviewOverlay is a view that sits on top of the preview. It serves to disambiguate * touch events, as {@link com.android.camera.app.CameraAppUI} has a touch listener * set on it. As a result, touch events that happen on preview will first go through * the touch listener in AppUI, which filters out swipes that should be handled on * the app level. The rest of the touch events will be handled here in * {@link #onTouchEvent(android.view.MotionEvent)}. *
* For scale gestures, if an {@link OnZoomChangedListener} is set, the listener * will receive callbacks as the scaling happens, and a zoom UI will be hosted in * this class. */ public class PreviewOverlay extends View implements PreviewStatusListener.PreviewAreaChangedListener { public static final float ZOOM_MIN_RATIO = 1.0f; private static final int NUM_ZOOM_LEVELS = 7; private static final float MIN_ZOOM = 1f; private static final Log.Tag TAG = new Log.Tag("PreviewOverlay"); /** Minimum time between calls to zoom listener. */ private static final long ZOOM_MINIMUM_WAIT_MILLIS = 33; /** Next time zoom change should be sent to listener. */ private long mDelayZoomCallUntilMillis = 0; private final ZoomGestureDetector mScaleDetector; private final ZoomProcessor mZoomProcessor = new ZoomProcessor(); private GestureDetector mGestureDetector = null; private View.OnTouchListener mTouchListener = null; private OnZoomChangedListener mZoomListener = null; private OnPreviewTouchedListener mOnPreviewTouchedListener; /** Maximum zoom; intialize to 1.0 (disabled) */ private float mMaxZoom = MIN_ZOOM; /** * Current zoom value in accessibility mode, ranging from MIN_ZOOM to * mMaxZoom. */ private float mCurrA11yZoom = MIN_ZOOM; /** * Current zoom level ranging between 1 and NUM_ZOOM_LEVELS. Each level is * associated with a discrete zoom value. */ private int mCurrA11yZoomLevel = 1; public interface OnZoomChangedListener { /** * This gets called when a zoom is detected and started. */ void onZoomStart(); /** * This gets called when zoom gesture has ended. */ void onZoomEnd(); /** * This gets called when scale gesture changes the zoom value. * * @param ratio zoom ratio, [1.0f,maximum] */ void onZoomValueChanged(float ratio); // only for immediate zoom } public interface OnPreviewTouchedListener { /** * This gets called on any preview touch event. */ public void onPreviewTouched(MotionEvent ev); } public PreviewOverlay(Context context, AttributeSet attrs) { super(context, attrs); mScaleDetector = new ZoomGestureDetector(); } /** * This sets up the zoom listener and zoom related parameters when * the range of zoom ratios is continuous. * * @param zoomMaxRatio max zoom ratio, [1.0f,+Inf) * @param zoom current zoom ratio, [1.0f,zoomMaxRatio] * @param zoomChangeListener a listener that receives callbacks when zoom changes */ public void setupZoom(float zoomMaxRatio, float zoom, OnZoomChangedListener zoomChangeListener) { mZoomListener = zoomChangeListener; mZoomProcessor.setupZoom(zoomMaxRatio, zoom); } /** * uZooms camera in when in accessibility mode. * * @param view is the current view * @param maxZoom is the maximum zoom value on the given device * @return float representing the current zoom value */ public float zoomIn(View view, float maxZoom) { mCurrA11yZoomLevel++; mMaxZoom = maxZoom; mCurrA11yZoom = getZoomAtLevel(mCurrA11yZoomLevel); mZoomListener.onZoomValueChanged(mCurrA11yZoom); view.announceForAccessibility(String.format( view.getResources(). getString(R.string.accessibility_zoom_announcement), mCurrA11yZoom)); return mCurrA11yZoom; } /** * Zooms camera out when in accessibility mode. * * @param view is the current view * @param maxZoom is the maximum zoom value on the given device * @return float representing the current zoom value */ public float zoomOut(View view, float maxZoom) { mCurrA11yZoomLevel--; mMaxZoom = maxZoom; mCurrA11yZoom = getZoomAtLevel(mCurrA11yZoomLevel); mZoomListener.onZoomValueChanged(mCurrA11yZoom); view.announceForAccessibility(String.format( view.getResources(). getString(R.string.accessibility_zoom_announcement), mCurrA11yZoom)); return mCurrA11yZoom; } /** * Method used in accessibility mode. Ensures that there are evenly spaced * zoom values ranging from MIN_ZOOM to NUM_ZOOM_LEVELS * * @param level is the zoom level being computed in the range * @return the zoom value at the given level */ private float getZoomAtLevel(int level) { return (MIN_ZOOM + ((level - 1) * ((mMaxZoom - MIN_ZOOM) / (NUM_ZOOM_LEVELS - 1)))); } @Override public boolean onTouchEvent(MotionEvent m) { // Pass the touch events to scale detector and gesture detector if (mGestureDetector != null) { mGestureDetector.onTouchEvent(m); } if (mTouchListener != null) { mTouchListener.onTouch(this, m); } mScaleDetector.onTouchEvent(m); if (mOnPreviewTouchedListener != null) { mOnPreviewTouchedListener.onPreviewTouched(m); } return true; } /** * Set an {@link OnPreviewTouchedListener} to be executed on any preview * touch event. */ public void setOnPreviewTouchedListener(OnPreviewTouchedListener listener) { mOnPreviewTouchedListener = listener; } @Override public void onPreviewAreaChanged(RectF previewArea) { mZoomProcessor.layout((int) previewArea.left, (int) previewArea.top, (int) previewArea.right, (int) previewArea.bottom); } @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); mZoomProcessor.draw(canvas); } /** * Each module can pass in their own gesture listener through App UI. When a gesture * is detected, the {@link GestureDetector.OnGestureListener} will be notified of * the gesture. * * @param gestureListener a listener from a module that defines how to handle gestures */ public void setGestureListener(GestureDetector.OnGestureListener gestureListener) { if (gestureListener != null) { mGestureDetector = new GestureDetector(getContext(), gestureListener); } } /** * Set a touch listener on the preview overlay. When a module doesn't support a * {@link GestureDetector.OnGestureListener}, this can be used instead. */ public void setTouchListener(View.OnTouchListener touchListener) { mTouchListener = touchListener; } /** * During module switch, connections to the previous module should be cleared. */ public void reset() { mZoomListener = null; mGestureDetector = null; mTouchListener = null; mCurrA11yZoomLevel = 1; mCurrA11yZoom = MIN_ZOOM; } /** * Custom scale gesture detector that ignores touch events when no * {@link OnZoomChangedListener} is set. Otherwise, it calculates the real-time * angle between two fingers in a scale gesture. */ private class ZoomGestureDetector extends ScaleGestureDetector { private float mDeltaX; private float mDeltaY; public ZoomGestureDetector() { super(getContext(), mZoomProcessor); } @Override public boolean onTouchEvent(MotionEvent ev) { if (mZoomListener == null) { return false; } else { boolean handled = super.onTouchEvent(ev); if (ev.getPointerCount() > 1) { mDeltaX = ev.getX(1) - ev.getX(0); mDeltaY = ev.getY(1) - ev.getY(0); } return handled; } } /** * Calculate the angle between two fingers. Range: [-pi, pi] */ public float getAngle() { return (float) Math.atan2(-mDeltaY, mDeltaX); } } /** * This class processes recognized scale gestures, notifies {@link OnZoomChangedListener} * of any change in scale, and draw the zoom UI on screen. */ private class ZoomProcessor implements ScaleGestureDetector.OnScaleGestureListener { private final Log.Tag TAG = new Log.Tag("ZoomProcessor"); // Diameter of Zoom UI as fraction of maximum possible without clipping. private static final float ZOOM_UI_SIZE = 0.8f; // Diameter of Zoom UI donut hole as fraction of Zoom UI diameter. private static final float ZOOM_UI_DONUT = 0.25f; private final float mMinRatio = 1.0f; private float mMaxRatio; // Continuous Zoom level [0,1]. private float mCurrentRatio; private double mFingerAngle; // in radians. private final Paint mPaint; private int mCenterX; private int mCenterY; private float mOuterRadius; private float mInnerRadius; private final int mZoomStroke; private boolean mVisible = false; private List