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