1/*
2 * Copyright (C) 2009 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.pinyin;
18
19import com.android.inputmethod.pinyin.SoftKeyboard.KeyRow;
20
21import java.util.List;
22
23import android.content.Context;
24import android.graphics.Canvas;
25import android.graphics.Paint;
26import android.graphics.Rect;
27import android.graphics.Paint.FontMetricsInt;
28import android.graphics.drawable.Drawable;
29import android.os.Vibrator;
30import android.util.AttributeSet;
31import android.view.View;
32
33/**
34 * Class used to show a soft keyboard.
35 *
36 * A soft keyboard view should not handle touch event itself, because we do bias
37 * correction, need a global strategy to map an event into a proper view to
38 * achieve better user experience.
39 */
40public class SoftKeyboardView extends View {
41    /**
42     * The definition of the soft keyboard for the current this soft keyboard
43     * view.
44     */
45    private SoftKeyboard mSoftKeyboard;
46
47    /**
48     * The popup balloon hint for key press/release.
49     */
50    private BalloonHint mBalloonPopup;
51
52    /**
53     * The on-key balloon hint for key press/release. If it is null, on-key
54     * highlight will be drawn on th soft keyboard view directly.
55     */
56    private BalloonHint mBalloonOnKey;
57
58    /** Used to play key sounds. */
59    private SoundManager mSoundManager;
60
61    /** The last key pressed. */
62    private SoftKey mSoftKeyDown;
63
64    /** Used to indicate whether the user is holding on a key. */
65    private boolean mKeyPressed = false;
66
67    /**
68     * The location offset of the view to the keyboard container.
69     */
70    private int mOffsetToSkbContainer[] = new int[2];
71
72    /**
73     * The location of the desired hint view to the keyboard container.
74     */
75    private int mHintLocationToSkbContainer[] = new int[2];
76
77    /**
78     * Text size for normal key.
79     */
80    private int mNormalKeyTextSize;
81
82    /**
83     * Text size for function key.
84     */
85    private int mFunctionKeyTextSize;
86
87    /**
88     * Long press timer used to response long-press.
89     */
90    private SkbContainer.LongPressTimer mLongPressTimer;
91
92    /**
93     * Repeated events for long press
94     */
95    private boolean mRepeatForLongPress = false;
96
97    /**
98     * If this parameter is true, the balloon will never be dismissed even if
99     * user moves a lot from the pressed point.
100     */
101    private boolean mMovingNeverHidePopupBalloon = false;
102
103    /** Vibration for key press. */
104    private Vibrator mVibrator;
105
106    /** Vibration pattern for key press. */
107    protected long[] mVibratePattern = new long[] {1, 20};
108
109    /**
110     * The dirty rectangle used to mark the area to re-draw during key press and
111     * release. Currently, whenever we can invalidate(Rect), view will call
112     * onDraw() and we MUST draw the whole view. This dirty information is for
113     * future use.
114     */
115    private Rect mDirtyRect = new Rect();
116
117    private Paint mPaint;
118    private FontMetricsInt mFmi;
119    private boolean mDimSkb;
120
121    public SoftKeyboardView(Context context, AttributeSet attrs) {
122        super(context, attrs);
123
124        mSoundManager = SoundManager.getInstance(mContext);
125
126        mPaint = new Paint();
127        mPaint.setAntiAlias(true);
128        mFmi = mPaint.getFontMetricsInt();
129    }
130
131    public boolean setSoftKeyboard(SoftKeyboard softSkb) {
132        if (null == softSkb) {
133            return false;
134        }
135        mSoftKeyboard = softSkb;
136        Drawable bg = softSkb.getSkbBackground();
137        if (null != bg) setBackgroundDrawable(bg);
138        return true;
139    }
140
141    public SoftKeyboard getSoftKeyboard() {
142        return mSoftKeyboard;
143    }
144
145    public void resizeKeyboard(int skbWidth, int skbHeight) {
146        mSoftKeyboard.setSkbCoreSize(skbWidth, skbHeight);
147    }
148
149    public void setBalloonHint(BalloonHint balloonOnKey,
150            BalloonHint balloonPopup, boolean movingNeverHidePopup) {
151        mBalloonOnKey = balloonOnKey;
152        mBalloonPopup = balloonPopup;
153        mMovingNeverHidePopupBalloon = movingNeverHidePopup;
154    }
155
156    public void setOffsetToSkbContainer(int offsetToSkbContainer[]) {
157        mOffsetToSkbContainer[0] = offsetToSkbContainer[0];
158        mOffsetToSkbContainer[1] = offsetToSkbContainer[1];
159    }
160
161    @Override
162    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
163        int measuredWidth = 0;
164        int measuredHeight = 0;
165        if (null != mSoftKeyboard) {
166            measuredWidth = mSoftKeyboard.getSkbCoreWidth();
167            measuredHeight = mSoftKeyboard.getSkbCoreHeight();
168            measuredWidth += mPaddingLeft + mPaddingRight;
169            measuredHeight += mPaddingTop + mPaddingBottom;
170        }
171        setMeasuredDimension(measuredWidth, measuredHeight);
172    }
173
174    private void showBalloon(BalloonHint balloon, int balloonLocationToSkb[],
175            boolean movePress) {
176        long delay = BalloonHint.TIME_DELAY_SHOW;
177        if (movePress) delay = 0;
178        if (balloon.needForceDismiss()) {
179            balloon.delayedDismiss(0);
180        }
181        if (!balloon.isShowing()) {
182            balloon.delayedShow(delay, balloonLocationToSkb);
183        } else {
184            balloon.delayedUpdate(delay, balloonLocationToSkb, balloon
185                    .getWidth(), balloon.getHeight());
186        }
187        long b = System.currentTimeMillis();
188    }
189
190    public void resetKeyPress(long balloonDelay) {
191        if (!mKeyPressed) return;
192        mKeyPressed = false;
193        if (null != mBalloonOnKey) {
194            mBalloonOnKey.delayedDismiss(balloonDelay);
195        } else {
196            if (null != mSoftKeyDown) {
197                if (mDirtyRect.isEmpty()) {
198                    mDirtyRect.set(mSoftKeyDown.mLeft, mSoftKeyDown.mTop,
199                            mSoftKeyDown.mRight, mSoftKeyDown.mBottom);
200                }
201                invalidate(mDirtyRect);
202            } else {
203                invalidate();
204            }
205        }
206        mBalloonPopup.delayedDismiss(balloonDelay);
207    }
208
209    // If movePress is true, means that this function is called because user
210    // moves his finger to this button. If movePress is false, means that this
211    // function is called when user just presses this key.
212    public SoftKey onKeyPress(int x, int y,
213            SkbContainer.LongPressTimer longPressTimer, boolean movePress) {
214        mKeyPressed = false;
215        boolean moveWithinPreviousKey = false;
216        if (movePress) {
217            SoftKey newKey = mSoftKeyboard.mapToKey(x, y);
218            if (newKey == mSoftKeyDown) moveWithinPreviousKey = true;
219            mSoftKeyDown = newKey;
220        } else {
221            mSoftKeyDown = mSoftKeyboard.mapToKey(x, y);
222        }
223        if (moveWithinPreviousKey || null == mSoftKeyDown) return mSoftKeyDown;
224        mKeyPressed = true;
225
226        if (!movePress) {
227            tryPlayKeyDown();
228            tryVibrate();
229        }
230
231        mLongPressTimer = longPressTimer;
232
233        if (!movePress) {
234            if (mSoftKeyDown.getPopupResId() > 0 || mSoftKeyDown.repeatable()) {
235                mLongPressTimer.startTimer();
236            }
237        } else {
238            mLongPressTimer.removeTimer();
239        }
240
241        int desired_width;
242        int desired_height;
243        float textSize;
244        Environment env = Environment.getInstance();
245
246        if (null != mBalloonOnKey) {
247            Drawable keyHlBg = mSoftKeyDown.getKeyHlBg();
248            mBalloonOnKey.setBalloonBackground(keyHlBg);
249
250            // Prepare the on-key balloon
251            int keyXMargin = mSoftKeyboard.getKeyXMargin();
252            int keyYMargin = mSoftKeyboard.getKeyYMargin();
253            desired_width = mSoftKeyDown.width() - 2 * keyXMargin;
254            desired_height = mSoftKeyDown.height() - 2 * keyYMargin;
255            textSize = env
256                    .getKeyTextSize(SoftKeyType.KEYTYPE_ID_NORMAL_KEY != mSoftKeyDown.mKeyType.mKeyTypeId);
257            Drawable icon = mSoftKeyDown.getKeyIcon();
258            if (null != icon) {
259                mBalloonOnKey.setBalloonConfig(icon, desired_width,
260                        desired_height);
261            } else {
262                mBalloonOnKey.setBalloonConfig(mSoftKeyDown.getKeyLabel(),
263                        textSize, true, mSoftKeyDown.getColorHl(),
264                        desired_width, desired_height);
265            }
266
267            mHintLocationToSkbContainer[0] = mPaddingLeft + mSoftKeyDown.mLeft
268                    - (mBalloonOnKey.getWidth() - mSoftKeyDown.width()) / 2;
269            mHintLocationToSkbContainer[0] += mOffsetToSkbContainer[0];
270            mHintLocationToSkbContainer[1] = mPaddingTop
271                    + (mSoftKeyDown.mBottom - keyYMargin)
272                    - mBalloonOnKey.getHeight();
273            mHintLocationToSkbContainer[1] += mOffsetToSkbContainer[1];
274            showBalloon(mBalloonOnKey, mHintLocationToSkbContainer, movePress);
275        } else {
276            mDirtyRect.union(mSoftKeyDown.mLeft, mSoftKeyDown.mTop,
277                    mSoftKeyDown.mRight, mSoftKeyDown.mBottom);
278            invalidate(mDirtyRect);
279        }
280
281        // Prepare the popup balloon
282        if (mSoftKeyDown.needBalloon()) {
283            Drawable balloonBg = mSoftKeyboard.getBalloonBackground();
284            mBalloonPopup.setBalloonBackground(balloonBg);
285
286            desired_width = mSoftKeyDown.width() + env.getKeyBalloonWidthPlus();
287            desired_height = mSoftKeyDown.height()
288                    + env.getKeyBalloonHeightPlus();
289            textSize = env
290                    .getBalloonTextSize(SoftKeyType.KEYTYPE_ID_NORMAL_KEY != mSoftKeyDown.mKeyType.mKeyTypeId);
291            Drawable iconPopup = mSoftKeyDown.getKeyIconPopup();
292            if (null != iconPopup) {
293                mBalloonPopup.setBalloonConfig(iconPopup, desired_width,
294                        desired_height);
295            } else {
296                mBalloonPopup.setBalloonConfig(mSoftKeyDown.getKeyLabel(),
297                        textSize, mSoftKeyDown.needBalloon(), mSoftKeyDown
298                                .getColorBalloon(), desired_width,
299                        desired_height);
300            }
301
302            // The position to show.
303            mHintLocationToSkbContainer[0] = mPaddingLeft + mSoftKeyDown.mLeft
304                    + -(mBalloonPopup.getWidth() - mSoftKeyDown.width()) / 2;
305            mHintLocationToSkbContainer[0] += mOffsetToSkbContainer[0];
306            mHintLocationToSkbContainer[1] = mPaddingTop + mSoftKeyDown.mTop
307                    - mBalloonPopup.getHeight();
308            mHintLocationToSkbContainer[1] += mOffsetToSkbContainer[1];
309            showBalloon(mBalloonPopup, mHintLocationToSkbContainer, movePress);
310        } else {
311            mBalloonPopup.delayedDismiss(0);
312        }
313
314        if (mRepeatForLongPress) longPressTimer.startTimer();
315        return mSoftKeyDown;
316    }
317
318    public SoftKey onKeyRelease(int x, int y) {
319        mKeyPressed = false;
320        if (null == mSoftKeyDown) return null;
321
322        mLongPressTimer.removeTimer();
323
324        if (null != mBalloonOnKey) {
325            mBalloonOnKey.delayedDismiss(BalloonHint.TIME_DELAY_DISMISS);
326        } else {
327            mDirtyRect.union(mSoftKeyDown.mLeft, mSoftKeyDown.mTop,
328                    mSoftKeyDown.mRight, mSoftKeyDown.mBottom);
329            invalidate(mDirtyRect);
330        }
331
332        if (mSoftKeyDown.needBalloon()) {
333            mBalloonPopup.delayedDismiss(BalloonHint.TIME_DELAY_DISMISS);
334        }
335
336        if (mSoftKeyDown.moveWithinKey(x - mPaddingLeft, y - mPaddingTop)) {
337            return mSoftKeyDown;
338        }
339        return null;
340    }
341
342    public SoftKey onKeyMove(int x, int y) {
343        if (null == mSoftKeyDown) return null;
344
345        if (mSoftKeyDown.moveWithinKey(x - mPaddingLeft, y - mPaddingTop)) {
346            return mSoftKeyDown;
347        }
348
349        // The current key needs to be updated.
350        mDirtyRect.union(mSoftKeyDown.mLeft, mSoftKeyDown.mTop,
351                mSoftKeyDown.mRight, mSoftKeyDown.mBottom);
352
353        if (mRepeatForLongPress) {
354            if (mMovingNeverHidePopupBalloon) {
355                return onKeyPress(x, y, mLongPressTimer, true);
356            }
357
358            if (null != mBalloonOnKey) {
359                mBalloonOnKey.delayedDismiss(0);
360            } else {
361                invalidate(mDirtyRect);
362            }
363
364            if (mSoftKeyDown.needBalloon()) {
365                mBalloonPopup.delayedDismiss(0);
366            }
367
368            if (null != mLongPressTimer) {
369                mLongPressTimer.removeTimer();
370            }
371            return onKeyPress(x, y, mLongPressTimer, true);
372        } else {
373            // When user moves between keys, repeated response is disabled.
374            return onKeyPress(x, y, mLongPressTimer, true);
375        }
376    }
377
378    private void tryVibrate() {
379        if (!Settings.getVibrate()) {
380            return;
381        }
382        if (mVibrator == null) {
383            mVibrator = (Vibrator)getContext().getSystemService(Context.VIBRATOR_SERVICE);
384        }
385        mVibrator.vibrate(mVibratePattern, -1);
386    }
387
388    private void tryPlayKeyDown() {
389        if (Settings.getKeySound()) {
390            mSoundManager.playKeyDown();
391        }
392    }
393
394    public void dimSoftKeyboard(boolean dimSkb) {
395        mDimSkb = dimSkb;
396        invalidate();
397    }
398
399    @Override
400    protected void onDraw(Canvas canvas) {
401        if (null == mSoftKeyboard) return;
402
403        canvas.translate(mPaddingLeft, mPaddingTop);
404
405        Environment env = Environment.getInstance();
406        mNormalKeyTextSize = env.getKeyTextSize(false);
407        mFunctionKeyTextSize = env.getKeyTextSize(true);
408        // Draw the last soft keyboard
409        int rowNum = mSoftKeyboard.getRowNum();
410        int keyXMargin = mSoftKeyboard.getKeyXMargin();
411        int keyYMargin = mSoftKeyboard.getKeyYMargin();
412        for (int row = 0; row < rowNum; row++) {
413            KeyRow keyRow = mSoftKeyboard.getKeyRowForDisplay(row);
414            if (null == keyRow) continue;
415            List<SoftKey> softKeys = keyRow.mSoftKeys;
416            int keyNum = softKeys.size();
417            for (int i = 0; i < keyNum; i++) {
418                SoftKey softKey = softKeys.get(i);
419                if (SoftKeyType.KEYTYPE_ID_NORMAL_KEY == softKey.mKeyType.mKeyTypeId) {
420                    mPaint.setTextSize(mNormalKeyTextSize);
421                } else {
422                    mPaint.setTextSize(mFunctionKeyTextSize);
423                }
424                drawSoftKey(canvas, softKey, keyXMargin, keyYMargin);
425            }
426        }
427
428        if (mDimSkb) {
429            mPaint.setColor(0xa0000000);
430            canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
431        }
432
433        mDirtyRect.setEmpty();
434    }
435
436    private void drawSoftKey(Canvas canvas, SoftKey softKey, int keyXMargin,
437            int keyYMargin) {
438        Drawable bg;
439        int textColor;
440        if (mKeyPressed && softKey == mSoftKeyDown) {
441            bg = softKey.getKeyHlBg();
442            textColor = softKey.getColorHl();
443        } else {
444            bg = softKey.getKeyBg();
445            textColor = softKey.getColor();
446        }
447
448        if (null != bg) {
449            bg.setBounds(softKey.mLeft + keyXMargin, softKey.mTop + keyYMargin,
450                    softKey.mRight - keyXMargin, softKey.mBottom - keyYMargin);
451            bg.draw(canvas);
452        }
453
454        String keyLabel = softKey.getKeyLabel();
455        Drawable keyIcon = softKey.getKeyIcon();
456        if (null != keyIcon) {
457            Drawable icon = keyIcon;
458            int marginLeft = (softKey.width() - icon.getIntrinsicWidth()) / 2;
459            int marginRight = softKey.width() - icon.getIntrinsicWidth()
460                    - marginLeft;
461            int marginTop = (softKey.height() - icon.getIntrinsicHeight()) / 2;
462            int marginBottom = softKey.height() - icon.getIntrinsicHeight()
463                    - marginTop;
464            icon.setBounds(softKey.mLeft + marginLeft,
465                    softKey.mTop + marginTop, softKey.mRight - marginRight,
466                    softKey.mBottom - marginBottom);
467            icon.draw(canvas);
468        } else if (null != keyLabel) {
469            mPaint.setColor(textColor);
470            float x = softKey.mLeft
471                    + (softKey.width() - mPaint.measureText(keyLabel)) / 2.0f;
472            int fontHeight = mFmi.bottom - mFmi.top;
473            float marginY = (softKey.height() - fontHeight) / 2.0f;
474            float y = softKey.mTop + marginY - mFmi.top + mFmi.bottom / 1.5f;
475            canvas.drawText(keyLabel, x, y + 1, mPaint);
476        }
477    }
478}
479