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