Home | History | Annotate | Download | only in internal
      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