Home | History | Annotate | Download | only in ui
      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