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 public void reset() { 105 if (DEBUG) Log.d(TAG, "#reset"); 106 107 mStreamPosition = -1; 108 cancelStreamAnimation(); 109 setText(""); 110 } 111 112 public void updateRecognizedText(String stableText, String pendingText) { 113 if (DEBUG) Log.d(TAG, "updateText(" + stableText + "," + pendingText + ")"); 114 115 if (stableText == null) { 116 stableText = ""; 117 } 118 119 SpannableStringBuilder displayText = new SpannableStringBuilder(stableText); 120 121 if (DOTS_FOR_STABLE) { 122 addDottySpans(displayText, stableText, 0); 123 } 124 125 if (pendingText != null) { 126 int pendingTextStart = displayText.length(); 127 displayText.append(pendingText); 128 if (DOTS_FOR_PENDING) { 129 addDottySpans(displayText, pendingText, pendingTextStart); 130 } else { 131 int pendingColor = getResources().getColor( 132 R.color.lb_search_plate_hint_text_color); 133 addColorSpan(displayText, pendingColor, pendingText, pendingTextStart); 134 } 135 } 136 137 // Start streaming in dots from beginning of partials, or current position, 138 // whichever is larger 139 mStreamPosition = Math.max(stableText.length(), mStreamPosition); 140 141 // Copy the text and spans to a SpannedString, since editable text 142 // doesn't redraw in invalidate() when hardware accelerated 143 // if the text or spans havent't changed. (probably a framework bug) 144 updateText(new SpannedString(displayText)); 145 146 if (ANIMATE_DOTS_FOR_PENDING) { 147 startStreamAnimation(); 148 } 149 } 150 151 private int getStreamPosition() { 152 return mStreamPosition; 153 } 154 155 private void setStreamPosition(int streamPosition) { 156 mStreamPosition = streamPosition; 157 invalidate(); 158 } 159 160 private void startStreamAnimation() { 161 cancelStreamAnimation(); 162 int pos = getStreamPosition(); 163 int totalLen = length(); 164 int animLen = totalLen - pos; 165 if (animLen > 0) { 166 if (mStreamingAnimation == null) { 167 mStreamingAnimation = new ObjectAnimator(); 168 mStreamingAnimation.setTarget(this); 169 mStreamingAnimation.setProperty(STREAM_POSITION_PROPERTY); 170 } 171 mStreamingAnimation.setIntValues(pos, totalLen); 172 mStreamingAnimation.setDuration(STREAM_UPDATE_DELAY_MILLIS * animLen); 173 mStreamingAnimation.start(); 174 } 175 } 176 177 private void cancelStreamAnimation() { 178 if (mStreamingAnimation != null) { 179 mStreamingAnimation.cancel(); 180 } 181 } 182 183 private void addDottySpans(SpannableStringBuilder displayText, String text, int textStart) { 184 Matcher m = SPLIT_PATTERN.matcher(text); 185 while (m.find()) { 186 int wordStart = textStart + m.start(); 187 int wordEnd = textStart + m.end(); 188 DottySpan span = new DottySpan(text.charAt(m.start()), wordStart); 189 displayText.setSpan(span, wordStart, wordEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 190 } 191 } 192 193 private void addColorSpan(SpannableStringBuilder displayText, int color, String text, 194 int textStart) { 195 ForegroundColorSpan span = new ForegroundColorSpan(color); 196 int start = textStart; 197 int end = textStart + text.length(); 198 displayText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 199 } 200 201 /** 202 * Sets the final, non changing, full text result. This should only happen at the very end of 203 * a recognition. 204 * 205 * @param finalText to the view to. 206 */ 207 public void setFinalRecognizedText(CharSequence finalText) { 208 if (DEBUG) Log.d(TAG, "setFinalRecognizedText(" + finalText + ")"); 209 210 updateText(finalText); 211 } 212 213 private void updateText(CharSequence displayText) { 214 setText(displayText); 215 bringPointIntoView(length()); 216 } 217 218 /** 219 * This is required to make the View findable by uiautomator 220 */ 221 @Override 222 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 223 super.onInitializeAccessibilityNodeInfo(info); 224 info.setClassName(StreamingTextView.class.getCanonicalName()); 225 } 226 227 private class DottySpan extends ReplacementSpan { 228 229 private final int mSeed; 230 private final int mPosition; 231 232 public DottySpan(int seed, int pos) { 233 mSeed = seed; 234 mPosition = pos; 235 } 236 237 @Override 238 public void draw(Canvas canvas, CharSequence text, int start, int end, 239 float x, int top, int y, int bottom, Paint paint) { 240 241 int width = (int) paint.measureText(text, start, end); 242 243 int dotWidth = mOneDot.getWidth(); 244 int sliceWidth = 2 * dotWidth; 245 int sliceCount = width / sliceWidth; 246 int excess = width % sliceWidth; 247 int prop = excess / 2; 248 boolean rtl = isLayoutRtl(StreamingTextView.this); 249 250 mRandom.setSeed(mSeed); 251 int oldAlpha = paint.getAlpha(); 252 for (int i = 0; i < sliceCount; i++) { 253 if (ANIMATE_DOTS_FOR_PENDING) { 254 if (mPosition + i >= mStreamPosition) break; 255 } 256 257 float left = i * sliceWidth + prop + dotWidth / 2; 258 float dotLeft = rtl ? x + width - left - dotWidth : x + left; 259 260 // give the dots some visual variety 261 paint.setAlpha((mRandom.nextInt(4) + 1) * 63); 262 263 if (mRandom.nextBoolean()) { 264 canvas.drawBitmap(mTwoDot, dotLeft, y - mTwoDot.getHeight(), paint); 265 } else { 266 canvas.drawBitmap(mOneDot, dotLeft, y - mOneDot.getHeight(), paint); 267 } 268 } 269 paint.setAlpha(oldAlpha); 270 } 271 272 @Override 273 public int getSize(Paint paint, CharSequence text, int start, int end, 274 Paint.FontMetricsInt fontMetricsInt) { 275 return (int) paint.measureText(text, start, end); 276 } 277 } 278 279 public static boolean isLayoutRtl(View view) { 280 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { 281 return View.LAYOUT_DIRECTION_RTL == view.getLayoutDirection(); 282 } else { 283 return false; 284 } 285 } 286 287 public void updateRecognizedText(String stableText, List<Float> rmsValues) {} 288} 289