Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 75028af2 authored by Tadashi G. Takaoka's avatar Tadashi G. Takaoka
Browse files

Interpolate gesture preview trails

There is the boolean flag to kill interpolation.

Bug: 7167303
Change-Id: Iac7e4cb88cf437c2ee77c003c9cddb92416025c7
parent 43341ba0
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ final class GesturePreviewTrail {
    // The wall time of the zero value in {@link #mEventTimes}
    private long mCurrentTimeBase;
    private int mTrailStartIndex;
    private int mLastInterpolatedDrawIndex;

    static final class Params {
        public final int mTrailColor;
@@ -96,6 +97,17 @@ final class GesturePreviewTrail {
        }
        final int[] eventTimes = mEventTimes.getPrimitiveArray();
        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) {
            final int elapsedTime = (int)(downTime - mCurrentTimeBase);
            for (int i = mTrailStartIndex; i < trailSize; i++) {
@@ -216,6 +228,10 @@ final class GesturePreviewTrail {
                System.arraycopy(eventTimes, startIndex, eventTimes, 0, newSize);
                System.arraycopy(xCoords, startIndex, xCoords, 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);
            mXCoordinates.setLength(newSize);
+122 −16
Original line number Diff line number Diff line
@@ -21,19 +21,32 @@ import com.android.inputmethod.latin.ResizableIntArray;
public final class GestureStrokeWithPreviewPoints extends GestureStroke {
    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 mPreviewXCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
    private final ResizableIntArray mPreviewYCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);

    private int mStrokeId;
    private int mLastPreviewSize;
    private final HermiteInterpolator mInterpolator = new HermiteInterpolator();
    private int mLastInterpolatedPreviewIndex;

    private int mMinPreviewSampleLengthSquare;
    private int mMinPreviewSamplingDistanceSquared;
    private int mLastX;
    private int mLastY;
    private double mMinPreviewSamplingDistance;
    private double mDistanceFromLastSample;

    // TODO: Move this to resource.
    private static final float MIN_PREVIEW_SAMPLE_LENGTH_RATIO_TO_KEY_WIDTH = 0.1f;
    // TODO: Move these constants to resource.
    // 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) {
        super(pointerId, params);
@@ -44,6 +57,7 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
        super.reset();
        mStrokeId++;
        mLastPreviewSize = 0;
        mLastInterpolatedPreviewIndex = 0;
        mPreviewEventTimes.setLength(0);
        mPreviewXCoordinates.setLength(0);
        mPreviewYCoordinates.setLength(0);
@@ -53,35 +67,49 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
        return mStrokeId;
    }

    public int getGestureStrokePreviewSize() {
        return mPreviewEventTimes.getLength();
    }

    @Override
    public void setKeyboardGeometry(final int keyWidth, final int keyboardHeight) {
        super.setKeyboardGeometry(keyWidth, keyboardHeight);
        final float sampleLength = keyWidth * MIN_PREVIEW_SAMPLE_LENGTH_RATIO_TO_KEY_WIDTH;
        mMinPreviewSampleLengthSquare = (int)(sampleLength * sampleLength);
        final float samplingRatioToKeyWidth = ENABLE_INTERPOLATION
                ? 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, 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;
        }

    private boolean needsSampling(final int x, final int y) {
        final int dx = x - mLastX;
        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
    public boolean addPointOnKeyboard(final int x, final int y, final int time,
            final boolean isMajorEvent) {
        final boolean onValidArea = super.addPointOnKeyboard(x, y, time, isMajorEvent);
        if (isMajorEvent || needsSampling(x, y)) {
        if (needsSampling(x, y, isMajorEvent)) {
            mPreviewEventTimes.add(time);
            mPreviewXCoordinates.add(x);
            mPreviewYCoordinates.add(y);
            mLastX = x;
            mLastY = y;
        }
        return onValidArea;
        return super.addPointOnKeyboard(x, y, time, isMajorEvent);

    }

    public void appendPreviewStroke(final ResizableIntArray eventTimes,
@@ -95,4 +123,82 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke {
        yCoords.append(mPreviewYCoordinates, mLastPreviewSize, length);
        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;
    }
}
+166 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+203 −0

File added.

Preview size limit exceeded, changes collapsed.