1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 package android.support.v17.leanback.widget;
15 
16 import android.support.v17.leanback.R;
17 import android.animation.ObjectAnimator;
18 import android.content.Context;
19 import android.graphics.Bitmap;
20 import android.graphics.BitmapFactory;
21 import android.graphics.Canvas;
22 import android.graphics.Paint;
23 import android.text.SpannableStringBuilder;
24 import android.text.Spanned;
25 import android.text.SpannedString;
26 import android.text.style.ForegroundColorSpan;
27 import android.text.style.ReplacementSpan;
28 import android.util.AttributeSet;
29 import android.util.Log;
30 import android.util.Property;
31 import android.view.View;
32 import android.view.accessibility.AccessibilityNodeInfo;
33 import android.widget.EditText;
34 import android.widget.TextView;
35 
36 import java.util.List;
37 import java.util.Random;
38 import java.util.regex.Matcher;
39 import java.util.regex.Pattern;
40 
41 /**
42  * Shows the recognized text as a continuous stream of words.
43  */
44 class StreamingTextView extends EditText {
45 
46     private static final boolean DEBUG = false;
47     private static final String TAG = "StreamingTextView";
48 
49     private static final float TEXT_DOT_SCALE = 1.3F;
50     private static final boolean DOTS_FOR_STABLE = false;
51     private static final boolean DOTS_FOR_PENDING = true;
52     private static final boolean ANIMATE_DOTS_FOR_PENDING = true;
53 
54     private static final long STREAM_UPDATE_DELAY_MILLIS = 50;
55 
56     private static final Pattern SPLIT_PATTERN = Pattern.compile("\\S+");
57 
58     private static final Property<StreamingTextView,Integer> STREAM_POSITION_PROPERTY =
59             new Property<StreamingTextView,Integer>(Integer.class, "streamPosition") {
60 
61         @Override
62         public Integer get(StreamingTextView view) {
63             return view.getStreamPosition();
64         }
65 
66         @Override
67         public void set(StreamingTextView view, Integer value) {
68             view.setStreamPosition(value);
69         }
70     };
71 
72     private final Random mRandom = new Random();
73 
74     private Bitmap mOneDot;
75     private Bitmap mTwoDot;
76 
77     private int mStreamPosition;
78     private ObjectAnimator mStreamingAnimation;
79 
StreamingTextView(Context context, AttributeSet attrs)80     public StreamingTextView(Context context, AttributeSet attrs) {
81         super(context, attrs);
82     }
83 
StreamingTextView(Context context, AttributeSet attrs, int defStyle)84     public StreamingTextView(Context context, AttributeSet attrs, int defStyle) {
85         super(context, attrs, defStyle);
86     }
87 
88     @Override
onFinishInflate()89     protected void onFinishInflate() {
90         super.onFinishInflate();
91 
92         mOneDot = getScaledBitmap(R.drawable.lb_text_dot_one, TEXT_DOT_SCALE);
93         mTwoDot = getScaledBitmap(R.drawable.lb_text_dot_two, TEXT_DOT_SCALE);
94 
95         reset();
96     }
97 
getScaledBitmap(int resourceId, float scaled)98     private Bitmap getScaledBitmap(int resourceId, float scaled) {
99         Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resourceId);
100         return Bitmap.createScaledBitmap(bitmap, (int) (bitmap.getWidth() * scaled),
101                 (int) (bitmap.getHeight() * scaled), false);
102     }
103 
104     /**
105      * Resets the text view.
106      */
reset()107     public void reset() {
108         if (DEBUG) Log.d(TAG, "#reset");
109 
110         mStreamPosition = -1;
111         cancelStreamAnimation();
112         setText("");
113     }
114 
115     /**
116      * Updates the recognized text.
117      */
updateRecognizedText(String stableText, String pendingText)118     public void updateRecognizedText(String stableText, String pendingText) {
119         if (DEBUG) Log.d(TAG, "updateText(" + stableText + "," + pendingText + ")");
120 
121         if (stableText == null) {
122             stableText = "";
123         }
124 
125         SpannableStringBuilder displayText = new SpannableStringBuilder(stableText);
126 
127         if (DOTS_FOR_STABLE) {
128             addDottySpans(displayText, stableText, 0);
129         }
130 
131         if (pendingText != null) {
132             int pendingTextStart = displayText.length();
133             displayText.append(pendingText);
134             if (DOTS_FOR_PENDING) {
135                 addDottySpans(displayText, pendingText, pendingTextStart);
136             } else {
137                 int pendingColor = getResources().getColor(
138                         R.color.lb_search_plate_hint_text_color);
139                 addColorSpan(displayText, pendingColor, pendingText, pendingTextStart);
140             }
141         }
142 
143         // Start streaming in dots from beginning of partials, or current position,
144         // whichever is larger
145         mStreamPosition = Math.max(stableText.length(), mStreamPosition);
146 
147         // Copy the text and spans to a SpannedString, since editable text
148         // doesn't redraw in invalidate() when hardware accelerated
149         // if the text or spans havent't changed. (probably a framework bug)
150         updateText(new SpannedString(displayText));
151 
152         if (ANIMATE_DOTS_FOR_PENDING) {
153             startStreamAnimation();
154         }
155     }
156 
getStreamPosition()157     private int getStreamPosition() {
158         return mStreamPosition;
159     }
160 
setStreamPosition(int streamPosition)161     private void setStreamPosition(int streamPosition) {
162         mStreamPosition = streamPosition;
163         invalidate();
164     }
165 
startStreamAnimation()166     private void startStreamAnimation() {
167         cancelStreamAnimation();
168         int pos = getStreamPosition();
169         int totalLen = length();
170         int animLen = totalLen - pos;
171         if (animLen > 0) {
172             if (mStreamingAnimation == null) {
173                 mStreamingAnimation = new ObjectAnimator();
174                 mStreamingAnimation.setTarget(this);
175                 mStreamingAnimation.setProperty(STREAM_POSITION_PROPERTY);
176             }
177             mStreamingAnimation.setIntValues(pos, totalLen);
178             mStreamingAnimation.setDuration(STREAM_UPDATE_DELAY_MILLIS * animLen);
179             mStreamingAnimation.start();
180         }
181     }
182 
cancelStreamAnimation()183     private void cancelStreamAnimation() {
184         if (mStreamingAnimation != null) {
185             mStreamingAnimation.cancel();
186         }
187     }
188 
addDottySpans(SpannableStringBuilder displayText, String text, int textStart)189     private void addDottySpans(SpannableStringBuilder displayText, String text, int textStart) {
190         Matcher m = SPLIT_PATTERN.matcher(text);
191         while (m.find()) {
192             int wordStart = textStart + m.start();
193             int wordEnd = textStart + m.end();
194             DottySpan span = new DottySpan(text.charAt(m.start()), wordStart);
195             displayText.setSpan(span, wordStart, wordEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
196         }
197     }
198 
addColorSpan(SpannableStringBuilder displayText, int color, String text, int textStart)199     private void addColorSpan(SpannableStringBuilder displayText, int color, String text,
200             int textStart) {
201         ForegroundColorSpan span = new ForegroundColorSpan(color);
202         int start = textStart;
203         int end = textStart + text.length();
204         displayText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
205     }
206 
207     /**
208      * Sets the final, non changing, full text result. This should only happen at the very end of
209      * a recognition.
210      *
211      * @param finalText to the view to.
212      */
setFinalRecognizedText(CharSequence finalText)213     public void setFinalRecognizedText(CharSequence finalText) {
214         if (DEBUG) Log.d(TAG, "setFinalRecognizedText(" + finalText + ")");
215 
216         updateText(finalText);
217     }
218 
updateText(CharSequence displayText)219     private void updateText(CharSequence displayText) {
220         setText(displayText);
221         bringPointIntoView(length());
222     }
223 
224     /**
225      * This is required to make the View findable by uiautomator.
226      */
227     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)228     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
229         super.onInitializeAccessibilityNodeInfo(info);
230         info.setClassName(StreamingTextView.class.getCanonicalName());
231     }
232 
233     private class DottySpan extends ReplacementSpan {
234 
235         private final int mSeed;
236         private final int mPosition;
237 
DottySpan(int seed, int pos)238         public DottySpan(int seed, int pos) {
239             mSeed = seed;
240             mPosition = pos;
241         }
242 
243         @Override
draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint)244         public void draw(Canvas canvas, CharSequence text, int start, int end,
245                 float x, int top, int y, int bottom, Paint paint) {
246 
247             int width = (int) paint.measureText(text, start, end);
248 
249             int dotWidth = mOneDot.getWidth();
250             int sliceWidth = 2 * dotWidth;
251             int sliceCount = width / sliceWidth;
252             int excess = width % sliceWidth;
253             int prop = excess / 2;
254             boolean rtl = isLayoutRtl(StreamingTextView.this);
255 
256             mRandom.setSeed(mSeed);
257             int oldAlpha = paint.getAlpha();
258             for (int i = 0; i < sliceCount; i++) {
259                 if (ANIMATE_DOTS_FOR_PENDING) {
260                     if (mPosition + i >= mStreamPosition) break;
261                 }
262 
263                 float left = i * sliceWidth + prop + dotWidth / 2;
264                 float dotLeft = rtl ? x + width - left - dotWidth : x + left;
265 
266                 // give the dots some visual variety
267                 paint.setAlpha((mRandom.nextInt(4) + 1) * 63);
268 
269                 if (mRandom.nextBoolean()) {
270                     canvas.drawBitmap(mTwoDot, dotLeft, y - mTwoDot.getHeight(), paint);
271                 } else {
272                     canvas.drawBitmap(mOneDot, dotLeft, y - mOneDot.getHeight(), paint);
273                 }
274             }
275             paint.setAlpha(oldAlpha);
276         }
277 
278         @Override
getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fontMetricsInt)279         public int getSize(Paint paint, CharSequence text, int start, int end,
280                 Paint.FontMetricsInt fontMetricsInt) {
281             return (int) paint.measureText(text, start, end);
282         }
283     }
284 
isLayoutRtl(View view)285     public static boolean isLayoutRtl(View view) {
286         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
287             return View.LAYOUT_DIRECTION_RTL == view.getLayoutDirection();
288         } else {
289             return false;
290         }
291     }
292 
updateRecognizedText(String stableText, List<Float> rmsValues)293     public void updateRecognizedText(String stableText, List<Float> rmsValues) {}
294 }
295