Instantly share code, notes, and snippets.
Created
August 28, 2018 05:41
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save tranductam2802/a8c9c1063909e3308d9636d20905396e to your computer and use it in GitHub Desktop.
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 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