/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.react.views.text; import javax.annotation.Nullable; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.text.Layout; import android.text.Spanned; import android.text.TextUtils; import android.view.Gravity; import android.view.ViewGroup; import android.widget.TextView; import com.facebook.react.uimanager.ReactCompoundView; import com.facebook.react.uimanager.ViewDefaults; import com.facebook.react.views.view.ReactViewBackgroundDrawable; public class ReactTextView extends TextView implements ReactCompoundView { private static final ViewGroup.LayoutParams EMPTY_LAYOUT_PARAMS = new ViewGroup.LayoutParams(0, 0); private boolean mContainsImages; private int mDefaultGravityHorizontal; private int mDefaultGravityVertical; private boolean mTextIsSelectable; private float mLineHeight = Float.NaN; private int mTextAlign = Gravity.NO_GRAVITY; private int mNumberOfLines = ViewDefaults.NUMBER_OF_LINES; private TextUtils.TruncateAt mEllipsizeLocation = TextUtils.TruncateAt.END; private ReactViewBackgroundDrawable mReactBackgroundDrawable; public ReactTextView(Context context) { super(context); mDefaultGravityHorizontal = getGravity() & (Gravity.HORIZONTAL_GRAVITY_MASK | Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK); mDefaultGravityVertical = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; } public void setText(ReactTextUpdate update) { mContainsImages = update.containsImages(); // Android's TextView crashes when it tries to relayout if LayoutParams are // null; explicitly set the LayoutParams to prevent this crash. See: // https://github.com/facebook/react-native/pull/7011 if (getLayoutParams() == null) { setLayoutParams(EMPTY_LAYOUT_PARAMS); } setText(update.getText()); setPadding( (int) Math.floor(update.getPaddingLeft()), (int) Math.floor(update.getPaddingTop()), (int) Math.floor(update.getPaddingRight()), (int) Math.floor(update.getPaddingBottom())); int nextTextAlign = update.getTextAlign(); if (mTextAlign != nextTextAlign) { mTextAlign = nextTextAlign; } setGravityHorizontal(mTextAlign); } @Override public int reactTagForTouch(float touchX, float touchY) { Spanned text = (Spanned) getText(); int target = getId(); int x = (int) touchX; int y = (int) touchY; Layout layout = getLayout(); if (layout == null) { // If the layout is null, the view hasn't been properly laid out yet. Therefore, we can't find // the exact text tag that has been touched, and the correct tag to return is the default one. return target; } int line = layout.getLineForVertical(y); int lineStartX = (int) layout.getLineLeft(line); int lineEndX = (int) layout.getLineRight(line); // TODO(5966918): Consider extending touchable area for text spans by some DP constant if (x >= lineStartX && x <= lineEndX) { int index = layout.getOffsetForHorizontal(line, x); // We choose the most inner span (shortest) containing character at the given index // if no such span can be found we will send the textview's react id as a touch handler // In case when there are more than one spans with same length we choose the last one // from the spans[] array, since it correspond to the most inner react element ReactTagSpan[] spans = text.getSpans(index, index, ReactTagSpan.class); if (spans != null) { int targetSpanTextLength = text.length(); for (int i = 0; i < spans.length; i++) { int spanStart = text.getSpanStart(spans[i]); int spanEnd = text.getSpanEnd(spans[i]); if (spanEnd > index && (spanEnd - spanStart) <= targetSpanTextLength) { target = spans[i].getReactTag(); targetSpanTextLength = (spanEnd - spanStart); } } } } return target; } @Override public void setTextIsSelectable(boolean selectable) { mTextIsSelectable = selectable; super.setTextIsSelectable(selectable); } @Override protected boolean verifyDrawable(Drawable drawable) { if (mContainsImages && getText() instanceof Spanned) { Spanned text = (Spanned) getText(); TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class); for (TextInlineImageSpan span : spans) { if (span.getDrawable() == drawable) { return true; } } } return super.verifyDrawable(drawable); } @Override public void invalidateDrawable(Drawable drawable) { if (mContainsImages && getText() instanceof Spanned) { Spanned text = (Spanned) getText(); TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class); for (TextInlineImageSpan span : spans) { if (span.getDrawable() == drawable) { invalidate(); } } } super.invalidateDrawable(drawable); } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mContainsImages && getText() instanceof Spanned) { Spanned text = (Spanned) getText(); TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class); for (TextInlineImageSpan span : spans) { span.onDetachedFromWindow(); } } } @Override public void onStartTemporaryDetach() { super.onStartTemporaryDetach(); if (mContainsImages && getText() instanceof Spanned) { Spanned text = (Spanned) getText(); TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class); for (TextInlineImageSpan span : spans) { span.onStartTemporaryDetach(); } } } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); if (mContainsImages && getText() instanceof Spanned) { Spanned text = (Spanned) getText(); TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class); for (TextInlineImageSpan span : spans) { span.onAttachedToWindow(); } } } @Override public void onFinishTemporaryDetach() { super.onFinishTemporaryDetach(); if (mContainsImages && getText() instanceof Spanned) { Spanned text = (Spanned) getText(); TextInlineImageSpan[] spans = text.getSpans(0, text.length(), TextInlineImageSpan.class); for (TextInlineImageSpan span : spans) { span.onFinishTemporaryDetach(); } } } @Override public void setBackgroundColor(int color) { if (color == Color.TRANSPARENT && mReactBackgroundDrawable == null) { // don't do anything, no need to allocate ReactBackgroundDrawable for transparent background } else { getOrCreateReactViewBackground().setColor(color); } } /* package */ void setGravityHorizontal(int gravityHorizontal) { if (gravityHorizontal == 0) { gravityHorizontal = mDefaultGravityHorizontal; } setGravity( (getGravity() & ~Gravity.HORIZONTAL_GRAVITY_MASK & ~Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) | gravityHorizontal); } /* package */ void setGravityVertical(int gravityVertical) { if (gravityVertical == 0) { gravityVertical = mDefaultGravityVertical; } setGravity((getGravity() & ~Gravity.VERTICAL_GRAVITY_MASK) | gravityVertical); } public void setNumberOfLines(int numberOfLines) { mNumberOfLines = numberOfLines == 0 ? ViewDefaults.NUMBER_OF_LINES : numberOfLines; setMaxLines(mNumberOfLines); } public void setEllipsizeLocation(TextUtils.TruncateAt ellipsizeLocation) { mEllipsizeLocation = ellipsizeLocation; } public void updateView() { @Nullable TextUtils.TruncateAt ellipsizeLocation = mNumberOfLines == ViewDefaults.NUMBER_OF_LINES ? null : mEllipsizeLocation; setEllipsize(ellipsizeLocation); } public void setBorderWidth(int position, float width) { getOrCreateReactViewBackground().setBorderWidth(position, width); } public void setBorderColor(int position, float color, float alpha) { getOrCreateReactViewBackground().setBorderColor(position, color, alpha); } public void setBorderRadius(float borderRadius) { getOrCreateReactViewBackground().setRadius(borderRadius); } public void setBorderRadius(float borderRadius, int position) { getOrCreateReactViewBackground().setRadius(borderRadius, position); } public void setBorderStyle(@Nullable String style) { getOrCreateReactViewBackground().setBorderStyle(style); } private ReactViewBackgroundDrawable getOrCreateReactViewBackground() { if (mReactBackgroundDrawable == null) { mReactBackgroundDrawable = new ReactViewBackgroundDrawable(); Drawable backgroundDrawable = getBackground(); super.setBackground(null); // required so that drawable callback is cleared before we add the // drawable back as a part of LayerDrawable if (backgroundDrawable == null) { super.setBackground(mReactBackgroundDrawable); } else { LayerDrawable layerDrawable = new LayerDrawable(new Drawable[]{mReactBackgroundDrawable, backgroundDrawable}); super.setBackground(layerDrawable); } } return mReactBackgroundDrawable; } }