Added TransformGestureDetector (still in progress)
Modified VelocityTracker to track multiple pointers
Added TransformTest
diff --git a/core/java/android/view/TransformGestureDetector.java b/core/java/android/view/TransformGestureDetector.java
new file mode 100644
index 0000000..196716a
--- /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:
+ * <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();
+    }
+}
diff --git a/core/java/android/view/VelocityTracker.java b/core/java/android/view/VelocityTracker.java
index 5d89c46..9581080 100644
--- a/core/java/android/view/VelocityTracker.java
+++ b/core/java/android/view/VelocityTracker.java
@@ -55,12 +55,12 @@
                 }
             }, 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 @@
      * 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 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 @@
             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 @@
      * 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 @@
      * @return The previously computed X velocity.
      */
     public float getXVelocity() {
-        return mXVelocity;
+        return mXVelocity[0];
     }
     
     /**
@@ -247,6 +256,32 @@
      * @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 0000000..2d3637d
--- /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 0000000..5c9995f
--- /dev/null
+++ b/tests/TransformTest/AndroidManifest.xml
@@ -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>
diff --git a/tests/TransformTest/res/drawable/logo.png b/tests/TransformTest/res/drawable/logo.png
new file mode 100644
index 0000000..4d717a8
--- /dev/null
+++ b/tests/TransformTest/res/drawable/logo.png
Binary files differ
diff --git a/tests/TransformTest/res/values/strings.xml b/tests/TransformTest/res/values/strings.xml
new file mode 100644
index 0000000..a0eb81f
--- /dev/null
+++ b/tests/TransformTest/res/values/strings.xml
@@ -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>
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 0000000..52286d1
--- /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);
+        }
+    }
+}