Rounded Background text like Instagram, ReplacementSpan not working as required

后端 未结 4 494
日久生厌
日久生厌 2020-12-08 17:48

I was trying to do something similar to Instagram below -

But i want this curves like Instagram -

Now i am stuck in one more problem - When i typ

相关标签:
4条回答
  • 2020-12-08 18:21

    Enhancing BackgroundColorSpan by @tttzof351 to support alignment:

    import android.graphics.Canvas
    import android.graphics.Paint
    import android.graphics.Path
    import android.graphics.RectF
    import android.text.style.LineBackgroundSpan
    import kotlin.math.abs
    import kotlin.math.sign
    
    class BackgroundColorSpan(backgroundColor: Int,
                              private val padding: Int,
                              private val radius: Int) : LineBackgroundSpan {
        private val rect = RectF()
        private val paint = Paint()
        private val paintStroke = Paint()
        private val path = Path()
        private var prevWidth = -1f
        private var prevLeft = -1f
        private var prevRight = -1f
        private var prevBottom = -1f
        private var prevTop = -1f
    
        private val ALIGN_CENTER = 0
        private val ALIGN_START = 1
        private val ALIGN_END = 2
    
        init {
            paint.color = backgroundColor
            paintStroke.color = backgroundColor
        }
    
        private var align = ALIGN_CENTER
    
        fun setAlignment(alignment: Int) {
            align = alignment
        }
    
        override fun drawBackground(
                c: Canvas,
                p: Paint,
                left: Int,
                right: Int,
                top: Int,
                baseline: Int,
                bottom: Int,
                text: CharSequence,
                start: Int,
                end: Int,
                lnum: Int) {
    
    
            val width = p.measureText(text, start, end) + 2f * padding
            val shiftLeft: Float
            val shiftRight: Float
    
    
            when (align) {
                ALIGN_START -> {
                    shiftLeft = 0f - padding
                    shiftRight = width + shiftLeft
                }
    
                ALIGN_END -> {
                    shiftLeft = right - width + padding
                    shiftRight = (right + padding).toFloat()
                }
                else -> {
                    shiftLeft = (right - width) / 2
                    shiftRight = right - shiftLeft
                }
            }
    
            rect.set(shiftLeft, top.toFloat(), shiftRight, bottom.toFloat())
    
    
            if (lnum == 0) {
                c.drawRoundRect(rect, radius.toFloat(), radius.toFloat(), paint)
            } else {
                path.reset()
                val difference = width - prevWidth
                val diff = -sign(difference) * (2f * radius).coerceAtMost(abs(difference / 2f)) / 2f
                path.moveTo(
                        prevLeft, prevBottom - radius
                )
    
                if (align != ALIGN_START) {
                    path.cubicTo(//1
                            prevLeft, prevBottom - radius,
                            prevLeft, rect.top,
                            prevLeft + diff, rect.top
                    )
                } else {
                    path.lineTo(prevLeft, prevBottom + radius)
                }
                path.lineTo(
                        rect.left - diff, rect.top
                )
                path.cubicTo(//2
                        rect.left - diff, rect.top,
                        rect.left, rect.top,
                        rect.left, rect.top + radius
                )
                path.lineTo(
                        rect.left, rect.bottom - radius
                )
                path.cubicTo(//3
                        rect.left, rect.bottom - radius,
                        rect.left, rect.bottom,
                        rect.left + radius, rect.bottom
                )
                path.lineTo(
                        rect.right - radius, rect.bottom
                )
                path.cubicTo(//4
                        rect.right - radius, rect.bottom,
                        rect.right, rect.bottom,
                        rect.right, rect.bottom - radius
                )
                path.lineTo(
                        rect.right, rect.top + radius
                )
    
                if (align != ALIGN_END) {
                    path.cubicTo(//5
                            rect.right, rect.top + radius,
                            rect.right, rect.top,
                            rect.right + diff, rect.top
                    )
                    path.lineTo(
                            prevRight - diff, rect.top
                    )
                    path.cubicTo(//6
                            prevRight - diff, rect.top,
                            prevRight, rect.top,
                            prevRight, prevBottom - radius
                    )
    
                } else {
                    path.lineTo(prevRight, prevBottom - radius)
                }
                path.cubicTo(//7
                        prevRight, prevBottom - radius,
                        prevRight, prevBottom,
                        prevRight - radius, prevBottom
                )
    
                path.lineTo(
                        prevLeft + radius, prevBottom
                )
    
                path.cubicTo(//8
                        prevLeft + radius, prevBottom,
                        prevLeft, prevBottom,
                        prevLeft, rect.top - radius
                )
                c.drawPath(path, paintStroke)
    
            }
            prevWidth = width
            prevLeft = rect.left
            prevRight = rect.right
            prevBottom = rect.bottom
            prevTop = rect.top
        }
    }
    

    Results:

    0 讨论(0)
  • 2020-12-08 18:23

    I implement new RoundedBackgroundSpan.kt class extends LineBackgroundSpan, becose it can draw decorate layer for text line-by-line.

    class RoundedBackgroundSpan(
      backgroundColor: Int,
      private val padding: Float,
      private val radius: Float
    ) : LineBackgroundSpan {
    
      companion object {
        private const val NO_INIT = -1f
      }
    
      private val rect = RectF()
      private val paint = Paint().apply {
        color = backgroundColor
        isAntiAlias = true
      }
      private val path = Path()
    
      private var prevWidth = NO_INIT
      private var prevRight = NO_INIT
    
      override fun drawBackground(
        c: Canvas,
        p: Paint,
        left: Int,
        right: Int,
        top: Int,
        baseline: Int,
        bottom: Int,
        text: CharSequence,
        start: Int,
        end: Int,
        lineNumber: Int
      ) {
    
        val actualWidth = p.measureText(text, start, end) + 2f * padding
        val widthDiff = abs(prevWidth - actualWidth)
    
        val width = if (lineNumber == 0) {
          actualWidth
        } else if ((actualWidth < prevWidth) && (widthDiff < 2f * radius)) {
          prevWidth
        } else if ((actualWidth > prevWidth) && (widthDiff < 2f * radius)) {
          actualWidth + (2f * radius - widthDiff)
        } else {
          actualWidth
        }
    
        val shiftLeft = 0f - padding
        val shiftRight = width + shiftLeft
    
        rect.set(shiftLeft, top.toFloat(), shiftRight, bottom.toFloat())
    
        c.drawRoundRect(rect, radius, radius, paint)
    
        if (lineNumber > 0) {
          drawCornerType1(c, rect, radius)
    
          when {
            prevWidth < width -> drawCornerType2(c, rect, radius)
            prevWidth > width -> drawCornerType3(c, rect, radius)
            else              -> drawCornerType4(c, rect, radius)
          }
        }
    
        prevWidth = width
        prevRight = rect.right
      }
    
      private fun drawLeftCorner(c: Canvas, rect: RectF, radius: Float) {
        path.reset()
        path.moveTo(rect.left, rect.top + radius)
        path.lineTo(rect.left, rect.top - radius)
        path.lineTo(rect.left + radius, rect.top)
        path.lineTo(rect.left, rect.top + radius)
    
        c.drawPath(path, paint)
      }
    
      private fun drawTopCorner(c: Canvas, rect: RectF, radius: Float) {
        path.reset()
        path.moveTo(prevRight + radius, rect.top)
        path.lineTo(prevRight - radius, rect.top)
        path.lineTo(prevRight, rect.top - radius)
        path.cubicTo(
          prevRight, rect.top - radius,
          prevRight, rect.top,
          prevRight + radius, rect.top
        )
    
        c.drawPath(path, paint)
      }
    
      private fun drawBottomCorner(c: Canvas, rect: RectF, radius: Float) {
        path.reset()
        path.moveTo(rect.right + radius, rect.top)
        path.lineTo(rect.right - radius, rect.top)
        path.lineTo(rect.right, rect.top + radius)
        path.cubicTo(
          rect.right, rect.top + radius,
          rect.right, rect.top,
          rect.right + radius, rect.top
        )
    
        c.drawPath(path, paint)
      }
    
      private fun drawRightCorner(c: Canvas, rect: RectF, radius: Float) {
        path.reset()
        path.moveTo(rect.right, rect.top - radius)
        path.lineTo(rect.right, rect.top + radius)
        path.lineTo(rect.right - radius, rect.top)
        path.lineTo(rect.right, rect.top - radius)
    
        c.drawPath(path, paint)
      }
    }
    
    

    And use it:

    private fun initSpannableText() {
        val span = RoundedBackgroundSpan(
            backgroundColor = colors.random(),
            padding = dp(5),
            radius = dp(5)
        )
    
        with(spanText) {
            setShadowLayer(dp(10), 0f, 0f, 0) // it's important for padding working
    
            text = androidx.core.text.buildSpannedString { inSpans(span) { append(text.toString()) } }
        }
    }
    

    More details about implementation in this article: https://medium.com/@Semper_Viventem/simple-implementation-of-rounded-background-for-text-in-android-60a7706c0419

    0 讨论(0)
  • 2020-12-08 18:35

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/holo_purple"
        tools:context="com.tttzof.demotext.MainActivity">
    
        <EditText
            android:id="@+id/editText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="Enter text"
            android:textSize="30sp"
            android:gravity="center"
            android:textColor="@android:color/black"
            android:background="@android:color/transparent"
            android:layout_gravity="center"/>
    
    </FrameLayout>
    

    MainActivity.java

    import android.graphics.Color;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.text.Editable;
    import android.text.Spannable;
    import android.text.TextWatcher;
    import android.widget.EditText;
    
    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            final EditText editText = (EditText) findViewById(R.id.editText);
    
            int padding = dp(8);
            int radius = dp(5);
    
            final Object span = new BackgroundColorSpan(
                            Color.WHITE,
                            (float)padding,
                            (float) radius
            );
    
            editText.setShadowLayer(padding, 0f, 0f, 0);
            editText.setPadding(padding, padding, padding, padding);
    
            editText.addTextChangedListener(new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
    
                }
    
                @Override
                public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
    
                }
    
                @Override
                public void afterTextChanged(Editable s) {
                    s.setSpan(span, 0, s.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            });
        }
    
        private int dp(int value) {
            return (int) (getResources().getDisplayMetrics().density * value + 0.5f);
        }
    }
    

    BackgroundColorSpan.java

    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.graphics.Path;
    import android.graphics.RectF;
    import android.text.style.LineBackgroundSpan;
    
    public class BackgroundColorSpan implements LineBackgroundSpan {
        private float padding;
        private float radius;
    
        private RectF rect = new RectF();
        private Paint paint = new Paint();
        private Paint paintStroke = new Paint();
        private Path path = new Path();
    
        private float prevWidth = -1f;
        private float prevLeft = -1f;
        private float prevRight = -1f;
        private float prevBottom = -1f;
        private float prevTop = -1f;
    
    
        public BackgroundColorSpan(int backgroundColor,
                                   float padding,
                                   float radius) {
            this.padding = padding;
            this.radius = radius;
    
            paint.setColor(backgroundColor);
            //paintStroke.setStyle(Paint.Style.STROKE);
            //paintStroke.setStrokeWidth(5f);
            paintStroke.setColor(backgroundColor);
        }
    
        @Override
        public void drawBackground(
                        final Canvas c,
                        final Paint p,
                        final int left,
                        final int right,
                        final int top,
                        final int baseline,
                        final int bottom,
                        final CharSequence text,
                        final int start,
                        final int end,
                        final int lnum) {
    
            float width = p.measureText(text, start, end) + 2f * padding;
            float shift = (right - width) / 2f;
    
            rect.set(shift, top, right - shift, bottom);
    
            if (lnum == 0) {
                c.drawRoundRect(rect, radius, radius, paint);
            } else {
                path.reset();
                float dr = width - prevWidth;
                float diff = -Math.signum(dr) * Math.min(2f * radius, Math.abs(dr/2f))/2f;
                path.moveTo(
                                prevLeft, prevBottom - radius
                );
    
                path.cubicTo(
                                prevLeft, prevBottom - radius,
                                prevLeft, rect.top,
                                prevLeft + diff, rect.top
                );
                path.lineTo(
                                rect.left - diff, rect.top
                );
                path.cubicTo(
                                rect.left - diff, rect.top,
                                rect.left, rect.top,
                                rect.left, rect.top + radius
                );
                path.lineTo(
                                rect.left, rect.bottom - radius
                );
                path.cubicTo(
                                rect.left, rect.bottom - radius,
                                rect.left, rect.bottom,
                                rect.left + radius, rect.bottom
                );
                path.lineTo(
                                rect.right - radius, rect.bottom
                );
                path.cubicTo(
                                rect.right - radius, rect.bottom,
                                rect.right, rect.bottom,
                                rect.right, rect.bottom - radius
                );
                path.lineTo(
                                rect.right, rect.top + radius
                );
                path.cubicTo(
                                rect.right, rect.top + radius,
                                rect.right, rect.top,
                                rect.right + diff, rect.top
                );
                path.lineTo(
                                prevRight - diff, rect.top
                );
                path.cubicTo(
                                prevRight - diff, rect.top,
                                prevRight, rect.top,
                                prevRight, prevBottom - radius
                );
                path.cubicTo(
                                prevRight, prevBottom - radius,
                                prevRight, prevBottom,
                                prevRight - radius, prevBottom
    
                );
                path.lineTo(
                                prevLeft + radius, prevBottom
                );
                path.cubicTo(
                                prevLeft + radius, prevBottom,
                                prevLeft, prevBottom,
                                prevLeft, rect.top - radius
                );
                c.drawPath(path, paintStroke);
            }
    
            prevWidth = width;
            prevLeft = rect.left;
            prevRight = rect.right;
            prevBottom = rect.bottom;
            prevTop = rect.top;
        }
    }
    
    0 讨论(0)
  • 2020-12-08 18:35

    Modified @Rahul_Tiwari's version to automatically scale the padding and corner radius when the text size changes. It scales based on percent change from a default text size value. Plus setShadowLayer as needed. It also adds padding to the top and bottom of the text so the padding is equal on all sides.

    import android.graphics.Canvas
    import android.graphics.Paint
    import android.graphics.Path
    import android.graphics.RectF
    import android.text.style.LineBackgroundSpan
    import android.view.Gravity
    import android.widget.TextView
    import kotlin.math.abs
    import kotlin.math.sign
    
    class BackgroundColorSpan(private val tv: TextView,
                              backgroundColor: Int,
                              private val defaultTextSizePx: Float,
                              private val paddingToTextSizeRatio : Float = 0.125f,
                              gravityAlignment: Int = Gravity.CENTER) : LineBackgroundSpan {
        private val rect = RectF()
        private val paint = Paint()
        private val paintStroke = Paint()
        private val path = Path()
        private var prevWidth = -1f
        private var prevLeft = -1f
        private var prevRight = -1f
        private var prevBottom = -1f
        private var prevTop = -1f
    
        /***
         * Gravity.CENTER_HORIZONTAL
         * Gravity.LEFT
         * Gravity.RIGHT
         */
        private var gravityAlignment : Int
    
        init {
            tv.includeFontPadding = false
            paint.color = backgroundColor
            paintStroke.color = backgroundColor
            this.gravityAlignment = gravityAlignment and Gravity.HORIZONTAL_GRAVITY_MASK
        }
    
        private val paddingForDefaultTextSize: Float get() =  defaultTextSizePx * paddingToTextSizeRatio
    
        private fun getTextScale(currentPaint: Paint) : Float  = currentPaint.textSize / defaultTextSizePx
    
        private fun getTagWidth(text: CharSequence, start: Int, end: Int, paint: Paint, padding: Float): Float =
                padding + paint.measureText(text, start, end) + padding
    
        private fun updatePaddingAndShadowLayerRadius(padding: Float) {
            if (tv.shadowRadius != padding) {
                tv.setShadowLayer(padding/* radius */, 0.toFloat(), 0.toFloat(), 0 /* transparent */)
            }
            val paddingI= padding.toInt()
            if (tv.paddingLeft != paddingI && tv.paddingRight != paddingI){
                tv.setPadding(paddingI, paddingI, paddingI, paddingI)
                tv.setLineSpacing(padding, 1.0f)
            }
        }
    
        override fun drawBackground(
                c: Canvas,
                p: Paint,
                left: Int,
                right: Int,
                top: Int,
                baseline: Int,
                bottom: Int,
                text: CharSequence,
                start: Int,
                end: Int,
                lnum: Int) {
    
            val paddingForTextSize = paddingForDefaultTextSize * getTextScale(p)
            updatePaddingAndShadowLayerRadius(paddingForTextSize)
            val width = getTagWidth(text, start, end, p, paddingForTextSize)
            val shiftLeft: Float
            val shiftRight: Float
            val fm = p.fontMetrics
            val tagBottom: Float = baseline + fm.descent + paddingForTextSize
            val topPadding = if (lnum == 0 ) paddingForTextSize else 0f
            val tagTop: Float = baseline + fm.ascent - topPadding
    
            val tagHeight = tagBottom - tagTop
            val radius = tagHeight / 10
    
    
            when (gravityAlignment) {
                Gravity.LEFT -> {
                    shiftLeft = 0f - paddingForTextSize
                    shiftRight = width + shiftLeft
                }
    
                Gravity.RIGHT -> {
                    shiftLeft = right - width + paddingForTextSize
                    shiftRight = (right + paddingForTextSize)
                }
                else -> {
                    shiftLeft = (right - width) / 2
                    shiftRight = right - shiftLeft
                }
            }
    
            rect.set(shiftLeft, tagTop, shiftRight, tagBottom)
    
    
            if (lnum == 0) {
                c.drawRoundRect(rect, radius, radius, paint)
            } else {
                path.reset()
                val difference = width - prevWidth
                val diff = -sign(difference) * (2f * radius).coerceAtMost(abs(difference / 2f)) / 2f
                path.moveTo(
                        prevLeft, prevBottom - radius
                )
    
                if (gravityAlignment != Gravity.LEFT) {
                    path.cubicTo(//1
                            prevLeft, prevBottom - radius,
                            prevLeft, rect.top,
                            prevLeft + diff, rect.top
                    )
                } else {
                    path.lineTo(prevLeft, prevBottom + radius)
                }
                path.lineTo(
                        rect.left - diff, rect.top
                )
                path.cubicTo(//2
                        rect.left - diff, rect.top,
                        rect.left, rect.top,
                        rect.left, rect.top + radius
                )
                path.lineTo(
                        rect.left, rect.bottom - radius
                )
                path.cubicTo(//3
                        rect.left, rect.bottom - radius,
                        rect.left, rect.bottom,
                        rect.left + radius, rect.bottom
                )
                path.lineTo(
                        rect.right - radius, rect.bottom
                )
                path.cubicTo(//4
                        rect.right - radius, rect.bottom,
                        rect.right, rect.bottom,
                        rect.right, rect.bottom - radius
                )
                path.lineTo(
                        rect.right, rect.top + radius
                )
    
                if (gravityAlignment != Gravity.RIGHT) {
                    path.cubicTo(//5
                            rect.right, rect.top + radius,
                            rect.right, rect.top,
                            rect.right + diff, rect.top
                    )
                    path.lineTo(
                            prevRight - diff, rect.top
                    )
                    path.cubicTo(//6
                            prevRight - diff, rect.top,
                            prevRight, rect.top,
                            prevRight, prevBottom - radius
                    )
    
                } else {
                    path.lineTo(prevRight, prevBottom - radius)
                }
                path.cubicTo(//7
                        prevRight, prevBottom - radius,
                        prevRight, prevBottom,
                        prevRight - radius, prevBottom
                )
    
                path.lineTo(
                        prevLeft + radius, prevBottom
                )
    
                path.cubicTo(//8
                        prevLeft + radius, prevBottom,
                        prevLeft, prevBottom,
                        prevLeft, rect.top - radius
                )
                c.drawPath(path, paintStroke)
    
            }
            prevWidth = width
            prevLeft = rect.left
            prevRight = rect.right
            prevBottom = rect.bottom
            prevTop = rect.top
        }
    }
    
    0 讨论(0)
提交回复
热议问题