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