/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package android.support.v17.leanback.widget; import android.support.v17.leanback.R; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; import android.text.style.ForegroundColorSpan; import android.text.style.ReplacementSpan; import android.util.AttributeSet; import android.util.Log; import android.util.Property; import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.EditText; import android.widget.TextView; import java.util.List; import java.util.Random; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Shows the recognized text as a continuous stream of words. */ class StreamingTextView extends EditText { private static final boolean DEBUG = false; private static final String TAG = "StreamingTextView"; private static final float TEXT_DOT_SCALE = 1.3F; private static final boolean DOTS_FOR_STABLE = false; private static final boolean DOTS_FOR_PENDING = true; static final boolean ANIMATE_DOTS_FOR_PENDING = true; private static final long STREAM_UPDATE_DELAY_MILLIS = 50; private static final Pattern SPLIT_PATTERN = Pattern.compile("\\S+"); private static final Property STREAM_POSITION_PROPERTY = new Property(Integer.class, "streamPosition") { @Override public Integer get(StreamingTextView view) { return view.getStreamPosition(); } @Override public void set(StreamingTextView view, Integer value) { view.setStreamPosition(value); } }; final Random mRandom = new Random(); Bitmap mOneDot; Bitmap mTwoDot; int mStreamPosition; private ObjectAnimator mStreamingAnimation; public StreamingTextView(Context context, AttributeSet attrs) { super(context, attrs); } public StreamingTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected void onFinishInflate() { super.onFinishInflate(); mOneDot = getScaledBitmap(R.drawable.lb_text_dot_one, TEXT_DOT_SCALE); mTwoDot = getScaledBitmap(R.drawable.lb_text_dot_two, TEXT_DOT_SCALE); reset(); } private Bitmap getScaledBitmap(int resourceId, float scaled) { Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resourceId); return Bitmap.createScaledBitmap(bitmap, (int) (bitmap.getWidth() * scaled), (int) (bitmap.getHeight() * scaled), false); } /** * Resets the text view. */ public void reset() { if (DEBUG) Log.d(TAG, "#reset"); mStreamPosition = -1; cancelStreamAnimation(); setText(""); } /** * Updates the recognized text. */ public void updateRecognizedText(String stableText, String pendingText) { if (DEBUG) Log.d(TAG, "updateText(" + stableText + "," + pendingText + ")"); if (stableText == null) { stableText = ""; } SpannableStringBuilder displayText = new SpannableStringBuilder(stableText); if (DOTS_FOR_STABLE) { addDottySpans(displayText, stableText, 0); } if (pendingText != null) { int pendingTextStart = displayText.length(); displayText.append(pendingText); if (DOTS_FOR_PENDING) { addDottySpans(displayText, pendingText, pendingTextStart); } else { int pendingColor = getResources().getColor( R.color.lb_search_plate_hint_text_color); addColorSpan(displayText, pendingColor, pendingText, pendingTextStart); } } // Start streaming in dots from beginning of partials, or current position, // whichever is larger mStreamPosition = Math.max(stableText.length(), mStreamPosition); // Copy the text and spans to a SpannedString, since editable text // doesn't redraw in invalidate() when hardware accelerated // if the text or spans haven't changed. (probably a framework bug) updateText(new SpannedString(displayText)); if (ANIMATE_DOTS_FOR_PENDING) { startStreamAnimation(); } } int getStreamPosition() { return mStreamPosition; } void setStreamPosition(int streamPosition) { mStreamPosition = streamPosition; invalidate(); } private void startStreamAnimation() { cancelStreamAnimation(); int pos = getStreamPosition(); int totalLen = length(); int animLen = totalLen - pos; if (animLen > 0) { if (mStreamingAnimation == null) { mStreamingAnimation = new ObjectAnimator(); mStreamingAnimation.setTarget(this); mStreamingAnimation.setProperty(STREAM_POSITION_PROPERTY); } mStreamingAnimation.setIntValues(pos, totalLen); mStreamingAnimation.setDuration(STREAM_UPDATE_DELAY_MILLIS * animLen); mStreamingAnimation.start(); } } private void cancelStreamAnimation() { if (mStreamingAnimation != null) { mStreamingAnimation.cancel(); } } private void addDottySpans(SpannableStringBuilder displayText, String text, int textStart) { Matcher m = SPLIT_PATTERN.matcher(text); while (m.find()) { int wordStart = textStart + m.start(); int wordEnd = textStart + m.end(); DottySpan span = new DottySpan(text.charAt(m.start()), wordStart); displayText.setSpan(span, wordStart, wordEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } private void addColorSpan(SpannableStringBuilder displayText, int color, String text, int textStart) { ForegroundColorSpan span = new ForegroundColorSpan(color); int start = textStart; int end = textStart + text.length(); displayText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } /** * Sets the final, non changing, full text result. This should only happen at the very end of * a recognition. * * @param finalText to the view to. */ public void setFinalRecognizedText(CharSequence finalText) { if (DEBUG) Log.d(TAG, "setFinalRecognizedText(" + finalText + ")"); updateText(finalText); } private void updateText(CharSequence displayText) { setText(displayText); bringPointIntoView(length()); } /** * This is required to make the View findable by uiautomator. */ @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(StreamingTextView.class.getCanonicalName()); } private class DottySpan extends ReplacementSpan { private final int mSeed; private final int mPosition; public DottySpan(int seed, int pos) { mSeed = seed; mPosition = pos; } @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { int width = (int) paint.measureText(text, start, end); int dotWidth = mOneDot.getWidth(); int sliceWidth = 2 * dotWidth; int sliceCount = width / sliceWidth; int excess = width % sliceWidth; int prop = excess / 2; boolean rtl = isLayoutRtl(StreamingTextView.this); mRandom.setSeed(mSeed); int oldAlpha = paint.getAlpha(); for (int i = 0; i < sliceCount; i++) { if (ANIMATE_DOTS_FOR_PENDING) { if (mPosition + i >= mStreamPosition) break; } float left = i * sliceWidth + prop + dotWidth / 2; float dotLeft = rtl ? x + width - left - dotWidth : x + left; // give the dots some visual variety paint.setAlpha((mRandom.nextInt(4) + 1) * 63); if (mRandom.nextBoolean()) { canvas.drawBitmap(mTwoDot, dotLeft, y - mTwoDot.getHeight(), paint); } else { canvas.drawBitmap(mOneDot, dotLeft, y - mOneDot.getHeight(), paint); } } paint.setAlpha(oldAlpha); } @Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fontMetricsInt) { return (int) paint.measureText(text, start, end); } } public static boolean isLayoutRtl(View view) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { return View.LAYOUT_DIRECTION_RTL == view.getLayoutDirection(); } else { return false; } } public void updateRecognizedText(String stableText, List rmsValues) {} }