CenterAlignedTextView
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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