diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt index c0a9d3e147aab5..4214d3a1440f49 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt @@ -27,8 +27,8 @@ import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.uimanager.BackgroundStyleApplicator import com.facebook.react.uimanager.ReactCompoundView import com.facebook.react.uimanager.style.Overflow -import com.facebook.react.views.text.internal.span.DiscordShadowStyleSpan import com.facebook.react.views.text.internal.span.ReactTagSpan +import com.facebook.react.views.text.internal.span.StrokeStyleSpan import kotlin.collections.ArrayList import kotlin.math.roundToInt @@ -102,18 +102,14 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re override fun onDraw(canvas: Canvas) { val layout = preparedLayout?.layout - // Get shadow adjustment from custom span if configured - val spanned = layout?.text as? Spanned - val shadowAdj = DiscordShadowStyleSpan.getShadowAdjustment(spanned) - - if (overflow != Overflow.VISIBLE && !shadowAdj.hasShadow) { + if (overflow != Overflow.VISIBLE) { BackgroundStyleApplicator.clipToPaddingBox(this, canvas) } super.onDraw(canvas) canvas.translate( - paddingLeft.toFloat() + shadowAdj.leftOffset, + paddingLeft.toFloat(), paddingTop.toFloat() + (preparedLayout?.verticalOffset ?: 0f)) if (layout != null) { @@ -122,10 +118,17 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re selectionColor ?: DefaultStyleValuesUtil.getDefaultTextColorHighlight(context)) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - Api34Utils.draw(layout, canvas, selection?.path, selectionPaint) - } else { - layout.draw(canvas, selection?.path, selectionPaint, 0) + val drawLayout = Runnable { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + Api34Utils.draw(layout, canvas, selection?.path, selectionPaint) + } else { + layout.draw(canvas, selection?.path, selectionPaint, 0) + } + } + + val strokeSpan = StrokeStyleSpan.getStrokeSpan(layout.text as? Spanned) + if (strokeSpan == null || !strokeSpan.draw(layout.paint, drawLayout)) { + drawLayout.run() } } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java index f97dbed7a34ffd..9fdc2a62b98433 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java @@ -13,7 +13,9 @@ import android.text.Layout; import android.text.Spannable; import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.TextUtils; +import android.text.style.LeadingMarginSpan; import android.view.Gravity; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; @@ -48,7 +50,7 @@ import com.facebook.react.views.text.internal.span.ReactTagSpan; import com.facebook.react.views.text.internal.span.ReactUnderlineSpan; import com.facebook.react.views.text.internal.span.SetSpanOperation; -import com.facebook.react.views.text.internal.span.DiscordShadowStyleSpan; +import com.facebook.react.views.text.internal.span.ShadowStyleSpan; import com.facebook.react.views.text.internal.span.StrokeStyleSpan; import com.facebook.react.views.text.internal.span.TextInlineImageSpan; import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan; @@ -226,31 +228,29 @@ private static void buildSpannedFromShadowNode( if (textShadowNode.mIsLineThroughTextDecorationSet) { ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan())); } + ShadowStyleSpan shadowSpan = null; if ((textShadowNode.mTextShadowOffsetDx != 0 || textShadowNode.mTextShadowOffsetDy != 0 || textShadowNode.mTextShadowRadius != 0) && Color.alpha(textShadowNode.mTextShadowColor) != 0) { - ops.add( - new SetSpanOperation( - start, - end, - new DiscordShadowStyleSpan( - textShadowNode.mTextShadowOffsetDx, - textShadowNode.mTextShadowOffsetDy, - textShadowNode.mTextShadowRadius, - textShadowNode.mTextShadowColor))); + shadowSpan = new ShadowStyleSpan( + textShadowNode.mTextShadowOffsetDx, + textShadowNode.mTextShadowOffsetDy, + textShadowNode.mTextShadowRadius, + textShadowNode.mTextShadowColor); + ops.add(new SetSpanOperation(start, end, shadowSpan)); } + + StrokeStyleSpan strokeSpan = null; if (!Float.isNaN(textShadowNode.mTextStrokeWidth) && textShadowNode.mTextStrokeWidth > 0 && textShadowNode.mIsTextStrokeColorSet) { - ops.add( - new SetSpanOperation( - start, - end, - new StrokeStyleSpan( - textShadowNode.mTextStrokeWidth, - textShadowNode.mTextStrokeColor))); + strokeSpan = new StrokeStyleSpan( + textShadowNode.mTextStrokeWidth, + textShadowNode.mTextStrokeColor); + ops.add(new SetSpanOperation(start, end, strokeSpan)); } + float effectiveLineHeight = textAttributes.getEffectiveLineHeight(); if (!Float.isNaN(effectiveLineHeight) && (parentTextAttributes == null @@ -335,6 +335,20 @@ protected Spannable spannedFromShadowNode( textShadowNode.mTextAttributes.setHeightOfTallestInlineViewOrImage( heightOfTallestInlineViewOrImage); + // Add leading margin for stroke/shadow that extends beyond text bounds + StrokeStyleSpan strokeSpan = StrokeStyleSpan.getStrokeSpan(sb); + ShadowStyleSpan shadowSpan = ShadowStyleSpan.getShadowSpan(sb); + float strokeOffset = strokeSpan != null ? strokeSpan.getLeftOffset() : 0f; + float shadowOffset = shadowSpan != null ? shadowSpan.getLeftOffset() : 0f; + int leadingMargin = (int) Math.max(strokeOffset, shadowOffset); + if (leadingMargin > 0) { + sb.setSpan( + new LeadingMarginSpan.Standard(leadingMargin), + 0, + sb.length(), + Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + if (mReactTextViewManagerCallback != null) { mReactTextViewManagerCallback.onPostProcessSpannable(sb); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index 05ab157865940d..472e1141415946 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -9,12 +9,14 @@ import android.content.Context; import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.Layout; import android.text.Spannable; import android.text.Spanned; +import android.text.TextPaint; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.text.util.Linkify; @@ -52,8 +54,8 @@ import com.facebook.react.uimanager.style.BorderStyle; import com.facebook.react.uimanager.style.LogicalEdge; import com.facebook.react.uimanager.style.Overflow; -import com.facebook.react.views.text.internal.span.DiscordShadowStyleSpan; import com.facebook.react.views.text.internal.span.ReactTagSpan; +import com.facebook.react.views.text.internal.span.StrokeStyleSpan; import com.facebook.react.views.text.internal.span.TextInlineImageSpan; import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan; import com.facebook.yoga.YogaMeasureMode; @@ -361,19 +363,19 @@ protected void onDraw(Canvas canvas) { setText(spanned); } - // Get shadow adjustment from custom span if configured - DiscordShadowStyleSpan.ShadowAdjustment shadowAdj = - DiscordShadowStyleSpan.getShadowAdjustment(spanned); - - canvas.save(); - canvas.translate(shadowAdj.getLeftOffset(), 0); - - if (mOverflow != Overflow.VISIBLE && !shadowAdj.getHasShadow()) { + if (mOverflow != Overflow.VISIBLE) { + canvas.save(); BackgroundStyleApplicator.clipToPaddingBox(this, canvas); } - super.onDraw(canvas); - canvas.restore(); + StrokeStyleSpan strokeSpan = StrokeStyleSpan.getStrokeSpan((Spanned) getText()); + if (strokeSpan == null || !strokeSpan.draw(getPaint(), () -> super.onDraw(canvas))) { + super.onDraw(canvas); + } + + if (mOverflow != Overflow.VISIBLE) { + canvas.restore(); + } } } @@ -800,4 +802,5 @@ public void setOverflow(@Nullable String overflow) { invalidate(); } + } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt index 735dd1f87488eb..d85e9129a7c3b3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt @@ -21,6 +21,7 @@ import android.text.StaticLayout import android.text.TextDirectionHeuristics import android.text.TextPaint import android.text.TextUtils +import android.text.style.LeadingMarginSpan import android.util.LayoutDirection import android.view.Gravity import android.view.View @@ -47,10 +48,10 @@ import com.facebook.react.views.text.internal.span.ReactOpacitySpan import com.facebook.react.views.text.internal.span.ReactStrikethroughSpan import com.facebook.react.views.text.internal.span.ReactTagSpan import com.facebook.react.views.text.internal.span.ReactTextPaintHolderSpan -import com.facebook.react.views.text.internal.span.DiscordShadowStyleSpan +import com.facebook.react.views.text.internal.span.ShadowStyleSpan +import com.facebook.react.views.text.internal.span.StrokeStyleSpan import com.facebook.react.views.text.internal.span.ReactUnderlineSpan import com.facebook.react.views.text.internal.span.SetSpanOperation -import com.facebook.react.views.text.internal.span.StrokeStyleSpan import com.facebook.react.views.text.internal.span.TextInlineViewPlaceholderSpan import com.facebook.yoga.YogaMeasureMode import com.facebook.yoga.YogaMeasureOutput @@ -306,7 +307,7 @@ internal object TextLayoutManager { SetSpanOperation( start, end, - DiscordShadowStyleSpan( + ShadowStyleSpan( textAttributes.mTextShadowOffsetDx, textAttributes.mTextShadowOffsetDy, textAttributes.mTextShadowRadius, @@ -315,14 +316,15 @@ internal object TextLayoutManager { if (!textAttributes.textStrokeWidth.isNaN() && textAttributes.textStrokeWidth > 0 && textAttributes.isTextStrokeColorSet) { - val strokeWidth = PixelUtil.toPixelFromDIP(textAttributes.textStrokeWidth.toDouble()).toFloat() - val strokeColor = textAttributes.textStrokeColor ops.add( SetSpanOperation( start, end, - StrokeStyleSpan(strokeWidth, strokeColor))) + StrokeStyleSpan( + textAttributes.textStrokeWidth, + textAttributes.textStrokeColor))) } + if (!textAttributes.effectiveLineHeight.isNaN()) { ops.add( SetSpanOperation( @@ -470,7 +472,7 @@ internal object TextLayoutManager { fragment.props.textShadowRadius != 0f) && Color.alpha(fragment.props.textShadowColor) != 0) { spannable.setSpan( - DiscordShadowStyleSpan( + ShadowStyleSpan( fragment.props.textShadowOffsetDx, fragment.props.textShadowOffsetDy, fragment.props.textShadowRadius, @@ -483,9 +485,10 @@ internal object TextLayoutManager { if (!fragment.props.textStrokeWidth.isNaN() && fragment.props.textStrokeWidth > 0 && fragment.props.isTextStrokeColorSet) { - val strokeWidth = PixelUtil.toPixelFromDIP(fragment.props.textStrokeWidth.toDouble()).toFloat() spannable.setSpan( - StrokeStyleSpan(strokeWidth, fragment.props.textStrokeColor), + StrokeStyleSpan( + fragment.props.textStrokeWidth, + fragment.props.textStrokeColor), start, end, spanFlags) @@ -502,9 +505,25 @@ internal object TextLayoutManager { start = end } + addLeadingMarginForTextEffects(spannable) return spannable } + private fun addLeadingMarginForTextEffects(spannable: Spannable) { + val strokeSpan = StrokeStyleSpan.getStrokeSpan(spannable) + val shadowSpan = ShadowStyleSpan.getShadowSpan(spannable) + val strokeOffset = strokeSpan?.getLeftOffset() ?: 0f + val shadowOffset = shadowSpan?.getLeftOffset() ?: 0f + val leadingMargin = max(strokeOffset, shadowOffset).toInt() + if (leadingMargin > 0) { + spannable.setSpan( + LeadingMarginSpan.Standard(leadingMargin), + 0, + spannable.length, + Spanned.SPAN_INCLUSIVE_INCLUSIVE) + } + } + fun getOrCreateSpannableForText( context: Context, attributedString: MapBuffer, @@ -555,6 +574,7 @@ internal object TextLayoutManager { op.execute(sb, priorityIndex) } + addLeadingMarginForTextEffects(sb) reactTextViewManagerCallback?.onPostProcessSpannable(sb) return sb } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/DiscordShadowStyleSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/DiscordShadowStyleSpan.kt deleted file mode 100644 index 76afe866758b15..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/DiscordShadowStyleSpan.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (c) Discord, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.views.text.internal.span - -import android.graphics.Canvas -import android.graphics.Paint -import android.text.Spanned -import android.text.style.ReplacementSpan -import kotlin.math.max - -/** - * A span that applies text shadow with proper bounds calculation. - * Extends ReplacementSpan to control measurement and drawing, ensuring shadows render correctly. - * This is Discord's custom implementation that contains all shadow logic. - */ -public class DiscordShadowStyleSpan( - private val dx: Float, - private val dy: Float, - private val radius: Float, - private val color: Int -) : ReplacementSpan(), ReactSpan { - - // Getters for shadow properties (used by getShadowAdjustment) - public fun getShadowRadius(): Float = radius - public fun getShadowDx(): Float = dx - - override fun getSize( - paint: Paint, - text: CharSequence?, - start: Int, - end: Int, - fm: Paint.FontMetricsInt? - ): Int { - val width = paint.measureText(text, start, end) - - if (fm != null) { - paint.getFontMetricsInt(fm) - - val shadowTopNeeded = max(0f, radius - dy) - val shadowBottomNeeded = max(0f, radius + dy) - - val topExpansion = shadowTopNeeded.toInt() - val bottomExpansion = shadowBottomNeeded.toInt() - - // Adjust font metrics to account for shadow - fm.top -= topExpansion - fm.ascent -= topExpansion - fm.descent += bottomExpansion - fm.bottom += bottomExpansion - } - - val shadowLeftNeeded = max(0f, radius - dx) - val shadowRightNeeded = max(0f, radius + dx) - - // Subtract 1 pixel to prevent TextView ellipsization while keeping shadow mostly intact - return (width + shadowLeftNeeded + shadowRightNeeded).toInt() - 1 - } - - override fun draw( - canvas: Canvas, - text: CharSequence?, - start: Int, - end: Int, - x: Float, - top: Int, - y: Int, - bottom: Int, - paint: Paint - ) { - if (text == null) return - - val textToDraw = text.subSequence(start, end).toString() - - // Offset text to keep shadow in positive coordinates - val shadowLeftNeeded = max(0f, radius - dx) - - // Store original shadow settings - val originalShadowRadius = paint.shadowLayerRadius - val originalShadowDx = paint.shadowLayerDx - val originalShadowDy = paint.shadowLayerDy - val originalShadowColor = paint.shadowLayerColor - - paint.setShadowLayer(radius, dx, dy, color) - - if (text is Spanned && paint is android.text.TextPaint) { - val spans = text.getSpans(start, end, android.text.style.CharacterStyle::class.java) - for (span in spans) { - if (span !is DiscordShadowStyleSpan) { - span.updateDrawState(paint) - } - } - } - - // Offset text by shadowLeftNeeded to keep shadow in positive coordinates - // The view will compensate with canvas translation - canvas.drawText(textToDraw, x + shadowLeftNeeded, y.toFloat(), paint) - - // Restore original shadow settings - if (originalShadowRadius > 0f) { - paint.setShadowLayer( - originalShadowRadius, originalShadowDx, originalShadowDy, originalShadowColor) - } else { - paint.clearShadowLayer() - } - } - - /** - * Result class for shadow adjustment calculation. - * Contains the horizontal offset needed to compensate for shadow positioning - * and whether a shadow is present. - */ - public data class ShadowAdjustment( - val leftOffset: Float, - val hasShadow: Boolean - ) { - public companion object { - @JvmStatic - public val NONE: ShadowAdjustment = ShadowAdjustment(0f, false) - } - } - - public companion object { - /** - * Helper method for ReactTextView and PreparedLayoutTextView to get shadow adjustment values. - * Calculates the horizontal offset needed to compensate for shadow positioning - * when the span offsets text to keep shadows in positive coordinates. - * - * @param spanned The text to check for shadow spans, or null if no text - * @return ShadowAdjustment with negative leftOffset (ready to use in canvas.translate) - */ - @JvmStatic - public fun getShadowAdjustment(spanned: Spanned?): ShadowAdjustment { - if (spanned == null) { - return ShadowAdjustment.NONE - } - - val spans = spanned.getSpans(0, spanned.length, DiscordShadowStyleSpan::class.java) - if (spans.isEmpty()) { - return ShadowAdjustment.NONE - } - - // Use the first shadow span to calculate offset - val span = spans[0] - val radius = span.getShadowRadius() - val dx = span.getShadowDx() - // Return negative offset so views can use it directly in canvas.translate - val shadowLeftOffset = -max(0f, radius - dx) - - return ShadowAdjustment(shadowLeftOffset, true) - } - } -} - diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ShadowStyleSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ShadowStyleSpan.kt index 638f2bb89e3acb..cb6c0bade11ffc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ShadowStyleSpan.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ShadowStyleSpan.kt @@ -7,8 +7,10 @@ package com.facebook.react.views.text.internal.span +import android.text.Spanned import android.text.TextPaint import android.text.style.CharacterStyle +import kotlin.math.max internal class ShadowStyleSpan( private val dx: Float, @@ -19,4 +21,15 @@ internal class ShadowStyleSpan( override fun updateDrawState(textPaint: TextPaint) { textPaint.setShadowLayer(radius, dx, dy, color) } + + public fun getLeftOffset(): Float = max(0f, radius - dx) + + public companion object { + @JvmStatic + public fun getShadowSpan(spanned: Spanned?): ShadowStyleSpan? { + if (spanned == null) return null + val spans = spanned.getSpans(0, spanned.length, ShadowStyleSpan::class.java) + return spans.firstOrNull() + } + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/StrokeStyleSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/StrokeStyleSpan.kt index 574038a1728d41..ac07de3b2c4da7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/StrokeStyleSpan.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/StrokeStyleSpan.kt @@ -1,86 +1,76 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + package com.facebook.react.views.text.internal.span -import android.graphics.Canvas import android.graphics.Paint -import android.text.style.ReplacementSpan +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.text.Spanned +import android.text.TextPaint +import android.text.style.CharacterStyle -/** - * A span that applies text stroke styling with two-pass rendering. - * First draws stroke, then draws fill on top to create outer stroke effect. - */ public class StrokeStyleSpan( - private val strokeWidth: Float, - private val strokeColor: Int -) : ReplacementSpan(), ReactSpan { + public val width: Float, + public val color: Int +) : CharacterStyle(), ReactSpan { - public override fun getSize( - paint: Paint, - text: CharSequence?, - start: Int, - end: Int, - fm: Paint.FontMetricsInt? - ): Int { - val width = paint.measureText(text, start, end) - - if (fm != null) { - paint.getFontMetricsInt(fm) - val halfStroke = (strokeWidth / 2).toInt() - fm.top -= halfStroke - fm.ascent -= halfStroke - fm.descent += halfStroke - fm.bottom += halfStroke - } - - return width.toInt() + override fun updateDrawState(textPaint: TextPaint) { + // No-op - stroke drawing is handled by the view's onDraw } - public override fun draw( - canvas: Canvas, - text: CharSequence?, - start: Int, - end: Int, - x: Float, - top: Int, - y: Int, - bottom: Int, - paint: Paint - ) { - if (text == null) return + public fun hasStroke(): Boolean = width > 0 && color != 0 + + public fun getLeftOffset(): Float = if (hasStroke()) width / 2f else 0f - val textToDraw = text.subSequence(start, end).toString() - val strokeInset = strokeWidth / 2 + public fun draw(paint: Paint, drawCallback: Runnable): Boolean { + if (!hasStroke()) { + return false + } - // Store original paint settings val originalStyle = paint.style - val originalColor = paint.color val originalStrokeWidth = paint.strokeWidth val originalStrokeJoin = paint.strokeJoin val originalStrokeCap = paint.strokeCap + val originalColor = paint.color + val originalColorFilter = paint.colorFilter - // First pass: Draw stroke only (solid color) + // Stroke pass paint.style = Paint.Style.STROKE - paint.strokeWidth = strokeWidth + paint.strokeWidth = width paint.strokeJoin = Paint.Join.ROUND paint.strokeCap = Paint.Cap.ROUND - paint.color = strokeColor - canvas.drawText(textToDraw, x + strokeInset, y.toFloat(), paint) + paint.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) + drawCallback.run() - // Second pass: Draw fill on top + // Fill pass paint.style = Paint.Style.FILL + paint.strokeWidth = 0f paint.color = originalColor - if (text is android.text.Spanned && paint is android.text.TextPaint) { - val spans = text.getSpans(start, end, android.text.style.CharacterStyle::class.java) - for (span in spans) { - span.updateDrawState(paint) - } - } - canvas.drawText(textToDraw, x + strokeInset, y.toFloat(), paint) + paint.colorFilter = originalColorFilter + drawCallback.run() - // Restore original paint settings + // Restore paint.style = originalStyle - paint.color = originalColor paint.strokeWidth = originalStrokeWidth paint.strokeJoin = originalStrokeJoin paint.strokeCap = originalStrokeCap + paint.color = originalColor + paint.colorFilter = originalColorFilter + + return true + } + + public companion object { + @JvmStatic + public fun getStrokeSpan(spanned: Spanned?): StrokeStyleSpan? { + if (spanned == null) return null + val spans = spanned.getSpans(0, spanned.length, StrokeStyleSpan::class.java) + return spans.firstOrNull() + } } }