/* * 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 mZoomRatios; public ZoomProcessor() { Resources res = getResources(); mZoomStroke = res.getDimensionPixelSize(R.dimen.zoom_stroke); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setColor(Color.WHITE); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(mZoomStroke); mPaint.setStrokeCap(Paint.Cap.ROUND); } // Set maximum zoom ratio from Module. public void setZoomMax(float zoomMaxRatio) { mMaxRatio = zoomMaxRatio; } // Set current zoom ratio from Module. public void setZoom(float ratio) { mCurrentRatio = ratio; } public void layout(int l, int t, int r, int b) { mCenterX = (r + l) / 2; mCenterY = (b + t) / 2; // UI will extend from 20% to 80% of maximum inset circle. float insetCircleDiameter = Math.min(getWidth(), getHeight()); mOuterRadius = insetCircleDiameter * 0.5f * ZOOM_UI_SIZE; mInnerRadius = mOuterRadius * ZOOM_UI_DONUT; } public void draw(Canvas canvas) { if (!mVisible) { return; } // Draw background. mPaint.setAlpha(70); canvas.drawLine(mCenterX + mInnerRadius * (float) Math.cos(mFingerAngle), mCenterY - mInnerRadius * (float) Math.sin(mFingerAngle), mCenterX + mOuterRadius * (float) Math.cos(mFingerAngle), mCenterY - mOuterRadius * (float) Math.sin(mFingerAngle), mPaint); canvas.drawLine(mCenterX - mInnerRadius * (float) Math.cos(mFingerAngle), mCenterY + mInnerRadius * (float) Math.sin(mFingerAngle), mCenterX - mOuterRadius * (float) Math.cos(mFingerAngle), mCenterY + mOuterRadius * (float) Math.sin(mFingerAngle), mPaint); // Draw Zoom progress. mPaint.setAlpha(255); float fillRatio = (mCurrentRatio - mMinRatio) / (mMaxRatio - mMinRatio); float zoomRadius = mInnerRadius + fillRatio * (mOuterRadius - mInnerRadius); canvas.drawLine(mCenterX + mInnerRadius * (float) Math.cos(mFingerAngle), mCenterY - mInnerRadius * (float) Math.sin(mFingerAngle), mCenterX + zoomRadius * (float) Math.cos(mFingerAngle), mCenterY - zoomRadius * (float) Math.sin(mFingerAngle), mPaint); canvas.drawLine(mCenterX - mInnerRadius * (float) Math.cos(mFingerAngle), mCenterY + mInnerRadius * (float) Math.sin(mFingerAngle), mCenterX - zoomRadius * (float) Math.cos(mFingerAngle), mCenterY + zoomRadius * (float) Math.sin(mFingerAngle), mPaint); } @Override public boolean onScale(ScaleGestureDetector detector) { final float sf = detector.getScaleFactor(); mCurrentRatio = (0.33f + mCurrentRatio) * sf * sf - 0.33f; if (mCurrentRatio < mMinRatio) { mCurrentRatio = mMinRatio; } if (mCurrentRatio > mMaxRatio) { mCurrentRatio = mMaxRatio; } // Only call the listener with a certain frequency. This is // necessary because these listeners will make repeated // applySettings() calls into the portability layer, and doing this // too often can back up its handler and result in visible lag in // updating the zoom level and other controls. long now = SystemClock.uptimeMillis(); if (now > mDelayZoomCallUntilMillis) { if (mZoomListener != null) { mZoomListener.onZoomValueChanged(mCurrentRatio); } mDelayZoomCallUntilMillis = now + ZOOM_MINIMUM_WAIT_MILLIS; } mFingerAngle = mScaleDetector.getAngle(); invalidate(); return true; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { mZoomProcessor.showZoomUI(); if (mZoomListener == null) { return false; } if (mZoomListener != null) { mZoomListener.onZoomStart(); } mFingerAngle = mScaleDetector.getAngle(); invalidate(); return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { mZoomProcessor.hideZoomUI(); if (mZoomListener != null) { mZoomListener.onZoomEnd(); } invalidate(); } public boolean isVisible() { return mVisible; } public void showZoomUI() { if (mZoomListener == null) { return; } mVisible = true; mFingerAngle = mScaleDetector.getAngle(); invalidate(); } public void hideZoomUI() { if (mZoomListener == null) { return; } mVisible = false; invalidate(); } private void setupZoom(float zoomMax, float zoom) { setZoomMax(zoomMax); setZoom(zoom); } }; }