am 8acdb201: Added TransformGestureDetector (still in progress) Modified VelocityTracker to track multiple pointers Added TransformTest

Merge commit '8acdb201bdad2cd03c07ebad9cda29f7971ed164' into eclair-plus-aosp

* commit '8acdb201bdad2cd03c07ebad9cda29f7971ed164':
  Added TransformGestureDetector (still in progress)
This commit is contained in:
Adam Powell
2010-01-12 15:15:43 -08:00
committed by Android Git Automerger
7 changed files with 631 additions and 52 deletions

View File

@@ -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:
* <ul>
* <li>Create an instance of the {@code TransformGestureDetector} for your
* {@link View}
* <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
* {@link #onTouchEvent(MotionEvent)}. The methods defined in your
* callback will be executed when the events occur.
* </ul>
* @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();
}
}

View File

@@ -55,12 +55,12 @@ public final class VelocityTracker implements Poolable<VelocityTracker> {
}
}, 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<VelocityTracker> {
* 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<VelocityTracker> {
public void addMovement(MotionEvent ev) {
long time = ev.getEventTime();
final int N = ev.getHistorySize();
for (int i=0; i<N; i++) {
addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i),
ev.getHistoricalEventTime(i));
final int pointerCount = ev.getPointerCount();
for (int p = 0; p < pointerCount; p++) {
for (int i=0; i<N; i++) {
addPoint(p, ev.getHistoricalX(p, i), ev.getHistoricalY(p, i),
ev.getHistoricalEventTime(i));
}
addPoint(p, ev.getX(p), ev.getY(p), time);
}
addPoint(ev.getX(), ev.getY(), time);
}
private void addPoint(float x, float y, long time) {
private void addPoint(int pos, float x, float y, long time) {
int drop = -1;
int i;
if (localLOGV) Log.v(TAG, "Adding past y=" + y + " time=" + time);
final long[] pastTime = mPastTime;
final long[] pastTime = mPastTime[pos];
for (i=0; i<NUM_PAST; i++) {
if (pastTime[i] == 0) {
break;
@@ -146,8 +151,8 @@ public final class VelocityTracker implements Poolable<VelocityTracker> {
drop = 0;
}
if (drop == i) drop--;
final float[] pastX = mPastX;
final float[] pastY = mPastY;
final float[] pastX = mPastX[pos];
final float[] pastY = mPastY[pos];
if (drop >= 0) {
if (localLOGV) Log.v(TAG, "Dropping up to #" + drop);
final int start = drop+1;
@@ -190,44 +195,48 @@ public final class VelocityTracker implements Poolable<VelocityTracker> {
* 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<VelocityTracker> {
* @return The previously computed X velocity.
*/
public float getXVelocity() {
return mXVelocity;
return mXVelocity[0];
}
/**
@@ -247,6 +256,32 @@ public final class VelocityTracker implements Poolable<VelocityTracker> {
* @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];
}
}

View File

@@ -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)

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.test.transform">
<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="7" />
<application android:label="TransformTest">
<activity android:name="TransformTestActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2007 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.
-->
<resources>
<string name="act_title">TransformTest</string>
</resources>

View File

@@ -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);
}
}
}