Snippets
Created by
Max Diland
last modified
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | import android.graphics.Canvas
import android.text.Layout
import android.text.Layout.Alignment.ALIGN_CENTER
import android.text.Layout.Alignment.ALIGN_NORMAL
import android.text.Layout.Alignment.ALIGN_OPPOSITE
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import kotlin.math.ceil
/**
* Created by Max Diland
*/
/**
* Improved solution
* https://stackoverflow.com/questions/7439748/why-is-wrap-content-in-multiple-line-textview-filling-parent
* It is a hacky implementation and because of the hack please use it to display texts only!
* Now it supports any textAlignment, RTL.
* What is not supported (supported but unusable):
* - compound drawables,
* - background drawables
*/
class AccurateWidthTextView @JvmOverloads constructor(
context: android.content.Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
private var extraPaddingRight: Int? = null
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (layout == null || layout.lineCount < 2) return
val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt()
val uselessPaddingWidth = layout.width - maxLineWidth
val width = measuredWidth - uselessPaddingWidth
val height = measuredHeight
setMeasuredDimension(width, height)
}
private fun getMaxLineWidth(layout: Layout): Float {
return (0 until layout.lineCount)
.map { layout.getLineWidth(it) }
.max()
?: 0.0f
}
override fun onDraw(canvas: Canvas) {
if (layout == null || layout.lineCount < 2) return super.onDraw(canvas)
val explicitLayoutAlignment = layout.getExplicitAlignment()
if (explicitLayoutAlignment == ExplicitLayoutAlignment.MIXED) return super.onDraw(canvas)
val layoutWidth = layout.width
val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt()
if (layoutWidth == maxLineWidth) return super.onDraw(canvas)
when (explicitLayoutAlignment) {
ExplicitLayoutAlignment.RIGHT -> {
drawTranslatedHorizontally(
canvas,
-1 * (layoutWidth - maxLineWidth)
) { super.onDraw(it) }
return
}
ExplicitLayoutAlignment.CENTER -> {
drawTranslatedHorizontally(
canvas,
-1 * (layoutWidth - maxLineWidth) / 2
) { super.onDraw(it) }
return
}
else -> return super.onDraw(canvas)
}
}
private fun drawTranslatedHorizontally(
canvas: Canvas,
xTranslation: Int,
drawingAction: (Canvas) -> Unit
) {
extraPaddingRight = xTranslation
canvas.save()
canvas.translate(xTranslation.toFloat(), 0f)
drawingAction.invoke(canvas)
extraPaddingRight = null
canvas.restore()
}
/*
This textView does not support compound drawables correctly so the function is used not on purpose.
It affects clipRect's width which gets formed inside the onDraw() method.
Negative - increases.
Positive - shrinks
So before onDraw you should set some value to the field extraPaddingRight
to change clip rect bounds and set null right after onDraw
*/
override fun getCompoundPaddingRight(): Int {
return extraPaddingRight ?: super.getCompoundPaddingRight()
}
}
/*
It does not matter whether the text is LTR or RLT at the end of the day it is either aligned left
or right or centered. Mixed means the layout has more than 1 paragraph and the paragraphs have
different alignments
*/
private enum class ExplicitLayoutAlignment {
LEFT, CENTER, RIGHT, MIXED
}
private fun Layout.getExplicitAlignment(): ExplicitLayoutAlignment {
if (lineCount == 0) return ExplicitLayoutAlignment.LEFT
val explicitAlignments = (0 until this.lineCount)
.mapNotNull { this.getLineExplicitAlignment(it) }
.distinct()
return if (explicitAlignments.size > 1) {
ExplicitLayoutAlignment.MIXED
} else {
explicitAlignments.firstOrNull() ?: ExplicitLayoutAlignment.LEFT
}
}
private fun Layout.getLineExplicitAlignment(line: Int): ExplicitLayoutAlignment? {
if (line !in 0 until this.lineCount) return null
val isDirectionLtr = getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT
val alignment = getParagraphAlignment(line)
return when {
alignment.name == "ALIGN_RIGHT" -> ExplicitLayoutAlignment.RIGHT
alignment.name == "ALIGN_LEFT" -> ExplicitLayoutAlignment.LEFT
// LTR and RTL
alignment == ALIGN_CENTER -> ExplicitLayoutAlignment.CENTER
// LTR
isDirectionLtr && alignment == ALIGN_NORMAL -> ExplicitLayoutAlignment.LEFT
isDirectionLtr && alignment == ALIGN_OPPOSITE -> ExplicitLayoutAlignment.RIGHT
// RTL
alignment == ALIGN_NORMAL -> ExplicitLayoutAlignment.RIGHT
else -> ExplicitLayoutAlignment.LEFT
}
}
|
Comments (0)
You can clone a snippet to your computer for local editing. Learn more.