CenterAlignedTextView
package me.rajin.centeralignedtextview | |
import android.content.Context | |
import android.graphics.Rect | |
import android.graphics.Typeface | |
import android.text.SpannableStringBuilder | |
import android.text.Spanned | |
import android.text.TextPaint | |
import android.text.style.CharacterStyle | |
import android.text.style.UpdateAppearance | |
import android.util.AttributeSet | |
import androidx.appcompat.widget.AppCompatTextView | |
import androidx.core.text.getSpans | |
import java.util.WeakHashMap | |
class CenterAlignedTextView : AppCompatTextView { | |
companion object { | |
/** | |
* ratio is offset / fontHeight | |
* fontHeight = ascent - descent | |
* offset = (capHeight - ascent - descent) / 2 (in iOS) | |
* Note that in android, ascent and descent is multiplied by -1 (because yAxis is downward) | |
*/ | |
val typeface2fontHeightOffsetRatio = mutableMapOf<Typeface, Float>() | |
/** | |
* This map is for calculated offset ratio. | |
*/ | |
val weakTypeface2fontHeightOffsetRatio = WeakHashMap<Typeface, Float>() | |
} | |
var realText: CharSequence? = null | |
constructor(context: Context) : super(context) { | |
init(context, null) | |
} | |
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { | |
init(context, attrs) | |
} | |
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { | |
init(context, attrs) | |
} | |
private fun init(context: Context, attrs: AttributeSet?) { | |
// refresh span | |
this.text = text | |
} | |
/** | |
* overriding setText(text: CharSequence?, type: BufferType?) is not complete solution. | |
* There is one setText method in AppCompatTextView that does not use this method. | |
* Because there is one setText method that does not call this overridable method. | |
* setText(char[] text, int start, int len) | |
* link : https://github.com/aosp-mirror/platform_frameworks_base/blob/e0dd9516b516bc9042ad5d6f4b7fef053ce07d0e/core/java/android/widget/TextView.java#L4063-L4070 | |
*/ | |
override fun setText(text: CharSequence?, type: BufferType?) { | |
realText = text | |
val spannedText = text?.let { | |
SpannableStringBuilder(it).apply { | |
val spanList = getSpans<VerticalCenterAlignSpan>() | |
spanList.forEach { span -> | |
removeSpan(span) | |
} | |
setSpan( | |
VerticalCenterAlignSpan(), | |
0, it.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | |
) | |
} | |
} ?: text | |
super.setText(spannedText, type) | |
} | |
private class VerticalCenterAlignSpan : CharacterStyle(), UpdateAppearance { | |
override fun updateDrawState(paint: TextPaint?) { | |
if (paint != null) { | |
val ratio = typeface2fontHeightOffsetRatio[paint.typeface] ?: { | |
// as fallback, calculate offset using "X"'s text bounds | |
// note that this is not accurate because | |
// 1. text bounds of "X" can be different from typeface's meta data. | |
// 2. getTextBounds returns in pixel(int). | |
// to improve accuracy, we measure ratio with large text size. | |
weakTypeface2fontHeightOffsetRatio[paint.typeface] ?: { | |
val oldTextSize = paint.textSize | |
paint.textSize = 30f | |
val newFontMetric = paint.fontMetrics | |
val bounds = Rect() | |
paint.getTextBounds("X", 0, 1, bounds) | |
val capHeight = -bounds.top | |
val calculatedOffset = | |
(capHeight + newFontMetric.ascent + newFontMetric.descent) / 2f | |
val ratio = | |
calculatedOffset / (newFontMetric.descent - newFontMetric.ascent) | |
weakTypeface2fontHeightOffsetRatio[paint.typeface] = ratio | |
paint.textSize = oldTextSize | |
ratio | |
}() | |
}() | |
val fontMetric = paint.fontMetrics | |
val offsetY = (fontMetric.descent - fontMetric.ascent) * ratio | |
paint.baselineShift = offsetY.toInt() | |
} | |
} | |
} | |
} | |
// Before using CenterAlignedTextView, set font-ratio map. | |
val spoqaHansRatio = -0.036148648648648626f | |
val spoqaFonts = listOf( | |
ResourcesCompat.getFont(this, R.font.spoqa_han_sans_thin), | |
ResourcesCompat.getFont(this, R.font.spoqa_han_sans_light), | |
ResourcesCompat.getFont(this, R.font.spoqa_han_sans_bold), | |
ResourcesCompat.getFont(this, R.font.spoqa_han_sans_regular) | |
) | |
spoqaFonts.forEach { | |
if (it != null) { | |
typeface2fontHeightOffsetRatio[it] = spoqaHansRatio | |
} | |
} | |
// You may need to do this... | |
spoqaFonts.map { Typeface.create(it, Typeface.NORMAL) }.forEach { | |
if (it != null) { | |
typeface2fontHeightOffsetRatio[it] = spoqaHansRatio | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment