blob: b4b4b8f3689543e39f8c2214f75c1e7b241e32c3 [file] [log] [blame]
/*
* 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;
private 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<StreamingTextView,Integer> STREAM_POSITION_PROPERTY =
new Property<StreamingTextView,Integer>(Integer.class, "streamPosition") {
@Override
public Integer get(StreamingTextView view) {
return view.getStreamPosition();
}
@Override
public void set(StreamingTextView view, Integer value) {
view.setStreamPosition(value);
}
};
private final Random mRandom = new Random();
private Bitmap mOneDot;
private Bitmap mTwoDot;
private 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();
}
}
private int getStreamPosition() {
return mStreamPosition;
}
private 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<Float> rmsValues) {}
}