Home | History | Annotate | Download | only in basicmultitouch
      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.example.android.basicmultitouch;
     18 
     19 import android.content.Context;
     20 import android.graphics.Canvas;
     21 import android.graphics.Color;
     22 import android.graphics.Paint;
     23 import android.graphics.PointF;
     24 import android.util.AttributeSet;
     25 import android.util.SparseArray;
     26 import android.view.MotionEvent;
     27 import android.view.View;
     28 
     29 import com.example.android.basicmultitouch.Pools.SimplePool;
     30 
     31 /**
     32  * View that shows touch events and their history. This view demonstrates the
     33  * use of {@link #onTouchEvent(android.view.MotionEvent)} and {@link android.view.MotionEvent}s to keep
     34  * track of touch pointers across events.
     35  */
     36 public class TouchDisplayView extends View {
     37 
     38     // Hold data for active touch pointer IDs
     39     private SparseArray<TouchHistory> mTouches;
     40 
     41     // Is there an active touch?
     42     private boolean mHasTouch = false;
     43 
     44     /**
     45      * Holds data related to a touch pointer, including its current position,
     46      * pressure and historical positions. Objects are allocated through an
     47      * object pool using {@link #obtain()} and {@link #recycle()} to reuse
     48      * existing objects.
     49      */
     50     static final class TouchHistory {
     51 
     52         // number of historical points to store
     53         public static final int HISTORY_COUNT = 20;
     54 
     55         public float x;
     56         public float y;
     57         public float pressure = 0f;
     58         public String label = null;
     59 
     60         // current position in history array
     61         public int historyIndex = 0;
     62         public int historyCount = 0;
     63 
     64         // arrray of pointer position history
     65         public PointF[] history = new PointF[HISTORY_COUNT];
     66 
     67         private static final int MAX_POOL_SIZE = 10;
     68         private static final SimplePool<TouchHistory> sPool =
     69                 new SimplePool<TouchHistory>(MAX_POOL_SIZE);
     70 
     71         public static TouchHistory obtain(float x, float y, float pressure) {
     72             TouchHistory data = sPool.acquire();
     73             if (data == null) {
     74                 data = new TouchHistory();
     75             }
     76 
     77             data.setTouch(x, y, pressure);
     78 
     79             return data;
     80         }
     81 
     82         public TouchHistory() {
     83 
     84             // initialise history array
     85             for (int i = 0; i < HISTORY_COUNT; i++) {
     86                 history[i] = new PointF();
     87             }
     88         }
     89 
     90         public void setTouch(float x, float y, float pressure) {
     91             this.x = x;
     92             this.y = y;
     93             this.pressure = pressure;
     94         }
     95 
     96         public void recycle() {
     97             this.historyIndex = 0;
     98             this.historyCount = 0;
     99             sPool.release(this);
    100         }
    101 
    102         /**
    103          * Add a point to its history. Overwrites oldest point if the maximum
    104          * number of historical points is already stored.
    105          *
    106          * @param point
    107          */
    108         public void addHistory(float x, float y) {
    109             PointF p = history[historyIndex];
    110             p.x = x;
    111             p.y = y;
    112 
    113             historyIndex = (historyIndex + 1) % history.length;
    114 
    115             if (historyCount < HISTORY_COUNT) {
    116                 historyCount++;
    117             }
    118         }
    119 
    120     }
    121 
    122     public TouchDisplayView(Context context, AttributeSet attrs) {
    123         super(context, attrs);
    124 
    125         // SparseArray for touch events, indexed by touch id
    126         mTouches = new SparseArray<TouchHistory>(10);
    127 
    128         initialisePaint();
    129     }
    130 
    131     // BEGIN_INCLUDE(onTouchEvent)
    132     @Override
    133     public boolean onTouchEvent(MotionEvent event) {
    134 
    135         final int action = event.getAction();
    136 
    137         /*
    138          * Switch on the action. The action is extracted from the event by
    139          * applying the MotionEvent.ACTION_MASK. Alternatively a call to
    140          * event.getActionMasked() would yield in the action as well.
    141          */
    142         switch (action & MotionEvent.ACTION_MASK) {
    143 
    144             case MotionEvent.ACTION_DOWN: {
    145                 // first pressed gesture has started
    146 
    147                 /*
    148                  * Only one touch event is stored in the MotionEvent. Extract
    149                  * the pointer identifier of this touch from the first index
    150                  * within the MotionEvent object.
    151                  */
    152                 int id = event.getPointerId(0);
    153 
    154                 TouchHistory data = TouchHistory.obtain(event.getX(0), event.getY(0),
    155                         event.getPressure(0));
    156                 data.label = "id: " + 0;
    157 
    158                 /*
    159                  * Store the data under its pointer identifier. The pointer
    160                  * number stays consistent for the duration of a gesture,
    161                  * accounting for other pointers going up or down.
    162                  */
    163                 mTouches.put(id, data);
    164 
    165                 mHasTouch = true;
    166 
    167                 break;
    168             }
    169 
    170             case MotionEvent.ACTION_POINTER_DOWN: {
    171                 /*
    172                  * A non-primary pointer has gone down, after an event for the
    173                  * primary pointer (ACTION_DOWN) has already been received.
    174                  */
    175 
    176                 /*
    177                  * The MotionEvent object contains multiple pointers. Need to
    178                  * extract the index at which the data for this particular event
    179                  * is stored.
    180                  */
    181                 int index = event.getActionIndex();
    182                 int id = event.getPointerId(index);
    183 
    184                 TouchHistory data = TouchHistory.obtain(event.getX(index), event.getY(index),
    185                         event.getPressure(index));
    186                 data.label = "id: " + id;
    187 
    188                 /*
    189                  * Store the data under its pointer identifier. The index of
    190                  * this pointer can change over multiple events, but this
    191                  * pointer is always identified by the same identifier for this
    192                  * active gesture.
    193                  */
    194                 mTouches.put(id, data);
    195 
    196                 break;
    197             }
    198 
    199             case MotionEvent.ACTION_UP: {
    200                 /*
    201                  * Final pointer has gone up and has ended the last pressed
    202                  * gesture.
    203                  */
    204 
    205                 /*
    206                  * Extract the pointer identifier for the only event stored in
    207                  * the MotionEvent object and remove it from the list of active
    208                  * touches.
    209                  */
    210                 int id = event.getPointerId(0);
    211                 TouchHistory data = mTouches.get(id);
    212                 mTouches.remove(id);
    213                 data.recycle();
    214 
    215                 mHasTouch = false;
    216 
    217                 break;
    218             }
    219 
    220             case MotionEvent.ACTION_POINTER_UP: {
    221                 /*
    222                  * A non-primary pointer has gone up and other pointers are
    223                  * still active.
    224                  */
    225 
    226                 /*
    227                  * The MotionEvent object contains multiple pointers. Need to
    228                  * extract the index at which the data for this particular event
    229                  * is stored.
    230                  */
    231                 int index = event.getActionIndex();
    232                 int id = event.getPointerId(index);
    233 
    234                 TouchHistory data = mTouches.get(id);
    235                 mTouches.remove(id);
    236                 data.recycle();
    237 
    238                 break;
    239             }
    240 
    241             case MotionEvent.ACTION_MOVE: {
    242                 /*
    243                  * A change event happened during a pressed gesture. (Between
    244                  * ACTION_DOWN and ACTION_UP or ACTION_POINTER_DOWN and
    245                  * ACTION_POINTER_UP)
    246                  */
    247 
    248                 /*
    249                  * Loop through all active pointers contained within this event.
    250                  * Data for each pointer is stored in a MotionEvent at an index
    251                  * (starting from 0 up to the number of active pointers). This
    252                  * loop goes through each of these active pointers, extracts its
    253                  * data (position and pressure) and updates its stored data. A
    254                  * pointer is identified by its pointer number which stays
    255                  * constant across touch events as long as it remains active.
    256                  * This identifier is used to keep track of a pointer across
    257                  * events.
    258                  */
    259                 for (int index = 0; index < event.getPointerCount(); index++) {
    260                     // get pointer id for data stored at this index
    261                     int id = event.getPointerId(index);
    262 
    263                     // get the data stored externally about this pointer.
    264                     TouchHistory data = mTouches.get(id);
    265 
    266                     // add previous position to history and add new values
    267                     data.addHistory(data.x, data.y);
    268                     data.setTouch(event.getX(index), event.getY(index),
    269                             event.getPressure(index));
    270 
    271                 }
    272 
    273                 break;
    274             }
    275         }
    276 
    277         // trigger redraw on UI thread
    278         this.postInvalidate();
    279 
    280         return true;
    281     }
    282 
    283     // END_INCLUDE(onTouchEvent)
    284 
    285     @Override
    286     protected void onDraw(Canvas canvas) {
    287         super.onDraw(canvas);
    288 
    289         // Canvas background color depends on whether there is an active touch
    290         if (mHasTouch) {
    291             canvas.drawColor(BACKGROUND_ACTIVE);
    292         } else {
    293             // draw inactive border
    294             canvas.drawRect(mBorderWidth, mBorderWidth, getWidth() - mBorderWidth, getHeight()
    295                     - mBorderWidth, mBorderPaint);
    296         }
    297 
    298         // loop through all active touches and draw them
    299         for (int i = 0; i < mTouches.size(); i++) {
    300 
    301             // get the pointer id and associated data for this index
    302             int id = mTouches.keyAt(i);
    303             TouchHistory data = mTouches.valueAt(i);
    304 
    305             // draw the data and its history to the canvas
    306             drawCircle(canvas, id, data);
    307         }
    308     }
    309 
    310     /*
    311      * Below are only helper methods and variables required for drawing.
    312      */
    313 
    314     // radius of active touch circle in dp
    315     private static final float CIRCLE_RADIUS_DP = 75f;
    316     // radius of historical circle in dp
    317     private static final float CIRCLE_HISTORICAL_RADIUS_DP = 7f;
    318 
    319     // calculated radiuses in px
    320     private float mCircleRadius;
    321     private float mCircleHistoricalRadius;
    322 
    323     private Paint mCirclePaint = new Paint();
    324     private Paint mTextPaint = new Paint();
    325 
    326     private static final int BACKGROUND_ACTIVE = Color.WHITE;
    327 
    328     // inactive border
    329     private static final float INACTIVE_BORDER_DP = 15f;
    330     private static final int INACTIVE_BORDER_COLOR = 0xFFffd060;
    331     private Paint mBorderPaint = new Paint();
    332     private float mBorderWidth;
    333 
    334     public final int[] COLORS = {
    335             0xFF33B5E5, 0xFFAA66CC, 0xFF99CC00, 0xFFFFBB33, 0xFFFF4444,
    336             0xFF0099CC, 0xFF9933CC, 0xFF669900, 0xFFFF8800, 0xFFCC0000
    337     };
    338 
    339     /**
    340      * Sets up the required {@link android.graphics.Paint} objects for the screen density of this
    341      * device.
    342      */
    343     private void initialisePaint() {
    344 
    345         // Calculate radiuses in px from dp based on screen density
    346         float density = getResources().getDisplayMetrics().density;
    347         mCircleRadius = CIRCLE_RADIUS_DP * density;
    348         mCircleHistoricalRadius = CIRCLE_HISTORICAL_RADIUS_DP * density;
    349 
    350         // Setup text paint for circle label
    351         mTextPaint.setTextSize(27f);
    352         mTextPaint.setColor(Color.BLACK);
    353 
    354         // Setup paint for inactive border
    355         mBorderWidth = INACTIVE_BORDER_DP * density;
    356         mBorderPaint.setStrokeWidth(mBorderWidth);
    357         mBorderPaint.setColor(INACTIVE_BORDER_COLOR);
    358         mBorderPaint.setStyle(Paint.Style.STROKE);
    359 
    360     }
    361 
    362     /**
    363      * Draws the data encapsulated by a {@link TouchDisplayView.TouchHistory} object to a canvas.
    364      * A large circle indicates the current position held by the
    365      * {@link TouchDisplayView.TouchHistory} object, while a smaller circle is drawn for each
    366      * entry in its history. The size of the large circle is scaled depending on
    367      * its pressure, clamped to a maximum of <code>1.0</code>.
    368      *
    369      * @param canvas
    370      * @param id
    371      * @param data
    372      */
    373     protected void drawCircle(Canvas canvas, int id, TouchHistory data) {
    374         // select the color based on the id
    375         int color = COLORS[id % COLORS.length];
    376         mCirclePaint.setColor(color);
    377 
    378         /*
    379          * Draw the circle, size scaled to its pressure. Pressure is clamped to
    380          * 1.0 max to ensure proper drawing. (Reported pressure values can
    381          * exceed 1.0, depending on the calibration of the touch screen).
    382          */
    383         float pressure = Math.min(data.pressure, 1f);
    384         float radius = pressure * mCircleRadius;
    385 
    386         canvas.drawCircle(data.x, (data.y) - (radius / 2f), radius,
    387                 mCirclePaint);
    388 
    389         // draw all historical points with a lower alpha value
    390         mCirclePaint.setAlpha(125);
    391         for (int j = 0; j < data.history.length && j < data.historyCount; j++) {
    392             PointF p = data.history[j];
    393             canvas.drawCircle(p.x, p.y, mCircleHistoricalRadius, mCirclePaint);
    394         }
    395 
    396         // draw its label next to the main circle
    397         canvas.drawText(data.label, data.x + radius, data.y
    398                 - radius, mTextPaint);
    399     }
    400 
    401 }
    402