EllipsizedTextView icon indicating copy to clipboard operation
EllipsizedTextView copied to clipboard

Multiple issues with current solution

Open Loic-Dumas opened this issue 2 years ago • 2 comments

I noticed multiple issues with the 1.0.0 implementation.

  1. If the text contains "...", before the final ellipsis, the custom ellipsis is added in the text, not at the end. Example : Capture d’écran 2023-04-13 à 11 33 13

  2. As mentioned in issue #1

availableTextWidth = (availableScreenWidth - paint.measureText(ellipsis)) * maxLines

maxLines should only apply to the availableScreenWidth and not the ellipsis width. So the line should be :

availableTextWidth = availableScreenWidth  * maxLines - paint.measureText(ellipsis)

Otherwise, the more line there is, the ellispis will be more distant to the end of last line.

  1. Anyway, the way ellipsizedText is computed with TextUtils.ellipsize(...) works only on single line. With multiple line, when computing.
availableTextWidth = (availableScreenWidth - paint.measureText(ellipsis)) * maxLines
ellipsizedText = TextUtils.ellipsize(text, paint, availableTextWidth, ellipsize)

this code doesn't consider that the text is not justified. So every end of line will take spaces, which will be reported on the last line. So the ellipsis can be ellipsed it self by TextView. Example : Capture d’écran 2023-04-13 à 11 32 52

Based on your solution, I wrote this version of EllipsizedTextView which fix previously mentioned issues. I reused TextUtils.ellipsize() but only on the last line retrieved from the layout, to avoid issue 3. Also, instead of using overriding onMeasure(), I overridden onLayout() Note : Only Ellipsize end is handled.

class EllipsizedTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatTextView(context, attrs, defStyleAttr) {

    private var ellipsis = getDefaultEllipsis().toString()
    private var ellipsisColor = getDefaultEllipsisColor()

    private val ellipsisSpannable: SpannableString
    private val spannableStringBuilder = SpannableStringBuilder()

    init {

        if (ellipsize != TextUtils.TruncateAt.END) {
            throw java.lang.IllegalStateException("Only end ellipsize is handled.")
        }

        if (attrs != null) {
            val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.EllipsizedTextView, 0, 0)
            ellipsis = typedArray.getString(R.styleable.EllipsizedTextView_ellipsis) ?: getDefaultEllipsis().toString()
            ellipsisColor = typedArray.getColor(R.styleable.EllipsizedTextView_ellipsisColor, getDefaultEllipsisColor())
            typedArray.recycle()
        }

        ellipsisSpannable = SpannableString(ellipsis)
        ellipsisSpannable.setSpan(ForegroundColorSpan(ellipsisColor), 0, ellipsis.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        if (changed && layout.lineCount >= maxLines) {

            val lastLineNb = maxLines - 1
            val ellipsisCount = layout.getEllipsisCount(lastLineNb)

            if (ellipsisCount > 0) {
                val availableScreenWidth = measuredWidth - compoundPaddingLeft.toFloat() - compoundPaddingRight.toFloat()
                val lastLineAvailableTextWidth = availableScreenWidth - paint.measureText(ellipsis)

                val lastLineStart = layout.getLineStart(lastLineNb)
                val lastLineEnd = layout.getLineEnd(lastLineNb)
                val textWithoutLastLine = text.subSequence(0, lastLineStart)
                val lastLineText = text.subSequence(lastLineStart, lastLineEnd)
                val ellipsizedLasLine = TextUtils.ellipsize(lastLineText, paint, lastLineAvailableTextWidth, ellipsize)

                spannableStringBuilder.clear()
                val result = spannableStringBuilder.append(textWithoutLastLine).append(ellipsizedLasLine).append(ellipsisSpannable)
                text = result
            }
        }
    }

    private fun getDefaultEllipsis(): Char {
        return Typography.ellipsis
    }

    private fun getDefaultEllipsisColor(): Int {
        return textColors.defaultColor
    }

    fun isEllipsized(): Boolean {
        if (TextUtils.isEmpty(text) || text.length < ellipsis.length) {
            return false
        }
        val finalWord = text.subSequence(text.length - ellipsis.length, text.length)
        return TextUtils.equals(finalWord, ellipsis)
    }
}

I didn't opened a pull request, since the project is not building with latest Android Studio. I still share my solution it may help other.

Loic-Dumas avatar Apr 13 '23 09:04 Loic-Dumas

it's recycled overtime when I use RecyclerView, it won't showing the ellipsis again @Loic-Dumas And since you use onLayout instead of onMeasure, does it will degrade the performance?

jboxx avatar May 24 '23 15:05 jboxx

Hello @jboxx, In my use case, I don't use RecyclerView and haven't tested on it. After some time in production, I didn't noticed degradation in app performance using onLayout().

But if maybe it's better to still do the ellipsize in onMeasure(). The code can be easily shift to this overridden method.

Loic-Dumas avatar Oct 10 '23 13:10 Loic-Dumas