Home | History | Annotate | Download | only in keyboard
      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 
     17 package com.android.inputmethod.keyboard;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.graphics.Canvas;
     22 import android.graphics.Color;
     23 import android.graphics.Matrix;
     24 import android.graphics.Paint;
     25 import android.graphics.Path;
     26 import android.graphics.RectF;
     27 import android.graphics.drawable.ColorDrawable;
     28 import android.inputmethodservice.InputMethodService;
     29 import android.util.DisplayMetrics;
     30 import android.util.TypedValue;
     31 import android.view.Gravity;
     32 import android.view.View;
     33 import android.view.View.OnClickListener;
     34 import android.view.ViewGroup;
     35 import android.view.ViewGroup.LayoutParams;
     36 import android.view.ViewParent;
     37 import android.widget.PopupWindow;
     38 import android.widget.RelativeLayout;
     39 
     40 import com.android.inputmethod.latin.R;
     41 
     42 /**
     43  * Used as the UI component of {@link TextDecorator}.
     44  */
     45 public 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 }