Add gesture trail feedback.

Change-Id: I32709fac0dec3165678a052aa286e2fb3d90721b
This commit is contained in:
Tom Ouyang 2012-07-19 17:20:54 +09:00
parent 48ded4e3b1
commit 4daf32b6c0
5 changed files with 132 additions and 13 deletions

View file

@ -38,6 +38,7 @@ import android.view.ViewGroup;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.StaticInnerHandlerWrapper; import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
@ -94,7 +95,8 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
// The maximum key label width in the proportion to the key width. // The maximum key label width in the proportion to the key width.
private static final float MAX_LABEL_RATIO = 0.90f; private static final float MAX_LABEL_RATIO = 0.90f;
public final static int ALPHA_OPAQUE = 255; private final static int GESTURE_DRAWING_WIDTH = 5;
private final static int GESTURE_DRAWING_COLOR = 0xff33b5e5;
// Main keyboard // Main keyboard
private Keyboard mKeyboard; private Keyboard mKeyboard;
@ -118,11 +120,14 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
private final HashSet<Key> mInvalidatedKeys = new HashSet<Key>(); private final HashSet<Key> mInvalidatedKeys = new HashSet<Key>();
/** The region of invalidated keys */ /** The region of invalidated keys */
private final Rect mInvalidatedKeysRect = new Rect(); private final Rect mInvalidatedKeysRect = new Rect();
/** The region of invalidated gestures */
private final Rect mInvalidatedGesturesRect = new Rect();
/** The keyboard bitmap buffer for faster updates */ /** The keyboard bitmap buffer for faster updates */
private Bitmap mBuffer; private Bitmap mBuffer;
/** The canvas for the above mutable keyboard bitmap */ /** The canvas for the above mutable keyboard bitmap */
private Canvas mCanvas; private Canvas mCanvas;
private final Paint mPaint = new Paint(); private final Paint mPaint = new Paint();
private final Paint mGesturePaint = new Paint();
private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics(); private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics();
// This sparse array caches key label text height in pixel indexed by key label text size. // This sparse array caches key label text height in pixel indexed by key label text size.
private static final SparseArray<Float> sTextHeightCache = new SparseArray<Float>(); private static final SparseArray<Float> sTextHeightCache = new SparseArray<Float>();
@ -264,7 +269,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
public void blendAlpha(Paint paint) { public void blendAlpha(Paint paint) {
final int color = paint.getColor(); final int color = paint.getColor();
paint.setARGB((paint.getAlpha() * mAnimAlpha) / ALPHA_OPAQUE, paint.setARGB((paint.getAlpha() * mAnimAlpha) / Constants.Color.ALPHA_OPAQUE,
Color.red(color), Color.green(color), Color.blue(color)); Color.red(color), Color.green(color), Color.blue(color));
} }
} }
@ -372,6 +377,13 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
mDelayAfterPreview = mKeyPreviewDrawParams.mLingerTimeout; mDelayAfterPreview = mKeyPreviewDrawParams.mLingerTimeout;
mPaint.setAntiAlias(true); mPaint.setAntiAlias(true);
// TODO: These paint parameters should be specified via attribute of the view and styleable.
mGesturePaint.setAntiAlias(true);
mGesturePaint.setStyle(Paint.Style.STROKE);
mGesturePaint.setStrokeJoin(Paint.Join.ROUND);
mGesturePaint.setColor(GESTURE_DRAWING_COLOR);
mGesturePaint.setStrokeWidth(GESTURE_DRAWING_WIDTH);
} }
// Read fraction value in TypedArray as float. // Read fraction value in TypedArray as float.
@ -517,7 +529,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
final int keyDrawY = key.mY + getPaddingTop(); final int keyDrawY = key.mY + getPaddingTop();
canvas.translate(keyDrawX, keyDrawY); canvas.translate(keyDrawX, keyDrawY);
params.mAnimAlpha = ALPHA_OPAQUE; params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE;
if (!key.isSpacer()) { if (!key.isSpacer()) {
onDrawKeyBackground(key, canvas, params); onDrawKeyBackground(key, canvas, params);
} }
@ -860,17 +872,60 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker); mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker);
} }
private static class PreviewView extends RelativeLayout {
KeyPreviewDrawParams mParams;
Paint mGesturePaint;
public PreviewView(Context context, KeyPreviewDrawParams params, Paint gesturePaint) {
super(context);
setWillNotDraw(false);
mParams = params;
mGesturePaint = gesturePaint;
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(mParams.mCoordinates[0], mParams.mCoordinates[1]);
PointerTracker.drawGestureTrailForAllPointerTrackers(canvas, mGesturePaint);
}
}
private void addKeyPreview(TextView keyPreview) { private void addKeyPreview(TextView keyPreview) {
if (mPreviewPlacer == null) { if (mPreviewPlacer == null) {
mPreviewPlacer = new RelativeLayout(getContext()); createPreviewPlacer();
final ViewGroup windowContentView =
(ViewGroup)getRootView().findViewById(android.R.id.content);
windowContentView.addView(mPreviewPlacer);
} }
mPreviewPlacer.addView( mPreviewPlacer.addView(
keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacer, 0, 0)); keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacer, 0, 0));
} }
private void createPreviewPlacer() {
mPreviewPlacer = new PreviewView(getContext(), mKeyPreviewDrawParams, mGesturePaint);
final ViewGroup windowContentView =
(ViewGroup)getRootView().findViewById(android.R.id.content);
windowContentView.addView(mPreviewPlacer);
}
@Override
public void showGestureTrail(PointerTracker tracker) {
if (mPreviewPlacer == null) {
createPreviewPlacer();
}
final Rect r = tracker.getDrawingRect();
if (!r.isEmpty()) {
// Invalidate the rectangular region encompassing the gesture. This is needed because
// past points along the gesture will fade and gradually disappear.
final KeyPreviewDrawParams params = mKeyPreviewDrawParams;
mInvalidatedGesturesRect.set(r.left + params.mCoordinates[0] - GESTURE_DRAWING_WIDTH,
r.top + params.mCoordinates[1] - GESTURE_DRAWING_WIDTH,
r.right + params.mCoordinates[0] + GESTURE_DRAWING_WIDTH,
r.bottom + params.mCoordinates[1] + GESTURE_DRAWING_WIDTH);
mPreviewPlacer.invalidate(mInvalidatedGesturesRect);
} else {
mPreviewPlacer.invalidate();
}
}
@SuppressWarnings("deprecation") // setBackgroundDrawable is replaced by setBackground in API16 @SuppressWarnings("deprecation") // setBackgroundDrawable is replaced by setBackground in API16
@Override @Override
public void showKeyPreview(PointerTracker tracker) { public void showKeyPreview(PointerTracker tracker) {

View file

@ -43,6 +43,7 @@ import com.android.inputmethod.accessibility.AccessibilityUtils;
import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy; import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
import com.android.inputmethod.keyboard.PointerTracker.TimerProxy; import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.LatinIME; import com.android.inputmethod.latin.LatinIME;
import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.R;
@ -82,7 +83,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator; private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator;
private boolean mNeedsToDisplayLanguage; private boolean mNeedsToDisplayLanguage;
private boolean mHasMultipleEnabledIMEsOrSubtypes; private boolean mHasMultipleEnabledIMEsOrSubtypes;
private int mLanguageOnSpacebarAnimAlpha = ALPHA_OPAQUE; private int mLanguageOnSpacebarAnimAlpha = Constants.Color.ALPHA_OPAQUE;
private final float mSpacebarTextRatio; private final float mSpacebarTextRatio;
private float mSpacebarTextSize; private float mSpacebarTextSize;
private final int mSpacebarTextColor; private final int mSpacebarTextColor;
@ -98,7 +99,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
// Stuff to draw altCodeWhileTyping keys. // Stuff to draw altCodeWhileTyping keys.
private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator; private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator;
private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator; private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator;
private int mAltCodeKeyWhileTypingAnimAlpha = ALPHA_OPAQUE; private int mAltCodeKeyWhileTypingAnimAlpha = Constants.Color.ALPHA_OPAQUE;
// More keys keyboard // More keys keyboard
private PopupWindow mMoreKeysWindow; private PopupWindow mMoreKeysWindow;
@ -361,7 +362,8 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
mSpacebarTextShadowColor = a.getColor( mSpacebarTextShadowColor = a.getColor(
R.styleable.LatinKeyboardView_spacebarTextShadowColor, 0); R.styleable.LatinKeyboardView_spacebarTextShadowColor, 0);
mLanguageOnSpacebarFinalAlpha = a.getInt( mLanguageOnSpacebarFinalAlpha = a.getInt(
R.styleable.LatinKeyboardView_languageOnSpacebarFinalAlpha, ALPHA_OPAQUE); R.styleable.LatinKeyboardView_languageOnSpacebarFinalAlpha,
Constants.Color.ALPHA_OPAQUE);
final int languageOnSpacebarFadeoutAnimatorResId = a.getResourceId( final int languageOnSpacebarFadeoutAnimatorResId = a.getResourceId(
R.styleable.LatinKeyboardView_languageOnSpacebarFadeoutAnimator, 0); R.styleable.LatinKeyboardView_languageOnSpacebarFadeoutAnimator, 0);
final int altCodeKeyWhileTypingFadeoutAnimatorResId = a.getResourceId( final int altCodeKeyWhileTypingFadeoutAnimatorResId = a.getResourceId(
@ -468,7 +470,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
mSpaceKey = keyboard.getKey(Keyboard.CODE_SPACE); mSpaceKey = keyboard.getKey(Keyboard.CODE_SPACE);
mSpaceIcon = (mSpaceKey != null) mSpaceIcon = (mSpaceKey != null)
? mSpaceKey.getIcon(keyboard.mIconsSet, ALPHA_OPAQUE) : null; ? mSpaceKey.getIcon(keyboard.mIconsSet, Constants.Color.ALPHA_OPAQUE) : null;
final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap; final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
mSpacebarTextSize = keyHeight * mSpacebarTextRatio; mSpacebarTextSize = keyHeight * mSpacebarTextRatio;
if (ProductionFlag.IS_EXPERIMENTAL) { if (ProductionFlag.IS_EXPERIMENTAL) {
@ -870,7 +872,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
mNeedsToDisplayLanguage = false; mNeedsToDisplayLanguage = false;
} else { } else {
if (subtypeChanged && needsToDisplayLanguage) { if (subtypeChanged && needsToDisplayLanguage) {
setLanguageOnSpacebarAnimAlpha(ALPHA_OPAQUE); setLanguageOnSpacebarAnimAlpha(Constants.Color.ALPHA_OPAQUE);
if (animator.isStarted()) { if (animator.isStarted()) {
animator.cancel(); animator.cancel();
} }

View file

@ -16,6 +16,9 @@
package com.android.inputmethod.keyboard; package com.android.inputmethod.keyboard;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import android.view.MotionEvent; import android.view.MotionEvent;
@ -76,6 +79,7 @@ public class PointerTracker {
public TextView inflateKeyPreviewText(); public TextView inflateKeyPreviewText();
public void showKeyPreview(PointerTracker tracker); public void showKeyPreview(PointerTracker tracker);
public void dismissKeyPreview(PointerTracker tracker); public void dismissKeyPreview(PointerTracker tracker);
public void showGestureTrail(PointerTracker tracker);
} }
public interface TimerProxy { public interface TimerProxy {
@ -283,6 +287,15 @@ public class PointerTracker {
sAggregratedPointers.reset(); sAggregratedPointers.reset();
} }
// TODO: To handle multi-touch gestures we may want to move this method to
// {@link PointerTrackerQueue}.
public static void drawGestureTrailForAllPointerTrackers(Canvas canvas, Paint paint) {
for (final PointerTracker tracker : sTrackers) {
tracker.mGestureStroke.drawGestureTrail(canvas, paint, tracker.getLastX(),
tracker.getLastY());
}
}
private PointerTracker(int id, KeyEventHandler handler) { private PointerTracker(int id, KeyEventHandler handler) {
if (handler == null) if (handler == null)
throw new NullPointerException(); throw new NullPointerException();
@ -511,6 +524,9 @@ public class PointerTracker {
public long getDownTime() { public long getDownTime() {
return mDownTime; return mDownTime;
} }
public Rect getDrawingRect() {
return mGestureStroke.getDrawingRect();
}
private Key onDownKey(int x, int y, long eventTime) { private Key onDownKey(int x, int y, long eventTime) {
mDownTime = eventTime; mDownTime = eventTime;
@ -696,6 +712,7 @@ public class PointerTracker {
if (key != null && mInGesture) { if (key != null && mInGesture) {
final InputPointers batchPoints = getIncrementalBatchPoints(); final InputPointers batchPoints = getIncrementalBatchPoints();
mDrawingProxy.showGestureTrail(this);
if (updateBatchInputRecognitionState(eventTime, batchPoints.getPointerSize())) { if (updateBatchInputRecognitionState(eventTime, batchPoints.getPointerSize())) {
updateBatchInput(batchPoints); updateBatchInput(batchPoints);
} }
@ -868,6 +885,7 @@ public class PointerTracker {
callListenerOnRelease(mCurrentKey, mCurrentKey.mCode, true); callListenerOnRelease(mCurrentKey, mCurrentKey.mCode, true);
mCurrentKey = null; mCurrentKey = null;
} }
mDrawingProxy.showGestureTrail(this);
return; return;
} }
// This event will be recognized as a regular code input. Clear unused batch points so they // This event will be recognized as a regular code input. Clear unused batch points so they

View file

@ -14,8 +14,12 @@
package com.android.inputmethod.keyboard.internal; package com.android.inputmethod.keyboard.internal;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.FloatMath; import android.util.FloatMath;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.InputPointers; import com.android.inputmethod.latin.InputPointers;
public class GestureStroke { public class GestureStroke {
@ -35,6 +39,7 @@ public class GestureStroke {
private int mMinGestureLength; private int mMinGestureLength;
private int mMinGestureLengthWhileInGesture; private int mMinGestureLengthWhileInGesture;
private int mMinGestureSampleLength; private int mMinGestureSampleLength;
private final Rect mDrawingRect = new Rect();
// TODO: Move some of these to resource. // TODO: Move some of these to resource.
private static final float MIN_GESTURE_LENGTH_RATIO_TO_KEY_WIDTH = 1.0f; private static final float MIN_GESTURE_LENGTH_RATIO_TO_KEY_WIDTH = 1.0f;
@ -47,6 +52,10 @@ public class GestureStroke {
private static final float DOUBLE_PI = (float)(2 * Math.PI); private static final float DOUBLE_PI = (float)(2 * 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(int pointerId) {
mPointerId = pointerId; mPointerId = pointerId;
reset(); reset();
@ -77,6 +86,7 @@ public class GestureStroke {
mLastIncrementalBatchSize = 0; mLastIncrementalBatchSize = 0;
mLastPointTime = 0; mLastPointTime = 0;
mInputPointers.reset(); mInputPointers.reset();
mDrawingRect.setEmpty();
} }
private void updateLastPoint(final int x, final int y, final int time) { private void updateLastPoint(final int x, final int y, final int time) {
@ -94,7 +104,6 @@ public class GestureStroke {
} }
return; return;
} }
final int[] xCoords = mInputPointers.getXCoordinates(); final int[] xCoords = mInputPointers.getXCoordinates();
final int[] yCoords = mInputPointers.getYCoordinates(); final int[] yCoords = mInputPointers.getYCoordinates();
final int lastX = xCoords[size - 1]; final int lastX = xCoords[size - 1];
@ -102,6 +111,11 @@ public class GestureStroke {
final float dist = getDistance(lastX, lastY, x, y); final float dist = getDistance(lastX, lastY, x, y);
if (dist > mMinGestureSampleLength) { if (dist > mMinGestureSampleLength) {
mInputPointers.addPointer(x, y, mPointerId, time); mInputPointers.addPointer(x, y, mPointerId, time);
if (mDrawingRect.isEmpty()) {
mDrawingRect.set(x - 1, y - 1, x + 1, y + 1);
} else {
mDrawingRect.union(x, y);
}
mLength += dist; mLength += dist;
final float angle = getAngle(lastX, lastY, x, y); final float angle = getAngle(lastX, lastY, x, y);
if (size > 1) { if (size > 1) {
@ -161,4 +175,27 @@ public class GestureStroke {
} }
return diff; return diff;
} }
public void drawGestureTrail(Canvas canvas, Paint paint, int lastX, int lastY) {
// 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 = mInputPointers.getPointerSize();
int[] xCoords = mInputPointers.getXCoordinates();
int[] yCoords = mInputPointers.getYCoordinates();
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);
if (i == size - 1) {
canvas.drawLine(lastX, lastY, xCoords[i], yCoords[i], paint);
}
}
}
public Rect getDrawingRect() {
return mDrawingRect;
}
} }

View file

@ -19,6 +19,13 @@ package com.android.inputmethod.latin;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
public final class Constants { public final class Constants {
public static final class Color {
/**
* The alpha value for fully opaque.
*/
public final static int ALPHA_OPAQUE = 255;
}
public static final class ImeOption { public static final class ImeOption {
/** /**
* The private IME option used to indicate that no microphone should be shown for a given * The private IME option used to indicate that no microphone should be shown for a given