From 8acdb201bdad2cd03c07ebad9cda29f7971ed164 Mon Sep 17 00:00:00 2001 From: Adam Powell Date: Wed, 6 Jan 2010 17:33:52 -0800 Subject: [PATCH] Added TransformGestureDetector (still in progress) Modified VelocityTracker to track multiple pointers Added TransformTest --- .../view/TransformGestureDetector.java | 316 ++++++++++++++++++ core/java/android/view/VelocityTracker.java | 139 +++++--- tests/TransformTest/Android.mk | 10 + tests/TransformTest/AndroidManifest.xml | 28 ++ tests/TransformTest/res/drawable/logo.png | Bin 0 -> 13388 bytes tests/TransformTest/res/values/strings.xml | 19 ++ .../test/transform/TransformTestActivity.java | 171 ++++++++++ 7 files changed, 631 insertions(+), 52 deletions(-) create mode 100644 core/java/android/view/TransformGestureDetector.java create mode 100644 tests/TransformTest/Android.mk create mode 100644 tests/TransformTest/AndroidManifest.xml create mode 100644 tests/TransformTest/res/drawable/logo.png create mode 100644 tests/TransformTest/res/values/strings.xml create mode 100644 tests/TransformTest/src/com/google/android/test/transform/TransformTestActivity.java diff --git a/core/java/android/view/TransformGestureDetector.java b/core/java/android/view/TransformGestureDetector.java new file mode 100644 index 0000000000000..196716adc0fb4 --- /dev/null +++ b/core/java/android/view/TransformGestureDetector.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2010 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 android.view; + +import android.content.Context; +import android.util.Log; +import android.view.GestureDetector.SimpleOnGestureListener; + +/** + * Detects transformation gestures involving more than one pointer ("multitouch") + * using the supplied {@link MotionEvent}s. The {@link OnGestureListener} callback + * will notify users when a particular gesture event has occurred. This class + * should only be used with {@link MotionEvent}s reported via touch. + * + * To use this class: + * + * @hide Pending API approval + */ +public class TransformGestureDetector { + /** + * The listener for receiving notifications when gestures occur. + * If you want to listen for all the different gestures then implement + * this interface. If you only want to listen for a subset it might + * be easier to extend {@link SimpleOnGestureListener}. + * + * An application will receive events in the following order: + * One onTransformBegin() + * Zero or more onTransform() + * One onTransformEnd() or onTransformFling() + */ + public interface OnTransformGestureListener { + /** + * Responds to transformation events for a gesture in progress. + * Reported by pointer motion. + * + * @param detector The detector reporting the event - use this to + * retrieve extended info about event state. + * @return true if the event was handled, false otherwise. + */ + public boolean onTransform(TransformGestureDetector detector); + + /** + * Responds to the beginning of a transformation gesture. Reported by + * new pointers going down. + * + * @param detector The detector reporting the event - use this to + * retrieve extended info about event state. + * @return true if the event was handled, false otherwise. + */ + public boolean onTransformBegin(TransformGestureDetector detector); + + /** + * Responds to the end of a transformation gesture. Reported by existing + * pointers going up. If the end of a gesture would result in a fling, + * onTransformFling is called instead. + * + * @param detector The detector reporting the event - use this to + * retrieve extended info about event state. + * @return true if the event was handled, false otherwise. + */ + public boolean onTransformEnd(TransformGestureDetector detector); + + /** + * Responds to the end of a transformation gesture that begins a fling. + * Reported by existing pointers going up. If the end of a gesture + * would not result in a fling, onTransformEnd is called instead. + * + * @param detector The detector reporting the event - use this to + * retrieve extended info about event state. + * @return true if the event was handled, false otherwise. + */ + public boolean onTransformFling(TransformGestureDetector detector); + } + + private static final boolean DEBUG = false; + + private static final int INITIAL_EVENT_IGNORES = 2; + + private Context mContext; + private float mTouchSizeScale; + private OnTransformGestureListener mListener; + private int mVelocityTimeUnits; + private MotionEvent mInitialEvent; + + private MotionEvent mPrevEvent; + private MotionEvent mCurrEvent; + private VelocityTracker mVelocityTracker; + + private float mCenterX; + private float mCenterY; + private float mTransX; + private float mTransY; + private float mPrevFingerDiffX; + private float mPrevFingerDiffY; + private float mCurrFingerDiffX; + private float mCurrFingerDiffY; + private float mRotateDegrees; + private float mCurrLen; + private float mPrevLen; + private float mScaleFactor; + + // Units in pixels. Current value is pulled out of thin air for debugging only. + private float mPointerJumpLimit = 30; + + private int mEventIgnoreCount; + + public TransformGestureDetector(Context context, OnTransformGestureListener listener, + int velocityTimeUnits) { + mContext = context; + mListener = listener; + mTouchSizeScale = context.getResources().getDisplayMetrics().widthPixels/3; + mVelocityTimeUnits = velocityTimeUnits; + mEventIgnoreCount = INITIAL_EVENT_IGNORES; + } + + public TransformGestureDetector(Context context, OnTransformGestureListener listener) { + this(context, listener, 1000); + } + + public boolean onTouchEvent(MotionEvent event) { + final int action = event.getAction(); + boolean handled = true; + + if (mInitialEvent == null) { + // No transform gesture in progress + if ((action == MotionEvent.ACTION_POINTER_1_DOWN || + action == MotionEvent.ACTION_POINTER_2_DOWN) && + event.getPointerCount() >= 2) { + // We have a new multi-finger gesture + mInitialEvent = MotionEvent.obtain(event); + mPrevEvent = MotionEvent.obtain(event); + mVelocityTracker = VelocityTracker.obtain(); + handled = mListener.onTransformBegin(this); + } + } else { + // Transform gesture in progress - attempt to handle it + switch (action) { + case MotionEvent.ACTION_POINTER_1_UP: + case MotionEvent.ACTION_POINTER_2_UP: + // Gesture ended + handled = mListener.onTransformEnd(this); + + reset(); + break; + + case MotionEvent.ACTION_CANCEL: + handled = mListener.onTransformEnd(this); + + reset(); + break; + + case MotionEvent.ACTION_MOVE: + setContext(event); + + // Our first few events can be crazy from some touchscreens - drop them. + if (mEventIgnoreCount == 0) { + mVelocityTracker.addMovement(event); + handled = mListener.onTransform(this); + } else { + mEventIgnoreCount--; + } + + mPrevEvent.recycle(); + mPrevEvent = MotionEvent.obtain(event); + break; + } + } + return handled; + } + + private void setContext(MotionEvent curr) { + mCurrEvent = MotionEvent.obtain(curr); + + mRotateDegrees = -1; + mCurrLen = -1; + mPrevLen = -1; + mScaleFactor = -1; + + final MotionEvent prev = mPrevEvent; + + float px0 = prev.getX(0); + float py0 = prev.getY(0); + float px1 = prev.getX(1); + float py1 = prev.getY(1); + float cx0 = curr.getX(0); + float cy0 = curr.getY(0); + float cx1 = curr.getX(1); + float cy1 = curr.getY(1); + + // Some touchscreens do weird things with pointer values where points are + // too close along one axis. Try to detect this here and smooth things out. + // The main indicator is that we get the X or Y value from the other pointer. + final float dx0 = cx0 - px0; + final float dy0 = cy0 - py0; + final float dx1 = cx1 - px1; + final float dy1 = cy1 - py1; + + if (cx0 == cx1) { + if (Math.abs(dx0) > mPointerJumpLimit) { + cx0 = px0; + } else if (Math.abs(dx1) > mPointerJumpLimit) { + cx1 = px1; + } + } else if (cy0 == cy1) { + if (Math.abs(dy0) > mPointerJumpLimit) { + cy0 = py0; + } else if (Math.abs(dy1) > mPointerJumpLimit) { + cy1 = py1; + } + } + + final float pvx = px1 - px0; + final float pvy = py1 - py0; + final float cvx = cx1 - cx0; + final float cvy = cy1 - cy0; + mPrevFingerDiffX = pvx; + mPrevFingerDiffY = pvy; + mCurrFingerDiffX = cvx; + mCurrFingerDiffY = cvy; + + final float pmidx = px0 + pvx * 0.5f; + final float pmidy = py0 + pvy * 0.5f; + final float cmidx = cx0 + cvx * 0.5f; + final float cmidy = cy0 + cvy * 0.5f; + + mCenterX = cmidx; + mCenterY = cmidy; + mTransX = cmidx - pmidx; + mTransY = cmidy - pmidy; + } + + private void reset() { + if (mInitialEvent != null) { + mInitialEvent.recycle(); + mInitialEvent = null; + } + if (mPrevEvent != null) { + mPrevEvent.recycle(); + mPrevEvent = null; + } + if (mCurrEvent != null) { + mCurrEvent.recycle(); + mCurrEvent = null; + } + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + mEventIgnoreCount = INITIAL_EVENT_IGNORES; + } + + public float getCenterX() { + return mCenterX; + } + + public float getCenterY() { + return mCenterY; + } + + public float getTranslateX() { + return mTransX; + } + + public float getTranslateY() { + return mTransY; + } + + public float getCurrentSpan() { + if (mCurrLen == -1) { + final float cvx = mCurrFingerDiffX; + final float cvy = mCurrFingerDiffY; + mCurrLen = (float)Math.sqrt(cvx*cvx + cvy*cvy); + } + return mCurrLen; + } + + public float getPreviousSpan() { + if (mPrevLen == -1) { + final float pvx = mPrevFingerDiffX; + final float pvy = mPrevFingerDiffY; + mPrevLen = (float)Math.sqrt(pvx*pvx + pvy*pvy); + } + return mPrevLen; + } + + public float getScaleFactor() { + if (mScaleFactor == -1) { + mScaleFactor = getCurrentSpan() / getPreviousSpan(); + } + return mScaleFactor; + } + + public float getRotation() { + throw new UnsupportedOperationException(); + } +} diff --git a/core/java/android/view/VelocityTracker.java b/core/java/android/view/VelocityTracker.java index 5d89c46a3db1a..95810807e55b9 100644 --- a/core/java/android/view/VelocityTracker.java +++ b/core/java/android/view/VelocityTracker.java @@ -55,12 +55,12 @@ public final class VelocityTracker implements Poolable { } }, 2)); - final float mPastX[] = new float[NUM_PAST]; - final float mPastY[] = new float[NUM_PAST]; - final long mPastTime[] = new long[NUM_PAST]; + final float mPastX[][] = new float[MotionEvent.BASE_AVAIL_POINTERS][NUM_PAST]; + final float mPastY[][] = new float[MotionEvent.BASE_AVAIL_POINTERS][NUM_PAST]; + final long mPastTime[][] = new long[MotionEvent.BASE_AVAIL_POINTERS][NUM_PAST]; - float mYVelocity; - float mXVelocity; + float mYVelocity[] = new float[MotionEvent.BASE_AVAIL_POINTERS]; + float mXVelocity[] = new float[MotionEvent.BASE_AVAIL_POINTERS]; private VelocityTracker mNext; @@ -105,7 +105,9 @@ public final class VelocityTracker implements Poolable { * Reset the velocity tracker back to its initial state. */ public void clear() { - mPastTime[0] = 0; + for (int i = 0; i < MotionEvent.BASE_AVAIL_POINTERS; i++) { + mPastTime[i][0] = 0; + } } /** @@ -120,18 +122,21 @@ public final class VelocityTracker implements Poolable { public void addMovement(MotionEvent ev) { long time = ev.getEventTime(); final int N = ev.getHistorySize(); - for (int i=0; i { * must be positive. */ public void computeCurrentVelocity(int units, float maxVelocity) { - final float[] pastX = mPastX; - final float[] pastY = mPastY; - final long[] pastTime = mPastTime; - - // Kind-of stupid. - final float oldestX = pastX[0]; - final float oldestY = pastY[0]; - final long oldestTime = pastTime[0]; - float accumX = 0; - float accumY = 0; - int N=0; - while (N < NUM_PAST) { - if (pastTime[N] == 0) { - break; + for (int pos = 0; pos < MotionEvent.BASE_AVAIL_POINTERS; pos++) { + final float[] pastX = mPastX[pos]; + final float[] pastY = mPastY[pos]; + final long[] pastTime = mPastTime[pos]; + + // Kind-of stupid. + final float oldestX = pastX[0]; + final float oldestY = pastY[0]; + final long oldestTime = pastTime[0]; + float accumX = 0; + float accumY = 0; + int N=0; + while (N < NUM_PAST) { + if (pastTime[N] == 0) { + break; + } + N++; } - N++; + // Skip the last received event, since it is probably pretty noisy. + if (N > 3) N--; + + for (int i=1; i < N; i++) { + final int dur = (int)(pastTime[i] - oldestTime); + if (dur == 0) continue; + float dist = pastX[i] - oldestX; + float vel = (dist/dur) * units; // pixels/frame. + if (accumX == 0) accumX = vel; + else accumX = (accumX + vel) * .5f; + + dist = pastY[i] - oldestY; + vel = (dist/dur) * units; // pixels/frame. + if (accumY == 0) accumY = vel; + else accumY = (accumY + vel) * .5f; + } + mXVelocity[pos] = accumX < 0.0f ? Math.max(accumX, -maxVelocity) + : Math.min(accumX, maxVelocity); + mYVelocity[pos] = accumY < 0.0f ? Math.max(accumY, -maxVelocity) + : Math.min(accumY, maxVelocity); + + if (localLOGV) Log.v(TAG, "Y velocity=" + mYVelocity +" X velocity=" + + mXVelocity + " N=" + N); } - // Skip the last received event, since it is probably pretty noisy. - if (N > 3) N--; - - for (int i=1; i < N; i++) { - final int dur = (int)(pastTime[i] - oldestTime); - if (dur == 0) continue; - float dist = pastX[i] - oldestX; - float vel = (dist/dur) * units; // pixels/frame. - if (accumX == 0) accumX = vel; - else accumX = (accumX + vel) * .5f; - - dist = pastY[i] - oldestY; - vel = (dist/dur) * units; // pixels/frame. - if (accumY == 0) accumY = vel; - else accumY = (accumY + vel) * .5f; - } - mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity) : Math.min(accumX, maxVelocity); - mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity) : Math.min(accumY, maxVelocity); - - if (localLOGV) Log.v(TAG, "Y velocity=" + mYVelocity +" X velocity=" - + mXVelocity + " N=" + N); } /** @@ -237,7 +246,7 @@ public final class VelocityTracker implements Poolable { * @return The previously computed X velocity. */ public float getXVelocity() { - return mXVelocity; + return mXVelocity[0]; } /** @@ -247,6 +256,32 @@ public final class VelocityTracker implements Poolable { * @return The previously computed Y velocity. */ public float getYVelocity() { - return mYVelocity; + return mYVelocity[0]; + } + + /** + * Retrieve the last computed X velocity. You must first call + * {@link #computeCurrentVelocity(int)} before calling this function. + * + * @param pos Which pointer's velocity to return. + * @return The previously computed X velocity. + * + * @hide Pending API approval + */ + public float getXVelocity(int pos) { + return mXVelocity[pos]; + } + + /** + * Retrieve the last computed Y velocity. You must first call + * {@link #computeCurrentVelocity(int)} before calling this function. + * + * @param pos Which pointer's velocity to return. + * @return The previously computed Y velocity. + * + * @hide Pending API approval + */ + public float getYVelocity(int pos) { + return mYVelocity[pos]; } } diff --git a/tests/TransformTest/Android.mk b/tests/TransformTest/Android.mk new file mode 100644 index 0000000000000..2d3637da569a3 --- /dev/null +++ b/tests/TransformTest/Android.mk @@ -0,0 +1,10 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_PACKAGE_NAME := TransformTest + +LOCAL_MODULE_TAGS := tests + +include $(BUILD_PACKAGE) diff --git a/tests/TransformTest/AndroidManifest.xml b/tests/TransformTest/AndroidManifest.xml new file mode 100644 index 0000000000000..5c9995f25ec6f --- /dev/null +++ b/tests/TransformTest/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + diff --git a/tests/TransformTest/res/drawable/logo.png b/tests/TransformTest/res/drawable/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4d717a86143dd680bbdd9f6f54c6623d5a33b03b GIT binary patch literal 13388 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 + + + + TransformTest + diff --git a/tests/TransformTest/src/com/google/android/test/transform/TransformTestActivity.java b/tests/TransformTest/src/com/google/android/test/transform/TransformTestActivity.java new file mode 100644 index 0000000000000..52286d188bdb1 --- /dev/null +++ b/tests/TransformTest/src/com/google/android/test/transform/TransformTestActivity.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2008 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.google.android.test.transform; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.TransformGestureDetector; +import android.view.View; +import android.widget.LinearLayout; + +public class TransformTestActivity extends Activity { + public TransformTestActivity() { + super(); + init(false); + } + + public TransformTestActivity(boolean noCompat) { + super(); + init(noCompat); + } + + public void init(boolean noCompat) { + + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final LayoutInflater li = (LayoutInflater)getSystemService( + LAYOUT_INFLATER_SERVICE); + + this.setTitle(R.string.act_title); + LinearLayout root = new LinearLayout(this); + root.setOrientation(LinearLayout.VERTICAL); + + TransformView view = new TransformView(getApplicationContext()); + Drawable drawable = getResources().getDrawable(R.drawable.logo); + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicWidth()); + view.setDrawable(drawable); + + root.addView(view); + setContentView(root); + } + + private class TransformView extends View { + private Drawable mDrawable; + private float mPosX; + private float mPosY; + private float mScale = 1.f; + private Matrix mMatrix; + private TransformGestureDetector mDetector; + + private class Listener implements TransformGestureDetector.OnTransformGestureListener { + + public boolean onTransform(TransformGestureDetector detector) { + Log.d("ttest", "Translation: (" + detector.getTranslateX() + + ", " + detector.getTranslateY() + ")"); + float scale = detector.getScaleFactor(); + Log.d("ttest", "Scale: " + scale); + if (mScale * scale > 0.1f) { + if (mScale * scale < 10.f) { + mScale *= scale; + } else { + mScale = 10.f; + } + } else { + mScale = 0.1f; + } + + mPosX += detector.getTranslateX(); + mPosY += detector.getTranslateY(); + + Log.d("ttest", "mScale: " + mScale + " mPos: (" + mPosX + ", " + mPosY + ")"); + + float sizeX = mDrawable.getIntrinsicWidth()/2; + float sizeY = mDrawable.getIntrinsicHeight()/2; + float centerX = detector.getCenterX(); + float centerY = detector.getCenterY(); + float diffX = centerX - mPosX; + float diffY = centerY - mPosY; + diffX = diffX*scale - diffX; + diffY = diffY*scale - diffY; + mPosX -= diffX; + mPosY -= diffY; + mMatrix.reset(); + mMatrix.postTranslate(-sizeX, -sizeY); + mMatrix.postScale(mScale, mScale); + mMatrix.postTranslate(mPosX, mPosY); + + invalidate(); + + return true; + } + + public boolean onTransformBegin(TransformGestureDetector detector) { + return true; + } + + public boolean onTransformEnd(TransformGestureDetector detector) { + return true; + } + + public boolean onTransformFling(TransformGestureDetector detector) { + return false; + } + + } + + public TransformView(Context context) { + super(context); + mMatrix = new Matrix(); + mDetector = new TransformGestureDetector(context, new Listener()); + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + mPosX = metrics.widthPixels/2; + mPosY = metrics.heightPixels/2; + } + + public void setDrawable(Drawable d) { + mDrawable = d; + + float sizeX = mDrawable.getIntrinsicWidth()/2; + float sizeY = mDrawable.getIntrinsicHeight()/2; + mMatrix.reset(); + mMatrix.postTranslate(-sizeX, -sizeY); + mMatrix.postScale(mScale, mScale); + mMatrix.postTranslate(mPosX, mPosY); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean handled = mDetector.onTouchEvent(event); + + int pointerCount = event.getPointerCount(); + Log.d("ttest", "pointerCount: " + pointerCount); + + return handled; + } + + @Override + public void onDraw(Canvas canvas) { + int saveCount = canvas.getSaveCount(); + canvas.save(); + canvas.concat(mMatrix); + mDrawable.draw(canvas); + canvas.restoreToCount(saveCount); + } + } +}