mirror of
https://github.com/BetterAndroid/PanguText.git
synced 2025-09-04 09:45:37 +08:00
Initial commit
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
package com.highcapable.pangutext
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.highcapable.pangutext.test", appContext.packageName)
|
||||
}
|
||||
}
|
4
pangutext-android/src/main/AndroidManifest.xml
Normal file
4
pangutext-android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/PanguText
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/1/12.
|
||||
*/
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package com.highcapable.pangutext.android
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.CharacterStyle
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.Px
|
||||
import com.highcapable.pangutext.android.core.PanguMarginSpan
|
||||
import com.highcapable.pangutext.android.core.PanguPatterns
|
||||
import com.highcapable.pangutext.android.extension.injectPanguText
|
||||
import com.highcapable.pangutext.android.extension.injectRealTimePanguText
|
||||
import com.highcapable.pangutext.android.extension.setTextWithPangu
|
||||
import com.highcapable.yukireflection.factory.classOf
|
||||
|
||||
/**
|
||||
* The library core of Pangu text processor.
|
||||
*
|
||||
* Bigger thanks for [this](https://github.com/vinta/pangu.java) project.
|
||||
* @see PanguPatterns
|
||||
*/
|
||||
object PanguText {
|
||||
|
||||
/**
|
||||
* This is a placeholder character for replacing the content of the regular expression,
|
||||
* with no actual meaning.
|
||||
*/
|
||||
private const val PH = '\u001C'
|
||||
|
||||
/**
|
||||
* The global configuration of [PanguText].
|
||||
*/
|
||||
val globalConfig = PanguTextConfig()
|
||||
|
||||
/**
|
||||
* Use [PanguText] to format specified text.
|
||||
*
|
||||
* [PanguText] will automatically set [PanguMarginSpan] for some characters in
|
||||
* the text to achieve white space typesetting effect without actually inserting
|
||||
* any characters or changing the length of the original text.
|
||||
*
|
||||
* This function will insert a style for the current given [text] without actually changing the string position in the text.
|
||||
* If the current [text] is of type [Spannable], it will return the original unmodified object,
|
||||
* otherwise it will return the wrapped object [SpannableString] after.
|
||||
*
|
||||
* - Note: Processed [Spanned] text is in experimental stage and may not be fully supported,
|
||||
* if the text is not processed correctly, please disable [PanguTextConfig.isProcessedSpanned].
|
||||
* @see PanguTextConfig.isProcessedSpanned
|
||||
* @see PanguTextConfig.cjkSpacingRatio
|
||||
* @see TextView.injectPanguText
|
||||
* @see TextView.injectRealTimePanguText
|
||||
* @see TextView.setTextWithPangu
|
||||
* @param resources the current resources.
|
||||
* @param textSize the text size (px).
|
||||
* @param text text to be formatted.
|
||||
* @param config the configuration of [PanguText].
|
||||
* @return [CharSequence]
|
||||
*/
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun format(resources: Resources, @Px textSize: Float, text: CharSequence, config: PanguTextConfig = globalConfig): CharSequence {
|
||||
if (!config.isEnabled) return text
|
||||
if (text.isBlank()) return text
|
||||
val formatted = format(text, PH, config)
|
||||
return text.applySpans(formatted, resources, textSize, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use [PanguText] to format the current text content.
|
||||
*
|
||||
* Using this function will add extra [whiteSpace] as character spacing to the text,
|
||||
* changing the length of the original text.
|
||||
*
|
||||
* - Note: Processed [Spanned] text is in experimental stage and may not be fully supported,
|
||||
* if the text is not processed correctly, please disable [PanguTextConfig.isProcessedSpanned].
|
||||
* @see PanguTextConfig.isProcessedSpanned
|
||||
* @param text text to be formatted.
|
||||
* @param whiteSpace the spacing character, default is 'U+200A'.
|
||||
* @param config the configuration of [PanguText].
|
||||
* @return [CharSequence]
|
||||
*/
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun format(text: CharSequence, whiteSpace: Char = ' ', config: PanguTextConfig = globalConfig): CharSequence {
|
||||
if (!config.isEnabled) return text
|
||||
// In any case, always perform a cleanup operation before accepting text.
|
||||
val processed = text.clearSpans()
|
||||
val patterns = config.excludePatterns.toTypedArray()
|
||||
return if ((config.isProcessedSpanned || text !is Spanned) && text.isNotBlank() && text.length > 1)
|
||||
PanguPatterns.matchAndReplace(processed, whiteSpace, *patterns)
|
||||
else processed
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the [PanguMarginSpan] to the text.
|
||||
* @receiver [CharSequence]
|
||||
* @param formatted the formatted text.
|
||||
* @param resources the current resources.
|
||||
* @param textSize the text size (px).
|
||||
* @param config the configuration of [PanguText].
|
||||
* @param whiteSpace the spacing character, default is [PH].
|
||||
* @return [CharSequence]
|
||||
*/
|
||||
private fun CharSequence.applySpans(
|
||||
formatted: CharSequence,
|
||||
resources: Resources,
|
||||
@Px textSize: Float,
|
||||
config: PanguTextConfig = globalConfig,
|
||||
whiteSpace: Char = PH
|
||||
): CharSequence {
|
||||
val builder = SpannableStringBuilder(formatted)
|
||||
formatted.forEachIndexed { index, c ->
|
||||
// Add spacing to the previous character.
|
||||
if (c == whiteSpace && index in 0..formatted.lastIndex) {
|
||||
val span = PanguMarginSpan.Placeholder()
|
||||
builder.setSpan(span, index - 1, index, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
// Delete the placeholder character.
|
||||
for (i in (builder.length - 1) downTo 0) {
|
||||
if (builder[i] == whiteSpace) builder.delete(i, i + 1)
|
||||
}
|
||||
// Find the [PanguMarginSpan.Placeholder] subscript in [builder] and use [PanguMarginSpan] to set it to [original].
|
||||
val builderSpans = builder.getSpans(0, builder.length, classOf<PanguMarginSpan.Placeholder>())
|
||||
val spannable = if (this !is Spannable) SpannableString(this) else this
|
||||
// Add new [PanguMarginSpan].
|
||||
builderSpans.forEach {
|
||||
val start = builder.getSpanStart(it)
|
||||
val end = builder.getSpanEnd(it)
|
||||
val span = PanguMarginSpan.create(resources, textSize, config.cjkSpacingRatio)
|
||||
spannable.setSpan(span, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
}; builder.clear()
|
||||
return spannable
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the [PanguMarginSpan] from the text.
|
||||
*
|
||||
* Workaround for the issue that the [PanguMarginSpan] repeatedly sets
|
||||
* the same range causes performance degradation.
|
||||
* @receiver [CharSequence]
|
||||
* @return [CharSequence]
|
||||
*/
|
||||
private fun CharSequence.clearSpans(): CharSequence {
|
||||
if (this !is Spannable || isBlank() || !hasSpan<PanguMarginSpan>()) return this
|
||||
getSpans(0, length, classOf<PanguMarginSpan>()).forEach { span ->
|
||||
val start = getSpanStart(span)
|
||||
val end = getSpanEnd(span)
|
||||
// Clear the [PanguMarginSpan].
|
||||
if (start < length && end > 0) removeSpan(span)
|
||||
}; return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the text contains a specific span [T].
|
||||
* @receiver [CharSequence]
|
||||
* @return [Boolean]
|
||||
*/
|
||||
private inline fun <reified T : CharacterStyle> CharSequence.hasSpan(): Boolean {
|
||||
val spannable = this as? Spanned ?: return false
|
||||
val spans = spannable.getSpans(0, spannable.length, classOf<T>())
|
||||
return spans.isNotEmpty()
|
||||
}
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/PanguText
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/2/6.
|
||||
*/
|
||||
package com.highcapable.pangutext.android
|
||||
|
||||
import android.text.Spanned
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* The [PanguText] configuration.
|
||||
*/
|
||||
class PanguTextConfig internal constructor() : Serializable {
|
||||
|
||||
private companion object {
|
||||
|
||||
/**
|
||||
* The default CJK spacing ratio, adjusted to 7f.
|
||||
* This ratio is considered to be the most comfortable size for reading after a series of comparisons.
|
||||
*/
|
||||
private const val DEFAULT_CJK_SPACING_RATIO = 7f
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the [PanguText].
|
||||
*
|
||||
* This is a global switch that can be used to enable or disable the [PanguText] processor.
|
||||
*/
|
||||
var isEnabled = true
|
||||
|
||||
/**
|
||||
* Processed [Spanned] text (experimental).
|
||||
*
|
||||
* - Note: This feature is in experimental stage and may not be fully supported,
|
||||
* if the text is not processed correctly, please disable this feature.
|
||||
*/
|
||||
var isProcessedSpanned = true
|
||||
|
||||
/**
|
||||
* The regular expression for text content that needs to be excluded.
|
||||
* [PanguText] processing will be skipped after matching these texts.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* ```kotlin
|
||||
* val config: PanguTextConfig
|
||||
* // Exclude all URLs.
|
||||
* config.excludePatterns.add("https?://\\S+".toRegex())
|
||||
* // Exclude emoji symbol placeholder like "[doge]".
|
||||
* config.excludePatterns.add("\\[.*?]".toRegex())
|
||||
* ```
|
||||
*/
|
||||
val excludePatterns = mutableSetOf<Regex>()
|
||||
|
||||
/**
|
||||
* The CJK spacing ratio, default is [DEFAULT_CJK_SPACING_RATIO].
|
||||
*
|
||||
* The larger the value, the smaller the spacing, and cannot be less than 0.1f.
|
||||
*
|
||||
* It is recommended to adjust with caution, it will only affect the spacing of CJK characters.
|
||||
*/
|
||||
var cjkSpacingRatio = DEFAULT_CJK_SPACING_RATIO
|
||||
|
||||
/**
|
||||
* Copy the current configuration.
|
||||
* @param body the configuration body.
|
||||
* @return [PanguTextConfig]
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun copy(body: PanguTextConfig.() -> Unit = {}) = PanguTextConfig().also {
|
||||
it.isEnabled = this.isEnabled
|
||||
it.isProcessedSpanned = this.isProcessedSpanned
|
||||
it.excludePatterns.addAll(this.excludePatterns)
|
||||
it.cjkSpacingRatio = this.cjkSpacingRatio
|
||||
it.body()
|
||||
}
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/PanguText
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/1/14.
|
||||
*/
|
||||
package com.highcapable.pangutext.android.core
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.text.Spanned
|
||||
import android.text.TextPaint
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.CharacterStyle
|
||||
import android.text.style.ReplacementSpan
|
||||
import androidx.annotation.Px
|
||||
import androidx.core.text.getSpans
|
||||
import com.highcapable.betterandroid.ui.extension.component.base.toDp
|
||||
import com.highcapable.betterandroid.ui.extension.component.base.toPx
|
||||
import kotlin.math.round
|
||||
|
||||
/**
|
||||
* Pangu span with margin.
|
||||
* @param margin the margin size (px).
|
||||
*/
|
||||
internal class PanguMarginSpan(@Px val margin: Int) : ReplacementSpan() {
|
||||
|
||||
internal companion object {
|
||||
|
||||
/**
|
||||
* Create a new instance of [PanguMarginSpan].
|
||||
* @param resources the current resources.
|
||||
* @param textSize the text size (px).
|
||||
* @param ratio the CJK spacing ratio.
|
||||
* @return [PanguMarginSpan]
|
||||
*/
|
||||
internal fun create(resources: Resources, @Px textSize: Float, ratio: Float) =
|
||||
PanguMarginSpan(getSpanMargin(resources, textSize, ratio))
|
||||
|
||||
/**
|
||||
* Get the margin size (px).
|
||||
* @param resources the current resources.
|
||||
* @param textSize the text size (px).
|
||||
* @param ratio the CJK spacing ratio.
|
||||
* @return [Int]
|
||||
*/
|
||||
private fun getSpanMargin(resources: Resources, @Px textSize: Float, ratio: Float) =
|
||||
round(textSize.toDp(resources) / ratio.coerceAtLeast(0.1f)).toInt().toPx(resources)
|
||||
}
|
||||
|
||||
override fun getContentDescription() = "PanguMarginSpan"
|
||||
|
||||
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?) =
|
||||
(paint.measureText(text, start, end) + margin).toInt()
|
||||
|
||||
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
|
||||
if (text is Spanned) text.getSpans<Any>(start, end).forEach { span ->
|
||||
when {
|
||||
span is BackgroundColorSpan -> {
|
||||
// Get background color.
|
||||
val color = span.backgroundColor
|
||||
val originalColor = paint.color
|
||||
// Save the current [paint] color.
|
||||
paint.color = color
|
||||
// Get the width of the text.
|
||||
val textWidth = paint.measureText(text, start, end)
|
||||
// Draw background rectangle.
|
||||
canvas.drawRect(x, top.toFloat(), x + textWidth + margin, bottom.toFloat(), paint)
|
||||
// Restore original color.
|
||||
paint.color = originalColor
|
||||
}
|
||||
span is CharacterStyle && paint is TextPaint -> span.updateDrawState(paint)
|
||||
}
|
||||
}; text?.let { canvas.drawText(it, start, end, x, y.toFloat(), paint) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A placeholder span.
|
||||
*/
|
||||
class Placeholder : ReplacementSpan() {
|
||||
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?) = 0
|
||||
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {}
|
||||
}
|
||||
}
|
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/PanguText
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/1/20.
|
||||
*/
|
||||
@file:Suppress("RegExpRedundantEscape", "RegExpSimplifiable")
|
||||
|
||||
package com.highcapable.pangutext.android.core
|
||||
|
||||
import com.highcapable.pangutext.android.PanguText
|
||||
import com.highcapable.pangutext.android.extension.replaceAndPreserveSpans
|
||||
|
||||
/**
|
||||
* The regular expression patterns for [PanguText].
|
||||
*
|
||||
* Some schemes are copied from [Pangu.java](https://github.com/vinta/pangu.java/blob/master/src/main/java/ws/vinta/pangu/Pangu.java),
|
||||
* and some modifications have been made to adapt to the Android environment.
|
||||
*/
|
||||
internal object PanguPatterns {
|
||||
|
||||
private const val CJK = "\u2e80-\u2eff\u2f00-\u2fdf\u3040-\u309f\u30a0-\u30fa\u30fc-" +
|
||||
"\u30ff\u3100-\u312f\u3200-\u32ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff"
|
||||
|
||||
private val ANY_CJK = "[$CJK]".toRegex()
|
||||
|
||||
private val DOTS_CJK = "([\\.]{2,}|\\u2026)([$CJK])".toRegex()
|
||||
|
||||
private val FIX_CJK_COLON_ANS = "([$CJK])\\:([A-Z0-9\\(\\)])".toRegex()
|
||||
private val CJK_QUOTE = "([$CJK])([\\`\"\\u05f4])".toRegex()
|
||||
|
||||
private val QUOTE_CJK = "([\\`\"\\u05f4])([$CJK])".toRegex()
|
||||
private val FIX_QUOTE_ANY_QUOTE = "([`\"\\u05f4]+)[ ]*(.+?)[ ]*([`\"\\u05f4]+)".toRegex()
|
||||
private val CJK_SINGLE_QUOTE_BUT_POSSESSIVE = "([$CJK])('[^s])".toRegex()
|
||||
|
||||
private val SINGLE_QUOTE_CJK = "(')([$CJK])".toRegex()
|
||||
private val HASH_ANS_CJK_HASH = "([$CJK])(#)([$CJK]+)(#)([$CJK])".toRegex()
|
||||
|
||||
private val CJK_HASH = "([$CJK])(#([^ ]))".toRegex()
|
||||
private val HASH_CJK = "(([^ ])#)([$CJK])".toRegex()
|
||||
private val CJK_OPERATOR_ANS = "([$CJK])([\\+\\-\\*\\/=&\\|<>])([A-Za-z0-9])".toRegex()
|
||||
|
||||
private val ANS_OPERATOR_CJK = "([A-Za-z0-9])([\\+\\-\\*\\/=&\\|<>])([$CJK])".toRegex()
|
||||
private val FIX_SLASH_AS = "([/]) ([a-z\\-\\_\\./]+)".toRegex()
|
||||
|
||||
private val FIX_SLASH_AS_SLASH = "([/\\.])([A-Za-z\\-\\_\\./]+) ([/])".toRegex()
|
||||
private val CJK_LEFT_BRACKET = "([$CJK])([\\(\\[\\{<>\\u201c])".toRegex()
|
||||
|
||||
private val RIGHT_BRACKET_CJK = "([\\)\\]\\}>\\u201d])([$CJK])".toRegex()
|
||||
private val FIX_LEFT_BRACKET_ANY_RIGHT_BRACKET = "([\\(\\[\\{<\\u201c]+)[ ]*(.+?)[ ]*([\\)\\]\\}>\u201d]+)".toRegex()
|
||||
private val AN_LEFT_BRACKET = "([A-Za-z0-9])([\\(\\[\\{])".toRegex()
|
||||
|
||||
private val RIGHT_BRACKET_AN = "([\\)\\]\\}])([A-Za-z0-9])".toRegex()
|
||||
private val CJK_ANS = ("([$CJK])([A-Za-z\\u0370-\\u03ff0-9@\$%\\^&\\*\\-\\+\\\\=\\|" +
|
||||
"/\\u00a1-\\u00ff\\u2150-\\u218f\\u2700—\\u27bf])").toRegex()
|
||||
|
||||
private val ANS_CJK = ("([A-Za-z\\u0370-\\u03ff0-9~\\\$%\\^&\\*\\-\\+\\\\=\\|" +
|
||||
"/!;:,\\.\\?\\u00a1-\\u00ff\\u2150-\\u218f\\u2700—\\u27bf])([$CJK])").toRegex()
|
||||
private val S_A = "(%)([A-Za-z])".toRegex()
|
||||
|
||||
/**
|
||||
* Match and replace the text with the given regular expression.
|
||||
* @param text text to be formatted.
|
||||
* @param whiteSpace the spacing character.
|
||||
* @param excludePatterns the regular expression to exclude from replacement.
|
||||
* @return [CharSequence]
|
||||
*/
|
||||
internal fun matchAndReplace(text: CharSequence, whiteSpace: Char, vararg excludePatterns: Regex) =
|
||||
if (ANY_CJK.containsMatchIn(text))
|
||||
text.replaceAndPreserveSpans(DOTS_CJK, "$1$whiteSpace$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(FIX_CJK_COLON_ANS, "$1:$whiteSpace$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(CJK_QUOTE, "$1$whiteSpace$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(QUOTE_CJK, "$1$whiteSpace$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(FIX_QUOTE_ANY_QUOTE, "$1$2$3", *excludePatterns)
|
||||
.replaceAndPreserveSpans(CJK_SINGLE_QUOTE_BUT_POSSESSIVE, "$1$whiteSpace$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(SINGLE_QUOTE_CJK, "$1$whiteSpace$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(HASH_ANS_CJK_HASH, "$1$whiteSpace$2$3$4$whiteSpace$5", *excludePatterns)
|
||||
.replaceAndPreserveSpans(CJK_HASH, "$1$whiteSpace$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(HASH_CJK, "$1$whiteSpace$3", *excludePatterns)
|
||||
.replaceAndPreserveSpans(CJK_OPERATOR_ANS, "$1$whiteSpace$2$whiteSpace$3", *excludePatterns)
|
||||
.replaceAndPreserveSpans(ANS_OPERATOR_CJK, "$1$whiteSpace$2$whiteSpace$3", *excludePatterns)
|
||||
.replaceAndPreserveSpans(FIX_SLASH_AS, "$1$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(FIX_SLASH_AS_SLASH, "$1$2$3", *excludePatterns)
|
||||
.replaceAndPreserveSpans(CJK_LEFT_BRACKET, "$1$whiteSpace$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(RIGHT_BRACKET_CJK, "$1$whiteSpace$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(FIX_LEFT_BRACKET_ANY_RIGHT_BRACKET, "$1$2$3", *excludePatterns)
|
||||
.replaceAndPreserveSpans(AN_LEFT_BRACKET, "$1$whiteSpace$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(RIGHT_BRACKET_AN, "$1$whiteSpace$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(CJK_ANS, "$1$whiteSpace$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(ANS_CJK, "$1$whiteSpace$2", *excludePatterns)
|
||||
.replaceAndPreserveSpans(S_A, "$1$whiteSpace$2", *excludePatterns)
|
||||
else text
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/PanguText
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/2/6.
|
||||
*/
|
||||
package com.highcapable.pangutext.android.core
|
||||
|
||||
import com.highcapable.pangutext.android.PanguText
|
||||
import com.highcapable.pangutext.android.PanguTextConfig
|
||||
|
||||
/**
|
||||
* The [PanguText] config interface.
|
||||
*/
|
||||
interface PanguTextView {
|
||||
|
||||
/**
|
||||
* Configure the [PanguText].
|
||||
*
|
||||
* Configuring this item separately will override global settings.
|
||||
* @see PanguText.globalConfig
|
||||
*/
|
||||
fun configurePanguText(config: PanguTextConfig)
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/PanguText
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/1/26.
|
||||
*/
|
||||
package com.highcapable.pangutext.android.core
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.widget.TextView
|
||||
import com.highcapable.pangutext.android.PanguText
|
||||
import com.highcapable.pangutext.android.PanguTextConfig
|
||||
import com.highcapable.pangutext.android.extension.injectRealTimePanguText
|
||||
|
||||
/**
|
||||
* A [TextWatcher] that automatically applies [PanguText] to the text content.
|
||||
*
|
||||
* You don't need to create it manually, use [TextView.injectRealTimePanguText] instead.
|
||||
* @param base the base [TextView].
|
||||
* @param config the configuration of [PanguText].
|
||||
*/
|
||||
class PanguTextWatcher internal constructor(private val base: TextView, private val config: PanguTextConfig) : TextWatcher {
|
||||
override fun afterTextChanged(editable: Editable?) {
|
||||
editable?.let { PanguText.format(base.resources, base.textSize, it, config) }
|
||||
}
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
}
|
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/PanguText
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/1/12.
|
||||
*/
|
||||
@file:Suppress("unused")
|
||||
@file:JvmName("PanguTextUtils")
|
||||
|
||||
package com.highcapable.pangutext.android.extension
|
||||
|
||||
import android.util.Log
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.core.view.doOnDetach
|
||||
import com.highcapable.betterandroid.ui.extension.view.getTag
|
||||
import com.highcapable.pangutext.android.PanguText
|
||||
import com.highcapable.pangutext.android.PanguTextConfig
|
||||
import com.highcapable.pangutext.android.R
|
||||
import com.highcapable.pangutext.android.core.PanguTextWatcher
|
||||
import com.highcapable.pangutext.android.generated.PangutextAndroidProperties
|
||||
|
||||
/**
|
||||
* Create a new instance of [PanguTextConfig].
|
||||
* @see PanguTextConfig
|
||||
* @param copyFromGlobal whether to copy the [PanguText.globalConfig], default is true.
|
||||
* @param body the configuration body.
|
||||
* @return [PanguTextConfig]
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun PanguTextConfig(copyFromGlobal: Boolean = true, body: PanguTextConfig.() -> Unit) =
|
||||
if (copyFromGlobal) PanguText.globalConfig.copy(body) else PanguTextConfig().apply(body)
|
||||
|
||||
/**
|
||||
* Inject [PanguText] to the current text content once.
|
||||
* @see TextView.setTextWithPangu
|
||||
* @see TextView.setHintWithPangu
|
||||
* @see PanguText.format
|
||||
* @receiver [TextView]
|
||||
* @param injectHint whether to apply [TextView.setHint], default is true.
|
||||
* @param config the configuration of [PanguText].
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun TextView.injectPanguText(injectHint: Boolean = true, config: PanguTextConfig = PanguText.globalConfig) {
|
||||
if (!config.isEnabled) return
|
||||
setTextWithPangu(this.text, config)
|
||||
if (injectHint) setHintWithPangu(this.hint, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject [PanguText] to the current text content in real time.
|
||||
* @see TextView.setTextWithPangu
|
||||
* @see TextView.setHintWithPangu
|
||||
* @see PanguText.format
|
||||
* @receiver [TextView]
|
||||
* @param injectHint whether to apply [TextView.setHint], default is true.
|
||||
* @param config the configuration of [PanguText].
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun TextView.injectRealTimePanguText(injectHint: Boolean = true, config: PanguTextConfig = PanguText.globalConfig) {
|
||||
if (!config.isEnabled) return
|
||||
val observerKey = R.id.flag_inject_real_time_pangu_text
|
||||
if (getTag<Boolean>(observerKey) == true) return run {
|
||||
Log.w(PangutextAndroidProperties.PROJECT_NAME, "Duplicate injection of real-time PanguText ($this).")
|
||||
}
|
||||
setTag(observerKey, injectHint)
|
||||
injectPanguText(injectHint, config)
|
||||
var currentHint = this.hint
|
||||
val textWatcher = PanguTextWatcher(base = this, config)
|
||||
val listener = ViewTreeObserver.OnGlobalLayoutListener {
|
||||
val self = this@injectRealTimePanguText
|
||||
if (self.hint != currentHint)
|
||||
self.setHintWithPangu(self.hint, config)
|
||||
currentHint = self.hint
|
||||
}
|
||||
doOnAttach {
|
||||
addTextChangedListener(textWatcher)
|
||||
// Add a global layout listener to monitor the hint text changes.
|
||||
if (injectHint) viewTreeObserver?.addOnGlobalLayoutListener(listener)
|
||||
doOnDetach {
|
||||
removeTextChangedListener(textWatcher)
|
||||
// Remove the global layout listener when the view is detached.
|
||||
if (injectHint) viewTreeObserver?.removeOnGlobalLayoutListener(listener)
|
||||
setTag(observerKey, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use [PanguText.format] to format the text content.
|
||||
* @see PanguText.format
|
||||
* @receiver [TextView]
|
||||
* @param text the text content.
|
||||
* @param config the configuration of [PanguText].
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun TextView.setTextWithPangu(text: CharSequence?, config: PanguTextConfig = PanguText.globalConfig) {
|
||||
if (!config.isEnabled) return
|
||||
this.text = text?.let { PanguText.format(resources, textSize, it, config) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Use [PanguText.format] to format the hint text content.
|
||||
* @see PanguText.format
|
||||
* @receiver [TextView]
|
||||
* @param text the text content.
|
||||
* @param config the configuration of [PanguText].
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun TextView.setHintWithPangu(text: CharSequence?, config: PanguTextConfig = PanguText.globalConfig) {
|
||||
if (!config.isEnabled) return
|
||||
this.hint = text?.let { PanguText.format(resources, textSize, it, config) }
|
||||
}
|
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/PanguText
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/1/16.
|
||||
*/
|
||||
@file:JvmName("ReplacementUtils")
|
||||
|
||||
package com.highcapable.pangutext.android.extension
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.util.Log
|
||||
import com.highcapable.pangutext.android.generated.PangutextAndroidProperties
|
||||
import com.highcapable.yukireflection.factory.classOf
|
||||
import com.highcapable.yukireflection.factory.field
|
||||
import java.util.regex.Matcher
|
||||
|
||||
/**
|
||||
* Replace the text content and preserve the original span style.
|
||||
* @see CharSequence.replace
|
||||
* @receiver [CharSequence]
|
||||
* @param regex the regular expression.
|
||||
* @param replacement the replacement text.
|
||||
* @param excludePatterns the regular expression to exclude from replacement, default is null.
|
||||
* @return [CharSequence]
|
||||
*/
|
||||
internal fun CharSequence.replaceAndPreserveSpans(regex: Regex, replacement: String, vararg excludePatterns: Regex) =
|
||||
runCatching {
|
||||
val builder = SpannableStringBuilder(this)
|
||||
val matcher = regex.toPattern().matcher(this)
|
||||
val excludeMatchers = excludePatterns.map { it.toPattern().matcher(this) }
|
||||
val excludeIndexs = mutableSetOf<Pair<Int, Int>>()
|
||||
excludeMatchers.forEach {
|
||||
while (it.find()) excludeIndexs.add(it.start() to it.end())
|
||||
}
|
||||
var offset = 0
|
||||
// Offset adjustment to account for changes in the text length after replacements.
|
||||
while (matcher.find()) {
|
||||
val start = matcher.start() + offset
|
||||
val end = matcher.end() + offset
|
||||
// Skip the replacement if the matched range is excluded.
|
||||
// The character range offset is adjusted by 1 to avoid the exclusion of the matched range.
|
||||
if (excludeIndexs.any { it.first <= start + 1 && it.second >= end - 1 }) continue
|
||||
// Perform the replacement.
|
||||
val replacementText = matcher.buildReplacementText(replacement)
|
||||
builder.replace(start, end, replacementText)
|
||||
// Adjust offset based on the length of the replacement.
|
||||
offset += replacementText.length - (end - start)
|
||||
}; builder
|
||||
}.onFailure {
|
||||
Log.w(PangutextAndroidProperties.PROJECT_NAME, "Failed to replace span text content.", it)
|
||||
}.getOrNull() ?: this
|
||||
|
||||
/**
|
||||
* Build the replacement text based on the matched groups.
|
||||
* @receiver [Matcher]
|
||||
* @param replacement the replacement text.
|
||||
* @return [String]
|
||||
*/
|
||||
private fun Matcher.buildReplacementText(replacement: String): String {
|
||||
val matcher = this
|
||||
var result = replacement
|
||||
// Check for group references (like $1, $2, ...).
|
||||
val pattern = "\\$(\\d+)".toRegex()
|
||||
result = pattern.replace(result) { matchResult ->
|
||||
val groupIndex = matchResult.groupValues[1].toInt()
|
||||
if (groupIndex <= matcher.groupCount())
|
||||
matcher.group(groupIndex) ?: ""
|
||||
else ""
|
||||
}
|
||||
// Check for named groups (like ${groupName}).
|
||||
val namedGroupPattern = "\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}".toRegex()
|
||||
result = namedGroupPattern.replace(result) { matchResult ->
|
||||
val groupName = matchResult.groupValues[1]
|
||||
val groupIndex = matcher.getNamedGroupIndex(groupName)
|
||||
if (groupIndex >= 0)
|
||||
matcher.group(groupIndex) ?: ""
|
||||
else ""
|
||||
}; return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to find the group index for a named group.
|
||||
* @receiver [Matcher]
|
||||
* @param groupName the group name.
|
||||
* @return [Int]
|
||||
*/
|
||||
private fun Matcher.getNamedGroupIndex(groupName: String): Int {
|
||||
val namedGroups = classOf<Matcher>()
|
||||
.field { name = "namedGroups" }
|
||||
.ignored()
|
||||
.get(this)
|
||||
.cast<Map<String, Int>>()
|
||||
return namedGroups?.get(groupName) ?: -1
|
||||
}
|
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/PanguText
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/1/19.
|
||||
*/
|
||||
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package com.highcapable.pangutext.android.factory
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import com.highcapable.betterandroid.ui.extension.view.layoutInflater
|
||||
import com.highcapable.pangutext.android.generated.PangutextAndroidProperties
|
||||
import com.highcapable.yukireflection.factory.field
|
||||
import com.highcapable.yukireflection.type.android.LayoutInflaterClass
|
||||
|
||||
/**
|
||||
* Pangu text factory 2 for [LayoutInflater.Factory2].
|
||||
* @param base the base factory.
|
||||
*/
|
||||
class PanguTextFactory2 private constructor(private val base: LayoutInflater.Factory2?) : LayoutInflater.Factory2 {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Inject [PanguTextFactory2] to the current [LayoutInflater] of [context].
|
||||
*
|
||||
* Simple Usage:
|
||||
*
|
||||
* ```kotlin
|
||||
* class MainActivity : AppCompactActivity() {
|
||||
*
|
||||
* val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
|
||||
*
|
||||
* override fun onCreate(savedInstanceState: Bundle?) {
|
||||
* super.onCreate(savedInstanceState)
|
||||
* // Inject here.
|
||||
* PanguTextFactory2.inject(this)
|
||||
* setContentView(binding.root)
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Traditional Usage:
|
||||
*
|
||||
* ```kotlin
|
||||
* class MainActivity : Activity() {
|
||||
*
|
||||
* override fun onCreate(savedInstanceState: Bundle?) {
|
||||
* super.onCreate(savedInstanceState)
|
||||
* // Inject here.
|
||||
* PanguTextFactory2.inject(this)
|
||||
* setContentView(R.layout.activity_main)
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Usage with BetterAndroid's AppBindingActivity:
|
||||
*
|
||||
* ```kotlin
|
||||
* class MainActivity : AppBindingActivity<ActivityMainBinding>() {
|
||||
*
|
||||
* override fun onPrepareContentView(savedInstanceState: Bundle?): LayoutInflater {
|
||||
* val inflater = super.onPrepareContentView(savedInstanceState)
|
||||
* // Inject here.
|
||||
* PanguTextFactory2.inject(inflater)
|
||||
* return inflater
|
||||
* }
|
||||
*
|
||||
* override fun onCreate(savedInstanceState: Bundle?) {
|
||||
* super.onCreate(savedInstanceState)
|
||||
* // Your code here.
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
* @param context the current context.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun inject(context: Context) = inject(context.layoutInflater)
|
||||
|
||||
/**
|
||||
* Inject [PanguTextFactory2] to the current [LayoutInflater].
|
||||
* @see inject
|
||||
* @param inflater the current inflater.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun inject(inflater: LayoutInflater) {
|
||||
val original = inflater.factory2
|
||||
if (original is PanguTextFactory2) return run {
|
||||
Log.w(PangutextAndroidProperties.PROJECT_NAME, "PanguTextFactory2 was already injected.")
|
||||
}
|
||||
val replacement = PanguTextFactory2(original)
|
||||
if (original != null)
|
||||
LayoutInflaterClass.field {
|
||||
name = "mFactory2"
|
||||
}.ignored().onNoSuchField {
|
||||
Log.e(PangutextAndroidProperties.PROJECT_NAME, "LayoutInflater.mFactory2 not found.", it)
|
||||
}.get(inflater).set(replacement)
|
||||
else inflater.factory2 = replacement
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet) =
|
||||
base?.onCreateView(parent, name, context, attrs).let {
|
||||
PanguWidget.process(name, it, context, attrs) ?: it
|
||||
}
|
||||
|
||||
override fun onCreateView(name: String, context: Context, attrs: AttributeSet) =
|
||||
base?.onCreateView(name, context, attrs).let {
|
||||
PanguWidget.process(name, it, context, attrs) ?: it
|
||||
}
|
||||
}
|
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* PanguText - A typographic solution for the optimal alignment of CJK characters, English words, and half-width digits.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/PanguText
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/1/19.
|
||||
*/
|
||||
package com.highcapable.pangutext.android.factory
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.doOnAttach
|
||||
import com.highcapable.betterandroid.ui.extension.component.base.getBooleanOrNull
|
||||
import com.highcapable.betterandroid.ui.extension.component.base.getFloatOrNull
|
||||
import com.highcapable.betterandroid.ui.extension.component.base.getStringOrNull
|
||||
import com.highcapable.betterandroid.ui.extension.component.base.obtainStyledAttributes
|
||||
import com.highcapable.pangutext.android.PanguText
|
||||
import com.highcapable.pangutext.android.PanguTextConfig
|
||||
import com.highcapable.pangutext.android.R
|
||||
import com.highcapable.pangutext.android.core.PanguTextView
|
||||
import com.highcapable.pangutext.android.extension.injectPanguText
|
||||
import com.highcapable.pangutext.android.extension.injectRealTimePanguText
|
||||
import com.highcapable.pangutext.android.generated.PangutextAndroidProperties
|
||||
import com.highcapable.yukireflection.factory.classOf
|
||||
import com.highcapable.yukireflection.factory.constructor
|
||||
import com.highcapable.yukireflection.factory.notExtends
|
||||
import com.highcapable.yukireflection.factory.toClassOrNull
|
||||
import com.highcapable.yukireflection.type.android.AttributeSetClass
|
||||
import com.highcapable.yukireflection.type.android.ContextClass
|
||||
|
||||
/**
|
||||
* A widgets processor that automatically applies [PanguText] to the text content.
|
||||
*/
|
||||
internal object PanguWidget {
|
||||
|
||||
/** The text regex split symbol. */
|
||||
private const val TEXT_REGEX_SPLITE_SYMBOL = "|@|"
|
||||
|
||||
/**
|
||||
* Process the widget by the given name.
|
||||
* @param name the widget name.
|
||||
* @param view the current view.
|
||||
* @param context the context.
|
||||
* @param attrs the attributes.
|
||||
* @return [View] or null.
|
||||
*/
|
||||
fun process(name: String, view: View?, context: Context, attrs: AttributeSet): View? {
|
||||
val instance = view ?: name.let {
|
||||
// There will be commonly used view class names in the XML layout, which is converted here.
|
||||
if (!it.contains(".")) "android.widget.$it" else it
|
||||
}.toClassOrNull()?.let {
|
||||
// Avoid creating unnecessary components for waste.
|
||||
if (it notExtends classOf<TextView>()) return null
|
||||
val twoParams = it.constructor {
|
||||
param(ContextClass, AttributeSetClass)
|
||||
}.ignored().get()
|
||||
val onceParam = it.constructor {
|
||||
param(ContextClass)
|
||||
}.ignored().get()
|
||||
twoParams.newInstance<View>(context, attrs) ?: onceParam.newInstance<View>(context)
|
||||
}
|
||||
// Ignore if the instance is not a [TextView].
|
||||
if (instance !is TextView) return null
|
||||
var config = PanguText.globalConfig
|
||||
if (instance is PanguTextView) {
|
||||
val configCopy = config.copy()
|
||||
instance.configurePanguText(configCopy)
|
||||
config = configCopy
|
||||
if (!config.isEnabled) return instance
|
||||
} else instance.obtainStyledAttributes(attrs, R.styleable.PanguTextHelper) {
|
||||
val isEnabled = it.getBooleanOrNull(R.styleable.PanguTextHelper_panguText_enabled)
|
||||
val isProcessedSpanned = it.getBooleanOrNull(R.styleable.PanguTextHelper_panguText_processedSpanned)
|
||||
val cjkSpacingRatio = it.getFloatOrNull(R.styleable.PanguTextHelper_panguText_cjkSpacingRatio)
|
||||
val excludePatterns = it.getStringOrNull(R.styleable.PanguTextHelper_panguText_excludePatterns)
|
||||
?.split(TEXT_REGEX_SPLITE_SYMBOL)?.mapNotNull { regex ->
|
||||
runCatching { regex.toRegex() }.onFailure { th ->
|
||||
Log.e(PangutextAndroidProperties.PROJECT_NAME, "Invalid exclude pattern of $instance: $regex", th)
|
||||
}.getOrNull()
|
||||
}?.toTypedArray() ?: emptyArray()
|
||||
if (isEnabled == false) return instance
|
||||
if (isProcessedSpanned != null || cjkSpacingRatio != null || excludePatterns.isNotEmpty()) {
|
||||
val configCopy = config.copy()
|
||||
configCopy.isProcessedSpanned = isProcessedSpanned ?: config.isProcessedSpanned
|
||||
configCopy.cjkSpacingRatio = cjkSpacingRatio ?: config.cjkSpacingRatio
|
||||
if (excludePatterns.isNotEmpty()) {
|
||||
config.excludePatterns.clear()
|
||||
config.excludePatterns.addAll(excludePatterns)
|
||||
}; config = configCopy
|
||||
}
|
||||
}
|
||||
when (instance.javaClass.name) {
|
||||
// Specialize those components because loading "hint" style after [doOnAttachRepeatable] causes problems.
|
||||
"com.google.android.material.textfield.TextInputEditText",
|
||||
"com.google.android.material.textfield.MaterialAutoCompleteTextView" -> {
|
||||
instance.injectPanguText(config = config)
|
||||
instance.doOnAttachRepeatable(config) { it.injectRealTimePanguText(injectHint = false, config) }
|
||||
}
|
||||
else -> instance.doOnAttachRepeatable(config) {
|
||||
it.injectRealTimePanguText(config = config)
|
||||
}
|
||||
}; return instance
|
||||
}
|
||||
|
||||
/** Copied from [View.doOnAttach]. */
|
||||
private inline fun <reified V : View> V.doOnAttachRepeatable(config: PanguTextConfig, crossinline action: (view: V) -> Unit) {
|
||||
if (!config.isEnabled) return
|
||||
if (isAttachedToWindow) action(this)
|
||||
addOnAttachStateChangeListener(
|
||||
object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(view: View) {
|
||||
// Re-execute it every time to prevent layout re-creation problems
|
||||
// similar to [RecyclerView.Adapter] or [BaseAdapter] after reuse.
|
||||
if (config.isEnabled) action(view as V)
|
||||
}
|
||||
override fun onViewDetachedFromWindow(view: View) {}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
13
pangutext-android/src/main/res/values/attrs.xml
Normal file
13
pangutext-android/src/main/res/values/attrs.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="PanguTextHelper">
|
||||
<!-- Enable [PanguText] for this view. -->
|
||||
<attr name="panguText_enabled" format="boolean" />
|
||||
<!-- Processed [Spanned] text (experimental). -->
|
||||
<attr name="panguText_processedSpanned" format="boolean" />
|
||||
<!-- The regular expression for text content that needs to be excluded. -->
|
||||
<attr name="panguText_excludePatterns" format="string" />
|
||||
<!-- The CJK spacing ratio. -->
|
||||
<attr name="panguText_cjkSpacingRatio" format="float" />
|
||||
</declare-styleable>
|
||||
</resources>
|
4
pangutext-android/src/main/res/values/ids.xml
Normal file
4
pangutext-android/src/main/res/values/ids.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<id name="flag_inject_real_time_pangu_text" type="id" />
|
||||
</resources>
|
@@ -0,0 +1,18 @@
|
||||
package com.highcapable.pangutext
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user