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
17package com.android.inputmethod.keyboard.internal;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.Paint;
25import android.graphics.Paint.Align;
26import android.graphics.PorterDuff;
27import android.graphics.PorterDuffXfermode;
28import android.graphics.Rect;
29import android.graphics.RectF;
30import android.os.Message;
31import android.text.TextUtils;
32import android.util.AttributeSet;
33import android.util.SparseArray;
34import android.widget.RelativeLayout;
35
36import com.android.inputmethod.keyboard.PointerTracker;
37import com.android.inputmethod.keyboard.internal.GesturePreviewTrail.Params;
38import com.android.inputmethod.latin.CollectionUtils;
39import com.android.inputmethod.latin.R;
40import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
41
42public 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