Skip to content

Instantly share code, notes, and snippets.

@ephemient
Last active March 26, 2019 22:40
Show Gist options
  • Save ephemient/8d069f469135825e454c34f0670420bc to your computer and use it in GitHub Desktop.
Save ephemient/8d069f469135825e454c34f0670420bc to your computer and use it in GitHub Desktop.
import android.graphics.Canvas
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.text.style.LineBackgroundSpan
import android.text.style.UnderlineSpan
/** Draws a dotted underline (as opposed to solid [UnderlineSpan]). */
class DottedUnderlineSpan : LineBackgroundSpan {
private val paint = Paint()
private val dashIntervals = FloatArray(2)
override fun drawBackground(
c: Canvas,
p: Paint,
left: Int,
right: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
lnum: Int
) {
dashIntervals[1] = 2 * p.underlineThickness
paint.apply {
set(p)
style = Paint.Style.STROKE
strokeWidth = p.underlineThickness
strokeCap = Paint.Cap.ROUND
pathEffect = DashPathEffect(dashIntervals, p.underlineThickness)
}
val underlineCenterY = baseline.toFloat() + p.underlinePosition + p.underlineThickness / 2
c.drawLine(left.toFloat(), underlineCenterY, right.toFloat(), underlineCenterY, paint)
}
companion object {
// Copied from Skia's kStdUnderline_Offset/kStdUnderline_Thickness
private const val STDUNDERLINE_OFFSET = 1f / 9f
private const val STDUNDERLINE_THICKNESS = 1f / 18f
// [Paint.getUnderlinePosition()] is `@hide` :(
private val Paint.underlinePosition: Float
get() = textSize * STDUNDERLINE_OFFSET
// [Paint.getUnderlineThickness()] is `@hide` :(
private val Paint.underlineThickness: Float
get() = textSize * STDUNDERLINE_THICKNESS
}
}
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.text.style.LineBackgroundSpan
import android.text.style.StrikethroughSpan
import androidx.annotation.IntDef
/** Draws an oblique strikethrough (as opposed to horizontal [StrikethroughSpan]). */
class ObliqueStrikethroughSpan(
@param:StrikethroughDirection private val strikethroughDirection: Int
) : LineBackgroundSpan {
private val rect = Rect()
private val paint = Paint()
@Retention(AnnotationRetention.SOURCE)
@IntDef(STRIKETHROUGH_DIRECTION_RISING, STRIKETHROUGH_DIRECTION_FALLING)
annotation class StrikethroughDirection
override fun drawBackground(
c: Canvas,
p: Paint,
left: Int,
right: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
lnum: Int
) {
p.getTextBounds(text.toString(), start, end, rect)
paint.set(p)
paint.strokeWidth = p.textSize * STDUNDERLINE_THICKNESS
/* Suppose text="a".
*
* left right
* top ──╆━━━━━━━┪
* ┃ ┃ ___, ┬── fm.ascent
* ┃ ┃ / | │
* ┃ __, ┠── rect.top | | __, │
* ┃ / | ┃ | | / | │
* baseline ──┨ \_/|_/┃__ rect.bottom \__/\_/\_/|_/┼── baseline
* ┃ ┃ |\ │
* ┃ ┃ |/ │__ fm.descent
* bottom ──┺┯━━━━━┯┛
* rect.left rect.right
*
* We are drawing inside of Rect(left, top, right, bottom),
* this includes extra spacing and line height.
*
* We measure textBounds from the text region - although unstyled,
* due to the .toString() call. Hopefully that will be fine.
*
* This gives us tighter bounds than using FontMetrics,
* which does not take into account the current text.
*
* Keep in mind that the y coordinate grows downward.
* Upward measures (such as rect.top) are negative.
*/
val bottomX = if (strikethroughDirection == STRIKETHROUGH_DIRECTION_FALLING) right else left
val bottomY = Math.min(bottom, baseline + rect.bottom)
val topX = if (strikethroughDirection == STRIKETHROUGH_DIRECTION_FALLING) left else right
val topY = Math.max(top, baseline + rect.top)
c.drawLine(bottomX.toFloat(), bottomY.toFloat(), topX.toFloat(), topY.toFloat(), paint)
}
companion object {
// This constant is copied from kStdUnderline_Thickness in Skia,
// which is how normal underlines and strikethroughs are drawn.
private const val STDUNDERLINE_THICKNESS = 1f / 18
/** Strikethrough direction: bottom left to top right. */
const val STRIKETHROUGH_DIRECTION_RISING = 0
/** Strikethrough direction: bottom right to top left. */
const val STRIKETHROUGH_DIRECTION_FALLING = 1
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment