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}