// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser;
import android.animation.TimeAnimator;
import android.animation.TimeAnimator.TimeListener;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerProperties;
import android.view.MotionEvent.PointerCoords;
import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;
/**
* Provides a Java-side implementation for simulating touch gestures,
* such as scroll or pinch.
*/
@JNINamespace("content")
public class GenericTouchGesture {
private final ContentViewCore mContentViewCore;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private TimeAnimator mTimeAnimator;
private int mNativePtr;
private long mDownTime;
private final byte STATE_INITIAL = 0;
private final byte STATE_MOVING = 1;
private final byte STATE_PENDING_UP = 2;
private final byte STATE_FINAL = 3;
private byte state = STATE_INITIAL;
private static class TouchPointer {
private final float mStartX;
private final float mStartY;
private final float mDeltaX;
private final float mDeltaY;
private final float mStepX;
private final float mStepY;
private PointerProperties mProperties;
private PointerCoords mCoords;
// Class representing a single pointer being moved over the screen.
TouchPointer(int startX, int startY, int deltaX, int deltaY,
int id, float scale) {
mStartX = startX * scale;
mStartY = startY * scale;
mDeltaX = deltaX * scale;
mDeltaY = deltaY * scale;
if (deltaX != 0 || deltaY != 0) {
mStepX = mDeltaX / Math.abs(mDeltaX + mDeltaY);
mStepY = mDeltaY / Math.abs(mDeltaX + mDeltaY);
} else {
mStepX = 0;
mStepY = 0;
}
mProperties = new PointerProperties();
mProperties.id = id;
mProperties.toolType = MotionEvent.TOOL_TYPE_FINGER;
mCoords = new PointerCoords();
mCoords.x = mStartX;
mCoords.y = mStartY;
mCoords.pressure = 1.0f;
}
PointerProperties getProperties() {
return mProperties;
}
PointerCoords getCoords() {
return mCoords;
}
float getCurrentX() {
return mCoords.x;
}
float getCurrentY() {
return mCoords.y;
}
void moveBy(float delta) {
mCoords.x += mStepX * delta;
mCoords.y += mStepY * delta;
}
boolean hasArrived() {
return Math.abs(mCoords.x - mStartX) >= Math.abs(mDeltaX) &&
Math.abs(mCoords.y - mStartY) >= Math.abs(mDeltaY);
}
}
private TouchPointer[] mPointers;
private final PointerProperties[] mPointerProperties;
private final PointerCoords[] mPointerCoords;
GenericTouchGesture(ContentViewCore contentViewCore,
int startX, int startY, int deltaX, int deltaY) {
mContentViewCore = contentViewCore;
float scale = mContentViewCore.getRenderCoordinates().getDeviceScaleFactor();
mPointers = new TouchPointer[1];
mPointers[0] = new TouchPointer(startX, startY, deltaX, deltaY, 0, scale);
mPointerProperties = new PointerProperties[1];
mPointerProperties[0] = mPointers[0].getProperties();
mPointerCoords = new PointerCoords[1];
mPointerCoords[0] = mPointers[0].getCoords();
}
GenericTouchGesture(ContentViewCore contentViewCore,
int startX0, int startY0, int deltaX0, int deltaY0,
int startX1, int startY1, int deltaX1, int deltaY1) {
mContentViewCore = contentViewCore;
float scale = mContentViewCore.getRenderCoordinates().getDeviceScaleFactor();
mPointers = new TouchPointer[2];
mPointers[0] = new TouchPointer(startX0, startY0, deltaX0, deltaY0, 0, scale);
mPointers[1] = new TouchPointer(startX1, startY1, deltaX1, deltaY1, 1, scale);
mPointerProperties = new PointerProperties[2];
mPointerProperties[0] = mPointers[0].getProperties();
mPointerProperties[1] = mPointers[1].getProperties();
mPointerCoords = new PointerCoords[2];
mPointerCoords[0] = mPointers[0].getCoords();
mPointerCoords[1] = mPointers[1].getCoords();
}
@CalledByNative
void start(int nativePtr) {
assert mNativePtr == 0;
mNativePtr = nativePtr;
Runnable runnable = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ?
createJBRunnable() : createPreJBRunnable();
mHandler.post(runnable);
}
boolean sendEvent(long time) {
switch (state) {
case STATE_INITIAL: {
mDownTime = SystemClock.uptimeMillis();
// Touch the first pointer down. This initiates the gesture.
MotionEvent event = MotionEvent.obtain(mDownTime, time,
MotionEvent.ACTION_DOWN,
mPointers[0].getCurrentX(), mPointers[0].getCurrentY(), 0);
mContentViewCore.onTouchEvent(event);
event.recycle();
// If there are more pointers, touch them down too.
if (mPointers.length > 1) {
event = MotionEvent.obtain(mDownTime, time,
MotionEvent.ACTION_POINTER_DOWN,
mPointers.length, mPointerProperties, mPointerCoords,
0, 0, 1, 1, 0, 0, 0, 0);
mContentViewCore.onTouchEvent(event);
event.recycle();
}
state = STATE_MOVING;
break;
}
case STATE_MOVING: {
float delta = nativeGetDelta(
mNativePtr, mContentViewCore.getRenderCoordinates().getDeviceScaleFactor());
if (delta != 0) {
for (TouchPointer pointer : mPointers) {
pointer.moveBy((float)delta);
}
MotionEvent event = MotionEvent.obtain(mDownTime, time,
MotionEvent.ACTION_MOVE,
mPointers.length, mPointerProperties, mPointerCoords,
0, 0, 1, 1, 0, 0, 0, 0);
mContentViewCore.onTouchEvent(event);
event.recycle();
}
if (havePointersArrived())
state = STATE_PENDING_UP;
break;
}
case STATE_PENDING_UP: {
// If there are more than one pointers, lift them up first.
if (mPointers.length > 1) {
MotionEvent event = MotionEvent.obtain(mDownTime, time,
MotionEvent.ACTION_POINTER_UP,
mPointers.length, mPointerProperties, mPointerCoords,
0, 0, 1, 1, 0, 0, 0, 0);
mContentViewCore.onTouchEvent(event);
event.recycle();
}
// Finally, lift the first pointer up to finish the gesture.
MotionEvent event = MotionEvent.obtain(mDownTime, time,
MotionEvent.ACTION_UP,
mPointers[0].getCurrentX(), mPointers[0].getCurrentY(), 0);
mContentViewCore.onTouchEvent(event);
event.recycle();
nativeSetHasFinished(mNativePtr);
state = STATE_FINAL;
break;
}
}
return state != STATE_FINAL;
}
private boolean havePointersArrived() {
boolean arrived = true;
for (TouchPointer pointer : mPointers) {
arrived = arrived && pointer.hasArrived();
}
return arrived;
}
private Runnable createJBRunnable() {
// On JB, we rely on TimeAnimator to send events tied with vsync.
return new Runnable() {
@Override
public void run() {
mTimeAnimator = new TimeAnimator();
mTimeAnimator.setTimeListener(new TimeListener() {
@Override
public void onTimeUpdate(TimeAnimator animation, long totalTime,
long deltaTime) {
if (!sendEvent(mDownTime + totalTime)) {
mTimeAnimator.end();
}
}
});
mTimeAnimator.start();
}
};
}
private Runnable createPreJBRunnable() {
// Pre-JB there's no TimeAnimator, so we keep posting messages.
return new Runnable() {
@Override
public void run() {
if (sendEvent(SystemClock.uptimeMillis())) {
mHandler.post(this);
}
}
};
}
private native float nativeGetDelta(
int nativeGenericTouchGestureAndroid, float scale);
private native void nativeSetHasFinished(int nativeGenericTouchGestureAndroid);
}