diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 4975d65402cd4d2302be7c87fe8b71289a8462c7..cced45d9f50eb945bafed0aaa327fe6bece68d19 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -131,6 +131,12 @@
         <attr name="gestureFloatingPreviewTextConnectorWidth" format="dimension" />
         <!-- Delay after gesture input and gesture floating preview text dismissing in millisecond -->
         <attr name="gestureFloatingPreviewTextLingerTimeout" format="integer" />
+        <!-- Delay after gesture trail starts fading out in millisecond. -->
+        <attr name="gesturePreviewTrailFadeoutStartDelay" format="integer" />
+        <!-- Duration while gesture preview trail is fading out in millisecond. -->
+        <attr name="gesturePreviewTrailFadeoutDuration" format="integer" />
+        <!-- Interval of updating gesture preview trail in millisecond. -->
+        <attr name="gesturePreviewTrailUpdateInterval" format="integer" />
         <attr name="gesturePreviewTrailColor" format="color" />
         <attr name="gesturePreviewTrailWidth" format="dimension" />
     </declare-styleable>
diff --git a/java/res/values/config.xml b/java/res/values/config.xml
index 54a6687a3d459eeeb75fa13dd703dba1d23bfd3a..8477df054405ce36eac7d160a826318d8905af16 100644
--- a/java/res/values/config.xml
+++ b/java/res/values/config.xml
@@ -50,6 +50,9 @@
     -->
     <integer name="config_key_preview_linger_timeout">70</integer>
     <integer name="config_gesture_floating_preview_text_linger_timeout">200</integer>
+    <integer name="config_gesture_preview_trail_fadeout_start_delay">100</integer>
+    <integer name="config_gesture_preview_trail_fadeout_duration">1000</integer>
+    <integer name="config_gesture_preview_trail_update_interval">20</integer>
     <!--
          Configuration for MainKeyboardView
     -->
diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml
index ae67c4369ee865f33e5db7d4259deff1888c2bc6..e5e7fed805681c7f54cc1b386be38f2cc82edcdc 100644
--- a/java/res/values/styles.xml
+++ b/java/res/values/styles.xml
@@ -78,6 +78,9 @@
         <item name="gestureFloatingPreviewTextConnectorColor">@android:color/white</item>
         <item name="gestureFloatingPreviewTextConnectorWidth">@dimen/gesture_floating_preview_text_connector_width</item>
         <item name="gestureFloatingPreviewTextLingerTimeout">@integer/config_gesture_floating_preview_text_linger_timeout</item>
+        <item name="gesturePreviewTrailFadeoutStartDelay">@integer/config_gesture_preview_trail_fadeout_start_delay</item>
+        <item name="gesturePreviewTrailFadeoutDuration">@integer/config_gesture_preview_trail_fadeout_duration</item>
+        <item name="gesturePreviewTrailUpdateInterval">@integer/config_gesture_preview_trail_update_interval</item>
         <item name="gesturePreviewTrailColor">@android:color/holo_blue_light</item>
         <item name="gesturePreviewTrailWidth">@dimen/gesture_preview_trail_width</item>
         <!-- Common attributes of MainKeyboardView -->
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index 67f37a7e7f64c842802985867166fd341a176b5e..b5b3ef65f6dd00117e98b544d788baa8b8bb6634 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -17,14 +17,13 @@
 package com.android.inputmethod.keyboard;
 
 import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Paint;
 import android.os.SystemClock;
 import android.util.Log;
 import android.view.MotionEvent;
 
 import com.android.inputmethod.accessibility.AccessibilityUtils;
 import com.android.inputmethod.keyboard.internal.GestureStroke;
+import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewTrail;
 import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
 import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.InputPointers;
