From 7f77e0b30dd145593355908d46fc84180e359d0d Mon Sep 17 00:00:00 2001 From: minggo Date: Thu, 17 Mar 2011 09:49:31 +0800 Subject: [PATCH] [android] fixed #399: fix the bug that HelloWorld crashed when touching the screen. --- .../cocos2dx/lib/Cocos2dxAccelerometer.java | 52 ++ .../org/cocos2dx/lib/Cocos2dxActivity.java | 106 ++- .../cocos2dx/lib/Cocos2dxGLSurfaceView.java | 147 +++- .../src/org/cocos2dx/lib/Cocos2dxMusic.java | 148 ++++ .../org/cocos2dx/lib/Cocos2dxRenderer.java | 62 +- .../src/org/cocos2dx/lib/Cocos2dxSound.java | 148 ++++ .../controller/MultiTouchController.java | 812 ++++++++++++++++++ 7 files changed, 1400 insertions(+), 75 deletions(-) create mode 100644 HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxAccelerometer.java create mode 100644 HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxMusic.java create mode 100644 HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxSound.java create mode 100644 HelloWorld/android/src/org/cocos2dx/lib/touch/metalev/multitouch/controller/MultiTouchController.java diff --git a/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxAccelerometer.java b/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxAccelerometer.java new file mode 100644 index 000000000000..2978066f0e89 --- /dev/null +++ b/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxAccelerometer.java @@ -0,0 +1,52 @@ +package org.cocos2dx.lib; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +/** + * + * This class is used for controlling the Accelerometer + * + */ +public class Cocos2dxAccelerometer implements SensorEventListener { + + private static final String TAG = "Cocos2dxAccelerometer"; + private Context mContext; + private SensorManager mSensorManager; + private Sensor mAccelerometer; + + public Cocos2dxAccelerometer(Context context){ + mContext = context; + + //Get an instance of the SensorManager + mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); + mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + + public void enable() { + mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME); + } + + public void disable () { + mSensorManager.unregisterListener(this); + } + + @Override + public void onSensorChanged(SensorEvent event) { + + if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) + return; + + onSensorChanged(event.values[0], event.values[1], event.values[2], event.timestamp); + + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + private static native void onSensorChanged(float x, float y, float z, long timeStamp); +} diff --git a/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxActivity.java b/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxActivity.java index 1f9c169d1ca4..4678ba44f5ea 100644 --- a/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxActivity.java +++ b/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxActivity.java @@ -51,6 +51,14 @@ import android.util.Log; public class Cocos2dxActivity extends Activity{ + public static int screenWidth; + public static int screenHeight; + private static Cocos2dxMusic backgroundMusicPlayer; + private static Cocos2dxSound soundPlayer; + private static Cocos2dxAccelerometer accelerometer; + private static boolean accelerometerEnabled = false; + + private static native void nativeSetPaths(String apkPath); @Override protected void onCreate(Bundle savedInstanceState) { @@ -58,11 +66,103 @@ protected void onCreate(Bundle savedInstanceState) { // get frame size DisplayMetrics dm = new DisplayMetrics(); - getWindowManager().getDefaultDisplay().getMetrics(dm); + getWindowManager().getDefaultDisplay().getMetrics(dm); screenWidth = dm.widthPixels; screenHeight = dm.heightPixels; + accelerometer = new Cocos2dxAccelerometer(this); + + // init media player and sound player + backgroundMusicPlayer = new Cocos2dxMusic(this); + soundPlayer = new Cocos2dxSound(this); + } + + public static void enableAccelerometer() { + accelerometerEnabled = true; + accelerometer.enable(); + } + + public static void disableAccelerometer() { + accelerometerEnabled = false; + accelerometer.disable(); + } + + public static void playBackgroundMusic(String path, boolean isLoop){ + backgroundMusicPlayer.playBackgroundMusic(path, isLoop); + } + + public static void stopBackgroundMusic(){ + backgroundMusicPlayer.stopBackgroundMusic(); + } + + public static void pauseBackgroundMusic(){ + backgroundMusicPlayer.pauseBackgroundMusic(); + } + + public static void resumeBackgroundMusic(){ + backgroundMusicPlayer.resumeBackgroundMusic(); + } + + public static void rewindBackgroundMusic(){ + backgroundMusicPlayer.rewindBackgroundMusic(); + } + + public static boolean isBackgroundMusicPlaying(){ + return backgroundMusicPlayer.isBackgroundMusicPlaying(); + } + + public static float getBackgroundMusicVolume(){ + return backgroundMusicPlayer.getBackgroundVolume(); + } + + public static void setBackgroundMusicVolume(float volume){ + backgroundMusicPlayer.setBackgroundVolume(volume); } + public static int playEffect(String path){ + return soundPlayer.playEffect(path); + } + + public static void stopEffect(int soundId){ + soundPlayer.stopEffect(soundId); + } + + public static float getEffectsVolume(){ + return soundPlayer.getEffectsVolume(); + } + + public static void setEffectsVolume(float volume){ + soundPlayer.setEffectsVolume(volume); + } + + public static void preloadEffect(String path){ + soundPlayer.preloadEffect(path); + } + + public static void unloadEffect(String path){ + soundPlayer.unloadEffect(path); + } + + public static void end(){ + backgroundMusicPlayer.end(); + soundPlayer.end(); + } + + @Override + protected void onResume() { + super.onResume(); + if (accelerometerEnabled) { + accelerometer.enable(); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (accelerometerEnabled) { + accelerometer.disable(); + } + } + protected void setPackgeName(String packageName) { String apkFilePath = ""; ApplicationInfo appInfo = null; @@ -79,8 +179,4 @@ protected void setPackgeName(String packageName) { // add this link at the renderer class nativeSetPaths(apkFilePath); } - - public static int screenWidth; - public static int screenHeight; - private static native void nativeSetPaths(String apkPath); } diff --git a/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxGLSurfaceView.java b/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxGLSurfaceView.java index 8c31bfae5bda..973188908e7f 100644 --- a/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxGLSurfaceView.java +++ b/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxGLSurfaceView.java @@ -1,62 +1,119 @@ package org.cocos2dx.lib; +import org.cocos2dx.lib.touch.metalev.multitouch.controller.MultiTouchController; +import org.cocos2dx.lib.touch.metalev.multitouch.controller.MultiTouchController.MultiTouchObjectCanvas; +import org.cocos2dx.lib.touch.metalev.multitouch.controller.MultiTouchController.PointInfo; +import org.cocos2dx.lib.touch.metalev.multitouch.controller.MultiTouchController.PositionAndScale; + import android.content.Context; import android.opengl.GLSurfaceView; +import android.util.Log; import android.view.MotionEvent; -public class Cocos2dxGLSurfaceView extends GLSurfaceView { +public class Cocos2dxGLSurfaceView extends GLSurfaceView implements + MultiTouchObjectCanvas { + private static final String TAG = Cocos2dxGLSurfaceView.class + .getCanonicalName(); + private MultiTouchController mTouchController; + private Cocos2dxRenderer mRenderer; + public Cocos2dxGLSurfaceView(Context context) { super(context); mRenderer = new Cocos2dxRenderer(); setRenderer(mRenderer); + + mTouchController = new MultiTouchController(this); } public boolean onTouchEvent(final MotionEvent event) { - boolean result = false; - final float x = event.getX(); - final float y = event.getY(); - - switch (event.getAction()){ - case MotionEvent.ACTION_DOWN: - queueEvent(new Runnable() { - // This method will be called on the rendering thread - public void run() { - mRenderer.handleActionDown(x, y); - }}); - - result = true; - break; - case MotionEvent.ACTION_UP: - queueEvent(new Runnable() { - // This method will be called on the rendering thread - public void run() { - mRenderer.handleActionUp(x, y); - }}); - - result = true; - break; - case MotionEvent.ACTION_CANCEL: - queueEvent(new Runnable() { - // This method will be called on the rendering thread - public void run() { - mRenderer.handleActionCancel(x, y); - }}); - - result = true; - break; - case MotionEvent.ACTION_MOVE: - queueEvent(new Runnable() { - // This method will be called on the rendering thread - public void run() { - mRenderer.handleActionMove(x, y); - }}); - - result = true; - break; + if (mTouchController.onTouchEvent(event)) { + final PointInfo pt = mTouchController.mCurrPt; + final int ids[] = new int[pt.getNumTouchPoints()]; + final float xs[] = new float[pt.getNumTouchPoints()]; + final float ys[] = new float[pt.getNumTouchPoints()]; + + for (int i = 0; i < pt.getNumTouchPoints(); i++) { + ids[i] = pt.getPointerIds()[i]; + xs[i] = pt.getXs()[i]; + ys[i] = pt.getYs()[i]; + Log.d(TAG, + "ACTION: " + pt.getAction() + "id[i]=" + + pt.getPointerIds()[i] + "x[i]= " + + pt.getXs()[i] + " y[i]= " + pt.getYs()[i]); + } + + switch (pt.getAction()) { + case MotionEvent.ACTION_DOWN: + case 261: + case MotionEvent.ACTION_POINTER_DOWN: + Log.d(TAG, "ACTION_DOWN"); + queueEvent(new Runnable() { + @Override + public void run() { + mRenderer.handleActionDown(ids, xs, ys); + } + }); + break; + + case MotionEvent.ACTION_MOVE: + Log.d(TAG, "ACTION_MOVE"); + queueEvent(new Runnable() { + @Override + public void run() { + mRenderer.handleActionMove(ids, xs, ys); + } + }); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + Log.d(TAG, "ACTION_UP"); + queueEvent(new Runnable() { + @Override + public void run() { + mRenderer.handleActionUp(ids, xs, ys); + } + }); + break; + + case MotionEvent.ACTION_CANCEL: + queueEvent(new Runnable() { + @Override + public void run() { + mRenderer.handleActionCancel(ids, xs, ys); + } + }); + break; + } + + return true; } - - return result; + return false; + } + + @Override + public Object getDraggableObjectAtPoint(PointInfo touchPoint) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void getPositionAndScale(Object obj, + PositionAndScale objPosAndScaleOut) { + // TODO Auto-generated method stub + } - Cocos2dxRenderer mRenderer; + @Override + public boolean setPositionAndScale(Object obj, + PositionAndScale newObjPosAndScale, PointInfo touchPoint) { + // TODO Auto-generated method stub + return false; + } + + @Override + public void selectObject(Object obj, PointInfo touchPoint) { + // TODO Auto-generated method stub + + } } diff --git a/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxMusic.java b/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxMusic.java new file mode 100644 index 000000000000..75186d6813be --- /dev/null +++ b/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxMusic.java @@ -0,0 +1,148 @@ +package org.cocos2dx.lib; + +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.media.MediaPlayer; +import android.util.Log; + +/** + * + * This class is used for controlling background music + * + */ +public class Cocos2dxMusic { + + private static final String TAG = "Cocos2dxMusic"; + private float mLeftVolume; + private float mRightVolume; + private Context mContext; + private MediaPlayer mBackgroundMediaPlayer; + private boolean mIsPaused; + + public Cocos2dxMusic(Context context){ + this.mContext = context; + initData(); + } + + public void playBackgroundMusic(String path, boolean isLoop){ + if (mBackgroundMediaPlayer == null){ + mBackgroundMediaPlayer = createMediaplayerFromAssets(path); + } + + if (mBackgroundMediaPlayer == null){ + Log.e(TAG, "playBackgroundMusic: background media player is null"); + } else{ + // if the music is playing or paused, stop it + mBackgroundMediaPlayer.stop(); + + mBackgroundMediaPlayer.setLooping(isLoop); + + try { + mBackgroundMediaPlayer.prepare(); + mBackgroundMediaPlayer.start(); + } catch (Exception e){ + Log.e(TAG, "playBackgroundMusic: error state"); + } + } + } + + public void stopBackgroundMusic(){ + if (mBackgroundMediaPlayer != null){ + mBackgroundMediaPlayer.stop(); + } + } + + public void pauseBackgroundMusic(){ + if (mBackgroundMediaPlayer != null && mBackgroundMediaPlayer.isPlaying()){ + mBackgroundMediaPlayer.pause(); + this.mIsPaused = true; + } + } + + public void resumeBackgroundMusic(){ + if (mBackgroundMediaPlayer != null && this.mIsPaused){ + mBackgroundMediaPlayer.start(); + } + } + + public void rewindBackgroundMusic(){ + if (mBackgroundMediaPlayer != null){ + mBackgroundMediaPlayer.stop(); + + try { + mBackgroundMediaPlayer.prepare(); + mBackgroundMediaPlayer.start(); + } catch (Exception e){ + Log.e(TAG, "rewindBackgroundMusic: error state"); + } + } + } + + public boolean isBackgroundMusicPlaying(){ + boolean ret = false; + + if (mBackgroundMediaPlayer == null){ + ret = false; + } else { + ret = mBackgroundMediaPlayer.isPlaying(); + } + + return ret; + } + + public void end(){ + if (mBackgroundMediaPlayer != null) + { + mBackgroundMediaPlayer.release(); + } + + initData(); + } + + + public float getBackgroundVolume(){ + if (this.mBackgroundMediaPlayer != null){ + return (this.mLeftVolume + this.mRightVolume) / 2; + } else { + return 0.0f; + } + } + + public void setBackgroundVolume(float volume){ + if (this.mBackgroundMediaPlayer != null){ + this.mLeftVolume = this.mRightVolume = volume; + this.mBackgroundMediaPlayer.setVolume(this.mLeftVolume, this.mRightVolume); + } + } + + private void initData(){ + mLeftVolume =1.0f; + mRightVolume = 1.0f; + mBackgroundMediaPlayer = null; + mIsPaused = false; + } + + /** + * create mediaplayer for music + * @param path the path relative to assets + * @return + */ + private MediaPlayer createMediaplayerFromAssets(String path){ + MediaPlayer mediaPlayer = null; + + try{ + AssetFileDescriptor assetFileDescritor = mContext.getAssets().openFd(path); + + mediaPlayer = new MediaPlayer(); + mediaPlayer.setDataSource(assetFileDescritor.getFileDescriptor(), + assetFileDescritor.getStartOffset(), assetFileDescritor.getLength()); + mediaPlayer.prepare(); + + mediaPlayer.setVolume(mLeftVolume, mRightVolume); + }catch (Exception e) { + Log.e(TAG, "error: " + e.getMessage(), e); + } + + return mediaPlayer; + } +} diff --git a/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxRenderer.java b/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxRenderer.java index a69f6617d1f0..b75cb25aa913 100644 --- a/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxRenderer.java +++ b/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxRenderer.java @@ -6,55 +6,67 @@ import android.opengl.GLSurfaceView; public class Cocos2dxRenderer implements GLSurfaceView.Renderer { - private static long animationInterval = (long)(1.0 / 60 * 1000000000L); - private long now; + private final static long NANOSECONDSPERSECOND = 1000000000L; + private final static long NANOSECONDSPERMINISECOND = 1000000; + private static long animationInterval = (long)(1.0 / 60 * NANOSECONDSPERSECOND); private long last; - public void onSurfaceCreated(GL10 gl, EGLConfig config) { - now = last = System.nanoTime(); - nativeInit(Cocos2dxActivity.screenWidth, Cocos2dxActivity.screenHeight); + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + nativeInit(Cocos2dxActivity.screenWidth, Cocos2dxActivity.screenHeight); + last = System.nanoTime(); } public void onSurfaceChanged(GL10 gl, int w, int h) { } - + public void onDrawFrame(GL10 gl) { - now = System.nanoTime(); - if (now - last >= animationInterval){ - last = now; - nativeRender(); - } + long now = System.nanoTime(); + long interval = now - last; + + // should render a frame when onDrawFrame() is called + // or there is a "ghost" + nativeRender(); + + // fps controlling + if (interval < animationInterval){ + try { + // because we render it before, so we should sleep twice time interval + Thread.sleep((animationInterval - interval) * 2 / NANOSECONDSPERMINISECOND); + } catch (Exception e){} + } + + last = now; } - public void handleActionDown(float x, float y) + public void handleActionDown(int[] id, float[] x, float[] y) { - nativeTouchesBegin(x, y); + nativeTouchesBegin(id, x, y); } - public void handleActionUp(float x, float y) + public void handleActionUp(int[] id, float[] x, float[] y) { - nativeTouchesEnd(x, y); + nativeTouchesEnd(id, x, y); } - public void handleActionCancel(float x, float y) + public void handleActionCancel(int[] id, float[] x, float[] y) { - nativeTouchesCancel(x, y); + nativeTouchesCancel(id, x, y); } - public void handleActionMove(float x, float y) + public void handleActionMove(int[] id, float[] x, float[] y) { - nativeTouchesMove(x, y); + nativeTouchesMove(id, x, y); } public static void setAnimationInterval(double interval){ - animationInterval = (long)(interval * 1000000000L); + animationInterval = (long)(interval * NANOSECONDSPERSECOND); } - private static native void nativeTouchesBegin(float x, float y); - private static native void nativeTouchesEnd(float x, float y); - private static native void nativeTouchesMove(float x, float y); - private static native void nativeTouchesCancel(float x, float y); + private static native void nativeTouchesBegin(int[] id, float[] x, float[] y); + private static native void nativeTouchesEnd(int[] id, float[] x, float[] y); + private static native void nativeTouchesMove(int[] id, float[] x, float[] y); + private static native void nativeTouchesCancel(int[] id, float[] x, float[] y); private static native void nativeRender(); private static native void nativeInit(int w, int h); -} \ No newline at end of file +} diff --git a/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxSound.java b/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxSound.java new file mode 100644 index 000000000000..f6690f93a5f2 --- /dev/null +++ b/HelloWorld/android/src/org/cocos2dx/lib/Cocos2dxSound.java @@ -0,0 +1,148 @@ +package org.cocos2dx.lib; + +import java.util.HashMap; + +import android.content.Context; +import android.media.AudioManager; +import android.media.SoundPool; +import android.util.Log; + +/** + * + * This class is used for controlling effect + * + */ + +public class Cocos2dxSound { + private Context mContext; + private SoundPool mSoundPool; + private float mLeftVolume; + private float mRightVolume; + + // sound id and stream id map + private HashMap mSoundIdStreamIdMap; + // sound path and sound id map + private HashMap mPathSoundIDMap; + + private static final String TAG = "Cocos2dxSound"; + private static final int MAX_SIMULTANEOUS_STREAMS_DEFAULT = 5; + private static final float SOUND_RATE = 1.0f; + private static final int SOUND_PRIORITY = 1; + private static final int SOUND_LOOP_TIME = 0; + private static final int SOUND_QUALITY = 5; + + private final int INVALID_SOUND_ID = -1; + private final int INVALID_STREAM_ID = -1; + + public Cocos2dxSound(Context context){ + this.mContext = context; + initData(); + } + + public int preloadEffect(String path){ + int soundId = INVALID_SOUND_ID; + + // if the sound is preloaded, pass it + if (this.mPathSoundIDMap.get(path) != null){ + soundId = this.mPathSoundIDMap.get(path).intValue(); + } else { + soundId = createSoundIdFromAsset(path); + + if (soundId != INVALID_SOUND_ID){ + // the sound is loaded but has not been played + this.mSoundIdStreamIdMap.put(soundId, INVALID_STREAM_ID); + + // record path and sound id map + this.mPathSoundIDMap.put(path, soundId); + } + } + + + + return soundId; + } + + public void unloadEffect(String path){ + // get sound id and remove from mPathSoundIDMap + Integer soundId = this.mPathSoundIDMap.remove(path); + + if (soundId != null){ + // unload effect + this.mSoundPool.unload(soundId.intValue()); + + // remove record from mSoundIdStreamIdMap + this.mSoundIdStreamIdMap.remove(soundId); + } + } + + public int playEffect(String path){ + Integer soundId = this.mPathSoundIDMap.get(path); + + if (soundId != null){ + // the sound is preloaded + + // play sound + int streamId = this.mSoundPool.play(soundId.intValue(), this.mLeftVolume, + this.mRightVolume, SOUND_PRIORITY, SOUND_LOOP_TIME, SOUND_RATE); + + // record sound id and stream id map + this.mSoundIdStreamIdMap.put(soundId, streamId); + } else { + // the effect is not prepared + soundId = preloadEffect(path); + if (soundId == INVALID_SOUND_ID){ + // can not preload effect + return INVALID_SOUND_ID; + } + + playEffect(path); + } + + return soundId.intValue(); + } + + public void stopEffect(int soundId){ + Integer streamId = this.mSoundIdStreamIdMap.get(soundId); + + if (streamId != null && streamId.intValue() != INVALID_STREAM_ID){ + this.mSoundPool.stop(streamId.intValue()); + } + } + + public float getEffectsVolume(){ + return (this.mLeftVolume + this.mRightVolume) / 2; + } + + public void setEffectsVolume(float volume){ + this.mLeftVolume = this.mRightVolume = volume; + } + + public void end(){ + this.mSoundPool.release(); + this.mPathSoundIDMap.clear(); + this.mSoundIdStreamIdMap.clear(); + + initData(); + } + + public int createSoundIdFromAsset(String path){ + int soundId = INVALID_SOUND_ID; + + try { + soundId = mSoundPool.load(mContext.getAssets().openFd(path), 0); + } catch(Exception e){ + Log.e(TAG, "error: " + e.getMessage(), e); + } + + return soundId; + } + + private void initData(){ + this.mSoundIdStreamIdMap = new HashMap(); + mSoundPool = new SoundPool(MAX_SIMULTANEOUS_STREAMS_DEFAULT, AudioManager.STREAM_MUSIC, SOUND_QUALITY); + mPathSoundIDMap = new HashMap(); + + this.mLeftVolume = 0.5f; + this.mRightVolume = 0.5f; + } +} diff --git a/HelloWorld/android/src/org/cocos2dx/lib/touch/metalev/multitouch/controller/MultiTouchController.java b/HelloWorld/android/src/org/cocos2dx/lib/touch/metalev/multitouch/controller/MultiTouchController.java new file mode 100644 index 000000000000..983e09f72ae2 --- /dev/null +++ b/HelloWorld/android/src/org/cocos2dx/lib/touch/metalev/multitouch/controller/MultiTouchController.java @@ -0,0 +1,812 @@ +package org.cocos2dx.lib.touch.metalev.multitouch.controller; + +/** + * MultiTouchController.java + * + * Author: Luke Hutchison (luke.hutch@mit.edu) + * Please drop me an email if you use this code so I can list your project here! + * + * Usage: + * + * public class MyMTView extends View implements MultiTouchObjectCanvas { + * + * private MultiTouchController multiTouchController = new MultiTouchController(this); + * + * // Pass touch events to the MT controller + * public boolean onTouchEvent(MotionEvent event) { + * return multiTouchController.onTouchEvent(event); + * } + * + * // ... then implement the MultiTouchObjectCanvas interface here, see details in the comments of that interface. + * } + * + * + * Changelog: + * 2010-06-09 v1.5.1 Some API changes to make it possible to selectively update or not update scale / rotation. + * Fixed anisotropic zoom. Cleaned up rotation code. Added more comments. Better var names. (LH) + * 2010-06-09 v1.4 Added ability to track pinch rotation (Mickael Despesse, author of "Face Frenzy") and anisotropic pinch-zoom (LH) + * 2010-06-09 v1.3.3 Bugfixes for Android-2.1; added optional debug info (LH) + * 2010-06-09 v1.3 Ported to Android-2.2 (handle ACTION_POINTER_* actions); fixed several bugs; refactoring; documentation (LH) + * 2010-05-17 v1.2.1 Dual-licensed under Apache and GPL licenses + * 2010-02-18 v1.2 Support for compilation under Android 1.5/1.6 using introspection (mmin, author of handyCalc) + * 2010-01-08 v1.1.1 Bugfixes to Cyanogen's patch that only showed up in more complex uses of controller (LH) + * 2010-01-06 v1.1 Modified for official level 5 MT API (Cyanogen) + * 2009-01-25 v1.0 Original MT controller, released for hacked G1 kernel (LH) + * + * Planned features: + * - Add inertia (flick-pinch-zoom or flick-scroll) + * + * Known usages: + * - Mickael Despesse's "Face Frenzy" face distortion app, to be published to the Market soon + * - Yuan Chin's fork of ADW Launcher to support multitouch + * - David Byrne's fractal viewing app Fractoid + * - mmin's handyCalc calculator + * - My own "MultiTouch Visualizer 2" in the Market + * - Formerly: The browser in cyanogenmod (and before that, JesusFreke), and other firmwares like dwang5. This usage has been + * replaced with official pinch/zoom in Maps, Browser and Gallery[3D] as of API level 5. + * + * License: + * Dual-licensed under the Apache License v2 and the GPL v2. + */ + +import java.lang.reflect.Method; + +import android.util.Log; +import android.view.MotionEvent; + +/** + * A class that simplifies the implementation of multitouch in applications. Subclass this and read the fields here as needed in subclasses. + * + * @author Luke Hutchison + */ +public class MultiTouchController { + + /** + * Time in ms required after a change in event status (e.g. putting down or lifting off the second finger) before events actually do anything -- + * helps eliminate noisy jumps that happen on change of status + */ + private static final long EVENT_SETTLE_TIME_INTERVAL = 20; + + /** + * The biggest possible abs val of the change in x or y between multitouch events (larger dx/dy events are ignored) -- helps eliminate jumps in + * pointer position on finger 2 up/down. + */ + private static final float MAX_MULTITOUCH_POS_JUMP_SIZE = 30.0f; + + /** + * The biggest possible abs val of the change in multitouchWidth or multitouchHeight between multitouch events (larger-jump events are ignored) -- + * helps eliminate jumps in pointer position on finger 2 up/down. + */ + private static final float MAX_MULTITOUCH_DIM_JUMP_SIZE = 40.0f; + + /** The smallest possible distance between multitouch points (used to avoid div-by-zero errors and display glitches) */ + private static final float MIN_MULTITOUCH_SEPARATION = 30.0f; + + /** The max number of touch points that can be present on the screen at once */ + public static final int MAX_TOUCH_POINTS = 5; + + /** Generate tons of log entries for debugging */ + public static final boolean DEBUG = false; + + // ---------------------------------------------------------------------------------------------------------------------- + + MultiTouchObjectCanvas objectCanvas; + + /** The current touch point */ + public PointInfo mCurrPt; + + /** The previous touch point */ + private PointInfo mPrevPt; + + /** Fields extracted from mCurrPt */ + private float mCurrPtX, mCurrPtY, mCurrPtDiam, mCurrPtWidth, mCurrPtHeight, mCurrPtAng; + + /** + * Extract fields from mCurrPt, respecting the update* fields of mCurrPt. This just avoids code duplication. I hate that Java doesn't support + * higher-order functions, tuples or multiple return values from functions. + */ + private void extractCurrPtInfo() { + // Get new drag/pinch params. Only read multitouch fields that are needed, + // to avoid unnecessary computation (diameter and angle are expensive operations). + mCurrPtX = mCurrPt.getX(); + mCurrPtY = mCurrPt.getY(); + mCurrPtDiam = Math.max(MIN_MULTITOUCH_SEPARATION * .71f, !mCurrXform.updateScale ? 0.0f : mCurrPt.getMultiTouchDiameter()); + mCurrPtWidth = Math.max(MIN_MULTITOUCH_SEPARATION, !mCurrXform.updateScaleXY ? 0.0f : mCurrPt.getMultiTouchWidth()); + mCurrPtHeight = Math.max(MIN_MULTITOUCH_SEPARATION, !mCurrXform.updateScaleXY ? 0.0f : mCurrPt.getMultiTouchHeight()); + mCurrPtAng = !mCurrXform.updateAngle ? 0.0f : mCurrPt.getMultiTouchAngle(); + } + + // ---------------------------------------------------------------------------------------------------------------------- + + /** Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses */ + private boolean handleSingleTouchEvents; + + /** The object being dragged/stretched */ + private T selectedObject = null; + + /** Current position and scale of the dragged object */ + private PositionAndScale mCurrXform = new PositionAndScale(); + + /** Drag/pinch start time and time to ignore spurious events until (to smooth over event noise) */ + private long mSettleStartTime, mSettleEndTime; + + /** Conversion from object coords to screen coords */ + private float startPosX, startPosY; + + /** Conversion between scale and width, and object angle and start pinch angle */ + private float startScaleOverPinchDiam, startAngleMinusPinchAngle; + + /** Conversion between X scale and width, and Y scale and height */ + private float startScaleXOverPinchWidth, startScaleYOverPinchHeight; + + // ---------------------------------------------------------------------------------------------------------------------- + + /** No touch points down. */ + private static final int MODE_NOTHING = 0; + + /** One touch point down, dragging an object. */ + private static final int MODE_DRAG = 1; + + /** Two or more touch points down, stretching/rotating an object using the first two touch points. */ + private static final int MODE_PINCH = 2; + + /** Current drag mode */ + private int mMode = MODE_NOTHING; + + // ---------------------------------------------------------------------------------------------------------------------- + + /** Constructor that sets handleSingleTouchEvents to true */ + public MultiTouchController(MultiTouchObjectCanvas objectCanvas) { + this(objectCanvas, true); + } + + /** Full constructor */ + public MultiTouchController(MultiTouchObjectCanvas objectCanvas, boolean handleSingleTouchEvents) { + this.mCurrPt = new PointInfo(); + this.mPrevPt = new PointInfo(); + this.handleSingleTouchEvents = handleSingleTouchEvents; + this.objectCanvas = objectCanvas; + } + + // ------------------------------------------------------------------------------------ + + /** + * Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses. Default: true + */ + protected void setHandleSingleTouchEvents(boolean handleSingleTouchEvents) { + this.handleSingleTouchEvents = handleSingleTouchEvents; + } + + /** + * Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses. Default: true + */ + protected boolean getHandleSingleTouchEvents() { + return handleSingleTouchEvents; + } + + // ------------------------------------------------------------------------------------ + + public static final boolean multiTouchSupported; + private static Method m_getPointerCount; + private static Method m_getPointerId; + private static Method m_getPressure; + private static Method m_getHistoricalX; + private static Method m_getHistoricalY; + private static Method m_getHistoricalPressure; + private static Method m_getX; + private static Method m_getY; + private static int ACTION_POINTER_UP = 6; + private static int ACTION_POINTER_INDEX_SHIFT = 8; + + static { + boolean succeeded = false; + try { + // Android 2.0.1 stuff: + m_getPointerCount = MotionEvent.class.getMethod("getPointerCount"); + m_getPointerId = MotionEvent.class.getMethod("getPointerId", Integer.TYPE); + m_getPressure = MotionEvent.class.getMethod("getPressure", Integer.TYPE); + m_getHistoricalX = MotionEvent.class.getMethod("getHistoricalX", Integer.TYPE, Integer.TYPE); + m_getHistoricalY = MotionEvent.class.getMethod("getHistoricalY", Integer.TYPE, Integer.TYPE); + m_getHistoricalPressure = MotionEvent.class.getMethod("getHistoricalPressure", Integer.TYPE, Integer.TYPE); + m_getX = MotionEvent.class.getMethod("getX", Integer.TYPE); + m_getY = MotionEvent.class.getMethod("getY", Integer.TYPE); + succeeded = true; + } catch (Exception e) { + Log.e("MultiTouchController", "static initializer failed", e); + } + multiTouchSupported = succeeded; + if (multiTouchSupported) { + // Android 2.2+ stuff (the original Android 2.2 consts are declared above, + // and these actions aren't used previous to Android 2.2): + try { + ACTION_POINTER_UP = MotionEvent.class.getField("ACTION_POINTER_UP").getInt(null); + ACTION_POINTER_INDEX_SHIFT = MotionEvent.class.getField("ACTION_POINTER_INDEX_SHIFT").getInt(null); + } catch (Exception e) { + } + } + } + + // ------------------------------------------------------------------------------------ + + private static final float[] xVals = new float[MAX_TOUCH_POINTS]; + private static final float[] yVals = new float[MAX_TOUCH_POINTS]; + private static final float[] pressureVals = new float[MAX_TOUCH_POINTS]; + private static final int[] pointerIds = new int[MAX_TOUCH_POINTS]; + + /** Process incoming touch events */ + public boolean onTouchEvent(MotionEvent event) { + try { + int pointerCount = multiTouchSupported ? (Integer) m_getPointerCount.invoke(event) : 1; + if (DEBUG) + Log.i("MultiTouch", "Got here 1 - " + multiTouchSupported + " " + mMode + " " + handleSingleTouchEvents + " " + pointerCount); + if (mMode == MODE_NOTHING && !handleSingleTouchEvents && pointerCount == 1) + // Not handling initial single touch events, just pass them on + return false; + if (DEBUG) + Log.i("MultiTouch", "Got here 2"); + + // Handle history first (we sometimes get history with ACTION_MOVE events) + int action = event.getAction(); + int histLen = event.getHistorySize() / pointerCount; + for (int histIdx = 0; histIdx <= histLen; histIdx++) { + // Read from history entries until histIdx == histLen, then read from current event + boolean processingHist = histIdx < histLen; + if (!multiTouchSupported || pointerCount == 1) { + // Use single-pointer methods -- these are needed as a special case (for some weird reason) even if + // multitouch is supported but there's only one touch point down currently -- event.getX(0) etc. throw + // an exception if there's only one point down. + if (DEBUG) + Log.i("MultiTouch", "Got here 3"); + xVals[0] = processingHist ? event.getHistoricalX(histIdx) : event.getX(); + yVals[0] = processingHist ? event.getHistoricalY(histIdx) : event.getY(); + pressureVals[0] = processingHist ? event.getHistoricalPressure(histIdx) : event.getPressure(); + } else { + // Read x, y and pressure of each pointer + if (DEBUG) + Log.i("MultiTouch", "Got here 4"); + int numPointers = Math.min(pointerCount, MAX_TOUCH_POINTS); + if (DEBUG && pointerCount > MAX_TOUCH_POINTS) + Log.i("MultiTouch", "Got more pointers than MAX_TOUCH_POINTS"); + for (int ptrIdx = 0; ptrIdx < numPointers; ptrIdx++) { + int ptrId = (Integer) m_getPointerId.invoke(event, ptrIdx); + pointerIds[ptrIdx] = ptrId; + // N.B. if pointerCount == 1, then the following methods throw an array index out of range exception, + // and the code above is therefore required not just for Android 1.5/1.6 but also for when there is + // only one touch point on the screen -- pointlessly inconsistent :( + xVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalX.invoke(event, ptrIdx, histIdx) : m_getX.invoke(event, ptrIdx)); + yVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalY.invoke(event, ptrIdx, histIdx) : m_getY.invoke(event, ptrIdx)); + pressureVals[ptrIdx] = (Float) (processingHist ? m_getHistoricalPressure.invoke(event, ptrIdx, histIdx) : m_getPressure + .invoke(event, ptrIdx)); + } + } + // Decode event + decodeTouchEvent(pointerCount, xVals, yVals, pressureVals, pointerIds, // + /* action = */processingHist ? MotionEvent.ACTION_MOVE : action, // + /* down = */processingHist ? true : action != MotionEvent.ACTION_UP // + && (action & ((1 << ACTION_POINTER_INDEX_SHIFT) - 1)) != ACTION_POINTER_UP // + && action != MotionEvent.ACTION_CANCEL, // + processingHist ? event.getHistoricalEventTime(histIdx) : event.getEventTime()); + } + + return true; + } catch (Exception e) { + // In case any of the introspection stuff fails (it shouldn't) + Log.e("MultiTouchController", "onTouchEvent() failed", e); + return false; + } + } + + private void decodeTouchEvent(int pointerCount, float[] x, float[] y, float[] pressure, int[] pointerIds, int action, boolean down, long eventTime) { + if (DEBUG) + Log.i("MultiTouch", "Got here 5 - " + pointerCount + " " + action + " " + down); + + // Swap curr/prev points + PointInfo tmp = mPrevPt; + mPrevPt = mCurrPt; + mCurrPt = tmp; + // Overwrite old prev point + mCurrPt.set(pointerCount, x, y, pressure, pointerIds, action, down, eventTime); + multiTouchController(); + } + + // ------------------------------------------------------------------------------------ + + /** Start dragging/pinching, or reset drag/pinch to current point if something goes out of range */ + private void anchorAtThisPositionAndScale() { + if (selectedObject == null) + return; + + // Get selected object's current position and scale + objectCanvas.getPositionAndScale(selectedObject, mCurrXform); + + // Figure out the object coords of the drag start point's screen coords. + // All stretching should be around this point in object-coord-space. + // Also figure out out ratio between object scale factor and multitouch + // diameter at beginning of drag; same for angle and optional anisotropic + // scale. + float currScaleInv = 1.0f / (!mCurrXform.updateScale ? 1.0f : mCurrXform.scale == 0.0f ? 1.0f : mCurrXform.scale); + extractCurrPtInfo(); + startPosX = (mCurrPtX - mCurrXform.xOff) * currScaleInv; + startPosY = (mCurrPtY - mCurrXform.yOff) * currScaleInv; + startScaleOverPinchDiam = mCurrXform.scale / mCurrPtDiam; + startScaleXOverPinchWidth = mCurrXform.scaleX / mCurrPtWidth; + startScaleYOverPinchHeight = mCurrXform.scaleY / mCurrPtHeight; + startAngleMinusPinchAngle = mCurrXform.angle - mCurrPtAng; + } + + /** Drag/stretch/rotate the selected object using the current touch position(s) relative to the anchor position(s). */ + private void performDragOrPinch() { + // Don't do anything if we're not dragging anything + if (selectedObject == null) + return; + + // Calc new position of dragged object + float currScale = !mCurrXform.updateScale ? 1.0f : mCurrXform.scale == 0.0f ? 1.0f : mCurrXform.scale; + extractCurrPtInfo(); + float newPosX = mCurrPtX - startPosX * currScale; + float newPosY = mCurrPtY - startPosY * currScale; + float newScale = startScaleOverPinchDiam * mCurrPtDiam; + float newScaleX = startScaleXOverPinchWidth * mCurrPtWidth; + float newScaleY = startScaleYOverPinchHeight * mCurrPtHeight; + float newAngle = startAngleMinusPinchAngle + mCurrPtAng; + + // Set the new obj coords, scale, and angle as appropriate (notifying the subclass of the change). + mCurrXform.set(newPosX, newPosY, newScale, newScaleX, newScaleY, newAngle); + + boolean success = objectCanvas.setPositionAndScale(selectedObject, mCurrXform, mCurrPt); + if (!success) + ; // If we could't set those params, do nothing currently + } + + /** + * State-based controller for tracking switches between no-touch, single-touch and multi-touch situations. Includes logic for cleaning up the + * event stream, as events around touch up/down are noisy at least on early Synaptics sensors. + */ + private void multiTouchController() { + if (DEBUG) + Log.i("MultiTouch", "Got here 6 - " + mMode + " " + mCurrPt.getNumTouchPoints() + " " + mCurrPt.isDown() + mCurrPt.isMultiTouch()); + + switch (mMode) { + case MODE_NOTHING: + // Not doing anything currently + if (mCurrPt.isDown()) { + // Start a new single-point drag + selectedObject = objectCanvas.getDraggableObjectAtPoint(mCurrPt); + if (selectedObject != null) { + // Started a new single-point drag + mMode = MODE_DRAG; + objectCanvas.selectObject(selectedObject, mCurrPt); + anchorAtThisPositionAndScale(); + // Don't need any settling time if just placing one finger, there is no noise + mSettleStartTime = mSettleEndTime = mCurrPt.getEventTime(); + } + } + break; + + case MODE_DRAG: + // Currently in a single-point drag + if (!mCurrPt.isDown()) { + // First finger was released, stop dragging + mMode = MODE_NOTHING; + objectCanvas.selectObject((selectedObject = null), mCurrPt); + + } else if (mCurrPt.isMultiTouch()) { + // Point 1 was already down and point 2 was just placed down + mMode = MODE_PINCH; + // Restart the drag with the new drag position (that is at the midpoint between the touchpoints) + anchorAtThisPositionAndScale(); + // Need to let events settle before moving things, to help with event noise on touchdown + mSettleStartTime = mCurrPt.getEventTime(); + mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; + + } else { + // Point 1 is still down and point 2 did not change state, just do single-point drag to new location + if (mCurrPt.getEventTime() < mSettleEndTime) { + // Ignore the first few events if we just stopped stretching, because if finger 2 was kept down while + // finger 1 is lifted, then point 1 gets mapped to finger 2. Restart the drag from the new position. + anchorAtThisPositionAndScale(); + } else { + // Keep dragging, move to new point + performDragOrPinch(); + } + } + break; + + case MODE_PINCH: + // Two-point pinch-scale/rotate/translate + if (!mCurrPt.isMultiTouch() || !mCurrPt.isDown()) { + // Dropped one or both points, stop stretching + + if (!mCurrPt.isDown()) { + // Dropped both points, go back to doing nothing + mMode = MODE_NOTHING; + objectCanvas.selectObject((selectedObject = null), mCurrPt); + + } else { + // Just dropped point 2, downgrade to a single-point drag + mMode = MODE_DRAG; + // Restart the pinch with the single-finger position + anchorAtThisPositionAndScale(); + // Ignore the first few events after the drop, in case we dropped finger 1 and left finger 2 down + mSettleStartTime = mCurrPt.getEventTime(); + mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; + } + + } else { + // Still pinching + if (Math.abs(mCurrPt.getX() - mPrevPt.getX()) > MAX_MULTITOUCH_POS_JUMP_SIZE + || Math.abs(mCurrPt.getY() - mPrevPt.getY()) > MAX_MULTITOUCH_POS_JUMP_SIZE + || Math.abs(mCurrPt.getMultiTouchWidth() - mPrevPt.getMultiTouchWidth()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE + || Math.abs(mCurrPt.getMultiTouchHeight() - mPrevPt.getMultiTouchHeight()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE) { + // Jumped too far, probably event noise, reset and ignore events for a bit + anchorAtThisPositionAndScale(); + mSettleStartTime = mCurrPt.getEventTime(); + mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; + + } else if (mCurrPt.eventTime < mSettleEndTime) { + // Events have not yet settled, reset + anchorAtThisPositionAndScale(); + } else { + // Stretch to new position and size + performDragOrPinch(); + } + } + break; + } + if (DEBUG) + Log.i("MultiTouch", "Got here 7 - " + mMode + " " + mCurrPt.getNumTouchPoints() + " " + mCurrPt.isDown() + mCurrPt.isMultiTouch()); + } + + // ------------------------------------------------------------------------------------ + + /** A class that packages up all MotionEvent information with all derived multitouch information (if available) */ + public static class PointInfo { + // Multitouch information + private int numPoints; + private float[] xs = new float[MAX_TOUCH_POINTS]; + private float[] ys = new float[MAX_TOUCH_POINTS]; + private float[] pressures = new float[MAX_TOUCH_POINTS]; + private int[] pointerIds = new int[MAX_TOUCH_POINTS]; + + // Midpoint of pinch operations + private float xMid, yMid, pressureMid; + + // Width/diameter/angle of pinch operations + private float dx, dy, diameter, diameterSq, angle; + + // Whether or not there is at least one finger down (isDown) and/or at least two fingers down (isMultiTouch) + private boolean isDown, isMultiTouch; + + // Whether or not these fields have already been calculated, for caching purposes + private boolean diameterSqIsCalculated, diameterIsCalculated, angleIsCalculated; + + // Event action code and event time + private int action; + private long eventTime; + + // ------------------------------------------------------------------------------------------------------------------------------------------- + + /** Set all point info */ + private void set(int numPoints, float[] x, float[] y, float[] pressure, int[] pointerIds, int action, boolean isDown, long eventTime) { + if (DEBUG) + Log.i("MultiTouch", "Got here 8 - " + +numPoints + " " + x[0] + " " + y[0] + " " + (numPoints > 1 ? x[1] : x[0]) + " " + + (numPoints > 1 ? y[1] : y[0]) + " " + action + " " + isDown); + this.eventTime = eventTime; + this.action = action; + this.numPoints = numPoints; + for (int i = 0; i < numPoints; i++) { + this.xs[i] = x[i]; + this.ys[i] = y[i]; + this.pressures[i] = pressure[i]; + this.pointerIds[i] = pointerIds[i]; + } + this.isDown = isDown; + this.isMultiTouch = numPoints >= 2; + + if (isMultiTouch) { + xMid = (x[0] + x[1]) * .5f; + yMid = (y[0] + y[1]) * .5f; + pressureMid = (pressure[0] + pressure[1]) * .5f; + dx = Math.abs(x[1] - x[0]); + dy = Math.abs(y[1] - y[0]); + + } else { + // Single-touch event + xMid = x[0]; + yMid = y[0]; + pressureMid = pressure[0]; + dx = dy = 0.0f; + } + // Need to re-calculate the expensive params if they're needed + diameterSqIsCalculated = diameterIsCalculated = angleIsCalculated = false; + } + + /** + * Copy all fields from one PointInfo class to another. PointInfo objects are volatile so you should use this if you want to keep track of the + * last touch event in your own code. + */ + public void set(PointInfo other) { + this.numPoints = other.numPoints; + for (int i = 0; i < numPoints; i++) { + this.xs[i] = other.xs[i]; + this.ys[i] = other.ys[i]; + this.pressures[i] = other.pressures[i]; + this.pointerIds[i] = other.pointerIds[i]; + } + this.xMid = other.xMid; + this.yMid = other.yMid; + this.pressureMid = other.pressureMid; + this.dx = other.dx; + this.dy = other.dy; + this.diameter = other.diameter; + this.diameterSq = other.diameterSq; + this.angle = other.angle; + this.isDown = other.isDown; + this.action = other.action; + this.isMultiTouch = other.isMultiTouch; + this.diameterIsCalculated = other.diameterIsCalculated; + this.diameterSqIsCalculated = other.diameterSqIsCalculated; + this.angleIsCalculated = other.angleIsCalculated; + this.eventTime = other.eventTime; + } + + // ------------------------------------------------------------------------------------------------------------------------------------------- + + /** True if number of touch points >= 2. */ + public boolean isMultiTouch() { + return isMultiTouch; + } + + /** Difference between x coords of touchpoint 0 and 1. */ + public float getMultiTouchWidth() { + return isMultiTouch ? dx : 0.0f; + } + + /** Difference between y coords of touchpoint 0 and 1. */ + public float getMultiTouchHeight() { + return isMultiTouch ? dy : 0.0f; + } + + /** Fast integer sqrt, by Jim Ulery. Much faster than Math.sqrt() for integers. */ + private int julery_isqrt(int val) { + int temp, g = 0, b = 0x8000, bshft = 15; + do { + if (val >= (temp = (((g << 1) + b) << bshft--))) { + g += b; + val -= temp; + } + } while ((b >>= 1) > 0); + return g; + } + + /** Calculate the squared diameter of the multitouch event, and cache it. Use this if you don't need to perform the sqrt. */ + public float getMultiTouchDiameterSq() { + if (!diameterSqIsCalculated) { + diameterSq = (isMultiTouch ? dx * dx + dy * dy : 0.0f); + diameterSqIsCalculated = true; + } + return diameterSq; + } + + /** Calculate the diameter of the multitouch event, and cache it. Uses fast int sqrt but gives accuracy to 1/16px. */ + public float getMultiTouchDiameter() { + if (!diameterIsCalculated) { + if (!isMultiTouch) { + diameter = 0.0f; + } else { + // Get 1/16 pixel's worth of subpixel accuracy, works on screens up to 2048x2048 + // before we get overflow (at which point you can reduce or eliminate subpix + // accuracy, or use longs in julery_isqrt()) + float diamSq = getMultiTouchDiameterSq(); + diameter = (diamSq == 0.0f ? 0.0f : (float) julery_isqrt((int) (256 * diamSq)) / 16.0f); + // Make sure diameter is never less than dx or dy, for trig purposes + if (diameter < dx) + diameter = dx; + if (diameter < dy) + diameter = dy; + } + diameterIsCalculated = true; + } + return diameter; + } + + /** + * Calculate the angle of a multitouch event, and cache it. Actually gives the smaller of the two angles between the x axis and the line + * between the two touchpoints, so range is [0,Math.PI/2]. Uses Math.atan2(). + */ + public float getMultiTouchAngle() { + if (!angleIsCalculated) { + if (!isMultiTouch) + angle = 0.0f; + else + angle = (float) Math.atan2(ys[1] - ys[0], xs[1] - xs[0]); + angleIsCalculated = true; + } + return angle; + } + + // ------------------------------------------------------------------------------------------------------------------------------------------- + + /** Return the total number of touch points */ + public int getNumTouchPoints() { + return numPoints; + } + + /** Return the X coord of the first touch point if there's only one, or the midpoint between first and second touch points if two or more. */ + public float getX() { + return xMid; + } + + /** Return the array of X coords -- only the first getNumTouchPoints() of these is defined. */ + public float[] getXs() { + return xs; + } + + /** Return the X coord of the first touch point if there's only one, or the midpoint between first and second touch points if two or more. */ + public float getY() { + return yMid; + } + + /** Return the array of Y coords -- only the first getNumTouchPoints() of these is defined. */ + public float[] getYs() { + return ys; + } + + /** + * Return the array of pointer ids -- only the first getNumTouchPoints() of these is defined. These don't have to be all the numbers from 0 to + * getNumTouchPoints()-1 inclusive, numbers can be skipped if a finger is lifted and the touch sensor is capable of detecting that that + * particular touch point is no longer down. Note that a lot of sensors do not have this capability: when finger 1 is lifted up finger 2 + * becomes the new finger 1. However in theory these IDs can correct for that. Convert back to indices using MotionEvent.findPointerIndex(). + */ + public int[] getPointerIds() { + return pointerIds; + } + + /** Return the pressure the first touch point if there's only one, or the average pressure of first and second touch points if two or more. */ + public float getPressure() { + return pressureMid; + } + + /** Return the array of pressures -- only the first getNumTouchPoints() of these is defined. */ + public float[] getPressures() { + return pressures; + } + + // ------------------------------------------------------------------------------------------------------------------------------------------- + + public boolean isDown() { + return isDown; + } + + public int getAction() { + return action; + } + + public long getEventTime() { + return eventTime; + } + } + + // ------------------------------------------------------------------------------------ + + /** + * A class that is used to store scroll offsets and scale information for objects that are managed by the multitouch controller + */ + public static class PositionAndScale { + private float xOff, yOff, scale, scaleX, scaleY, angle; + private boolean updateScale, updateScaleXY, updateAngle; + + /** + * Set position and optionally scale, anisotropic scale, and/or angle. Where if the corresponding "update" flag is set to false, the field's + * value will not be changed during a pinch operation. If the value is not being updated *and* the value is not used by the client + * application, then the value can just be zero. However if the value is not being updated but the value *is* being used by the client + * application, the value should still be specified and the update flag should be false (e.g. angle of the object being dragged should still + * be specified even if the program is in "resize" mode rather than "rotate" mode). + */ + public void set(float xOff, float yOff, boolean updateScale, float scale, boolean updateScaleXY, float scaleX, float scaleY, + boolean updateAngle, float angle) { + this.xOff = xOff; + this.yOff = yOff; + this.updateScale = updateScale; + this.scale = scale == 0.0f ? 1.0f : scale; + this.updateScaleXY = updateScaleXY; + this.scaleX = scaleX == 0.0f ? 1.0f : scaleX; + this.scaleY = scaleY == 0.0f ? 1.0f : scaleY; + this.updateAngle = updateAngle; + this.angle = angle; + } + + /** Set position and optionally scale, anisotropic scale, and/or angle, without changing the "update" flags. */ + protected void set(float xOff, float yOff, float scale, float scaleX, float scaleY, float angle) { + this.xOff = xOff; + this.yOff = yOff; + this.scale = scale == 0.0f ? 1.0f : scale; + this.scaleX = scaleX == 0.0f ? 1.0f : scaleX; + this.scaleY = scaleY == 0.0f ? 1.0f : scaleY; + this.angle = angle; + } + + public float getXOff() { + return xOff; + } + + public float getYOff() { + return yOff; + } + + public float getScale() { + return !updateScale ? 1.0f : scale; + } + + /** Included in case you want to support anisotropic scaling */ + public float getScaleX() { + return !updateScaleXY ? 1.0f : scaleX; + } + + /** Included in case you want to support anisotropic scaling */ + public float getScaleY() { + return !updateScaleXY ? 1.0f : scaleY; + } + + public float getAngle() { + return !updateAngle ? 0.0f : angle; + } + } + + // ------------------------------------------------------------------------------------ + + public static interface MultiTouchObjectCanvas { + + /** + * See if there is a draggable object at the current point. Returns the object at the point, or null if nothing to drag. To start a multitouch + * drag/stretch operation, this routine must return some non-null reference to an object. This object is passed into the other methods in this + * interface when they are called. + * + * @param touchPoint + * The point being tested (in object coordinates). Return the topmost object under this point, or if dragging/stretching the whole + * canvas, just return a reference to the canvas. + * @return a reference to the object under the point being tested, or null to cancel the drag operation. If dragging/stretching the whole + * canvas (e.g. in a photo viewer), always return non-null, otherwise the stretch operation won't work. + */ + public T getDraggableObjectAtPoint(PointInfo touchPoint); + + /** + * Get the screen coords of the dragged object's origin, and scale multiplier to convert screen coords to obj coords. The job of this routine + * is to call the .set() method on the passed PositionAndScale object to record the initial position and scale of the object (in object + * coordinates) before any dragging/stretching takes place. + * + * @param obj + * The object being dragged/stretched. + * @param objPosAndScaleOut + * Output parameter: You need to call objPosAndScaleOut.set() to record the current position and scale of obj. + */ + public void getPositionAndScale(T obj, PositionAndScale objPosAndScaleOut); + + /** + * Callback to update the position and scale (in object coords) of the currently-dragged object. + * + * @param obj + * The object being dragged/stretched. + * @param newObjPosAndScale + * The new position and scale of the object, in object coordinates. Use this to move/resize the object before returning. + * @param touchPoint + * Info about the current touch point, including multitouch information and utilities to calculate and cache multitouch pinch + * diameter etc. (Note: touchPoint is volatile, if you want to keep any fields of touchPoint, you must copy them before the method + * body exits.) + * @return true if setting the position and scale of the object was successful, or false if the position or scale parameters are out of range + * for this object. + */ + public boolean setPositionAndScale(T obj, PositionAndScale newObjPosAndScale, PointInfo touchPoint); + + /** + * Select an object at the given point. Can be used to bring the object to top etc. Only called when first touchpoint goes down, not when + * multitouch is initiated. Also called with null on touch-up. + * + * @param obj + * The object being selected by single-touch, or null on touch-up. + * @param touchPoint + * The current touch point. + */ + public void selectObject(T obj, PointInfo touchPoint); + } +}