Home | History | Annotate | Download | only in widget
      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  */
     14 package androidx.leanback.widget;
     15 
     16 import android.animation.ObjectAnimator;
     17 import android.content.Context;
     18 import android.graphics.Bitmap;
     19 import android.graphics.BitmapFactory;
     20 import android.graphics.Canvas;
     21 import android.graphics.Paint;
     22 import android.text.SpannableStringBuilder;
     23 import android.text.Spanned;
     24 import android.text.SpannedString;
     25 import android.text.style.ForegroundColorSpan;
     26 import android.text.style.ReplacementSpan;
     27 import android.util.AttributeSet;
     28 import android.util.Log;
     29 import android.util.Property;
     30 import android.view.View;
     31 import android.view.accessibility.AccessibilityNodeInfo;
     32 import android.widget.EditText;
     33 
     34 import androidx.leanback.R;
     35 
     36 import java.util.List;
     37 import java.util.Random;
     38 import java.util.regex.Matcher;
     39 import java.util.regex.Pattern;
     40 
     41 /**
     42  * Shows the recognized text as a continuous stream of words.
     43  */
     44 class 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     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     final Random mRandom = new Random();
     73 
     74     Bitmap mOneDot;
     75     Bitmap mTwoDot;
     76 
     77     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 haven't changed. (probably a framework bug)
    150         updateText(new SpannedString(displayText));
    151 
    152         if (ANIMATE_DOTS_FOR_PENDING) {
    153             startStreamAnimation();
    154         }
    155     }
    156 
    157     int getStreamPosition() {
    158         return mStreamPosition;
    159     }
    160 
    161     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