Skip to content

Instantly share code, notes, and snippets.

@slaviboy
Last active November 19, 2021 01:53
Show Gist options
  • Save slaviboy/3ff8c71bd39a20e1a95b7275018fc29b to your computer and use it in GitHub Desktop.
Save slaviboy/3ff8c71bd39a20e1a95b7275018fc29b to your computer and use it in GitHub Desktop.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:focusableInTouchMode="true"
android:imeOptions="actionDone"
tools:context=".MainActivity">
<com.slaviboy.universaldictionarybg.ApostropheTextView
android:layout_width="350dp"
android:layout_height="700dp"
android:background="#1B1B1B"
android:gravity="center"
android:includeFontPadding="false"
android:letterSpacing="0.6"
android:lineSpacingExtra="30dp"
android:text="`а `б `в `г `д `е\n`ж `з `и `й `к `л\n`м `н `о `п `р `с\n`т `у `ф `х `ц `ч\n`ш `щ `ъ `ь `ю `я\n`А `Б `В `Г `Д `Е\n`Ж `З `И `Й `К `Л\n`М `Н `О `П `Р `С\n`Т `У `Ф `Х `Ц `Ч\n`Ш `Щ `Ъ `Ь `Ю `Я"
android:textColor="#6AE349"
android:textSize="22sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
package com.slaviboy.universaldictionarybg
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.Gravity
import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView
class ApostropheTextView : AppCompatTextView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
val textPaint: Paint = Paint().apply {
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = 2f
}
lateinit var charsMiddle: ArrayList<ArrayList<Float>>
lateinit var charsHeight: ArrayList<ArrayList<Float>>
lateinit var replaceChar: String
lateinit var linesLeft: ArrayList<Float>
lateinit var linesBottom: ArrayList<Float>
lateinit var originalText: String
lateinit var actualText: String
lateinit var apostropheIndicesPosition: ArrayList<ArrayList<Int>>
val apostrophePath: Path = Path()
override fun setText(text: CharSequence?, type: BufferType?) {
replaceChar = " "
// remove all apostrophes and get the bounds
originalText = text.toString()
actualText = originalText.replace("`", "").replace(" ", replaceChar)
super.setText(actualText, type)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
generate(originalText)
getBound(actualText)
}
/**
* For example if we have the text value "h`appy", it first finds the
* positions of all "`" chars and puts them in arraylist [1], then it
* returns the actual string "happy".
*/
fun generate(string: String = this.text.toString()) {
val lines = string.replace(" ", replaceChar).split("\n")
charsMiddle = arrayListOf()
charsHeight = arrayListOf()
apostropheIndicesPosition = arrayListOf()
lines.forEachIndexed { i, str ->
apostropheIndicesPosition.add(arrayListOf())
charsMiddle.add(arrayListOf())
charsHeight.add(arrayListOf())
val p = apostropheIndicesPosition.last()
val m = charsMiddle.last()
val h = charsHeight.last()
if (str.contains('`')) {
str.forEachIndexed { j, char ->
if (char == '`') {
p.add(j - p.size)
}
}
val str2 = str.replace("`", "")
p.forEach {
val w1 = paint.measureText(str2, 0, it + 1)
val w2 = paint.measureText(str2, 0, it)
val bounds3 = RectF()
val path = Path()
paint.getTextPath(str2, it, it + 1, 0f, 0f, path)
path.computeBounds(bounds3, true)
val height = Math.abs(bounds3.top) // the height from the baseline to the top of the character
val w3 = bounds3.width() / 2f
m.add(w2 + w3)
h.add(height)
}
}
}
}
/**
* Get the bound rectangles, that are indicate the size of each line, and later are used
* as indication of the position where the apostrophes should be placed
*/
fun getBound(string: String = this.text.toString()) {
val lines = string.replace(" ", replaceChar).split("\n")
var offset = 0
val boundVertical = Rect()
getLineBounds(0, boundVertical)
val firstTop = boundVertical.top
linesLeft = arrayListOf()
linesBottom = arrayListOf()
lines.forEachIndexed { i, str ->
// replace all tabs with _ char for measuring
val s = str.replace('\t', '_')
// get horizontal bound for each line
val boundHorizontal = Rect()
paint.getTextBounds(s, 0, s.length, boundHorizontal)
boundHorizontal.offset(
paddingStart + (layout?.getPrimaryHorizontal(offset)?.toInt() ?: 0),
0
)
val left = boundHorizontal.left
val bottom = firstTop + (layout?.getLineBaseline(i) ?: 0)
linesLeft.add(left.toFloat())
linesBottom.add(bottom.toFloat())
offset += (s.length + 1)
}
}
override fun onDraw(canvas: Canvas) {
textPaint.apply {
style = Paint.Style.FILL
color = currentTextColor
}
val size = width * textSize * 0.00005f
charsMiddle.forEachIndexed { i, m ->
m.forEachIndexed { j, it ->
val x = linesLeft[i] + it
val y = linesBottom[i] - charsHeight[i][j] - (size * 2)
apostrophePath.apply {
rewind()
moveTo(x, y)
lineTo(x - size, y)
lineTo(x - size * 2, y - size * 3)
lineTo(x - size, y - size * 3)
}
canvas.drawPath(apostrophePath, textPaint)
}
}
super.onDraw(canvas)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment