Merge "Interpolate gesture preview trails"
This commit is contained in:
commit
9413e957fa
4 changed files with 507 additions and 16 deletions
|
@ -44,6 +44,7 @@ final class GesturePreviewTrail {
|
||||||
// The wall time of the zero value in {@link #mEventTimes}
|
// The wall time of the zero value in {@link #mEventTimes}
|
||||||
private long mCurrentTimeBase;
|
private long mCurrentTimeBase;
|
||||||
private int mTrailStartIndex;
|
private int mTrailStartIndex;
|
||||||
|
private int mLastInterpolatedDrawIndex;
|
||||||
|
|
||||||
static final class Params {
|
static final class Params {
|
||||||
public final int mTrailColor;
|
public final int mTrailColor;
|
||||||
|
@ -96,6 +97,17 @@ final class GesturePreviewTrail {
|
||||||
}
|
}
|
||||||
final int[] eventTimes = mEventTimes.getPrimitiveArray();
|
final int[] eventTimes = mEventTimes.getPrimitiveArray();
|
||||||
final int strokeId = stroke.getGestureStrokeId();
|
final int strokeId = stroke.getGestureStrokeId();
|
||||||
|
// Because interpolation algorithm in {@link GestureStrokeWithPreviewPoints} can't determine
|
||||||
|
// the interpolated points in the last segment of gesture stroke, it may need recalculation
|
||||||
|
// of interpolation when new segments are added to the stroke.
|
||||||
|
// {@link #mLastInterpolatedDrawIndex} holds the start index of the last segment. It may
|
||||||
|
// be updated by the interpolation
|
||||||
|
// {@link GestureStrokeWithPreviewPoints#interpolatePreviewStroke}
|
||||||
|
// or by animation {@link #drawGestureTrail(Canvas,Paint,Rect,Params)} below.
|
||||||
|
final int lastInterpolatedIndex = (strokeId == mCurrentStrokeId)
|
||||||
|
? mLastInterpolatedDrawIndex : trailSize;
|
||||||
|
mLastInterpolatedDrawIndex = stroke.interpolateStrokeAndReturnStartIndexOfLastSegment(
|
||||||
|
lastInterpolatedIndex, mEventTimes, mXCoordinates, mYCoordinates);
|
||||||
if (strokeId != mCurrentStrokeId) {
|
if (strokeId != mCurrentStrokeId) {
|
||||||
final int elapsedTime = (int)(downTime - mCurrentTimeBase);
|
final int elapsedTime = (int)(downTime - mCurrentTimeBase);
|
||||||
for (int i = mTrailStartIndex; i < trailSize; i++) {
|
for (int i = mTrailStartIndex; i < trailSize; i++) {
|
||||||
|
@ -216,6 +228,10 @@ final class GesturePreviewTrail {
|
||||||
System.arraycopy(eventTimes, startIndex, eventTimes, 0, newSize);
|
System.arraycopy(eventTimes, startIndex, eventTimes, 0, newSize);
|
||||||
System.arraycopy(xCoords, startIndex, xCoords, 0, newSize);
|
System.arraycopy(xCoords, startIndex, xCoords, 0, newSize);
|
||||||
System.arraycopy(yCoords, startIndex, yCoords, 0, newSize);
|
System.arraycopy(yCoords, startIndex, yCoords, 0, newSize);
|
||||||
|
// The start index of the last segment of the stroke
|
||||||
|
// {@link mLastInterpolatedDrawIndex} should also be updated because all array
|
||||||
|
// elements have just been shifted for compaction.
|
||||||
|
mLastInterpolatedDrawIndex = Math.max(mLastInterpolatedDrawIndex - startIndex, 0);
|
||||||
}
|
}
|
||||||
mEventTimes.setLength(newSize);
|
mEventTimes.setLength(newSize);
|
||||||
mXCoordinates.setLength(newSize);
|
mXCoordinates.setLength(newSize);
|
||||||
|
|
|
@ -21,19 +21,32 @@ import com.android.inputmethod.latin.ResizableIntArray;
|
||||||
public final class GestureStrokeWithPreviewPoints extends GestureStroke {
|
public final class GestureStrokeWithPreviewPoints extends GestureStroke {
|
||||||
public static final int PREVIEW_CAPACITY = 256;
|
public static final int PREVIEW_CAPACITY = 256;
|
||||||
|
|
||||||
|
private static final boolean ENABLE_INTERPOLATION = true;
|
||||||
|
|
||||||
private final ResizableIntArray mPreviewEventTimes = new ResizableIntArray(PREVIEW_CAPACITY);
|
private final ResizableIntArray mPreviewEventTimes = new ResizableIntArray(PREVIEW_CAPACITY);
|
||||||
private final ResizableIntArray mPreviewXCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
|
private final ResizableIntArray mPreviewXCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
|
||||||
private final ResizableIntArray mPreviewYCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
|
private final ResizableIntArray mPreviewYCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
|
||||||
|
|
||||||
private int mStrokeId;
|
private int mStrokeId;
|
||||||
private int mLastPreviewSize;
|
private int mLastPreviewSize;
|
||||||
|
private final HermiteInterpolator mInterpolator = new HermiteInterpolator();
|
||||||
|
private int mLastInterpolatedPreviewIndex;
|
||||||
|
|
||||||
private int mMinPreviewSampleLengthSquare;
|
private int mMinPreviewSamplingDistanceSquared;
|
||||||
private int mLastX;
|
private int mLastX;
|
||||||
private int mLastY;
|
private int mLastY;
|
||||||
|
private double mMinPreviewSamplingDistance;
|
||||||
|
private double mDistanceFromLastSample;
|
||||||
|
|
||||||
// TODO: Move this to resource.
|
// TODO: Move these constants to resource.
|
||||||
private static final float MIN_PREVIEW_SAMPLE_LENGTH_RATIO_TO_KEY_WIDTH = 0.1f;
|
// The minimum linear distance between sample points for preview in keyWidth unit.
|
||||||
|
private static final float MIN_PREVIEW_SAMPLING_RATIO_TO_KEY_WIDTH = 0.1f;
|
||||||
|
// The minimum trail distance between sample points for preview in keyWidth unit when using
|
||||||
|
// interpolation.
|
||||||
|
private static final float MIN_PREVIEW_SAMPLING_RATIO_TO_KEY_WIDTH_WITH_INTERPOLATION = 0.2f;
|
||||||
|
// The angular threshold to use interpolation in radian. PI/12 is 15 degree.
|
||||||
|
private static final double INTERPOLATION_ANGULAR_THRESHOLD = Math.PI / 12.0d;
|
||||||
|
private static final int MAX_INTERPOLATION_PARTITION = 4;
|
||||||
|
|
||||||
public GestureStrokeWithPreviewPoints(final int pointerId, final GestureStrokeParams params) {
|
public GestureStrokeWithPreviewPoints(final int pointerId, final GestureStrokeParams params) {
|
||||||
super(pointerId, params);
|
super(pointerId, params);
|
||||||
|
@ -44,6 +57,7 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
|
||||||
super.reset();
|
super.reset();
|
||||||
mStrokeId++;
|
mStrokeId++;
|
||||||
mLastPreviewSize = 0;
|
mLastPreviewSize = 0;
|
||||||
|
mLastInterpolatedPreviewIndex = 0;
|
||||||
mPreviewEventTimes.setLength(0);
|
mPreviewEventTimes.setLength(0);
|
||||||
mPreviewXCoordinates.setLength(0);
|
mPreviewXCoordinates.setLength(0);
|
||||||
mPreviewYCoordinates.setLength(0);
|
mPreviewYCoordinates.setLength(0);
|
||||||
|
@ -53,35 +67,49 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
|
||||||
return mStrokeId;
|
return mStrokeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getGestureStrokePreviewSize() {
|
|
||||||
return mPreviewEventTimes.getLength();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setKeyboardGeometry(final int keyWidth, final int keyboardHeight) {
|
public void setKeyboardGeometry(final int keyWidth, final int keyboardHeight) {
|
||||||
super.setKeyboardGeometry(keyWidth, keyboardHeight);
|
super.setKeyboardGeometry(keyWidth, keyboardHeight);
|
||||||
final float sampleLength = keyWidth * MIN_PREVIEW_SAMPLE_LENGTH_RATIO_TO_KEY_WIDTH;
|
final float samplingRatioToKeyWidth = ENABLE_INTERPOLATION
|
||||||
mMinPreviewSampleLengthSquare = (int)(sampleLength * sampleLength);
|
? MIN_PREVIEW_SAMPLING_RATIO_TO_KEY_WIDTH_WITH_INTERPOLATION
|
||||||
|
: MIN_PREVIEW_SAMPLING_RATIO_TO_KEY_WIDTH;
|
||||||
|
mMinPreviewSamplingDistance = keyWidth * samplingRatioToKeyWidth;
|
||||||
|
mMinPreviewSamplingDistanceSquared = (int)(
|
||||||
|
mMinPreviewSamplingDistance * mMinPreviewSamplingDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean needsSampling(final int x, final int y) {
|
private boolean needsSampling(final int x, final int y, final boolean isMajorEvent) {
|
||||||
|
if (ENABLE_INTERPOLATION) {
|
||||||
|
mDistanceFromLastSample += Math.hypot(x - mLastX, y - mLastY);
|
||||||
|
mLastX = x;
|
||||||
|
mLastY = y;
|
||||||
|
if (mDistanceFromLastSample >= mMinPreviewSamplingDistance) {
|
||||||
|
mDistanceFromLastSample = 0.0d;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final int dx = x - mLastX;
|
final int dx = x - mLastX;
|
||||||
final int dy = y - mLastY;
|
final int dy = y - mLastY;
|
||||||
return dx * dx + dy * dy >= mMinPreviewSampleLengthSquare;
|
if (isMajorEvent || dx * dx + dy * dy >= mMinPreviewSamplingDistanceSquared) {
|
||||||
|
mLastX = x;
|
||||||
|
mLastY = y;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean addPointOnKeyboard(final int x, final int y, final int time,
|
public boolean addPointOnKeyboard(final int x, final int y, final int time,
|
||||||
final boolean isMajorEvent) {
|
final boolean isMajorEvent) {
|
||||||
final boolean onValidArea = super.addPointOnKeyboard(x, y, time, isMajorEvent);
|
if (needsSampling(x, y, isMajorEvent)) {
|
||||||
if (isMajorEvent || needsSampling(x, y)) {
|
|
||||||
mPreviewEventTimes.add(time);
|
mPreviewEventTimes.add(time);
|
||||||
mPreviewXCoordinates.add(x);
|
mPreviewXCoordinates.add(x);
|
||||||
mPreviewYCoordinates.add(y);
|
mPreviewYCoordinates.add(y);
|
||||||
mLastX = x;
|
|
||||||
mLastY = y;
|
|
||||||
}
|
}
|
||||||
return onValidArea;
|
return super.addPointOnKeyboard(x, y, time, isMajorEvent);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void appendPreviewStroke(final ResizableIntArray eventTimes,
|
public void appendPreviewStroke(final ResizableIntArray eventTimes,
|
||||||
|
@ -95,4 +123,82 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
|
||||||
yCoords.append(mPreviewYCoordinates, mLastPreviewSize, length);
|
yCoords.append(mPreviewYCoordinates, mLastPreviewSize, length);
|
||||||
mLastPreviewSize = mPreviewEventTimes.getLength();
|
mLastPreviewSize = mPreviewEventTimes.getLength();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate interpolated points between the last interpolated point and the end of the trail.
|
||||||
|
* And return the start index of the last interpolated segment of input arrays because it
|
||||||
|
* may need to recalculate the interpolated points in the segment if further segments are
|
||||||
|
* added to this stroke.
|
||||||
|
*
|
||||||
|
* @param lastInterpolatedIndex the start index of the last interpolated segment of
|
||||||
|
* <code>eventTimes</code>, <code>xCoords</code>, and <code>yCoords</code>.
|
||||||
|
* @param eventTimes the event time array of gesture preview trail to be drawn.
|
||||||
|
* @param xCoords the x-coordinates array of gesture preview trail to be drawn.
|
||||||
|
* @param yCoords the y-coordinates array of gesture preview trail to be drawn.
|
||||||
|
* @return the start index of the last interpolated segment of input arrays.
|
||||||
|
*/
|
||||||
|
public int interpolateStrokeAndReturnStartIndexOfLastSegment(final int lastInterpolatedIndex,
|
||||||
|
final ResizableIntArray eventTimes, final ResizableIntArray xCoords,
|
||||||
|
final ResizableIntArray yCoords) {
|
||||||
|
if (!ENABLE_INTERPOLATION) {
|
||||||
|
return lastInterpolatedIndex;
|
||||||
|
}
|
||||||
|
final int size = mPreviewEventTimes.getLength();
|
||||||
|
final int[] pt = mPreviewEventTimes.getPrimitiveArray();
|
||||||
|
final int[] px = mPreviewXCoordinates.getPrimitiveArray();
|
||||||
|
final int[] py = mPreviewYCoordinates.getPrimitiveArray();
|
||||||
|
mInterpolator.reset(px, py, 0, size);
|
||||||
|
// The last segment of gesture stroke needs to be interpolated again because the slope of
|
||||||
|
// the tangent at the last point isn't determined.
|
||||||
|
int lastInterpolatedDrawIndex = lastInterpolatedIndex;
|
||||||
|
int d1 = lastInterpolatedIndex;
|
||||||
|
for (int p2 = mLastInterpolatedPreviewIndex + 1; p2 < size; p2++) {
|
||||||
|
final int p1 = p2 - 1;
|
||||||
|
final int p0 = p1 - 1;
|
||||||
|
final int p3 = p2 + 1;
|
||||||
|
mLastInterpolatedPreviewIndex = p1;
|
||||||
|
lastInterpolatedDrawIndex = d1;
|
||||||
|
mInterpolator.setInterval(p0, p1, p2, p3);
|
||||||
|
final double m1 = Math.atan2(mInterpolator.mSlope1Y, mInterpolator.mSlope1X);
|
||||||
|
final double m2 = Math.atan2(mInterpolator.mSlope2Y, mInterpolator.mSlope2X);
|
||||||
|
final double dm = Math.abs(angularDiff(m2, m1));
|
||||||
|
final int partition = Math.min((int)Math.ceil(dm / INTERPOLATION_ANGULAR_THRESHOLD),
|
||||||
|
MAX_INTERPOLATION_PARTITION);
|
||||||
|
final int t1 = eventTimes.get(d1);
|
||||||
|
final int dt = pt[p2] - pt[p1];
|
||||||
|
d1++;
|
||||||
|
for (int i = 1; i < partition; i++) {
|
||||||
|
final float t = i / (float)partition;
|
||||||
|
mInterpolator.interpolate(t);
|
||||||
|
eventTimes.add(d1, (int)(dt * t) + t1);
|
||||||
|
xCoords.add(d1, (int)mInterpolator.mInterpolatedX);
|
||||||
|
yCoords.add(d1, (int)mInterpolator.mInterpolatedY);
|
||||||
|
d1++;
|
||||||
|
}
|
||||||
|
eventTimes.add(d1, pt[p2]);
|
||||||
|
xCoords.add(d1, px[p2]);
|
||||||
|
yCoords.add(d1, py[p2]);
|
||||||
|
}
|
||||||
|
return lastInterpolatedDrawIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final double TWO_PI = Math.PI * 2.0d;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the angular of rotation from <code>a0</code> to <code>a1</code>.
|
||||||
|
*
|
||||||
|
* @param a1 the angular to which the rotation ends.
|
||||||
|
* @param a0 the angular from which the rotation starts.
|
||||||
|
* @return the angular rotation value from a0 to a1, normalized to [-PI, +PI].
|
||||||
|
*/
|
||||||
|
private static double angularDiff(final double a1, final double a0) {
|
||||||
|
double deltaAngle = a1 - a0;
|
||||||
|
while (deltaAngle > Math.PI) {
|
||||||
|
deltaAngle -= TWO_PI;
|
||||||
|
}
|
||||||
|
while (deltaAngle < -Math.PI) {
|
||||||
|
deltaAngle += TWO_PI;
|
||||||
|
}
|
||||||
|
return deltaAngle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 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.android.inputmethod.keyboard.internal;
|
||||||
|
|
||||||
|
import com.android.inputmethod.annotations.UsedForTesting;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolates XY-coordinates using Cubic Hermite Curve.
|
||||||
|
*/
|
||||||
|
public final class HermiteInterpolator {
|
||||||
|
private int[] mXCoords;
|
||||||
|
private int[] mYCoords;
|
||||||
|
private int mMinPos;
|
||||||
|
private int mMaxPos;
|
||||||
|
|
||||||
|
// Working variable to calculate interpolated value.
|
||||||
|
/** The coordinates of the start point of the interval. */
|
||||||
|
public int mP1X, mP1Y;
|
||||||
|
/** The coordinates of the end point of the interval. */
|
||||||
|
public int mP2X, mP2Y;
|
||||||
|
/** The slope of the tangent at the start point. */
|
||||||
|
public float mSlope1X, mSlope1Y;
|
||||||
|
/** The slope of the tangent at the end point. */
|
||||||
|
public float mSlope2X, mSlope2Y;
|
||||||
|
/** The interpolated coordinates.
|
||||||
|
* The return variables of {@link #interpolate(float)} to avoid instantiations.
|
||||||
|
*/
|
||||||
|
public float mInterpolatedX, mInterpolatedY;
|
||||||
|
|
||||||
|
public HermiteInterpolator() {
|
||||||
|
// Nothing to do with here.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset this interpolator to point XY-coordinates data.
|
||||||
|
* @param xCoords the array of x-coordinates. Valid data are in left-open interval
|
||||||
|
* <code>[minPos, maxPos)</code>.
|
||||||
|
* @param yCoords the array of y-coordinates. Valid data are in left-open interval
|
||||||
|
* <code>[minPos, maxPos)</code>.
|
||||||
|
* @param minPos the minimum index of left-open interval of valid data.
|
||||||
|
* @param maxPos the maximum index of left-open interval of valid data.
|
||||||
|
*/
|
||||||
|
@UsedForTesting
|
||||||
|
public void reset(final int[] xCoords, final int[] yCoords, final int minPos,
|
||||||
|
final int maxPos) {
|
||||||
|
mXCoords = xCoords;
|
||||||
|
mYCoords = yCoords;
|
||||||
|
mMinPos = minPos;
|
||||||
|
mMaxPos = maxPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set interpolation interval.
|
||||||
|
* <p>
|
||||||
|
* The start and end coordinates of the interval will be set in {@link #mP1X}, {@link #mP1Y},
|
||||||
|
* {@link #mP2X}, and {@link #mP2Y}. The slope of the tangents at start and end points will be
|
||||||
|
* set in {@link #mSlope1X}, {@link #mSlope1Y}, {@link #mSlope2X}, and {@link #mSlope2Y}.
|
||||||
|
*
|
||||||
|
* @param p0 the index just before interpolation interval. If <code>p1</code> points the start
|
||||||
|
* of valid points, <code>p0</code> must be less than <code>minPos</code> of
|
||||||
|
* {@link #reset(int[],int[],int,int)}.
|
||||||
|
* @param p1 the start index of interpolation interval.
|
||||||
|
* @param p2 the end index of interpolation interval.
|
||||||
|
* @param p3 the index just after interpolation interval. If <code>p2</code> points the end of
|
||||||
|
* valid points, <code>p3</code> must be equal or greater than <code>maxPos</code> of
|
||||||
|
* {@link #reset(int[],int[],int,int)}.
|
||||||
|
*/
|
||||||
|
@UsedForTesting
|
||||||
|
public void setInterval(final int p0, final int p1, final int p2, final int p3) {
|
||||||
|
mP1X = mXCoords[p1];
|
||||||
|
mP1Y = mYCoords[p1];
|
||||||
|
mP2X = mXCoords[p2];
|
||||||
|
mP2Y = mYCoords[p2];
|
||||||
|
// A(ax,ay) is the vector p1->p2.
|
||||||
|
final int ax = mP2X - mP1X;
|
||||||
|
final int ay = mP2Y - mP1Y;
|
||||||
|
|
||||||
|
// Calculate the slope of the tangent at p1.
|
||||||
|
if (p0 >= mMinPos) {
|
||||||
|
// p1 has previous valid point p0.
|
||||||
|
// The slope of the tangent is half of the vector p0->p2.
|
||||||
|
mSlope1X = (mP2X - mXCoords[p0]) / 2.0f;
|
||||||
|
mSlope1Y = (mP2Y - mYCoords[p0]) / 2.0f;
|
||||||
|
} else if (p3 < mMaxPos) {
|
||||||
|
// p1 has no previous valid point, but p2 has next valid point p3.
|
||||||
|
// B(bx,by) is the slope vector of the tangent at p2.
|
||||||
|
final float bx = (mXCoords[p3] - mP1X) / 2.0f;
|
||||||
|
final float by = (mYCoords[p3] - mP1Y) / 2.0f;
|
||||||
|
final float crossProdAB = ax * by - ay * bx;
|
||||||
|
final float dotProdAB = ax * bx + ay * by;
|
||||||
|
final float normASquare = ax * ax + ay * ay;
|
||||||
|
final float invHalfNormASquare = 1.0f / normASquare / 2.0f;
|
||||||
|
// The slope of the tangent is the mirror image of vector B to vector A.
|
||||||
|
mSlope1X = invHalfNormASquare * (dotProdAB * ax + crossProdAB * ay);
|
||||||
|
mSlope1Y = invHalfNormASquare * (dotProdAB * ay - crossProdAB * ax);
|
||||||
|
} else {
|
||||||
|
// p1 and p2 have no previous valid point. (Interval has only point p1 and p2)
|
||||||
|
mSlope1X = ax;
|
||||||
|
mSlope1Y = ay;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the slope of the tangent at p2.
|
||||||
|
if (p3 < mMaxPos) {
|
||||||
|
// p2 has next valid point p3.
|
||||||
|
// The slope of the tangent is half of the vector p1->p3.
|
||||||
|
mSlope2X = (mXCoords[p3] - mP1X) / 2.0f;
|
||||||
|
mSlope2Y = (mYCoords[p3] - mP1Y) / 2.0f;
|
||||||
|
} else if (p0 >= mMinPos) {
|
||||||
|
// p2 has no next valid point, but p1 has previous valid point p0.
|
||||||
|
// B(bx,by) is the slope vector of the tangent at p1.
|
||||||
|
final float bx = (mP2X - mXCoords[p0]) / 2.0f;
|
||||||
|
final float by = (mP2Y - mYCoords[p0]) / 2.0f;
|
||||||
|
final float crossProdAB = ax * by - ay * bx;
|
||||||
|
final float dotProdAB = ax * bx + ay * by;
|
||||||
|
final float normASquare = ax * ax + ay * ay;
|
||||||
|
final float invHalfNormASquare = 1.0f / normASquare / 2.0f;
|
||||||
|
// The slope of the tangent is the mirror image of vector B to vector A.
|
||||||
|
mSlope2X = invHalfNormASquare * (dotProdAB * ax + crossProdAB * ay);
|
||||||
|
mSlope2Y = invHalfNormASquare * (dotProdAB * ay - crossProdAB * ax);
|
||||||
|
} else {
|
||||||
|
// p1 and p2 has no previous valid point. (Interval has only point p1 and p2)
|
||||||
|
mSlope2X = ax;
|
||||||
|
mSlope2Y = ay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate interpolation value at <code>t</code> in unit interval <code>[0,1]</code>.
|
||||||
|
* <p>
|
||||||
|
* On the unit interval [0,1], given a starting point p1 at t=0 and an ending point p2 at t=1
|
||||||
|
* with the slope of the tangent m1 at p1 and m2 at p2, the polynomial of cubic Hermite curve
|
||||||
|
* can be defined by
|
||||||
|
* p(t) = (1+2t)(1-t)(1-t)*p1 + t(1-t)(1-t)*m1 + (3-2t)t^2*p2 + (t-1)t^2*m2
|
||||||
|
* where t is an element of [0,1].
|
||||||
|
* <p>
|
||||||
|
* The interpolated XY-coordinates will be set in {@link #mInterpolatedX} and
|
||||||
|
* {@link #mInterpolatedY}.
|
||||||
|
*
|
||||||
|
* @param t the interpolation parameter. The value must be in close interval <code>[0,1]</code>.
|
||||||
|
*/
|
||||||
|
@UsedForTesting
|
||||||
|
public void interpolate(final float t) {
|
||||||
|
final float omt = 1.0f - t;
|
||||||
|
final float tm2 = 2.0f * t;
|
||||||
|
final float k1 = 1.0f + tm2;
|
||||||
|
final float k2 = 3.0f - tm2;
|
||||||
|
final float omt2 = omt * omt;
|
||||||
|
final float t2 = t * t;
|
||||||
|
mInterpolatedX = (k1 * mP1X + t * mSlope1X) * omt2 + (k2 * mP2X - omt * mSlope2X) * t2;
|
||||||
|
mInterpolatedY = (k1 * mP1Y + t * mSlope1Y) * omt2 + (k2 * mP2Y - omt * mSlope2Y) * t2;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,203 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 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.android.inputmethod.keyboard.internal;
|
||||||
|
|
||||||
|
import android.test.AndroidTestCase;
|
||||||
|
import android.test.suitebuilder.annotation.SmallTest;
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
public class HermiteInterpolatorTests extends AndroidTestCase {
|
||||||
|
private final HermiteInterpolator mInterpolator = new HermiteInterpolator();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setUp() throws Exception {
|
||||||
|
super.setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final float EPSLION = 0.0000005f;
|
||||||
|
|
||||||
|
private static void assertFloatEquals(final String message, float expected, float actual) {
|
||||||
|
if (Math.abs(expected - actual) >= EPSLION) {
|
||||||
|
fail(String.format("%s expected:<%s> but was:<%s>", message, expected, actual));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// t=0 p0=(0,1)
|
||||||
|
// t=1 p1=(1,0)
|
||||||
|
// t=2 p2=(3,2)
|
||||||
|
// t=3 p3=(2,3)
|
||||||
|
// y
|
||||||
|
// |
|
||||||
|
// 3 + o p3
|
||||||
|
// |
|
||||||
|
// 2 + o p2
|
||||||
|
// |
|
||||||
|
// 1 o p0
|
||||||
|
// | p1
|
||||||
|
// 0 +---o---+---+-- x
|
||||||
|
// 0 1 2 3
|
||||||
|
private final int[] mXCoords = { 0, 1, 3, 2 };
|
||||||
|
private final int[] mYCoords = { 1, 0, 2, 3 };
|
||||||
|
private static final int p0 = 0;
|
||||||
|
private static final int p1 = 1;
|
||||||
|
private static final int p2 = 2;
|
||||||
|
private static final int p3 = 3;
|
||||||
|
|
||||||
|
public void testP0P1() {
|
||||||
|
// [(p0 p1) p2 p3]
|
||||||
|
mInterpolator.reset(mXCoords, mYCoords, p0, p3 + 1);
|
||||||
|
mInterpolator.setInterval(p0 - 1, p0, p1, p1 + 1);
|
||||||
|
assertEquals("p0x", mXCoords[p0], mInterpolator.mP1X);
|
||||||
|
assertEquals("p0y", mYCoords[p0], mInterpolator.mP1Y);
|
||||||
|
assertEquals("p1x", mXCoords[p1], mInterpolator.mP2X);
|
||||||
|
assertEquals("p1y", mYCoords[p1], mInterpolator.mP2Y);
|
||||||
|
// XY-slope at p0=3.0 (-0.75/-0.25)
|
||||||
|
assertFloatEquals("slope x p0", -0.25f, mInterpolator.mSlope1X);
|
||||||
|
assertFloatEquals("slope y p0", -0.75f, mInterpolator.mSlope1Y);
|
||||||
|
// XY-slope at p1=1/3.0 (0.50/1.50)
|
||||||
|
assertFloatEquals("slope x p1", 1.50f, mInterpolator.mSlope2X);
|
||||||
|
assertFloatEquals("slope y p1", 0.50f, mInterpolator.mSlope2Y);
|
||||||
|
// t=0.0 (p0)
|
||||||
|
mInterpolator.interpolate(0.0f);
|
||||||
|
assertFloatEquals("t=0.0 x", 0.0f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.0 y", 1.0f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=0.2
|
||||||
|
mInterpolator.interpolate(0.2f);
|
||||||
|
assertFloatEquals("t=0.2 x", 0.02400f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.2 y", 0.78400f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=0.5
|
||||||
|
mInterpolator.interpolate(0.5f);
|
||||||
|
assertFloatEquals("t=0.5 x", 0.28125f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.5 y", 0.34375f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=0.8
|
||||||
|
mInterpolator.interpolate(0.8f);
|
||||||
|
assertFloatEquals("t=0.8 x", 0.69600f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.8 y", 0.01600f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=1.0 (p1)
|
||||||
|
mInterpolator.interpolate(1.0f);
|
||||||
|
assertFloatEquals("t=1.0 x", 1.0f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=1.0 y", 0.0f, mInterpolator.mInterpolatedY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testP1P2() {
|
||||||
|
// [p0 (p1 p2) p3]
|
||||||
|
mInterpolator.reset(mXCoords, mYCoords, p0, p3 + 1);
|
||||||
|
mInterpolator.setInterval(p1 - 1, p1, p2, p2 + 1);
|
||||||
|
assertEquals("p1x", mXCoords[p1], mInterpolator.mP1X);
|
||||||
|
assertEquals("p1y", mYCoords[p1], mInterpolator.mP1Y);
|
||||||
|
assertEquals("p2x", mXCoords[p2], mInterpolator.mP2X);
|
||||||
|
assertEquals("p2y", mYCoords[p2], mInterpolator.mP2Y);
|
||||||
|
// XY-slope at p1=1/3.0 (0.50/1.50)
|
||||||
|
assertFloatEquals("slope x p1", 1.50f, mInterpolator.mSlope1X);
|
||||||
|
assertFloatEquals("slope y p1", 0.50f, mInterpolator.mSlope1Y);
|
||||||
|
// XY-slope at p2=3.0 (1.50/0.50)
|
||||||
|
assertFloatEquals("slope x p2", 0.50f, mInterpolator.mSlope2X);
|
||||||
|
assertFloatEquals("slope y p2", 1.50f, mInterpolator.mSlope2Y);
|
||||||
|
// t=0.0 (p1)
|
||||||
|
mInterpolator.interpolate(0.0f);
|
||||||
|
assertFloatEquals("t=0.0 x", 1.0f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.0 y", 0.0f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=0.2
|
||||||
|
mInterpolator.interpolate(0.2f);
|
||||||
|
assertFloatEquals("t=0.2 x", 1.384f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.2 y", 0.224f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=0.5
|
||||||
|
mInterpolator.interpolate(0.5f);
|
||||||
|
assertFloatEquals("t=0.5 x", 2.125f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.5 y", 0.875f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=0.8
|
||||||
|
mInterpolator.interpolate(0.8f);
|
||||||
|
assertFloatEquals("t=0.8 x", 2.776f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.8 y", 1.616f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=1.0 (p2)
|
||||||
|
mInterpolator.interpolate(1.0f);
|
||||||
|
assertFloatEquals("t=1.0 x", 3.0f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=1.0 y", 2.0f, mInterpolator.mInterpolatedY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testP2P3() {
|
||||||
|
// [p0 p1 (p2 p3)]
|
||||||
|
mInterpolator.reset(mXCoords, mYCoords, p0, p3 + 1);
|
||||||
|
mInterpolator.setInterval(p2 - 1, p2, p3, p3 + 1);
|
||||||
|
assertEquals("p2x", mXCoords[p2], mInterpolator.mP1X);
|
||||||
|
assertEquals("p2y", mYCoords[p2], mInterpolator.mP1Y);
|
||||||
|
assertEquals("p3x", mXCoords[p3], mInterpolator.mP2X);
|
||||||
|
assertEquals("p3y", mYCoords[p3], mInterpolator.mP2Y);
|
||||||
|
// XY-slope at p2=3.0 (1.50/0.50)
|
||||||
|
assertFloatEquals("slope x p2", 0.50f, mInterpolator.mSlope1X);
|
||||||
|
assertFloatEquals("slope y p2", 1.50f, mInterpolator.mSlope1Y);
|
||||||
|
// XY-slope at p3=1/3.0 (-0.25/-0.75)
|
||||||
|
assertFloatEquals("slope x p3", -0.75f, mInterpolator.mSlope2X);
|
||||||
|
assertFloatEquals("slope y p3", -0.25f, mInterpolator.mSlope2Y);
|
||||||
|
// t=0.0 (p2)
|
||||||
|
mInterpolator.interpolate(0.0f);
|
||||||
|
assertFloatEquals("t=0.0 x", 3.0f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.0 y", 2.0f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=0.2
|
||||||
|
mInterpolator.interpolate(0.2f);
|
||||||
|
assertFloatEquals("t=0.2 x", 2.98400f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.2 y", 2.30400f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=0.5
|
||||||
|
mInterpolator.interpolate(0.5f);
|
||||||
|
assertFloatEquals("t=0.5 x", 2.65625f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.5 y", 2.71875f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=0.8
|
||||||
|
mInterpolator.interpolate(0.8f);
|
||||||
|
assertFloatEquals("t=0.8 x", 2.21600f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.8 y", 2.97600f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=1.0 (p3)
|
||||||
|
mInterpolator.interpolate(1.0f);
|
||||||
|
assertFloatEquals("t=1.0 x", 2.0f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=1.0 y", 3.0f, mInterpolator.mInterpolatedY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testJustP1P2() {
|
||||||
|
// [(p1 p2)]
|
||||||
|
mInterpolator.reset(mXCoords, mYCoords, p1, p2 + 1);
|
||||||
|
mInterpolator.setInterval(p1 - 1, p1, p2, p2 + 1);
|
||||||
|
assertEquals("p1x", mXCoords[p1], mInterpolator.mP1X);
|
||||||
|
assertEquals("p1y", mYCoords[p1], mInterpolator.mP1Y);
|
||||||
|
assertEquals("p2x", mXCoords[p2], mInterpolator.mP2X);
|
||||||
|
assertEquals("p2y", mYCoords[p2], mInterpolator.mP2Y);
|
||||||
|
// XY-slope at p1=1.0 (2.0/2.0)
|
||||||
|
assertFloatEquals("slope x p1", 2.00f, mInterpolator.mSlope1X);
|
||||||
|
assertFloatEquals("slope y p1", 2.00f, mInterpolator.mSlope1Y);
|
||||||
|
// XY-slope at p2=1.0 (2.0/2.0)
|
||||||
|
assertFloatEquals("slope x p2", 2.00f, mInterpolator.mSlope2X);
|
||||||
|
assertFloatEquals("slope y p2", 2.00f, mInterpolator.mSlope2Y);
|
||||||
|
// t=0.0 (p1)
|
||||||
|
mInterpolator.interpolate(0.0f);
|
||||||
|
assertFloatEquals("t=0.0 x", 1.0f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.0 y", 0.0f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=0.2
|
||||||
|
mInterpolator.interpolate(0.2f);
|
||||||
|
assertFloatEquals("t=0.2 x", 1.4f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.2 y", 0.4f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=0.5
|
||||||
|
mInterpolator.interpolate(0.5f);
|
||||||
|
assertFloatEquals("t=0.5 x", 2.0f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.5 y", 1.0f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=0.8
|
||||||
|
mInterpolator.interpolate(0.8f);
|
||||||
|
assertFloatEquals("t=0.8 x", 2.6f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=0.8 y", 1.6f, mInterpolator.mInterpolatedY);
|
||||||
|
// t=1.0 (p2)
|
||||||
|
mInterpolator.interpolate(1.0f);
|
||||||
|
assertFloatEquals("t=1.0 x", 3.0f, mInterpolator.mInterpolatedX);
|
||||||
|
assertFloatEquals("t=1.0 y", 2.0f, mInterpolator.mInterpolatedY);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue