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