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