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