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 com.android.camera.debug.Log;
     33 import com.android.camera2.R;
     34 
     35 import java.util.List;
     36 
     37 /**
     38  * PreviewOverlay is a view that sits on top of the preview. It serves to disambiguate
     39  * touch events, as {@link com.android.camera.app.CameraAppUI} has a touch listener
     40  * set on it. As a result, touch events that happen on preview will first go through
     41  * the touch listener in AppUI, which filters out swipes that should be handled on
     42  * the app level. The rest of the touch events will be handled here in
     43  * {@link #onTouchEvent(android.view.MotionEvent)}.
     44  * <p/>
     45  * For scale gestures, if an {@link OnZoomChangedListener} is set, the listener
     46  * will receive callbacks as the scaling happens, and a zoom UI will be hosted in
     47  * this class.
     48  */
     49 public class PreviewOverlay extends View
     50     implements PreviewStatusListener.PreviewAreaChangedListener {
     51 
     52     public static final float ZOOM_MIN_RATIO = 1.0f;
     53 
     54     private static final Log.Tag TAG = new Log.Tag("PreviewOverlay");
     55 
     56     /** Minimum time between calls to zoom listener. */
     57     private static final long ZOOM_MINIMUM_WAIT_MILLIS = 33;
     58 
     59     /** Next time zoom change should be sent to listener. */
     60     private long mDelayZoomCallUntilMillis = 0;
     61     private final ZoomGestureDetector mScaleDetector;
     62     private final ZoomProcessor mZoomProcessor = new ZoomProcessor();
     63     private GestureDetector mGestureDetector = null;
     64     private View.OnTouchListener mTouchListener = null;
     65     private OnZoomChangedListener mZoomListener = null;
     66     private OnPreviewTouchedListener mOnPreviewTouchedListener;
     67 
     68     public interface OnZoomChangedListener {
     69         /**
     70          * This gets called when a zoom is detected and started.
     71          */
     72         void onZoomStart();
     73 
     74         /**
     75          * This gets called when zoom gesture has ended.
     76          */
     77         void onZoomEnd();
     78 
     79         /**
     80          * This gets called when scale gesture changes the zoom value.
     81          *
     82          * @param ratio zoom ratio, [1.0f,maximum]
     83          */
     84         void onZoomValueChanged(float ratio);  // only for immediate zoom
     85     }
     86 
     87     public interface OnPreviewTouchedListener {
     88         /**
     89          * This gets called on any preview touch event.
     90          */
     91         public void onPreviewTouched(MotionEvent ev);
     92     }
     93 
     94     public PreviewOverlay(Context context, AttributeSet attrs) {
     95         super(context, attrs);
     96         mScaleDetector = new ZoomGestureDetector();
     97     }
     98 
     99     /**
    100      * This sets up the zoom listener and zoom related parameters when
    101      * the range of zoom ratios is continuous.
    102      *
    103      * @param zoomMaxRatio max zoom ratio, [1.0f,+Inf)
    104      * @param zoom current zoom ratio, [1.0f,zoomMaxRatio]
    105      * @param zoomChangeListener a listener that receives callbacks when zoom changes
    106      */
    107     public void setupZoom(float zoomMaxRatio, float zoom,
    108                           OnZoomChangedListener zoomChangeListener) {
    109         mZoomListener = zoomChangeListener;
    110         mZoomProcessor.setupZoom(zoomMaxRatio, zoom);
    111     }
    112 
    113     @Override
    114     public boolean onTouchEvent(MotionEvent m) {
    115         // Pass the touch events to scale detector and gesture detector
    116         if (mGestureDetector != null) {
    117             mGestureDetector.onTouchEvent(m);
    118         }
    119         if (mTouchListener != null) {
    120             mTouchListener.onTouch(this, m);
    121         }
    122         mScaleDetector.onTouchEvent(m);
    123         if (mOnPreviewTouchedListener != null) {
    124             mOnPreviewTouchedListener.onPreviewTouched(m);
    125         }
    126         return true;
    127     }
    128 
    129     /**
    130      * Set an {@link OnPreviewTouchedListener} to be executed on any preview
    131      * touch event.
    132      */
    133     public void setOnPreviewTouchedListener(OnPreviewTouchedListener listener) {
    134         mOnPreviewTouchedListener = listener;
    135     }
    136 
    137     @Override
    138     public void onPreviewAreaChanged(RectF previewArea) {
    139         mZoomProcessor.layout((int) previewArea.left, (int) previewArea.top,
    140                 (int) previewArea.right, (int) previewArea.bottom);
    141     }
    142 
    143     @Override
    144     public void onDraw(Canvas canvas) {
    145         super.onDraw(canvas);
    146         mZoomProcessor.draw(canvas);
    147     }
    148 
    149     /**
    150      * Each module can pass in their own gesture listener through App UI. When a gesture
    151      * is detected, the {@link GestureDetector.OnGestureListener} will be notified of
    152      * the gesture.
    153      *
    154      * @param gestureListener a listener from a module that defines how to handle gestures
    155      */
    156     public void setGestureListener(GestureDetector.OnGestureListener gestureListener) {
    157         if (gestureListener != null) {
    158             mGestureDetector = new GestureDetector(getContext(), gestureListener);
    159         }
    160     }
    161 
    162     /**
    163      * Set a touch listener on the preview overlay.  When a module doesn't support a
    164      * {@link GestureDetector.OnGestureListener}, this can be used instead.
    165      */
    166     public void setTouchListener(View.OnTouchListener touchListener) {
    167         mTouchListener = touchListener;
    168     }
    169 
    170     /**
    171      * During module switch, connections to the previous module should be cleared.
    172      */
    173     public void reset() {
    174         mZoomListener = null;
    175         mGestureDetector = null;
    176         mTouchListener = null;
    177     }
    178 
    179     /**
    180      * Custom scale gesture detector that ignores touch events when no
    181      * {@link OnZoomChangedListener} is set. Otherwise, it calculates the real-time
    182      * angle between two fingers in a scale gesture.
    183      */
    184     private class ZoomGestureDetector extends ScaleGestureDetector {
    185         private float mDeltaX;
    186         private float mDeltaY;
    187 
    188         public ZoomGestureDetector() {
    189             super(getContext(), mZoomProcessor);
    190         }
    191 
    192         @Override
    193         public boolean onTouchEvent(MotionEvent ev) {
    194             if (mZoomListener == null) {
    195                 return false;
    196             } else {
    197                 boolean handled = super.onTouchEvent(ev);
    198                 if (ev.getPointerCount() > 1) {
    199                     mDeltaX = ev.getX(1) - ev.getX(0);
    200                     mDeltaY = ev.getY(1) - ev.getY(0);
    201                 }
    202                 return handled;
    203             }
    204         }
    205 
    206         /**
    207          * Calculate the angle between two fingers. Range: [-pi, pi]
    208          */
    209         public float getAngle() {
    210             return (float) Math.atan2(-mDeltaY, mDeltaX);
    211         }
    212     }
    213 
    214     /**
    215      * This class processes recognized scale gestures, notifies {@link OnZoomChangedListener}
    216      * of any change in scale, and draw the zoom UI on screen.
    217      */
    218     private class ZoomProcessor implements ScaleGestureDetector.OnScaleGestureListener {
    219         private final Log.Tag TAG = new Log.Tag("ZoomProcessor");
    220 
    221         // Diameter of Zoom UI as fraction of maximum possible without clipping.
    222         private static final float ZOOM_UI_SIZE = 0.8f;
    223         // Diameter of Zoom UI donut hole as fraction of Zoom UI diameter.
    224         private static final float ZOOM_UI_DONUT = 0.25f;
    225 
    226         private final float mMinRatio = 1.0f;
    227         private float mMaxRatio;
    228         // Continuous Zoom level [0,1].
    229         private float mCurrentRatio;
    230         private double mFingerAngle;  // in radians.
    231         private final Paint mPaint;
    232         private int mCenterX;
    233         private int mCenterY;
    234         private float mOuterRadius;
    235         private float mInnerRadius;
    236         private final int mZoomStroke;
    237         private boolean mVisible = false;
    238         private List<Integer> mZoomRatios;
    239 
    240         public ZoomProcessor() {
    241             Resources res = getResources();
    242             mZoomStroke = res.getDimensionPixelSize(R.dimen.zoom_stroke);
    243             mPaint = new Paint();
    244             mPaint.setAntiAlias(true);
    245             mPaint.setColor(Color.WHITE);
    246             mPaint.setStyle(Paint.Style.STROKE);
    247             mPaint.setStrokeWidth(mZoomStroke);
    248             mPaint.setStrokeCap(Paint.Cap.ROUND);
    249         }
    250 
    251         // Set maximum zoom ratio from Module.
    252         public void setZoomMax(float zoomMaxRatio) {
    253             mMaxRatio = zoomMaxRatio;
    254         }
    255 
    256         // Set current zoom ratio from Module.
    257         public void setZoom(float ratio) {
    258             mCurrentRatio = ratio;
    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 fillRatio = (mCurrentRatio - mMinRatio) / (mMaxRatio - mMinRatio);
    288             float zoomRadius = mInnerRadius + fillRatio * (mOuterRadius - mInnerRadius);
    289             canvas.drawLine(mCenterX + mInnerRadius * (float) Math.cos(mFingerAngle),
    290                     mCenterY - mInnerRadius * (float) Math.sin(mFingerAngle),
    291                     mCenterX + zoomRadius * (float) Math.cos(mFingerAngle),
    292                     mCenterY - zoomRadius * (float) Math.sin(mFingerAngle), mPaint);
    293             canvas.drawLine(mCenterX - mInnerRadius * (float) Math.cos(mFingerAngle),
    294                     mCenterY + mInnerRadius * (float) Math.sin(mFingerAngle),
    295                     mCenterX - zoomRadius * (float) Math.cos(mFingerAngle),
    296                     mCenterY + zoomRadius * (float) Math.sin(mFingerAngle), mPaint);
    297         }
    298 
    299         @Override
    300         public boolean onScale(ScaleGestureDetector detector) {
    301             final float sf = detector.getScaleFactor();
    302             mCurrentRatio = (0.33f + mCurrentRatio) * sf * sf - 0.33f;
    303             if (mCurrentRatio < mMinRatio) {
    304                 mCurrentRatio = mMinRatio;
    305             }
    306             if (mCurrentRatio > mMaxRatio) {
    307                 mCurrentRatio = mMaxRatio;
    308             }
    309 
    310             // Only call the listener with a certain frequency. This is
    311             // necessary because these listeners will make repeated
    312             // applySettings() calls into the portability layer, and doing this
    313             // too often can back up its handler and result in visible lag in
    314             // updating the zoom level and other controls.
    315             long now = SystemClock.uptimeMillis();
    316             if (now > mDelayZoomCallUntilMillis) {
    317                 if (mZoomListener != null) {
    318                     mZoomListener.onZoomValueChanged(mCurrentRatio);
    319                 }
    320                 mDelayZoomCallUntilMillis = now + ZOOM_MINIMUM_WAIT_MILLIS;
    321             }
    322             mFingerAngle = mScaleDetector.getAngle();
    323             invalidate();
    324             return true;
    325         }
    326 
    327         @Override
    328         public boolean onScaleBegin(ScaleGestureDetector detector) {
    329             mZoomProcessor.showZoomUI();
    330             if (mZoomListener == null) {
    331                 return false;
    332             }
    333             if (mZoomListener != null) {
    334                 mZoomListener.onZoomStart();
    335             }
    336             mFingerAngle = mScaleDetector.getAngle();
    337             invalidate();
    338             return true;
    339         }
    340 
    341         @Override
    342         public void onScaleEnd(ScaleGestureDetector detector) {
    343             mZoomProcessor.hideZoomUI();
    344             if (mZoomListener != null) {
    345                 mZoomListener.onZoomEnd();
    346             }
    347             invalidate();
    348         }
    349 
    350         public boolean isVisible() {
    351             return mVisible;
    352         }
    353 
    354         public void showZoomUI() {
    355             if (mZoomListener == null) {
    356                 return;
    357             }
    358             mVisible = true;
    359             mFingerAngle = mScaleDetector.getAngle();
    360             invalidate();
    361         }
    362 
    363         public void hideZoomUI() {
    364             if (mZoomListener == null) {
    365                 return;
    366             }
    367             mVisible = false;
    368             invalidate();
    369         }
    370 
    371         private void setupZoom(float zoomMax, float zoom) {
    372             setZoomMax(zoomMax);
    373             setZoom(zoom);
    374         }
    375     };
    376 
    377 }
    378