Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2010 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.cooliris.media;
     18 
     19 import android.content.Context;
     20 import android.util.Log;
     21 import android.view.MotionEvent;
     22 
     23 /**
     24  * Detects transformation gestures involving more than one pointer
     25  * ("multitouch") using the supplied {@link MotionEvent}s. The
     26  * {@link OnScaleGestureListener} callback will notify users when a particular
     27  * gesture event has occurred. This class should only be used with
     28  * {@link MotionEvent}s reported via touch.
     29  *
     30  * To use this class:
     31  * <ul>
     32  * <li>Create an instance of the {@code ScaleGestureDetector} for your
     33  * {@link View}
     34  * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
     35  * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback will
     36  * be executed when the events occur.
     37  * </ul>
     38  *
     39  * @hide Pending API approval
     40  */
     41 public class ScaleGestureDetector {
     42     /**
     43      * The listener for receiving notifications when gestures occur. If you want
     44      * to listen for all the different gestures then implement this interface.
     45      * If you only want to listen for a subset it might be easier to extend
     46      * {@link SimpleOnScaleGestureListener}.
     47      *
     48      * An application will receive events in the following order:
     49      * <ul>
     50      * <li>One {@link OnScaleGestureListener#onScaleBegin()}
     51      * <li>Zero or more {@link OnScaleGestureListener#onScale()}
     52      * <li>One {@link OnScaleGestureListener#onTransformEnd()}
     53      * </ul>
     54      */
     55     public interface OnScaleGestureListener {
     56         /**
     57          * Responds to scaling events for a gesture in progress. Reported by
     58          * pointer motion.
     59          *
     60          * @param detector
     61          *            The detector reporting the event - use this to retrieve
     62          *            extended info about event state.
     63          * @return Whether or not the detector should consider this event as
     64          *         handled. If an event was not handled, the detector will
     65          *         continue to accumulate movement until an event is handled.
     66          *         This can be useful if an application, for example, only wants
     67          *         to update scaling factors if the change is greater than 0.01.
     68          */
     69         public boolean onScale(ScaleGestureDetector detector);
     70 
     71         /**
     72          * Responds to the beginning of a scaling gesture. Reported by new
     73          * pointers going down.
     74          *
     75          * @param detector
     76          *            The detector reporting the event - use this to retrieve
     77          *            extended info about event state.
     78          * @return Whether or not the detector should continue recognizing this
     79          *         gesture. For example, if a gesture is beginning with a focal
     80          *         point outside of a region where it makes sense,
     81          *         onScaleBegin() may return false to ignore the rest of the
     82          *         gesture.
     83          */
     84         public boolean onScaleBegin(ScaleGestureDetector detector);
     85 
     86         /**
     87          * Responds to the end of a scale gesture. Reported by existing pointers
     88          * going up. If the end of a gesture would result in a fling, {@link
     89          * onTransformFling()} is called instead.
     90          *
     91          * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()} and
     92          * {@link ScaleGestureDetector#getFocusY()} will return the location of
     93          * the pointer remaining on the screen.
     94          *
     95          * @param detector
     96          *            The detector reporting the event - use this to retrieve
     97          *            extended info about event state.
     98          */
     99         public void onScaleEnd(ScaleGestureDetector detector, boolean cancel);
    100     }
    101 
    102     /**
    103      * A convenience class to extend when you only want to listen for a subset
    104      * of scaling-related events. This implements all methods in
    105      * {@link OnScaleGestureListener} but does nothing.
    106      * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} and
    107      * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} return
    108      * {@code true}.
    109      */
    110     public class SimpleOnScaleGestureListener implements OnScaleGestureListener {
    111 
    112         public boolean onScale(ScaleGestureDetector detector) {
    113             return true;
    114         }
    115 
    116         public boolean onScaleBegin(ScaleGestureDetector detector) {
    117             return true;
    118         }
    119 
    120         public void onScaleEnd(ScaleGestureDetector detector, boolean cancel) {
    121             // Intentionally empty
    122         }
    123     }
    124 
    125     private static final float PRESSURE_THRESHOLD = 0.67f;
    126 
    127     private Context mContext;
    128     private OnScaleGestureListener mListener;
    129     private boolean mGestureInProgress;
    130 
    131     private MotionEvent mPrevEvent;
    132     private MotionEvent mCurrEvent;
    133 
    134     private float mFocusX;
    135     private float mFocusY;
    136     private float mPrevFingerDiffX;
    137     private float mPrevFingerDiffY;
    138     private float mCurrFingerDiffX;
    139     private float mCurrFingerDiffY;
    140     private float mCurrLen;
    141     private float mPrevLen;
    142     private float mScaleFactor;
    143     private float mCurrPressure;
    144     private float mPrevPressure;
    145     private long mTimeDelta;
    146 
    147     // Tracking individual fingers.
    148     private float mTopFingerBeginX;
    149     private float mTopFingerBeginY;
    150     private float mBottomFingerBeginX;
    151     private float mBottomFingerBeginY;
    152 
    153     private float mTopFingerCurrX;
    154     private float mTopFingerCurrY;
    155     private float mBottomFingerCurrX;
    156     private float mBottomFingerCurrY;
    157 
    158     private boolean mTopFingerIsPointer1;
    159     private boolean mPointerOneUp;
    160     private boolean mPointerTwoUp;
    161 
    162     private static final String TAG = "ScaleGestureDetector";
    163 
    164     public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
    165         mContext = context;
    166         mListener = listener;
    167     }
    168 
    169     public boolean onTouchEvent(MotionEvent event) {
    170         final int action = event.getAction();
    171         boolean handled = true;
    172 
    173         if (!mGestureInProgress) {
    174             // Track individual fingers.
    175             if (action == MotionEvent.ACTION_POINTER_1_DOWN) {
    176 
    177             }
    178             if (action == MotionEvent.ACTION_POINTER_2_DOWN) {
    179 
    180             }
    181             if ((action == MotionEvent.ACTION_POINTER_1_DOWN || action == MotionEvent.ACTION_POINTER_2_DOWN)
    182                     && event.getPointerCount() >= 2) {
    183                 // We have a new multi-finger gesture
    184                 mBottomFingerBeginX = event.getX(0);
    185                 mBottomFingerBeginY = event.getY(0);
    186                 mTopFingerBeginX = event.getX(1);
    187                 mTopFingerBeginY = event.getY(1);
    188 
    189                 mTopFingerCurrX = mTopFingerBeginX;
    190                 mTopFingerCurrY = mTopFingerBeginY;
    191                 mBottomFingerCurrX = mBottomFingerBeginX;
    192                 mBottomFingerCurrY = mBottomFingerBeginY;
    193                 mPointerOneUp = false;
    194                 mPointerTwoUp = false;
    195 
    196                 // Be paranoid in case we missed an event
    197                 reset();
    198 
    199                 // We decide which finger should be designated as the top finger
    200                 if (mTopFingerBeginY > mBottomFingerBeginY) {
    201                     mTopFingerIsPointer1 = false;
    202                 } else {
    203                     mTopFingerIsPointer1 = true;
    204                 }
    205 
    206                 mPrevEvent = MotionEvent.obtain(event);
    207                 mTimeDelta = 0;
    208 
    209                 setContext(event);
    210                 mGestureInProgress = mListener.onScaleBegin(this);
    211             }
    212         } else {
    213             // Transform gesture in progress - attempt to handle it
    214             switch (action) {
    215             case MotionEvent.ACTION_UP:
    216                 mPointerOneUp = true;
    217                 mPointerTwoUp = true;
    218             case MotionEvent.ACTION_POINTER_1_UP:
    219                 if (mPointerOneUp) {
    220                     mPointerTwoUp = true;
    221                 }
    222                 mPointerOneUp = true;
    223             case MotionEvent.ACTION_POINTER_2_UP:
    224                 if (action == MotionEvent.ACTION_POINTER_2_UP) {
    225                     if (mPointerTwoUp == true) {
    226                         mPointerOneUp = true;
    227                     }
    228                     mPointerTwoUp = true;
    229                 }
    230                 // Gesture ended
    231                 if (mPointerOneUp || mPointerTwoUp) {
    232                     setContext(event);
    233 
    234                     // Set focus point to the remaining finger
    235                     int id = (((action & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT) == 0) ? 1 : 0;
    236                     mFocusX = event.getX(id);
    237                     mFocusY = event.getY(id);
    238 
    239                     mListener.onScaleEnd(this, false);
    240                     mGestureInProgress = false;
    241 
    242                     reset();
    243                 }
    244                 break;
    245 
    246             case MotionEvent.ACTION_CANCEL:
    247                 mListener.onScaleEnd(this, true);
    248                 mGestureInProgress = false;
    249 
    250                 reset();
    251                 break;
    252 
    253             case MotionEvent.ACTION_MOVE:
    254                 setContext(event);
    255 
    256                 // Only accept the event if our relative pressure is within
    257                 // a certain limit - this can help filter shaky data as a
    258                 // finger is lifted.
    259                 if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
    260                     final boolean updatePrevious = mListener.onScale(this);
    261 
    262                     if (updatePrevious) {
    263                         mPrevEvent.recycle();
    264                         mPrevEvent = MotionEvent.obtain(event);
    265                     }
    266                 }
    267                 break;
    268             }
    269         }
    270         return handled;
    271     }
    272 
    273     private void setContext(MotionEvent curr) {
    274         if (mCurrEvent != null) {
    275             mCurrEvent.recycle();
    276         }
    277         mCurrEvent = MotionEvent.obtain(curr);
    278 
    279         mCurrLen = -1;
    280         mPrevLen = -1;
    281         mScaleFactor = -1;
    282 
    283         final MotionEvent prev = mPrevEvent;
    284 
    285         final float px0 = prev.getX(0);
    286         final float py0 = prev.getY(0);
    287         final float px1 = prev.getX(1);
    288         final float py1 = prev.getY(1);
    289         final float cx0 = curr.getX(0);
    290         final float cy0 = curr.getY(0);
    291         final float cx1 = curr.getX(1);
    292         final float cy1 = curr.getY(1);
    293 
    294         final float pvx = px1 - px0;
    295         final float pvy = py1 - py0;
    296         final float cvx = cx1 - cx0;
    297         final float cvy = cy1 - cy0;
    298         mPrevFingerDiffX = pvx;
    299         mPrevFingerDiffY = pvy;
    300         mCurrFingerDiffX = cvx;
    301         mCurrFingerDiffY = cvy;
    302 
    303         mFocusX = cx0 + cvx * 0.5f;
    304         mFocusY = cy0 + cvy * 0.5f;
    305         mTimeDelta = curr.getEventTime() - prev.getEventTime();
    306         mCurrPressure = curr.getPressure(0) + curr.getPressure(1);
    307         mPrevPressure = prev.getPressure(0) + prev.getPressure(1);
    308 
    309         // Update the correct finger.
    310         mBottomFingerCurrX = cx0;
    311         mBottomFingerCurrY = cy0;
    312         mTopFingerCurrX = cx1;
    313         mTopFingerCurrY = cy1;
    314     }
    315 
    316     private void reset() {
    317         if (mPrevEvent != null) {
    318             mPrevEvent.recycle();
    319             mPrevEvent = null;
    320         }
    321         if (mCurrEvent != null) {
    322             mCurrEvent.recycle();
    323             mCurrEvent = null;
    324         }
    325     }
    326 
    327     /**
    328      * Returns {@code true} if a two-finger scale gesture is in progress.
    329      *
    330      * @return {@code true} if a scale gesture is in progress, {@code false}
    331      *         otherwise.
    332      */
    333     public boolean isInProgress() {
    334         return mGestureInProgress;
    335     }
    336 
    337     /**
    338      * Get the X coordinate of the current gesture's focal point. If a gesture
    339      * is in progress, the focal point is directly between the two pointers
    340      * forming the gesture. If a gesture is ending, the focal point is the
    341      * location of the remaining pointer on the screen. If {@link
    342      * isInProgress()} would return false, the result of this function is
    343      * undefined.
    344      *
    345      * @return X coordinate of the focal point in pixels.
    346      */
    347     public float getFocusX() {
    348         return mFocusX;
    349     }
    350 
    351     /**
    352      * Get the Y coordinate of the current gesture's focal point. If a gesture
    353      * is in progress, the focal point is directly between the two pointers
    354      * forming the gesture. If a gesture is ending, the focal point is the
    355      * location of the remaining pointer on the screen. If {@link
    356      * isInProgress()} would return false, the result of this function is
    357      * undefined.
    358      *
    359      * @return Y coordinate of the focal point in pixels.
    360      */
    361     public float getFocusY() {
    362         return mFocusY;
    363     }
    364 
    365     /**
    366      * Return the current distance between the two pointers forming the gesture
    367      * in progress.
    368      *
    369      * @return Distance between pointers in pixels.
    370      */
    371     public float getCurrentSpan() {
    372         if (mCurrLen == -1) {
    373             final float cvx = mCurrFingerDiffX;
    374             final float cvy = mCurrFingerDiffY;
    375             mCurrLen = (float) Math.sqrt(cvx * cvx + cvy * cvy);
    376         }
    377         return mCurrLen;
    378     }
    379 
    380     /**
    381      * Return the previous distance between the two pointers forming the gesture
    382      * in progress.
    383      *
    384      * @return Previous distance between pointers in pixels.
    385      */
    386     public float getPreviousSpan() {
    387         if (mPrevLen == -1) {
    388             final float pvx = mPrevFingerDiffX;
    389             final float pvy = mPrevFingerDiffY;
    390             mPrevLen = (float) Math.sqrt(pvx * pvx + pvy * pvy);
    391         }
    392         return mPrevLen;
    393     }
    394 
    395     /**
    396      * Return the scaling factor from the previous scale event to the current
    397      * event. This value is defined as ({@link getCurrentSpan()} / {@link
    398      * getPreviousSpan()}).
    399      *
    400      * @return The current scaling factor.
    401      */
    402     public float getScaleFactor() {
    403         if (mScaleFactor == -1) {
    404             mScaleFactor = getCurrentSpan() / getPreviousSpan();
    405         }
    406         return mScaleFactor;
    407     }
    408 
    409     /**
    410      * Return the time difference in milliseconds between the previous accepted
    411      * scaling event and the current scaling event.
    412      *
    413      * @return Time difference since the last scaling event in milliseconds.
    414      */
    415     public long getTimeDelta() {
    416         return mTimeDelta;
    417     }
    418 
    419     /**
    420      * Return the event time of the current event being processed.
    421      *
    422      * @return Current event time in milliseconds.
    423      */
    424     public long getEventTime() {
    425         return mCurrEvent.getEventTime();
    426     }
    427 
    428     public float getTopFingerX() {
    429         return (mTopFingerIsPointer1) ? mTopFingerCurrX : mBottomFingerCurrX;
    430     }
    431 
    432     public float getTopFingerY() {
    433         return (mTopFingerIsPointer1) ? mTopFingerCurrY : mBottomFingerCurrY;
    434     }
    435 
    436     public float getTopFingerDeltaX() {
    437         return (mTopFingerIsPointer1) ? mTopFingerCurrX - mTopFingerBeginX : mBottomFingerCurrX - mBottomFingerBeginX;
    438     }
    439 
    440     public float getTopFingerDeltaY() {
    441         return (mTopFingerIsPointer1) ? mTopFingerCurrY - mTopFingerBeginY : mBottomFingerCurrY - mBottomFingerBeginY;
    442     }
    443 
    444     public float getBottomFingerX() {
    445         return (!mTopFingerIsPointer1) ? mTopFingerCurrX : mBottomFingerCurrX;
    446     }
    447 
    448     public float getBottomFingerY() {
    449         return (!mTopFingerIsPointer1) ? mTopFingerCurrY : mBottomFingerCurrY;
    450     }
    451 
    452     public float getBottomFingerDeltaX() {
    453         return (!mTopFingerIsPointer1) ? mTopFingerCurrX - mTopFingerBeginX : mBottomFingerCurrX - mBottomFingerBeginX;
    454     }
    455 
    456     public float getBottomFingerDeltaY() {
    457         return (!mTopFingerIsPointer1) ? mTopFingerCurrY - mTopFingerBeginY : mBottomFingerCurrY - mBottomFingerBeginY;
    458     }
    459 }
    460