1 /* 2 * Copyright (C) 2008-2009 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.example.android.softkeyboard; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Canvas; 22 import android.graphics.Paint; 23 import android.graphics.Rect; 24 import android.graphics.drawable.Drawable; 25 import android.view.GestureDetector; 26 import android.view.MotionEvent; 27 import android.view.View; 28 29 import java.util.ArrayList; 30 import java.util.List; 31 32 public class CandidateView extends View { 33 34 private static final int OUT_OF_BOUNDS = -1; 35 36 private SoftKeyboard mService; 37 private List<String> mSuggestions; 38 private int mSelectedIndex; 39 private int mTouchX = OUT_OF_BOUNDS; 40 private Drawable mSelectionHighlight; 41 private boolean mTypedWordValid; 42 43 private Rect mBgPadding; 44 45 private static final int MAX_SUGGESTIONS = 32; 46 private static final int SCROLL_PIXELS = 20; 47 48 private int[] mWordWidth = new int[MAX_SUGGESTIONS]; 49 private int[] mWordX = new int[MAX_SUGGESTIONS]; 50 51 private static final int X_GAP = 10; 52 53 private static final List<String> EMPTY_LIST = new ArrayList<String>(); 54 55 private int mColorNormal; 56 private int mColorRecommended; 57 private int mColorOther; 58 private int mVerticalPadding; 59 private Paint mPaint; 60 private boolean mScrolled; 61 private int mTargetScrollX; 62 63 private int mTotalWidth; 64 65 private GestureDetector mGestureDetector; 66 67 /** 68 * Construct a CandidateView for showing suggested words for completion. 69 * @param context 70 * @param attrs 71 */ CandidateView(Context context)72 public CandidateView(Context context) { 73 super(context); 74 mSelectionHighlight = context.getResources().getDrawable( 75 android.R.drawable.list_selector_background); 76 mSelectionHighlight.setState(new int[] { 77 android.R.attr.state_enabled, 78 android.R.attr.state_focused, 79 android.R.attr.state_window_focused, 80 android.R.attr.state_pressed 81 }); 82 83 Resources r = context.getResources(); 84 85 setBackgroundColor(r.getColor(R.color.candidate_background)); 86 87 mColorNormal = r.getColor(R.color.candidate_normal); 88 mColorRecommended = r.getColor(R.color.candidate_recommended); 89 mColorOther = r.getColor(R.color.candidate_other); 90 mVerticalPadding = r.getDimensionPixelSize(R.dimen.candidate_vertical_padding); 91 92 mPaint = new Paint(); 93 mPaint.setColor(mColorNormal); 94 mPaint.setAntiAlias(true); 95 mPaint.setTextSize(r.getDimensionPixelSize(R.dimen.candidate_font_height)); 96 mPaint.setStrokeWidth(0); 97 98 mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { 99 @Override 100 public boolean onScroll(MotionEvent e1, MotionEvent e2, 101 float distanceX, float distanceY) { 102 mScrolled = true; 103 int sx = getScrollX(); 104 sx += distanceX; 105 if (sx < 0) { 106 sx = 0; 107 } 108 if (sx + getWidth() > mTotalWidth) { 109 sx -= distanceX; 110 } 111 mTargetScrollX = sx; 112 scrollTo(sx, getScrollY()); 113 invalidate(); 114 return true; 115 } 116 }); 117 setHorizontalFadingEdgeEnabled(true); 118 setWillNotDraw(false); 119 setHorizontalScrollBarEnabled(false); 120 setVerticalScrollBarEnabled(false); 121 } 122 123 /** 124 * A connection back to the service to communicate with the text field 125 * @param listener 126 */ setService(SoftKeyboard listener)127 public void setService(SoftKeyboard listener) { 128 mService = listener; 129 } 130 131 @Override computeHorizontalScrollRange()132 public int computeHorizontalScrollRange() { 133 return mTotalWidth; 134 } 135 136 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)137 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 138 int measuredWidth = resolveSize(50, widthMeasureSpec); 139 140 // Get the desired height of the icon menu view (last row of items does 141 // not have a divider below) 142 Rect padding = new Rect(); 143 mSelectionHighlight.getPadding(padding); 144 final int desiredHeight = ((int)mPaint.getTextSize()) + mVerticalPadding 145 + padding.top + padding.bottom; 146 147 // Maximum possible width and desired height 148 setMeasuredDimension(measuredWidth, 149 resolveSize(desiredHeight, heightMeasureSpec)); 150 } 151 152 /** 153 * If the canvas is null, then only touch calculations are performed to pick the target 154 * candidate. 155 */ 156 @Override onDraw(Canvas canvas)157 protected void onDraw(Canvas canvas) { 158 if (canvas != null) { 159 super.onDraw(canvas); 160 } 161 mTotalWidth = 0; 162 if (mSuggestions == null) return; 163 164 if (mBgPadding == null) { 165 mBgPadding = new Rect(0, 0, 0, 0); 166 if (getBackground() != null) { 167 getBackground().getPadding(mBgPadding); 168 } 169 } 170 int x = 0; 171 final int count = mSuggestions.size(); 172 final int height = getHeight(); 173 final Rect bgPadding = mBgPadding; 174 final Paint paint = mPaint; 175 final int touchX = mTouchX; 176 final int scrollX = getScrollX(); 177 final boolean scrolled = mScrolled; 178 final boolean typedWordValid = mTypedWordValid; 179 final int y = (int) (((height - mPaint.getTextSize()) / 2) - mPaint.ascent()); 180 181 for (int i = 0; i < count; i++) { 182 String suggestion = mSuggestions.get(i); 183 float textWidth = paint.measureText(suggestion); 184 final int wordWidth = (int) textWidth + X_GAP * 2; 185 186 mWordX[i] = x; 187 mWordWidth[i] = wordWidth; 188 paint.setColor(mColorNormal); 189 if (touchX + scrollX >= x && touchX + scrollX < x + wordWidth && !scrolled) { 190 if (canvas != null) { 191 canvas.translate(x, 0); 192 mSelectionHighlight.setBounds(0, bgPadding.top, wordWidth, height); 193 mSelectionHighlight.draw(canvas); 194 canvas.translate(-x, 0); 195 } 196 mSelectedIndex = i; 197 } 198 199 if (canvas != null) { 200 if ((i == 1 && !typedWordValid) || (i == 0 && typedWordValid)) { 201 paint.setFakeBoldText(true); 202 paint.setColor(mColorRecommended); 203 } else if (i != 0) { 204 paint.setColor(mColorOther); 205 } 206 canvas.drawText(suggestion, x + X_GAP, y, paint); 207 paint.setColor(mColorOther); 208 canvas.drawLine(x + wordWidth + 0.5f, bgPadding.top, 209 x + wordWidth + 0.5f, height + 1, paint); 210 paint.setFakeBoldText(false); 211 } 212 x += wordWidth; 213 } 214 mTotalWidth = x; 215 if (mTargetScrollX != getScrollX()) { 216 scrollToTarget(); 217 } 218 } 219 scrollToTarget()220 private void scrollToTarget() { 221 int sx = getScrollX(); 222 if (mTargetScrollX > sx) { 223 sx += SCROLL_PIXELS; 224 if (sx >= mTargetScrollX) { 225 sx = mTargetScrollX; 226 requestLayout(); 227 } 228 } else { 229 sx -= SCROLL_PIXELS; 230 if (sx <= mTargetScrollX) { 231 sx = mTargetScrollX; 232 requestLayout(); 233 } 234 } 235 scrollTo(sx, getScrollY()); 236 invalidate(); 237 } 238 setSuggestions(List<String> suggestions, boolean completions, boolean typedWordValid)239 public void setSuggestions(List<String> suggestions, boolean completions, 240 boolean typedWordValid) { 241 clear(); 242 if (suggestions != null) { 243 mSuggestions = new ArrayList<String>(suggestions); 244 } 245 mTypedWordValid = typedWordValid; 246 scrollTo(0, 0); 247 mTargetScrollX = 0; 248 // Compute the total width 249 onDraw(null); 250 invalidate(); 251 requestLayout(); 252 } 253 clear()254 public void clear() { 255 mSuggestions = EMPTY_LIST; 256 mTouchX = OUT_OF_BOUNDS; 257 mSelectedIndex = -1; 258 invalidate(); 259 } 260 261 @Override onTouchEvent(MotionEvent me)262 public boolean onTouchEvent(MotionEvent me) { 263 264 if (mGestureDetector.onTouchEvent(me)) { 265 return true; 266 } 267 268 int action = me.getAction(); 269 int x = (int) me.getX(); 270 int y = (int) me.getY(); 271 mTouchX = x; 272 273 switch (action) { 274 case MotionEvent.ACTION_DOWN: 275 mScrolled = false; 276 invalidate(); 277 break; 278 case MotionEvent.ACTION_MOVE: 279 if (y <= 0) { 280 // Fling up!? 281 if (mSelectedIndex >= 0) { 282 mService.pickSuggestionManually(mSelectedIndex); 283 mSelectedIndex = -1; 284 } 285 } 286 invalidate(); 287 break; 288 case MotionEvent.ACTION_UP: 289 if (!mScrolled) { 290 if (mSelectedIndex >= 0) { 291 mService.pickSuggestionManually(mSelectedIndex); 292 } 293 } 294 mSelectedIndex = -1; 295 removeHighlight(); 296 requestLayout(); 297 break; 298 } 299 return true; 300 } 301 302 /** 303 * For flick through from keyboard, call this method with the x coordinate of the flick 304 * gesture. 305 * @param x 306 */ takeSuggestionAt(float x)307 public void takeSuggestionAt(float x) { 308 mTouchX = (int) x; 309 // To detect candidate 310 onDraw(null); 311 if (mSelectedIndex >= 0) { 312 mService.pickSuggestionManually(mSelectedIndex); 313 } 314 invalidate(); 315 } 316 removeHighlight()317 private void removeHighlight() { 318 mTouchX = OUT_OF_BOUNDS; 319 invalidate(); 320 } 321 } 322