TextDecoratorUi.java revision bea17c49ec23bf0f646cb548445c7756aa50d233
1/*
2 * Copyright (C) 2014 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;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Matrix;
24import android.graphics.Paint;
25import android.graphics.Path;
26import android.graphics.RectF;
27import android.graphics.drawable.ColorDrawable;
28import android.inputmethodservice.InputMethodService;
29import android.util.TypedValue;
30import android.view.Gravity;
31import android.view.View;
32import android.view.View.OnClickListener;
33import android.view.ViewGroup;
34import android.view.ViewGroup.LayoutParams;
35import android.view.ViewParent;
36import android.widget.PopupWindow;
37import android.widget.RelativeLayout;
38
39import com.android.inputmethod.latin.R;
40
41/**
42 * Used as the UI component of {@link TextDecorator}.
43 */
44public final class TextDecoratorUi implements TextDecoratorUiOperator {
45    private static final boolean VISUAL_DEBUG = false;
46    private static final int VISUAL_DEBUG_HIT_AREA_COLOR = 0x80ff8000;
47
48    private final RelativeLayout mLocalRootView;
49    private final CommitIndicatorView mCommitIndicatorView;
50    private final AddToDictionaryIndicatorView mAddToDictionaryIndicatorView;
51    private final PopupWindow mTouchEventWindow;
52    private final View mTouchEventWindowClickListenerView;
53    private final float mHitAreaMarginInPixels;
54
55    /**
56     * This constructor is designed to be called from {@link InputMethodService#setInputView(View)}.
57     * Other usages are not supported.
58     *
59     * @param context the context of the input method.
60     * @param inputView the view that is passed to {@link InputMethodService#setInputView(View)}.
61     */
62    public TextDecoratorUi(final Context context, final View inputView) {
63        final Resources resources = context.getResources();
64        final int hitAreaMarginInDP = resources.getInteger(
65                R.integer.text_decorator_hit_area_margin_in_dp);
66        mHitAreaMarginInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
67                hitAreaMarginInDP, resources.getDisplayMetrics());
68
69        mLocalRootView = new RelativeLayout(context);
70        mLocalRootView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
71                LayoutParams.MATCH_PARENT));
72        // TODO: Use #setBackground(null) for API Level >= 16.
73        mLocalRootView.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
74
75        final ViewGroup contentView = getContentView(inputView);
76        mCommitIndicatorView = new CommitIndicatorView(context);
77        mAddToDictionaryIndicatorView = new AddToDictionaryIndicatorView(context);
78        mLocalRootView.addView(mCommitIndicatorView);
79        mLocalRootView.addView(mAddToDictionaryIndicatorView);
80        if (contentView != null) {
81            contentView.addView(mLocalRootView);
82        }
83
84        // This popup window is used to avoid the limitation that the input method is not able to
85        // observe the touch events happening outside of InputMethodService.Insets#touchableRegion.
86        // We don't use this popup window for rendering the UI for performance reasons though.
87        mTouchEventWindow = new PopupWindow(context);
88        if (VISUAL_DEBUG) {
89            mTouchEventWindow.setBackgroundDrawable(new ColorDrawable(VISUAL_DEBUG_HIT_AREA_COLOR));
90        } else {
91            mTouchEventWindow.setBackgroundDrawable(null);
92        }
93        mTouchEventWindowClickListenerView = new View(context);
94        mTouchEventWindow.setContentView(mTouchEventWindowClickListenerView);
95    }
96
97    @Override
98    public void disposeUi() {
99        if (mLocalRootView != null) {
100            final ViewParent parent = mLocalRootView.getParent();
101            if (parent != null && parent instanceof ViewGroup) {
102                ((ViewGroup) parent).removeView(mLocalRootView);
103            }
104            mLocalRootView.removeAllViews();
105        }
106        if (mTouchEventWindow != null) {
107            mTouchEventWindow.dismiss();
108        }
109    }
110
111    @Override
112    public void hideUi() {
113        mCommitIndicatorView.setVisibility(View.GONE);
114        mAddToDictionaryIndicatorView.setVisibility(View.GONE);
115        mTouchEventWindow.dismiss();
116    }
117
118    @Override
119    public void layoutUi(final boolean isCommitMode, final Matrix matrix,
120            final RectF indicatorBounds, final RectF composingTextBounds) {
121        final RectF indicatorBoundsInScreenCoordinates = new RectF();
122        matrix.mapRect(indicatorBoundsInScreenCoordinates, indicatorBounds);
123        mCommitIndicatorView.setBounds(indicatorBoundsInScreenCoordinates);
124        mAddToDictionaryIndicatorView.setBounds(indicatorBoundsInScreenCoordinates);
125
126        final RectF hitAreaBounds = new RectF(composingTextBounds);
127        hitAreaBounds.union(indicatorBounds);
128        final RectF hitAreaBoundsInScreenCoordinates = new RectF();
129        matrix.mapRect(hitAreaBoundsInScreenCoordinates, hitAreaBounds);
130        hitAreaBoundsInScreenCoordinates.inset(-mHitAreaMarginInPixels, -mHitAreaMarginInPixels);
131
132        final int[] originScreen = new int[2];
133        mLocalRootView.getLocationOnScreen(originScreen);
134        final int viewOriginX = originScreen[0];
135        final int viewOriginY = originScreen[1];
136
137        final View toBeShown;
138        final View toBeHidden;
139        if (isCommitMode) {
140            toBeShown = mCommitIndicatorView;
141            toBeHidden = mAddToDictionaryIndicatorView;
142        } else {
143            toBeShown = mAddToDictionaryIndicatorView;
144            toBeHidden = mCommitIndicatorView;
145        }
146        toBeShown.setX(indicatorBoundsInScreenCoordinates.left - viewOriginX);
147        toBeShown.setY(indicatorBoundsInScreenCoordinates.top - viewOriginY);
148        toBeShown.setVisibility(View.VISIBLE);
149        toBeHidden.setVisibility(View.GONE);
150
151        if (mTouchEventWindow.isShowing()) {
152            mTouchEventWindow.update((int)hitAreaBoundsInScreenCoordinates.left - viewOriginX,
153                    (int)hitAreaBoundsInScreenCoordinates.top - viewOriginY,
154                    (int)hitAreaBoundsInScreenCoordinates.width(),
155                    (int)hitAreaBoundsInScreenCoordinates.height());
156        } else {
157            mTouchEventWindow.setWidth((int)hitAreaBoundsInScreenCoordinates.width());
158            mTouchEventWindow.setHeight((int)hitAreaBoundsInScreenCoordinates.height());
159            mTouchEventWindow.showAtLocation(mLocalRootView, Gravity.NO_GRAVITY,
160                    (int)hitAreaBoundsInScreenCoordinates.left - viewOriginX,
161                    (int)hitAreaBoundsInScreenCoordinates.top - viewOriginY);
162        }
163    }
164
165    @Override
166    public void setOnClickListener(final Runnable listener) {
167        mTouchEventWindowClickListenerView.setOnClickListener(new OnClickListener() {
168            @Override
169            public void onClick(final View arg0) {
170                listener.run();
171            }
172        });
173    }
174
175    private static class IndicatorView extends View {
176        private final Path mPath;
177        private final Path mTmpPath = new Path();
178        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
179        private final Matrix mMatrix = new Matrix();
180        private final int mBackgroundColor;
181        private final int mForegroundColor;
182        private final RectF mBounds = new RectF();
183        public IndicatorView(Context context, final int pathResourceId,
184                final int sizeResourceId, final int backgroundColorResourceId,
185                final int foregroundColroResourceId) {
186            super(context);
187            final Resources resources = context.getResources();
188            mPath = createPath(resources, pathResourceId, sizeResourceId);
189            mBackgroundColor = resources.getColor(backgroundColorResourceId);
190            mForegroundColor = resources.getColor(foregroundColroResourceId);
191        }
192
193        public void setBounds(final RectF rect) {
194            mBounds.set(rect);
195        }
196
197        @Override
198        protected void onDraw(Canvas canvas) {
199            mPaint.setColor(mBackgroundColor);
200            mPaint.setStyle(Paint.Style.FILL);
201            canvas.drawRect(0.0f, 0.0f, mBounds.width(), mBounds.height(), mPaint);
202
203            mMatrix.reset();
204            mMatrix.postScale(mBounds.width(), mBounds.height());
205            mPath.transform(mMatrix, mTmpPath);
206            mPaint.setColor(mForegroundColor);
207            canvas.drawPath(mTmpPath, mPaint);
208        }
209
210        private static Path createPath(final Resources resources, final int pathResourceId,
211                final int sizeResourceId) {
212            final int size = resources.getInteger(sizeResourceId);
213            final float normalizationFactor = 1.0f / size;
214            final int[] array = resources.getIntArray(pathResourceId);
215
216            final Path path = new Path();
217            for (int i = 0; i < array.length; i += 2) {
218                if (i == 0) {
219                    path.moveTo(array[i] * normalizationFactor, array[i + 1] * normalizationFactor);
220                } else {
221                    path.lineTo(array[i] * normalizationFactor, array[i + 1] * normalizationFactor);
222                }
223            }
224            path.close();
225            return path;
226        }
227    }
228
229    private static ViewGroup getContentView(final View view) {
230        final View rootView = view.getRootView();
231        if (rootView == null) {
232            return null;
233        }
234
235        final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content);
236        if (windowContentView == null) {
237            return null;
238        }
239        return windowContentView;
240    }
241
242    private static final class CommitIndicatorView extends TextDecoratorUi.IndicatorView {
243        public CommitIndicatorView(final Context context) {
244            super(context, R.array.text_decorator_commit_indicator_path,
245                    R.integer.text_decorator_commit_indicator_path_size,
246                    R.color.text_decorator_commit_indicator_background_color,
247                    R.color.text_decorator_commit_indicator_foreground_color);
248        }
249    }
250
251    private static final class AddToDictionaryIndicatorView extends TextDecoratorUi.IndicatorView {
252        public AddToDictionaryIndicatorView(final Context context) {
253            super(context, R.array.text_decorator_add_to_dictionary_indicator_path,
254                    R.integer.text_decorator_add_to_dictionary_indicator_path_size,
255                    R.color.text_decorator_add_to_dictionary_indicator_background_color,
256                    R.color.text_decorator_add_to_dictionary_indicator_foreground_color);
257        }
258    }
259}