| /* |
| * 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.example.android.basicmultitouch; |
| |
| import android.content.Context; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.PointF; |
| import android.util.AttributeSet; |
| import android.util.SparseArray; |
| import android.view.MotionEvent; |
| import android.view.View; |
| |
| import com.example.android.basicmultitouch.Pools.SimplePool; |
| |
| /** |
| * View that shows touch events and their history. This view demonstrates the |
| * use of {@link #onTouchEvent(android.view.MotionEvent)} and {@link android.view.MotionEvent}s to keep |
| * track of touch pointers across events. |
| */ |
| public class TouchDisplayView extends View { |
| |
| // Hold data for active touch pointer IDs |
| private SparseArray<TouchHistory> mTouches; |
| |
| // Is there an active touch? |
| private boolean mHasTouch = false; |
| |
| /** |
| * Holds data related to a touch pointer, including its current position, |
| * pressure and historical positions. Objects are allocated through an |
| * object pool using {@link #obtain()} and {@link #recycle()} to reuse |
| * existing objects. |
| */ |
| static final class TouchHistory { |
| |
| // number of historical points to store |
| public static final int HISTORY_COUNT = 20; |
| |
| public float x; |
| public float y; |
| public float pressure = 0f; |
| public String label = null; |
| |
| // current position in history array |
| public int historyIndex = 0; |
| public int historyCount = 0; |
| |
| // arrray of pointer position history |
| public PointF[] history = new PointF[HISTORY_COUNT]; |
| |
| private static final int MAX_POOL_SIZE = 10; |
| private static final SimplePool<TouchHistory> sPool = |
| new SimplePool<TouchHistory>(MAX_POOL_SIZE); |
| |
| public static TouchHistory obtain(float x, float y, float pressure) { |
| TouchHistory data = sPool.acquire(); |
| if (data == null) { |
| data = new TouchHistory(); |
| } |
| |
| data.setTouch(x, y, pressure); |
| |
| return data; |
| } |
| |
| public TouchHistory() { |
| |
| // initialise history array |
| for (int i = 0; i < HISTORY_COUNT; i++) { |
| history[i] = new PointF(); |
| } |
| } |
| |
| public void setTouch(float x, float y, float pressure) { |
| this.x = x; |
| this.y = y; |
| this.pressure = pressure; |
| } |
| |
| public void recycle() { |
| this.historyIndex = 0; |
| this.historyCount = 0; |
| sPool.release(this); |
| } |
| |
| /** |
| * Add a point to its history. Overwrites oldest point if the maximum |
| * number of historical points is already stored. |
| * |
| * @param point |
| */ |
| public void addHistory(float x, float y) { |
| PointF p = history[historyIndex]; |
| p.x = x; |
| p.y = y; |
| |
| historyIndex = (historyIndex + 1) % history.length; |
| |
| if (historyCount < HISTORY_COUNT) { |
| historyCount++; |
| } |
| } |
| |
| } |
| |
| public TouchDisplayView(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| |
| // SparseArray for touch events, indexed by touch id |
| mTouches = new SparseArray<TouchHistory>(10); |
| |
| initialisePaint(); |
| } |
| |
| // BEGIN_INCLUDE(onTouchEvent) |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| |
| final int action = event.getAction(); |
| |
| /* |
| * Switch on the action. The action is extracted from the event by |
| * applying the MotionEvent.ACTION_MASK. Alternatively a call to |
| * event.getActionMasked() would yield in the action as well. |
| */ |
| switch (action & MotionEvent.ACTION_MASK) { |
| |
| case MotionEvent.ACTION_DOWN: { |
| // first pressed gesture has started |
| |
| /* |
| * Only one touch event is stored in the MotionEvent. Extract |
| * the pointer identifier of this touch from the first index |
| * within the MotionEvent object. |
| */ |
| int id = event.getPointerId(0); |
| |
| TouchHistory data = TouchHistory.obtain(event.getX(0), event.getY(0), |
| event.getPressure(0)); |
| data.label = "id: " + 0; |
| |
| /* |
| * Store the data under its pointer identifier. The pointer |
| * number stays consistent for the duration of a gesture, |
| * accounting for other pointers going up or down. |
| */ |
| mTouches.put(id, data); |
| |
| mHasTouch = true; |
| |
| break; |
| } |
| |
| case MotionEvent.ACTION_POINTER_DOWN: { |
| /* |
| * A non-primary pointer has gone down, after an event for the |
| * primary pointer (ACTION_DOWN) has already been received. |
| */ |
| |
| /* |
| * The MotionEvent object contains multiple pointers. Need to |
| * extract the index at which the data for this particular event |
| * is stored. |
| */ |
| int index = event.getActionIndex(); |
| int id = event.getPointerId(index); |
| |
| TouchHistory data = TouchHistory.obtain(event.getX(index), event.getY(index), |
| event.getPressure(index)); |
| data.label = "id: " + id; |
| |
| /* |
| * Store the data under its pointer identifier. The index of |
| * this pointer can change over multiple events, but this |
| * pointer is always identified by the same identifier for this |
| * active gesture. |
| */ |
| mTouches.put(id, data); |
| |
| break; |
| } |
| |
| case MotionEvent.ACTION_UP: { |
| /* |
| * Final pointer has gone up and has ended the last pressed |
| * gesture. |
| */ |
| |
| /* |
| * Extract the pointer identifier for the only event stored in |
| * the MotionEvent object and remove it from the list of active |
| * touches. |
| */ |
| int id = event.getPointerId(0); |
| TouchHistory data = mTouches.get(id); |
| mTouches.remove(id); |
| data.recycle(); |
| |
| mHasTouch = false; |
| |
| break; |
| } |
| |
| case MotionEvent.ACTION_POINTER_UP: { |
| /* |
| * A non-primary pointer has gone up and other pointers are |
| * still active. |
| */ |
| |
| /* |
| * The MotionEvent object contains multiple pointers. Need to |
| * extract the index at which the data for this particular event |
| * is stored. |
| */ |
| int index = event.getActionIndex(); |
| int id = event.getPointerId(index); |
| |
| TouchHistory data = mTouches.get(id); |
| mTouches.remove(id); |
| data.recycle(); |
| |
| break; |
| } |
| |
| case MotionEvent.ACTION_MOVE: { |
| /* |
| * A change event happened during a pressed gesture. (Between |
| * ACTION_DOWN and ACTION_UP or ACTION_POINTER_DOWN and |
| * ACTION_POINTER_UP) |
| */ |
| |
| /* |
| * Loop through all active pointers contained within this event. |
| * Data for each pointer is stored in a MotionEvent at an index |
| * (starting from 0 up to the number of active pointers). This |
| * loop goes through each of these active pointers, extracts its |
| * data (position and pressure) and updates its stored data. A |
| * pointer is identified by its pointer number which stays |
| * constant across touch events as long as it remains active. |
| * This identifier is used to keep track of a pointer across |
| * events. |
| */ |
| for (int index = 0; index < event.getPointerCount(); index++) { |
| // get pointer id for data stored at this index |
| int id = event.getPointerId(index); |
| |
| // get the data stored externally about this pointer. |
| TouchHistory data = mTouches.get(id); |
| |
| // add previous position to history and add new values |
| data.addHistory(data.x, data.y); |
| data.setTouch(event.getX(index), event.getY(index), |
| event.getPressure(index)); |
| |
| } |
| |
| break; |
| } |
| } |
| |
| // trigger redraw on UI thread |
| this.postInvalidate(); |
| |
| return true; |
| } |
| |
| // END_INCLUDE(onTouchEvent) |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| super.onDraw(canvas); |
| |
| // Canvas background color depends on whether there is an active touch |
| if (mHasTouch) { |
| canvas.drawColor(BACKGROUND_ACTIVE); |
| } else { |
| // draw inactive border |
| canvas.drawRect(mBorderWidth, mBorderWidth, getWidth() - mBorderWidth, getHeight() |
| - mBorderWidth, mBorderPaint); |
| } |
| |
| // loop through all active touches and draw them |
| for (int i = 0; i < mTouches.size(); i++) { |
| |
| // get the pointer id and associated data for this index |
| int id = mTouches.keyAt(i); |
| TouchHistory data = mTouches.valueAt(i); |
| |
| // draw the data and its history to the canvas |
| drawCircle(canvas, id, data); |
| } |
| } |
| |
| /* |
| * Below are only helper methods and variables required for drawing. |
| */ |
| |
| // radius of active touch circle in dp |
| private static final float CIRCLE_RADIUS_DP = 75f; |
| // radius of historical circle in dp |
| private static final float CIRCLE_HISTORICAL_RADIUS_DP = 7f; |
| |
| // calculated radiuses in px |
| private float mCircleRadius; |
| private float mCircleHistoricalRadius; |
| |
| private Paint mCirclePaint = new Paint(); |
| private Paint mTextPaint = new Paint(); |
| |
| private static final int BACKGROUND_ACTIVE = Color.WHITE; |
| |
| // inactive border |
| private static final float INACTIVE_BORDER_DP = 15f; |
| private static final int INACTIVE_BORDER_COLOR = 0xFFffd060; |
| private Paint mBorderPaint = new Paint(); |
| private float mBorderWidth; |
| |
| public final int[] COLORS = { |
| 0xFF33B5E5, 0xFFAA66CC, 0xFF99CC00, 0xFFFFBB33, 0xFFFF4444, |
| 0xFF0099CC, 0xFF9933CC, 0xFF669900, 0xFFFF8800, 0xFFCC0000 |
| }; |
| |
| /** |
| * Sets up the required {@link android.graphics.Paint} objects for the screen density of this |
| * device. |
| */ |
| private void initialisePaint() { |
| |
| // Calculate radiuses in px from dp based on screen density |
| float density = getResources().getDisplayMetrics().density; |
| mCircleRadius = CIRCLE_RADIUS_DP * density; |
| mCircleHistoricalRadius = CIRCLE_HISTORICAL_RADIUS_DP * density; |
| |
| // Setup text paint for circle label |
| mTextPaint.setTextSize(27f); |
| mTextPaint.setColor(Color.BLACK); |
| |
| // Setup paint for inactive border |
| mBorderWidth = INACTIVE_BORDER_DP * density; |
| mBorderPaint.setStrokeWidth(mBorderWidth); |
| mBorderPaint.setColor(INACTIVE_BORDER_COLOR); |
| mBorderPaint.setStyle(Paint.Style.STROKE); |
| |
| } |
| |
| /** |
| * Draws the data encapsulated by a {@link TouchDisplayView.TouchHistory} object to a canvas. |
| * A large circle indicates the current position held by the |
| * {@link TouchDisplayView.TouchHistory} object, while a smaller circle is drawn for each |
| * entry in its history. The size of the large circle is scaled depending on |
| * its pressure, clamped to a maximum of <code>1.0</code>. |
| * |
| * @param canvas |
| * @param id |
| * @param data |
| */ |
| protected void drawCircle(Canvas canvas, int id, TouchHistory data) { |
| // select the color based on the id |
| int color = COLORS[id % COLORS.length]; |
| mCirclePaint.setColor(color); |
| |
| /* |
| * Draw the circle, size scaled to its pressure. Pressure is clamped to |
| * 1.0 max to ensure proper drawing. (Reported pressure values can |
| * exceed 1.0, depending on the calibration of the touch screen). |
| */ |
| float pressure = Math.min(data.pressure, 1f); |
| float radius = pressure * mCircleRadius; |
| |
| canvas.drawCircle(data.x, (data.y) - (radius / 2f), radius, |
| mCirclePaint); |
| |
| // draw all historical points with a lower alpha value |
| mCirclePaint.setAlpha(125); |
| for (int j = 0; j < data.history.length && j < data.historyCount; j++) { |
| PointF p = data.history[j]; |
| canvas.drawCircle(p.x, p.y, mCircleHistoricalRadius, mCirclePaint); |
| } |
| |
| // draw its label next to the main circle |
| canvas.drawText(data.label, data.x + radius, data.y |
| - radius, mTextPaint); |
| } |
| |
| } |