Skip to content

Instantly share code, notes, and snippets.

@Rajin9601
Last active July 13, 2019 08:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Rajin9601/7591e7cfd4dc62742f6f8a8911ae8695 to your computer and use it in GitHub Desktop.
Save Rajin9601/7591e7cfd4dc62742f6f8a8911ae8695 to your computer and use it in GitHub Desktop.
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