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