1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.keyboard.internal; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.graphics.Bitmap; 22 import android.graphics.Canvas; 23 import android.graphics.Color; 24 import android.graphics.Paint; 25 import android.graphics.Paint.Align; 26 import android.graphics.PorterDuff; 27 import android.graphics.PorterDuffXfermode; 28 import android.graphics.Rect; 29 import android.graphics.RectF; 30 import android.os.Message; 31 import android.text.TextUtils; 32 import android.util.AttributeSet; 33 import android.util.SparseArray; 34 import android.widget.RelativeLayout; 35 36 import com.android.inputmethod.keyboard.PointerTracker; 37 import com.android.inputmethod.keyboard.internal.GesturePreviewTrail.Params; 38 import com.android.inputmethod.latin.CollectionUtils; 39 import com.android.inputmethod.latin.R; 40 import com.android.inputmethod.latin.StaticInnerHandlerWrapper; 41 42 public final class PreviewPlacerView extends RelativeLayout { 43 // The height of extra area above the keyboard to draw gesture trails. 44 // Proportional to the keyboard height. 45 private static final float EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO = 0.25f; 46 47 private final int mGestureFloatingPreviewTextColor; 48 private final int mGestureFloatingPreviewTextOffset; 49 private final int mGestureFloatingPreviewColor; 50 private final float mGestureFloatingPreviewHorizontalPadding; 51 private final float mGestureFloatingPreviewVerticalPadding; 52 private final float mGestureFloatingPreviewRoundRadius; 53 54 private int mKeyboardViewOriginX; 55 private int mKeyboardViewOriginY; 56 57 private final SparseArray<GesturePreviewTrail> mGesturePreviewTrails = 58 CollectionUtils.newSparseArray(); 59 private final Params mGesturePreviewTrailParams; 60 private final Paint mGesturePaint; 61 private boolean mDrawsGesturePreviewTrail; 62 private int mOffscreenWidth; 63 private int mOffscreenHeight; 64 private int mOffscreenOffsetY; 65 private Bitmap mOffscreenBuffer; 66 private final Canvas mOffscreenCanvas = new Canvas(); 67 private final Rect mOffscreenDirtyRect = new Rect(); 68 private final Rect mGesturePreviewTrailBoundsRect = new Rect(); // per trail 69 70 private final Paint mTextPaint; 71 private String mGestureFloatingPreviewText; 72 private final int mGestureFloatingPreviewTextHeight; 73 // {@link RectF} is needed for {@link Canvas#drawRoundRect(RectF, float, float, Paint)}. 74 private final RectF mGestureFloatingPreviewRectangle = new RectF(); 75 private int mLastPointerX; 76 private int mLastPointerY; 77 private static final char[] TEXT_HEIGHT_REFERENCE_CHAR = { 'M' }; 78 private boolean mDrawsGestureFloatingPreviewText; 79 80 private final DrawingHandler mDrawingHandler; 81 82 private static final class DrawingHandler extends StaticInnerHandlerWrapper<PreviewPlacerView> { 83 private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 0; 84 private static final int MSG_UPDATE_GESTURE_PREVIEW_TRAIL = 1; 85 86 private final Params mGesturePreviewTrailParams; 87 private final int mGestureFloatingPreviewTextLingerTimeout; 88 89 public DrawingHandler(final PreviewPlacerView outerInstance, 90 final Params gesturePreviewTrailParams, 91 final int getstureFloatinPreviewTextLinerTimeout) { 92 super(outerInstance); 93 mGesturePreviewTrailParams = gesturePreviewTrailParams; 94 mGestureFloatingPreviewTextLingerTimeout = getstureFloatinPreviewTextLinerTimeout; 95 } 96 97 @Override 98 public void handleMessage(final Message msg) { 99 final PreviewPlacerView placerView = getOuterInstance(); 100 if (placerView == null) return; 101 switch (msg.what) { 102 case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT: 103 placerView.setGestureFloatingPreviewText(null); 104 break; 105 case MSG_UPDATE_GESTURE_PREVIEW_TRAIL: 106 placerView.invalidate(); 107 break; 108 } 109 } 110 111 public void dismissGestureFloatingPreviewText() { 112 removeMessages(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); 113 sendMessageDelayed(obtainMessage(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT), 114 mGestureFloatingPreviewTextLingerTimeout); 115 } 116 117 public void postUpdateGestureTrailPreview() { 118 removeMessages(MSG_UPDATE_GESTURE_PREVIEW_TRAIL); 119 sendMessageDelayed(obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_TRAIL), 120 mGesturePreviewTrailParams.mUpdateInterval); 121 } 122 } 123 124 public PreviewPlacerView(final Context context, final AttributeSet attrs) { 125 this(context, attrs, R.attr.keyboardViewStyle); 126 } 127 128 public PreviewPlacerView(final Context context, final AttributeSet attrs, final int defStyle) { 129 super(context); 130 setWillNotDraw(false); 131 132 final TypedArray keyboardViewAttr = context.obtainStyledAttributes( 133 attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView); 134 final int gestureFloatingPreviewTextSize = keyboardViewAttr.getDimensionPixelSize( 135 R.styleable.KeyboardView_gestureFloatingPreviewTextSize, 0); 136 mGestureFloatingPreviewTextColor = keyboardViewAttr.getColor( 137 R.styleable.KeyboardView_gestureFloatingPreviewTextColor, 0); 138 mGestureFloatingPreviewTextOffset = keyboardViewAttr.getDimensionPixelOffset( 139 R.styleable.KeyboardView_gestureFloatingPreviewTextOffset, 0); 140 mGestureFloatingPreviewColor = keyboardViewAttr.getColor( 141 R.styleable.KeyboardView_gestureFloatingPreviewColor, 0); 142 mGestureFloatingPreviewHorizontalPadding = keyboardViewAttr.getDimension( 143 R.styleable.KeyboardView_gestureFloatingPreviewHorizontalPadding, 0.0f); 144 mGestureFloatingPreviewVerticalPadding = keyboardViewAttr.getDimension( 145 R.styleable.KeyboardView_gestureFloatingPreviewVerticalPadding, 0.0f); 146 mGestureFloatingPreviewRoundRadius = keyboardViewAttr.getDimension( 147 R.styleable.KeyboardView_gestureFloatingPreviewRoundRadius, 0.0f); 148 final int gestureFloatingPreviewTextLingerTimeout = keyboardViewAttr.getInt( 149 R.styleable.KeyboardView_gestureFloatingPreviewTextLingerTimeout, 0); 150 mGesturePreviewTrailParams = new Params(keyboardViewAttr); 151 keyboardViewAttr.recycle(); 152 153 mDrawingHandler = new DrawingHandler(this, mGesturePreviewTrailParams, 154 gestureFloatingPreviewTextLingerTimeout); 155 156 final Paint gesturePaint = new Paint(); 157 gesturePaint.setAntiAlias(true); 158 gesturePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); 159 mGesturePaint = gesturePaint; 160 161 final Paint textPaint = new Paint(); 162 textPaint.setAntiAlias(true); 163 textPaint.setTextAlign(Align.CENTER); 164 textPaint.setTextSize(gestureFloatingPreviewTextSize); 165 mTextPaint = textPaint; 166 final Rect textRect = new Rect(); 167 textPaint.getTextBounds(TEXT_HEIGHT_REFERENCE_CHAR, 0, 1, textRect); 168 mGestureFloatingPreviewTextHeight = textRect.height(); 169 170 final Paint layerPaint = new Paint(); 171 layerPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)); 172 setLayerType(LAYER_TYPE_HARDWARE, layerPaint); 173 } 174 175 public void setKeyboardViewGeometry(final int x, final int y, final int w, final int h) { 176 mKeyboardViewOriginX = x; 177 mKeyboardViewOriginY = y; 178 mOffscreenOffsetY = (int)(h * EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO); 179 mOffscreenWidth = w; 180 mOffscreenHeight = mOffscreenOffsetY + h; 181 } 182 183 public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail, 184 final boolean drawsGestureFloatingPreviewText) { 185 mDrawsGesturePreviewTrail = drawsGesturePreviewTrail; 186 mDrawsGestureFloatingPreviewText = drawsGestureFloatingPreviewText; 187 } 188 189 public void invalidatePointer(final PointerTracker tracker, final boolean isOldestTracker) { 190 final boolean needsToUpdateLastPointer = 191 isOldestTracker && mDrawsGestureFloatingPreviewText; 192 if (needsToUpdateLastPointer) { 193 mLastPointerX = tracker.getLastX(); 194 mLastPointerY = tracker.getLastY(); 195 } 196 197 if (mDrawsGesturePreviewTrail) { 198 GesturePreviewTrail trail; 199 synchronized (mGesturePreviewTrails) { 200 trail = mGesturePreviewTrails.get(tracker.mPointerId); 201 if (trail == null) { 202 trail = new GesturePreviewTrail(); 203 mGesturePreviewTrails.put(tracker.mPointerId, trail); 204 } 205 } 206 trail.addStroke(tracker.getGestureStrokeWithPreviewPoints(), tracker.getDownTime()); 207 } 208 209 // TODO: Should narrow the invalidate region. 210 if (mDrawsGesturePreviewTrail || needsToUpdateLastPointer) { 211 invalidate(); 212 } 213 } 214 215 @Override 216 protected void onDetachedFromWindow() { 217 freeOffscreenBuffer(); 218 } 219 220 private void freeOffscreenBuffer() { 221 if (mOffscreenBuffer != null) { 222 mOffscreenBuffer.recycle(); 223 mOffscreenBuffer = null; 224 } 225 } 226 227 private void mayAllocateOffscreenBuffer() { 228 if (mOffscreenBuffer != null && mOffscreenBuffer.getWidth() == mOffscreenWidth 229 && mOffscreenBuffer.getHeight() == mOffscreenHeight) { 230 return; 231 } 232 freeOffscreenBuffer(); 233 mOffscreenBuffer = Bitmap.createBitmap( 234 mOffscreenWidth, mOffscreenHeight, Bitmap.Config.ARGB_8888); 235 mOffscreenCanvas.setBitmap(mOffscreenBuffer); 236 } 237 238 @Override 239 public void onDraw(final Canvas canvas) { 240 super.onDraw(canvas); 241 if (mDrawsGesturePreviewTrail) { 242 mayAllocateOffscreenBuffer(); 243 // Draw gesture trails to offscreen buffer. 244 final boolean needsUpdatingGesturePreviewTrail = drawGestureTrails( 245 mOffscreenCanvas, mGesturePaint, mOffscreenDirtyRect); 246 // Transfer offscreen buffer to screen. 247 if (!mOffscreenDirtyRect.isEmpty()) { 248 final int offsetY = mKeyboardViewOriginY - mOffscreenOffsetY; 249 canvas.translate(mKeyboardViewOriginX, offsetY); 250 canvas.drawBitmap(mOffscreenBuffer, mOffscreenDirtyRect, mOffscreenDirtyRect, 251 mGesturePaint); 252 canvas.translate(-mKeyboardViewOriginX, -offsetY); 253 // Note: Defer clearing the dirty rectangle here because we will get cleared 254 // rectangle on the canvas. 255 } 256 if (needsUpdatingGesturePreviewTrail) { 257 mDrawingHandler.postUpdateGestureTrailPreview(); 258 } 259 } 260 if (mDrawsGestureFloatingPreviewText) { 261 canvas.translate(mKeyboardViewOriginX, mKeyboardViewOriginY); 262 drawGestureFloatingPreviewText(canvas, mGestureFloatingPreviewText); 263 canvas.translate(-mKeyboardViewOriginX, -mKeyboardViewOriginY); 264 } 265 } 266 267 private boolean drawGestureTrails(final Canvas offscreenCanvas, final Paint paint, 268 final Rect dirtyRect) { 269 // Clear previous dirty rectangle. 270 if (!dirtyRect.isEmpty()) { 271 paint.setColor(Color.TRANSPARENT); 272 paint.setStyle(Paint.Style.FILL); 273 offscreenCanvas.drawRect(dirtyRect, paint); 274 } 275 dirtyRect.setEmpty(); 276 277 // Draw gesture trails to offscreen buffer. 278 offscreenCanvas.translate(0, mOffscreenOffsetY); 279 boolean needsUpdatingGesturePreviewTrail = false; 280 synchronized (mGesturePreviewTrails) { 281 // Trails count == fingers count that have ever been active. 282 final int trailsCount = mGesturePreviewTrails.size(); 283 for (int index = 0; index < trailsCount; index++) { 284 final GesturePreviewTrail trail = mGesturePreviewTrails.valueAt(index); 285 needsUpdatingGesturePreviewTrail |= 286 trail.drawGestureTrail(offscreenCanvas, paint, 287 mGesturePreviewTrailBoundsRect, mGesturePreviewTrailParams); 288 // {@link #mGesturePreviewTrailBoundsRect} has bounding box of the trail. 289 dirtyRect.union(mGesturePreviewTrailBoundsRect); 290 } 291 } 292 offscreenCanvas.translate(0, -mOffscreenOffsetY); 293 294 // Clip dirty rectangle with offscreen buffer width/height. 295 dirtyRect.offset(0, mOffscreenOffsetY); 296 clipRect(dirtyRect, 0, 0, mOffscreenWidth, mOffscreenHeight); 297 return needsUpdatingGesturePreviewTrail; 298 } 299 300 private static void clipRect(final Rect out, final int left, final int top, final int right, 301 final int bottom) { 302 out.set(Math.max(out.left, left), Math.max(out.top, top), Math.min(out.right, right), 303 Math.min(out.bottom, bottom)); 304 } 305 306 public void setGestureFloatingPreviewText(final String gestureFloatingPreviewText) { 307 if (!mDrawsGestureFloatingPreviewText) return; 308 mGestureFloatingPreviewText = gestureFloatingPreviewText; 309 invalidate(); 310 } 311 312 public void dismissGestureFloatingPreviewText() { 313 mDrawingHandler.dismissGestureFloatingPreviewText(); 314 } 315 316 private void drawGestureFloatingPreviewText(final Canvas canvas, 317 final String gestureFloatingPreviewText) { 318 if (TextUtils.isEmpty(gestureFloatingPreviewText)) { 319 return; 320 } 321 322 final Paint paint = mTextPaint; 323 final RectF rectangle = mGestureFloatingPreviewRectangle; 324 // TODO: Figure out how we should deal with the floating preview text with multiple moving 325 // fingers. 326 327 // Paint the round rectangle background. 328 final int textHeight = mGestureFloatingPreviewTextHeight; 329 final float textWidth = paint.measureText(gestureFloatingPreviewText); 330 final float hPad = mGestureFloatingPreviewHorizontalPadding; 331 final float vPad = mGestureFloatingPreviewVerticalPadding; 332 final float rectWidth = textWidth + hPad * 2.0f; 333 final float rectHeight = textHeight + vPad * 2.0f; 334 final int canvasWidth = canvas.getWidth(); 335 final float rectX = Math.min(Math.max(mLastPointerX - rectWidth / 2.0f, 0.0f), 336 canvasWidth - rectWidth); 337 final float rectY = mLastPointerY - mGestureFloatingPreviewTextOffset - rectHeight; 338 rectangle.set(rectX, rectY, rectX + rectWidth, rectY + rectHeight); 339 final float round = mGestureFloatingPreviewRoundRadius; 340 paint.setColor(mGestureFloatingPreviewColor); 341 canvas.drawRoundRect(rectangle, round, round, paint); 342 // Paint the text preview 343 paint.setColor(mGestureFloatingPreviewTextColor); 344 final float textX = rectX + hPad + textWidth / 2.0f; 345 final float textY = rectY + vPad + textHeight; 346 canvas.drawText(gestureFloatingPreviewText, textX, textY, paint); 347 } 348 } 349