/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.inputmethod.keyboard; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; import android.inputmethodservice.InputMethodService; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewParent; import android.widget.PopupWindow; import android.widget.RelativeLayout; import org.smc.inputmethod.indic.R; /** * Used as the UI component of {@link TextDecorator}. */ public final class TextDecoratorUi implements TextDecoratorUiOperator { private static final boolean VISUAL_DEBUG = false; private static final int VISUAL_DEBUG_HIT_AREA_COLOR = 0x80ff8000; private final RelativeLayout mLocalRootView; private final AddToDictionaryIndicatorView mAddToDictionaryIndicatorView; private final PopupWindow mTouchEventWindow; private final View mTouchEventWindowClickListenerView; private final float mHitAreaMarginInPixels; private final RectF mDisplayRect; /** * This constructor is designed to be called from {@link InputMethodService#setInputView(View)}. * Other usages are not supported. * * @param context the context of the input method. * @param inputView the view that is passed to {@link InputMethodService#setInputView(View)}. */ public TextDecoratorUi(final Context context, final View inputView) { final Resources resources = context.getResources(); final int hitAreaMarginInDP = resources.getInteger( R.integer.text_decorator_hit_area_margin_in_dp); mHitAreaMarginInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, hitAreaMarginInDP, resources.getDisplayMetrics()); final DisplayMetrics displayMetrics = resources.getDisplayMetrics(); mDisplayRect = new RectF(0.0f, 0.0f, displayMetrics.widthPixels, displayMetrics.heightPixels); mLocalRootView = new RelativeLayout(context); mLocalRootView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); // TODO: Use #setBackground(null) for API Level >= 16. mLocalRootView.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); final ViewGroup contentView = getContentView(inputView); mAddToDictionaryIndicatorView = new AddToDictionaryIndicatorView(context); mLocalRootView.addView(mAddToDictionaryIndicatorView); if (contentView != null) { contentView.addView(mLocalRootView); } // This popup window is used to avoid the limitation that the input method is not able to // observe the touch events happening outside of InputMethodService.Insets#touchableRegion. // We don't use this popup window for rendering the UI for performance reasons though. mTouchEventWindow = new PopupWindow(context); if (VISUAL_DEBUG) { mTouchEventWindow.setBackgroundDrawable(new ColorDrawable(VISUAL_DEBUG_HIT_AREA_COLOR)); } else { mTouchEventWindow.setBackgroundDrawable(null); } mTouchEventWindowClickListenerView = new View(context); mTouchEventWindow.setContentView(mTouchEventWindowClickListenerView); } @Override public void disposeUi() { if (mLocalRootView != null) { final ViewParent parent = mLocalRootView.getParent(); if (parent != null && parent instanceof ViewGroup) { ((ViewGroup) parent).removeView(mLocalRootView); } mLocalRootView.removeAllViews(); } if (mTouchEventWindow != null) { mTouchEventWindow.dismiss(); } } @Override public void hideUi() { mAddToDictionaryIndicatorView.setVisibility(View.GONE); mTouchEventWindow.dismiss(); } private static final RectF getIndicatorBoundsInScreenCoordinates(final Matrix matrix, final RectF composingTextBounds, final boolean showAtLeftSide) { final float indicatorSize = composingTextBounds.height(); final RectF indicatorBounds; if (showAtLeftSide) { indicatorBounds = new RectF(composingTextBounds.left - indicatorSize, composingTextBounds.top, composingTextBounds.left, composingTextBounds.top + indicatorSize); } else { indicatorBounds = new RectF(composingTextBounds.right, composingTextBounds.top, composingTextBounds.right + indicatorSize, composingTextBounds.top + indicatorSize); } matrix.mapRect(indicatorBounds); return indicatorBounds; } @Override public void layoutUi(final Matrix matrix, final RectF composingTextBounds, final boolean useRtlLayout) { RectF indicatorBoundsInScreenCoordinates = getIndicatorBoundsInScreenCoordinates(matrix, composingTextBounds, useRtlLayout /* showAtLeftSide */); if (indicatorBoundsInScreenCoordinates.left < mDisplayRect.left || mDisplayRect.right < indicatorBoundsInScreenCoordinates.right) { // The indicator is clipped by the screen. Show the indicator at the opposite side. indicatorBoundsInScreenCoordinates = getIndicatorBoundsInScreenCoordinates(matrix, composingTextBounds, !useRtlLayout /* showAtLeftSide */); } mAddToDictionaryIndicatorView.setBounds(indicatorBoundsInScreenCoordinates); final RectF hitAreaBoundsInScreenCoordinates = new RectF(); matrix.mapRect(hitAreaBoundsInScreenCoordinates, composingTextBounds); hitAreaBoundsInScreenCoordinates.union(indicatorBoundsInScreenCoordinates); hitAreaBoundsInScreenCoordinates.inset(-mHitAreaMarginInPixels, -mHitAreaMarginInPixels); final int[] originScreen = new int[2]; mLocalRootView.getLocationOnScreen(originScreen); final int viewOriginX = originScreen[0]; final int viewOriginY = originScreen[1]; mAddToDictionaryIndicatorView.setX(indicatorBoundsInScreenCoordinates.left - viewOriginX); mAddToDictionaryIndicatorView.setY(indicatorBoundsInScreenCoordinates.top - viewOriginY); mAddToDictionaryIndicatorView.setVisibility(View.VISIBLE); if (mTouchEventWindow.isShowing()) { mTouchEventWindow.update((int)hitAreaBoundsInScreenCoordinates.left - viewOriginX, (int)hitAreaBoundsInScreenCoordinates.top - viewOriginY, (int)hitAreaBoundsInScreenCoordinates.width(), (int)hitAreaBoundsInScreenCoordinates.height()); } else { mTouchEventWindow.setWidth((int)hitAreaBoundsInScreenCoordinates.width()); mTouchEventWindow.setHeight((int)hitAreaBoundsInScreenCoordinates.height()); mTouchEventWindow.showAtLocation(mLocalRootView, Gravity.NO_GRAVITY, (int)hitAreaBoundsInScreenCoordinates.left - viewOriginX, (int)hitAreaBoundsInScreenCoordinates.top - viewOriginY); } } @Override public void setOnClickListener(final Runnable listener) { mTouchEventWindowClickListenerView.setOnClickListener(new OnClickListener() { @Override public void onClick(final View arg0) { listener.run(); } }); } private static class IndicatorView extends View { private final Path mPath; private final Path mTmpPath = new Path(); private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Matrix mMatrix = new Matrix(); private final int mBackgroundColor; private final int mForegroundColor; private final RectF mBounds = new RectF(); public IndicatorView(Context context, final int pathResourceId, final int sizeResourceId, final int backgroundColorResourceId, final int foregroundColroResourceId) { super(context); final Resources resources = context.getResources(); mPath = createPath(resources, pathResourceId, sizeResourceId); mBackgroundColor = resources.getColor(backgroundColorResourceId); mForegroundColor = resources.getColor(foregroundColroResourceId); } public void setBounds(final RectF rect) { mBounds.set(rect); } @Override protected void onDraw(Canvas canvas) { mPaint.setColor(mBackgroundColor); mPaint.setStyle(Paint.Style.FILL); canvas.drawRect(0.0f, 0.0f, mBounds.width(), mBounds.height(), mPaint); mMatrix.reset(); mMatrix.postScale(mBounds.width(), mBounds.height()); mPath.transform(mMatrix, mTmpPath); mPaint.setColor(mForegroundColor); canvas.drawPath(mTmpPath, mPaint); } private static Path createPath(final Resources resources, final int pathResourceId, final int sizeResourceId) { final int size = resources.getInteger(sizeResourceId); final float normalizationFactor = 1.0f / size; final int[] array = resources.getIntArray(pathResourceId); final Path path = new Path(); for (int i = 0; i < array.length; i += 2) { if (i == 0) { path.moveTo(array[i] * normalizationFactor, array[i + 1] * normalizationFactor); } else { path.lineTo(array[i] * normalizationFactor, array[i + 1] * normalizationFactor); } } path.close(); return path; } } private static ViewGroup getContentView(final View view) { final View rootView = view.getRootView(); if (rootView == null) { return null; } final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content); if (windowContentView == null) { return null; } return windowContentView; } private static final class AddToDictionaryIndicatorView extends TextDecoratorUi.IndicatorView { public AddToDictionaryIndicatorView(final Context context) { super(context, R.array.text_decorator_add_to_dictionary_indicator_path, R.integer.text_decorator_add_to_dictionary_indicator_path_size, R.color.text_decorator_add_to_dictionary_indicator_background_color, R.color.text_decorator_add_to_dictionary_indicator_foreground_color); } } }