@@ -211,7 +210,7 @@ public class PointerTracker implements PointerTrackerQueue.Element {
     private static final KeyboardActionListener EMPTY_LISTENER =
             new KeyboardActionListener.Adapter();
 
-    private final GestureStroke mGestureStroke;
+    private final GestureStrokeWithPreviewTrail mGestureStrokeWithPreviewTrail;
 
     public static void init(boolean hasDistinctMultitouch,
             boolean needsPhantomSuddenMoveEventHack) {
@@ -297,7 +296,8 @@ public class PointerTracker implements PointerTrackerQueue.Element {
         final int trackersSize = sTrackers.size();
         for (int i = 0; i < trackersSize; ++i) {
             final PointerTracker tracker = sTrackers.get(i);
-            tracker.mGestureStroke.appendIncrementalBatchPoints(sAggregratedPointers);
+            tracker.mGestureStrokeWithPreviewTrail.appendIncrementalBatchPoints(
+                    sAggregratedPointers);
         }
         return sAggregratedPointers;
     }
@@ -308,7 +308,7 @@ public class PointerTracker implements PointerTrackerQueue.Element {
         final int trackersSize = sTrackers.size();
         for (int i = 0; i < trackersSize; ++i) {
             final PointerTracker tracker = sTrackers.get(i);
-            tracker.mGestureStroke.appendAllBatchPoints(sAggregratedPointers);
+            tracker.mGestureStrokeWithPreviewTrail.appendAllBatchPoints(sAggregratedPointers);
         }
         return sAggregratedPointers;
     }
@@ -319,7 +319,7 @@ public class PointerTracker implements PointerTrackerQueue.Element {
         final int trackersSize = sTrackers.size();
         for (int i = 0; i < trackersSize; ++i) {
             final PointerTracker tracker = sTrackers.get(i);
-            tracker.mGestureStroke.reset();
+            tracker.mGestureStrokeWithPreviewTrail.reset();
         }
         sAggregratedPointers.reset();
     }
@@ -329,7 +329,7 @@ public class PointerTracker implements PointerTrackerQueue.Element {
             throw new NullPointerException();
         }
         mPointerId = id;
-        mGestureStroke = new GestureStroke(id);
+        mGestureStrokeWithPreviewTrail = new GestureStrokeWithPreviewTrail(id);
         setKeyDetectorInner(handler.getKeyDetector());
         mListener = handler.getKeyboardActionListener();
         mDrawingProxy = handler.getDrawingProxy();
@@ -429,7 +429,7 @@ public class PointerTracker implements PointerTrackerQueue.Element {
         mKeyDetector = keyDetector;
         mKeyboard = keyDetector.getKeyboard();
         mIsAlphabetKeyboard = mKeyboard.mId.isAlphabetKeyboard();
-        mGestureStroke.setGestureSampleLength(mKeyboard.mMostCommonKeyWidth);
+        mGestureStrokeWithPreviewTrail.setGestureSampleLength(mKeyboard.mMostCommonKeyWidth);
         final Key newKey = mKeyDetector.detectHitKey(mKeyX, mKeyY);
         if (newKey != mCurrentKey) {
             if (mDrawingProxy != null) {
@@ -539,10 +539,8 @@ public class PointerTracker implements PointerTrackerQueue.Element {
         mDrawingProxy.invalidateKey(key);
     }
 
-    public void drawGestureTrail(final Canvas canvas, final Paint paint) {
-        if (mInGesture) {
-            mGestureStroke.drawGestureTrail(canvas, paint);
-        }
+    public GestureStrokeWithPreviewTrail getGestureStrokeWithPreviewTrail() {
+        return mGestureStrokeWithPreviewTrail;
     }
 
     public int getLastX() {
@@ -692,7 +690,7 @@ public class PointerTracker implements PointerTrackerQueue.Element {
                 mIsPossibleGesture = true;
                 // TODO: pointer times should be relative to first down even in entire batch input
                 // instead of resetting to 0 for each new down event.
-                mGestureStroke.addPoint(x, y, 0, false);
+                mGestureStrokeWithPreviewTrail.addPoint(x, y, 0, false);
             }
         }
     }
@@ -733,7 +731,7 @@ public class PointerTracker implements PointerTrackerQueue.Element {
             final long eventTime, final boolean isHistorical, final Key key) {
         final int gestureTime = (int)(eventTime - tracker.getDownTime());
         if (sShouldHandleGesture && mIsPossibleGesture) {
-            final GestureStroke stroke = mGestureStroke;
+            final GestureStroke stroke = mGestureStrokeWithPreviewTrail;
             stroke.addPoint(x, y, gestureTime, isHistorical);
             if (!mInGesture && stroke.isStartOfAGesture()) {
                 startBatchInput();
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
new file mode 100644
index 0000000000000000000000000000000000000000..edc20998fdd3a6cb5683cc07a8bd36442b333caa
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2012 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.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.os.SystemClock;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResizableIntArray;
+
+class GesturePreviewTrail {
+    private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewTrail.PREVIEW_CAPACITY;
+
+    private final GesturePreviewTrailParams mPreviewParams;
+    private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
+    private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
+    private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
+    private int mCurrentStrokeId;
+    private long mCurrentDownTime;
+
+    // Use this value as imaginary zero because x-coordinates may be zero.
+    private static final int DOWN_EVENT_MARKER = -128;
+
+    static class GesturePreviewTrailParams {
+        public final int mFadeoutStartDelay;
+        public final int mFadeoutDuration;
+        public final int mUpdateInterval;
+
+        public GesturePreviewTrailParams(final TypedArray keyboardViewAttr) {
+            mFadeoutStartDelay = keyboardViewAttr.getInt(
+                    R.styleable.KeyboardView_gesturePreviewTrailFadeoutStartDelay, 0);
+            mFadeoutDuration = keyboardViewAttr.getInt(
+                    R.styleable.KeyboardView_gesturePreviewTrailFadeoutDuration, 0);
+            mUpdateInterval = keyboardViewAttr.getInt(
+                    R.styleable.KeyboardView_gesturePreviewTrailUpdateInterval, 0);
+        }
+    }
+
+    public GesturePreviewTrail(final GesturePreviewTrailParams params) {
+        mPreviewParams = params;
+    }
+
+    private static int markAsDownEvent(final int xCoord) {
+        return DOWN_EVENT_MARKER - xCoord;
+    }
+
+    private static boolean isDownEventXCoord(final int xCoordOrMark) {
+        return xCoordOrMark <= DOWN_EVENT_MARKER;
+    }
+
+    private static int getXCoordValue(final int xCoordOrMark) {
+        return isDownEventXCoord(xCoordOrMark)
+                ? DOWN_EVENT_MARKER - xCoordOrMark : xCoordOrMark;
+    }
+
+    public void addStroke(final GestureStrokeWithPreviewTrail stroke, final long downTime) {
+        final int strokeId = stroke.getGestureStrokeId();
+        final boolean isNewStroke = strokeId != mCurrentStrokeId;
+        final int trailSize = mEventTimes.getLength();
+        stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates);
+        final int newTrailSize = mEventTimes.getLength();
+        if (stroke.getGestureStrokePreviewSize() == 0) {
+            return;
+        }
+        if (isNewStroke) {
+            final int elapsedTime = (int)(downTime - mCurrentDownTime);
+            final int[] eventTimes = mEventTimes.getPrimitiveArray();
+            for (int i = 0; i < trailSize; i++) {
+                eventTimes[i] -= elapsedTime;
+            }
+
+            if (newTrailSize > trailSize) {
+                final int[] xCoords = mXCoordinates.getPrimitiveArray();
+                xCoords[trailSize] = markAsDownEvent(xCoords[trailSize]);
+            }
+            mCurrentDownTime = downTime;
+            mCurrentStrokeId = strokeId;
+        }
+    }
+
+    private int getAlpha(final int elapsedTime) {
+        if (elapsedTime < mPreviewParams.mFadeoutStartDelay) {
+            return Constants.Color.ALPHA_OPAQUE;
+        }
+        final int decreasingAlpha = Constants.Color.ALPHA_OPAQUE
+                * (elapsedTime - mPreviewParams.mFadeoutStartDelay)
+                / mPreviewParams.mFadeoutDuration;
+        return Constants.Color.ALPHA_OPAQUE - decreasingAlpha;
+    }
+
+    /**
+     * Draw gesture preview trail
+     * @param canvas The canvas to draw the gesture preview trail
+     * @param paint The paint object to be used to draw the gesture preview trail
+     * @return true if some gesture preview trails remain to be drawn
+     */
+    public boolean drawGestureTrail(final Canvas canvas, final Paint paint) {
+        final int trailSize = mEventTimes.getLength();
+        if (trailSize == 0) {
+            return false;
+        }
+
+        final int[] eventTimes = mEventTimes.getPrimitiveArray();
+        final int[] xCoords = mXCoordinates.getPrimitiveArray();
+        final int[] yCoords = mYCoordinates.getPrimitiveArray();
+        final int sinceDown = (int)(SystemClock.uptimeMillis() - mCurrentDownTime);
+        final int lingeringDuration = mPreviewParams.mFadeoutStartDelay
+                + mPreviewParams.mFadeoutDuration;
+        int startIndex;
+        for (startIndex = 0; startIndex < trailSize; startIndex++) {
+            final int elapsedTime = sinceDown - eventTimes[startIndex];
+            // Skip too old trail points.
+            if (elapsedTime < lingeringDuration) {
+                break;
+            }
+        }
+
+        if (startIndex < trailSize) {
+            int lastX = getXCoordValue(xCoords[startIndex]);
+            int lastY = yCoords[startIndex];
+            for (int i = startIndex + 1; i < trailSize - 1; i++) {
+                final int x = xCoords[i];
+                final int y = yCoords[i];
+                final int elapsedTime = sinceDown - eventTimes[i];
+                // Draw trail line only when the current point isn't a down point.
+                if (!isDownEventXCoord(x)) {
+                    paint.setAlpha(getAlpha(elapsedTime));
+                    canvas.drawLine(lastX, lastY, x, y, paint);
+                }
+                lastX = getXCoordValue(x);
+                lastY = y;
+            }
+        }
+
+        // TODO: Implement ring buffer to avoid moving points.
+        // Discard faded out points.
+        final int newSize = trailSize - startIndex;
+        System.arraycopy(eventTimes, startIndex, eventTimes, 0, newSize);
+        System.arraycopy(xCoords, startIndex, xCoords, 0, newSize);
+        System.arraycopy(yCoords, startIndex, yCoords, 0, newSize);
+        mEventTimes.setLength(newSize);
+        mXCoordinates.setLength(newSize);
+        mYCoordinates.setLength(newSize);
+        return newSize > 0;
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
index 79e977a4037c2282ee81283d62a7285bd6bf3dd5..292842d22875607c6123ae489f0e897957f184ed 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
@@ -14,10 +14,6 @@
 
 package com.android.inputmethod.keyboard.internal;
 
-import android.graphics.Canvas;
-import android.graphics.Paint;
-
-import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.InputPointers;
 import com.android.inputmethod.latin.ResizableIntArray;
 
@@ -48,13 +44,8 @@ public class GestureStroke {
 
     private static final float DOUBLE_PI = (float)(2.0f * Math.PI);
 
-    // Fade based on number of gesture samples, see MIN_GESTURE_SAMPLING_RATIO_TO_KEY_HEIGHT
-    private static final int DRAWING_GESTURE_FADE_START = 10;
-    private static final int DRAWING_GESTURE_FADE_RATE = 6;
-
-    public GestureStroke(int pointerId) {
+    public GestureStroke(final int pointerId) {
         mPointerId = pointerId;
-        reset();
     }
 
     public void setGestureSampleLength(final int keyWidth) {
@@ -158,7 +149,7 @@ public class GestureStroke {
         if (dx == 0 && dy == 0) return 0;
         // Would it be faster to call atan2f() directly via JNI?  Not sure about what the JIT
         // does with Math.atan2().
-        return (float)Math.atan2((double)dy, (double)dx);
+        return (float)Math.atan2(dy, dx);
     }
 
     private static float getAngleDiff(final float a1, final float a2) {
@@ -168,20 +159,4 @@ public class GestureStroke {
         }
         return diff;
     }
-
-    public void drawGestureTrail(final Canvas canvas, final Paint paint) {
-        // TODO: These paint parameter interpolation should be tunable, possibly introduce an object
-        // that implements an interface such as Paint getPaint(int step, int strokePoints)
-        final int size = mXCoordinates.getLength();
-        final int[] xCoords = mXCoordinates.getPrimitiveArray();
-        final int[] yCoords = mYCoordinates.getPrimitiveArray();
-        int alpha = Constants.Color.ALPHA_OPAQUE;
-        for (int i = size - 1; i > 0 && alpha > 0; i--) {
-            paint.setAlpha(alpha);
-            if (size - i > DRAWING_GESTURE_FADE_START) {
-                alpha -= DRAWING_GESTURE_FADE_RATE;
-            }
-            canvas.drawLine(xCoords[i - 1], yCoords[i - 1], xCoords[i], yCoords[i], paint);
-        }
-    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewTrail.java
new file mode 100644
index 0000000000000000000000000000000000000000..6c1a9bc013133dc3302e4df8f7f9a653297ab9a8
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewTrail.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 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.latin.ResizableIntArray;
+
+public class GestureStrokeWithPreviewTrail extends GestureStroke {
+    public static final int PREVIEW_CAPACITY = 256;
+
+    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;
+
+    public GestureStrokeWithPreviewTrail(final int pointerId) {
+        super(pointerId);
+    }
+
+    @Override
+    public void reset() {
+        super.reset();
+        mStrokeId++;
+        mLastPreviewSize = 0;
+        mPreviewEventTimes.setLength(0);
+        mPreviewXCoordinates.setLength(0);
+        mPreviewYCoordinates.setLength(0);
+    }
+
+    public int getGestureStrokeId() {
+        return mStrokeId;
+    }
+
+    public int getGestureStrokePreviewSize() {
+        return mPreviewEventTimes.getLength();
+    }
+
+    @Override
+    public void addPoint(final int x, final int y, final int time, final boolean isHistorical) {
+        super.addPoint(x, y, time, isHistorical);
+        mPreviewEventTimes.add(time);
+        mPreviewXCoordinates.add(x);
+        mPreviewYCoordinates.add(y);
+    }
+
+    public void appendPreviewStroke(final ResizableIntArray eventTimes,
+            final ResizableIntArray xCoords, final ResizableIntArray yCoords) {
+        final int length = mPreviewEventTimes.getLength() - mLastPreviewSize;
+        if (length <= 0) {
+            return;
+        }
+        eventTimes.append(mPreviewEventTimes, mLastPreviewSize, length);
+        xCoords.append(mPreviewXCoordinates, mLastPreviewSize, length);
+        yCoords.append(mPreviewYCoordinates, mLastPreviewSize, length);
+        mLastPreviewSize = mPreviewEventTimes.getLength();
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
index 3f33aee5ac5dd69552c6d45b7ce0986aeacff1d5..269b202b50eaa39cabee988f519551d99f686269 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
@@ -28,6 +28,7 @@ import android.util.SparseArray;
 import android.widget.RelativeLayout;
 
 import com.android.inputmethod.keyboard.PointerTracker;
+import com.android.inputmethod.keyboard.internal.GesturePreviewTrail.GesturePreviewTrailParams;
 import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
@@ -48,7 +49,9 @@ public class PreviewPlacerView extends RelativeLayout {
     private int mXOrigin;
     private int mYOrigin;
 
-    private final SparseArray<PointerTracker> mPointers = CollectionUtils.newSparseArray();
+    private final SparseArray<GesturePreviewTrail> mGesturePreviewTrails =
+            CollectionUtils.newSparseArray();
+    private final GesturePreviewTrailParams mGesturePreviewTrailParams;
 
     private String mGestureFloatingPreviewText;
     private int mLastPointerX;
@@ -57,23 +60,31 @@ public class PreviewPlacerView extends RelativeLayout {
     private boolean mDrawsGesturePreviewTrail;
     private boolean mDrawsGestureFloatingPreviewText;
 
-    private final DrawingHandler mDrawingHandler = new DrawingHandler(this);
+    private final DrawingHandler mDrawingHandler;
 
     private static class DrawingHandler extends StaticInnerHandlerWrapper<PreviewPlacerView> {
         private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 0;
+        private static final int MSG_UPDATE_GESTURE_PREVIEW_TRAIL = 1;
 
-        public DrawingHandler(PreviewPlacerView outerInstance) {
+        private final GesturePreviewTrailParams mGesturePreviewTrailParams;
+
+        public DrawingHandler(final PreviewPlacerView outerInstance,
+                final GesturePreviewTrailParams gesturePreviewTrailParams) {
             super(outerInstance);
+            mGesturePreviewTrailParams = gesturePreviewTrailParams;
         }
 
         @Override
-        public void handleMessage(Message msg) {
+        public void handleMessage(final Message msg) {
             final PreviewPlacerView placerView = getOuterInstance();
             if (placerView == null) return;
             switch (msg.what) {
             case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT:
                 placerView.setGestureFloatingPreviewText(null);
                 break;
+            case MSG_UPDATE_GESTURE_PREVIEW_TRAIL:
+                placerView.invalidate();
+                break;
             }
         }
 
@@ -89,16 +100,27 @@ public class PreviewPlacerView extends RelativeLayout {
                     placerView.mGestureFloatingPreviewTextLingerTimeout);
         }
 
+        private void cancelUpdateGestureTrailPreview() {
+            removeMessages(MSG_UPDATE_GESTURE_PREVIEW_TRAIL);
+        }
+
+        public void postUpdateGestureTrailPreview() {
+            cancelUpdateGestureTrailPreview();
+            sendMessageDelayed(obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_TRAIL),
+                    mGesturePreviewTrailParams.mUpdateInterval);
+        }
+
         public void cancelAllMessages() {
             cancelDismissGestureFloatingPreviewText();
+            cancelUpdateGestureTrailPreview();
         }
     }
 
-    public PreviewPlacerView(Context context, AttributeSet attrs) {
+    public PreviewPlacerView(final Context context, final AttributeSet attrs) {
         this(context, attrs, R.attr.keyboardViewStyle);
     }
 
-    public PreviewPlacerView(Context context, AttributeSet attrs, int defStyle) {
+    public PreviewPlacerView(final Context context, final AttributeSet attrs, final int defStyle) {
         super(context);
         setWillNotDraw(false);
 
@@ -128,8 +150,11 @@ public class PreviewPlacerView extends RelativeLayout {
                 R.styleable.KeyboardView_gesturePreviewTrailColor, 0);
         final int gesturePreviewTrailWidth = keyboardViewAttr.getDimensionPixelSize(
                 R.styleable.KeyboardView_gesturePreviewTrailWidth, 0);
+        mGesturePreviewTrailParams = new GesturePreviewTrailParams(keyboardViewAttr);
         keyboardViewAttr.recycle();
 
+        mDrawingHandler = new DrawingHandler(this, mGesturePreviewTrailParams);
+
         mGesturePaint = new Paint();
         mGesturePaint.setAntiAlias(true);
         mGesturePaint.setStyle(Paint.Style.STROKE);
@@ -144,21 +169,28 @@ public class PreviewPlacerView extends RelativeLayout {
         mTextPaint.setTextSize(gestureFloatingPreviewTextSize);
     }
 
-    public void setOrigin(int x, int y) {
+    public void setOrigin(final int x, final int y) {
         mXOrigin = x;
         mYOrigin = y;
     }
 
-    public void setGesturePreviewMode(boolean drawsGesturePreviewTrail,
-            boolean drawsGestureFloatingPreviewText) {
+    public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail,
+            final boolean drawsGestureFloatingPreviewText) {
         mDrawsGesturePreviewTrail = drawsGesturePreviewTrail;
         mDrawsGestureFloatingPreviewText = drawsGestureFloatingPreviewText;
     }
 
-    public void invalidatePointer(PointerTracker tracker) {
-        synchronized (mPointers) {
-            mPointers.put(tracker.mPointerId, tracker);
+    public void invalidatePointer(final PointerTracker tracker) {
+        GesturePreviewTrail trail;
+        synchronized (mGesturePreviewTrails) {
+            trail = mGesturePreviewTrails.get(tracker.mPointerId);
+            if (trail == null) {
+                trail = new GesturePreviewTrail(mGesturePreviewTrailParams);
+                mGesturePreviewTrails.put(tracker.mPointerId, trail);
+            }
         }
+        trail.addStroke(tracker.getGestureStrokeWithPreviewTrail(), tracker.getDownTime());
+
         mLastPointerX = tracker.getLastX();
         mLastPointerY = tracker.getLastY();
         // TODO: Should narrow the invalidate region.
@@ -166,17 +198,23 @@ public class PreviewPlacerView extends RelativeLayout {
     }
 
     @Override
-    public void onDraw(Canvas canvas) {
+    public void onDraw(final Canvas canvas) {
         super.onDraw(canvas);
         canvas.translate(mXOrigin, mYOrigin);
         if (mDrawsGesturePreviewTrail) {
-            synchronized (mPointers) {
-                final int trackerCount = mPointers.size();
-                for (int index = 0; index < trackerCount; index++) {
-                    final PointerTracker tracker = mPointers.valueAt(index);
-                    tracker.drawGestureTrail(canvas, mGesturePaint);
+            boolean needsUpdatingGesturePreviewTrail = false;
+            synchronized (mGesturePreviewTrails) {
+                // Trails count == fingers count that have ever been active.
+                final int trailsCount = mGesturePreviewTrails.size();
+                for (int index = 0; index < trailsCount; index++) {
+                    final GesturePreviewTrail trail = mGesturePreviewTrails.valueAt(index);
+                    needsUpdatingGesturePreviewTrail |=
+                            trail.drawGestureTrail(canvas, mGesturePaint);
                 }
             }
+            if (needsUpdatingGesturePreviewTrail) {
+                mDrawingHandler.postUpdateGestureTrailPreview();
+            }
         }
         if (mDrawsGestureFloatingPreviewText) {
             drawGestureFloatingPreviewText(canvas, mGestureFloatingPreviewText);
@@ -184,7 +222,7 @@ public class PreviewPlacerView extends RelativeLayout {
         canvas.translate(-mXOrigin, -mYOrigin);
     }
 
-    public void setGestureFloatingPreviewText(String gestureFloatingPreviewText) {
+    public void setGestureFloatingPreviewText(final String gestureFloatingPreviewText) {
         mGestureFloatingPreviewText = gestureFloatingPreviewText;
         invalidate();
     }
@@ -197,7 +235,8 @@ public class PreviewPlacerView extends RelativeLayout {
         mDrawingHandler.cancelAllMessages();
     }
 
-    private void drawGestureFloatingPreviewText(Canvas canvas, String gestureFloatingPreviewText) {
+    private void drawGestureFloatingPreviewText(final Canvas canvas,
+            final String gestureFloatingPreviewText) {
         if (TextUtils.isEmpty(gestureFloatingPreviewText)) {
             return;
         }