Skip to content

Instantly share code, notes, and snippets.

@tranductam2802
Created August 28, 2018 05:41
Show Gist options
  • Save tranductam2802/a8c9c1063909e3308d9636d20905396e to your computer and use it in GitHub Desktop.
Save tranductam2802/a8c9c1063909e3308d9636d20905396e to your computer and use it in GitHub Desktop.
package io.github.view
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import io.github.utils.DimensionUtils
import java.util.regex.Pattern
/**
* Furigana view extent for view and make a view similar TextView with furigana display feature.
*/
class FuriganaView : View {
/* View configuration*/
companion object {
private val TAG = Companion::class.simpleName!!
val FURIGANA_PERCENT_NORMAL = 50
val DEFAULT_TEXT_SIZE = DimensionUtils.dpToPx(18)
val DEFAULT_FURIGANA_PERCENT = FURIGANA_PERCENT_NORMAL
val DEFAULT_TEXT_COLOR = Color.BLACK
val DEFAULT_FURIGANA_COLOR = Color.BLUE
}
/* Content */
val mCharacterRegexCode = "[a-zA-Z\\s\u3040-ゟ゠-ヿ\uFF00-\uFFEF一-龯]"
val mFuriganaRegexCode = "\\[($mCharacterRegexCode+)]\\(($mCharacterRegexCode+)\\)"
val mFuriganaRegex = Pattern.compile(mFuriganaRegexCode)!!
val mContentBuilder = ArrayList<Furigana>()
/* View setting */
var mFuriganaPercent = DEFAULT_FURIGANA_PERCENT
var mIsShowFurigana = true
/* Drawer tool */
val mContentPaint = Paint(Paint.ANTI_ALIAS_FLAG)
val mTextPaint = Paint(Paint.ANTI_ALIAS_FLAG)
val mFuriganaPaint = Paint(Paint.ANTI_ALIAS_FLAG)
constructor(context: Context?) : super(context) {
initView(context, null, 0)
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
initView(context, attrs, 0)
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initView(context, attrs, defStyleAttr)
}
fun furiganaCount(): Int {
var furiganaCount = 0
mContentBuilder.filter { !it.furigana.isNullOrEmpty() }.forEach({ furiganaCount++ })
return furiganaCount
}
fun count(): Int {
return mContentBuilder.size
}
fun setText(text: String) {
if (text.isEmpty())
return
val furiganaGroup = mFuriganaRegex.matcher(text)
// Clear list data before add
mContentBuilder.clear()
text.split(mFuriganaRegex).forEach {
if (it.isNotEmpty()) {
mContentBuilder.add(Furigana(it))
}
// Find the furigana and add it to list
if (furiganaGroup.find()) {
val count = furiganaGroup.groupCount()
if (count == 1) {
val bottom = furiganaGroup.group(1)
if (!bottom.isNullOrEmpty()) {
mContentBuilder.add(Furigana(bottom))
}
} else if (count > 1) {
val bottom = furiganaGroup.group(1)
val top = furiganaGroup.group(2)
if (!bottom.isNullOrEmpty() && !top.isNullOrEmpty()) {
mContentBuilder.add(Furigana(bottom, top))
}
}
}
}
}
/**
* A great opportunity to prepare view for an initial drawing, make various calculation, set
* default values or whatever we need to prepare this view control.
*/
fun initView(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) {
mContentPaint.textSize = DEFAULT_TEXT_SIZE
mContentPaint.color = DEFAULT_TEXT_COLOR
mTextPaint.textSize = DEFAULT_TEXT_SIZE
mTextPaint.color = DEFAULT_FURIGANA_COLOR
mFuriganaPaint.textSize = DEFAULT_TEXT_SIZE * mFuriganaPercent / 100
mFuriganaPaint.color = DEFAULT_FURIGANA_COLOR
}
/**
* This view attached to the window and ready to load and detect view child.
* If this view is working with other views located in same layout.xml it is good place to find
* them by id (which you can set by attributes) and save as a global reference (if needed).
*/
override fun onAttachedToWindow() {
super.onAttachedToWindow()
}
/**
* Means that our custom view is on stage to find out it's own size. It's very important method,
* as for most cases you will need your view to have specific size to fit in this layout.
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
val widthSize = View.MeasureSpec.getSize(widthMeasureSpec)
// Self measure the width dimension.
val width: Int
if (widthMode == View.MeasureSpec.EXACTLY) {
width = if (widthSize < 0) 0 else widthSize
} else {
var desiredWidth = 0F
mContentBuilder.forEach {
val characterWidth: Float
if (mIsShowFurigana) {
val textWidth = mContentPaint.measureText(it.text)
val furiganaWidth = mContentPaint.measureText(it.furigana)
characterWidth = Math.max(textWidth, furiganaWidth)
} else {
characterWidth = mContentPaint.measureText(it.text)
}
desiredWidth += characterWidth
}
desiredWidth += paddingLeft + paddingRight
if (widthMode == View.MeasureSpec.AT_MOST) {
val desired = Math.min(desiredWidth.toInt(), widthSize)
width = if (desired < 0) 0 else desired
} else {
width = if (desiredWidth < 0) 0 else desiredWidth.toInt()
}
}
val heightMode = View.MeasureSpec.getMode(heightMeasureSpec)
val heightSize = View.MeasureSpec.getSize(heightMeasureSpec)
// Self measure the height dimension.
val height: Int
if (heightMode == View.MeasureSpec.EXACTLY) {
height = if (heightSize < 0) 0 else heightSize
} else {
// First line initial
val linePadding = mContentPaint.textSize / 10
var desiredHeight = mContentPaint.textSize + linePadding
if (mIsShowFurigana) {
desiredHeight += mFuriganaPaint.textSize
}
var desiredWidth = 0F
mContentBuilder.forEach {
val characterWidth: Float
if (mIsShowFurigana) {
val textWidth = mContentPaint.measureText(it.text)
val furiganaWidth = mContentPaint.measureText(it.furigana)
characterWidth = Math.max(textWidth, furiganaWidth)
} else {
characterWidth = mContentPaint.measureText(it.text)
}
desiredWidth += characterWidth
if (desiredWidth > width) {
desiredWidth = characterWidth
desiredHeight += mContentPaint.textSize + linePadding
if (mIsShowFurigana) {
desiredHeight += mFuriganaPaint.textSize
}
}
}
if (heightMode == View.MeasureSpec.AT_MOST) {
val desired = Math.min(desiredHeight.toInt(), heightSize)
height = if (desired < 0) 0 else desired
} else {
height = if (desiredHeight < 0) 0 else desiredHeight.toInt()
}
}
// Update the real measured dimension.
setMeasuredDimension(width, height)
}
/**
* Means that our custom view is on stage to find out it's own size. It's very important method,
* as for most cases you will need your view to have specific size to fit in this layout.
* 1. Calculate this view content desired size (width and height).
* 2. Get the view MeasureSpec (width and height) for size and mode.
* 3. Check MeasureSpec mode that user set and adjust size of your view (for width and height).
*/
override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {
super.onSizeChanged(w, h, oldW, oldH)
}
/**
* Incorporates assigning a size and position to each of its children. Because of that, we are
* looking into a flat custom view (that extends a simple View) that does not have any children
* so there is no reason to override this method.
*/
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
// Do nothing. There one reason to override this method is make it final.
}
/**
* That’s where the magic happens. Having both Canvas and Paint objects will allow this view
* draw anything it need. While making a custom view, always keep in mind that onDraw calls lots
* of time, like really a lot. While having some changes, scrolling, swiping will be redrawn. So
* that's why even Android Studio recommend to avoid object allocation during onDraw operation,
* instead to create it once and reuse further on.
*/
override fun onDraw(canvas: Canvas?) {
if (mIsShowFurigana) {
// Calculate the base line.
val linePadding = mContentPaint.textSize / 10
val fullLineHeight = mContentPaint.textSize + mFuriganaPaint.textSize + linePadding
// Set the default start horizontal.
var calculateX = paddingLeft.toFloat()
// Calculate the start vertical.
var calculateY = paddingTop.toFloat()
// Detect the case have a too small line height.
val offset = mContentPaint.textSize * 1.2F - linePadding / 2
if (calculateY + fullLineHeight > height) {
calculateY += height.toFloat() - offset
} else {
calculateY += fullLineHeight - offset
}
// Draw the group of content.
mContentBuilder.forEach {
val textWidth = mContentPaint.measureText(it.text)
val furiganaWidth = mFuriganaPaint.measureText(it.furigana)
// Detect the writable in line.
val fullTextWidth = Math.max(textWidth, furiganaWidth)
if (calculateX + fullTextWidth > width - paddingRight) {
calculateX = paddingLeft.toFloat()
calculateY += fullLineHeight
}
it.x = calculateX
it.y = calculateY
if (it.furigana.isEmpty()) {
// Draw the text.
val textY = it.y + mContentPaint.textSize
canvas?.drawText(it.text, it.x, textY, mContentPaint)
} else {
if (textWidth == furiganaWidth) {
// Draw the furigana.
canvas?.drawText(it.furigana, it.x, it.y, mFuriganaPaint)
// Draw the text.
val textY = it.y + mTextPaint.textSize
canvas?.drawText(it.text, it.x, textY, mTextPaint)
} else if (textWidth > furiganaWidth) {
// Remove all space of this furigana to check the real width.
val furiganaChars = it.furigana.replace(" ", " ").trim().split(" ")
var furiganaNoSpace = ""
furiganaChars.forEach {
furiganaNoSpace += it
}
// Get the real width.
val newFuriganaWidth = mFuriganaPaint.measureText(furiganaNoSpace)
val space = (textWidth - newFuriganaWidth) / (furiganaChars.size + 1)
// Draw the group of furigana.
var furiganaX = it.x + space
val furiganaY = it.y
furiganaChars.forEach {
canvas?.drawText(it, furiganaX, furiganaY, mFuriganaPaint)
// The next character.
furiganaX += space + mFuriganaPaint.measureText(it)
}
// Draw the text.
val textY = it.y + mTextPaint.textSize
canvas?.drawText(it.text, it.x, textY, mTextPaint)
} else {
// Draw the furigana.
val furiganaX = it.x
canvas?.drawText(it.furigana, furiganaX, it.y, mFuriganaPaint)
// Remove all space of this text to check the real width.
val textChars = it.text.replace(" ", " ").trim().split(" ")
var textNoSpace = ""
textChars.forEach {
textNoSpace += it
}
// Get the real width.
val newTextWidth = mTextPaint.measureText(textNoSpace)
val space = (furiganaWidth - newTextWidth) / (textChars.size + 1)
// Draw the text.
var textX = it.x + space
val textY = it.y + mTextPaint.textSize
textChars.forEach {
canvas?.drawText(it, textX, textY, mTextPaint)
// The next character.
textX += space + mTextPaint.measureText(it)
}
}
}
// Update the full width for next text.
calculateX += fullTextWidth
}
} else {
// TODO: When not show furigana
}
}
data class Furigana(val text: String, val furigana: String = "", var x: Float = 0f, var y: Float = 0f)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment