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.support.v17.leanback.R; 17import android.animation.ObjectAnimator; 18import android.content.Context; 19import android.graphics.Bitmap; 20import android.graphics.BitmapFactory; 21import android.graphics.Canvas; 22import android.graphics.Paint; 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; 34import android.widget.TextView; 35 36import java.util.List; 37import java.util.Random; 38import java.util.regex.Matcher; 39import java.util.regex.Pattern; 40 41/** 42 * Shows the recognized text as a continuous stream of words. 43 */ 44class 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 80 public StreamingTextView(Context context, AttributeSet attrs) { 81 super(context, attrs); 82 } 83 84 public StreamingTextView(Context context, AttributeSet attrs, int defStyle) { 85 super(context, attrs, defStyle); 86 } 87 88 @Override 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 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 */ 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 */ 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 157 private int getStreamPosition() { 158 return mStreamPosition; 159 } 160 161 private void setStreamPosition(int streamPosition) { 162 mStreamPosition = streamPosition; 163 invalidate(); 164 } 165 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 183 private void cancelStreamAnimation() { 184 if (mStreamingAnimation != null) { 185 mStreamingAnimation.cancel(); 186 } 187 } 188 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 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 */ 213 public void setFinalRecognizedText(CharSequence finalText) { 214 if (DEBUG) Log.d(TAG, "setFinalRecognizedText(" + finalText + ")"); 215 216 updateText(finalText); 217 } 218 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 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 238 public DottySpan(int seed, int pos) { 239 mSeed = seed; 240 mPosition = pos; 241 } 242 243 @Override 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 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 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 293 public void updateRecognizedText(String stableText, List<Float> rmsValues) {} 294} 295