像Instagram这样的圆形背景文字,ReplacementSpan不能按要求工作

时间:2018-01-04 13:50:29

标签: android android-edittext instagram spannable

我试图在下面做类似于Instagram的事情 -

This is what i have done

但我希望这条曲线像Instagram一样 - This is what i want

现在我又遇到了一个问题 - 当我打字时,文本不会自动进入下一行,我必须按返回,就像通常editText工作在固定宽度。 (简而言之multilineReplacementSpan

无法正常工作

以下是我所做的示例代码 -

public class EditextActivity extends AppCompatActivity {

    EditText edittext;
    RoundedBackgroundSpan roundedBackgroundSpan;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.editext_screen);
        edittext=(EditText)findViewById(R.id.edittext);
       // edittext.setText("Hello My name is Karandeep Atwal.\n\n Hii this is test");
        roundedBackgroundSpan= new RoundedBackgroundSpan(Color.RED,Color.WHITE);
        edittext.getText().setSpan(roundedBackgroundSpan, 0, edittext.getText().length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
    }


    public class RoundedBackgroundSpan extends ReplacementSpan implements LineHeightSpan {

        private static final int CORNER_RADIUS = 15;
        private static final int PADDING_X = 10;

        private int   mBackgroundColor;
        private int   mTextColor;

        /**
         * @param backgroundColor background color
         * @param textColor text color
         */
        public RoundedBackgroundSpan(int backgroundColor, int textColor) {
            mBackgroundColor = backgroundColor;
            mTextColor = textColor;
        }

        @Override
        public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
            return (int) (PADDING_X + paint.measureText(text,start, end) + PADDING_X);
        }

        @Override
        public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
            float width = paint.measureText(text,start, end);
            RectF rect = new RectF(x, top, x + width + 2 * PADDING_X, bottom);
            paint.setColor(mBackgroundColor);
            canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, paint);
            paint.setColor(mTextColor);
            canvas.drawText(text, start, end, x + PADDING_X, y, paint);
        }

        @Override
        public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fontMetricsInt) {

        }
    }

}

下面是我的xml -

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    android:layout_gravity="center"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:padding="5dp"
        android:background="@drawable/border"
        android:id="@+id/edittext"
        android:layout_centerInParent="true"
        android:textColor="@android:color/black"
        android:gravity="center"
        android:hint="hi"
        android:singleLine="false"
        android:inputType="textMultiLine"
        android:textSize="30sp"
        android:maxWidth="100dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

以下是我使用setSpan -

键入时的内容

enter image description here

这是固定宽度的正常行为,我想要 -

enter image description here

4 个答案:

答案 0 :(得分:3)

enter image description here activity_main.xml中

var express = require('express');
var app = express();
var exec = require('ssh-exec');
var arg1 = "abc";
var arg2 = "def";
var arg3 = "ghd";
exec(`"./test.sh" ${arg1} ${arg2} ${arg3}` {
    user: 'ubuntu',
    host: 'a.b.c.d'
}, function(err, stdout) {
    if (err) {
        throw err;
    }
    console.log(stdout);
    console.log('success');
});

MainActivity.java

<?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>

BackgroundColorSpan.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);
    }
}

答案 1 :(得分:3)

通过@ tttzof351增强BackgroundColorSpan以支持对齐:

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
    }
}

结果:

enter image description here

答案 2 :(得分:1)

我实现了新的RoundedBackgroundSpan.kt类扩展LineBackgroundSpan,因为它可以逐行绘制装饰层。

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)
  }
}

并使用它:

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()) } }
    }
}

有关本文实现的更多详细信息: https://medium.com/@Semper_Viventem/simple-implementation-of-rounded-background-for-text-in-android-60a7706c0419

答案 3 :(得分:0)

修改了@Rahul_Tiwari的版本,以在文本大小更改时自动缩放填充和拐角半径。它根据默认文本大小值的百分比变化来缩放。根据需要加上setShadowLayer。它还在文本的顶部和底部添加了填充,因此填充在所有面上都是相同的。

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
    }
}