| /* | |
| * 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); | |
| } | |
| } |