mirror of
https://github.com/BetterAndroid/PanguText.git
synced 2025-09-04 01:35:37 +08:00
refactor: merge to BetterAndroid new usage
This commit is contained in:
@@ -42,6 +42,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.pangutextAndroid)
|
implementation(projects.pangutextAndroid)
|
||||||
implementation(com.highcapable.betterandroid.ui.component)
|
implementation(com.highcapable.betterandroid.ui.component)
|
||||||
|
implementation(com.highcapable.betterandroid.ui.component.adapter)
|
||||||
implementation(com.highcapable.betterandroid.ui.extension)
|
implementation(com.highcapable.betterandroid.ui.extension)
|
||||||
implementation(com.highcapable.betterandroid.system.extension)
|
implementation(com.highcapable.betterandroid.system.extension)
|
||||||
implementation(androidx.core.core.ktx)
|
implementation(androidx.core.core.ktx)
|
||||||
|
@@ -35,6 +35,7 @@ class ListActivity : BaseActivity<ActivityListBinding>() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
binding.recyclerView.bindAdapter<String> {
|
binding.recyclerView.bindAdapter<String> {
|
||||||
onBindData { listData }
|
onBindData { listData }
|
||||||
onBindItemView<AdapterListBinding> { binding, text, _ ->
|
onBindItemView<AdapterListBinding> { binding, text, _ ->
|
||||||
|
@@ -46,9 +46,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
binding.root.handleOnWindowInsetsChanged(animated = true) { linearLayout, insetsWrapper ->
|
binding.root.handleOnWindowInsetsChanged(animated = true) { linearLayout, insetsWrapper ->
|
||||||
linearLayout.setInsetsPadding(insetsWrapper.safeDrawing)
|
linearLayout.setInsetsPadding(insetsWrapper.safeDrawing)
|
||||||
}
|
}
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
binding.textViewPanguText,
|
binding.textViewPanguText,
|
||||||
binding.textViewPanguTextCjkSpacingRatio,
|
binding.textViewPanguTextCjkSpacingRatio,
|
||||||
|
@@ -32,6 +32,7 @@ open class BaseActivity<VB : ViewBinding> : AppBindingActivity<VB>() {
|
|||||||
override fun onPrepareContentView(savedInstanceState: Bundle?): LayoutInflater {
|
override fun onPrepareContentView(savedInstanceState: Bundle?): LayoutInflater {
|
||||||
val inflater = super.onPrepareContentView(savedInstanceState)
|
val inflater = super.onPrepareContentView(savedInstanceState)
|
||||||
PanguTextFactory2.inject(inflater)
|
PanguTextFactory2.inject(inflater)
|
||||||
|
|
||||||
return inflater
|
return inflater
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -31,11 +31,13 @@ plugins:
|
|||||||
libraries:
|
libraries:
|
||||||
com.highcapable.betterandroid:
|
com.highcapable.betterandroid:
|
||||||
ui-component:
|
ui-component:
|
||||||
version: 1.0.7
|
version: 1.0.8
|
||||||
|
ui-component-adapter:
|
||||||
|
version: 1.0.0
|
||||||
ui-extension:
|
ui-extension:
|
||||||
version: 1.0.6
|
version: 1.0.7
|
||||||
system-extension:
|
system-extension:
|
||||||
version: 1.0.2
|
version: 1.0.3
|
||||||
com.highcapable.kavaref:
|
com.highcapable.kavaref:
|
||||||
kavaref-core:
|
kavaref-core:
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
|
@@ -88,6 +88,7 @@ object PanguText {
|
|||||||
fun format(resources: Resources, @Px textSize: Float, text: CharSequence, config: PanguTextConfig = globalConfig): CharSequence {
|
fun format(resources: Resources, @Px textSize: Float, text: CharSequence, config: PanguTextConfig = globalConfig): CharSequence {
|
||||||
if (!config.isEnabled) return text
|
if (!config.isEnabled) return text
|
||||||
if (text.isBlank()) return text
|
if (text.isBlank()) return text
|
||||||
|
|
||||||
val formatted = format(text, PH, config)
|
val formatted = format(text, PH, config)
|
||||||
return text.applySpans(formatted, resources, textSize, config)
|
return text.applySpans(formatted, resources, textSize, config)
|
||||||
}
|
}
|
||||||
@@ -110,9 +111,11 @@ object PanguText {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun format(text: CharSequence, whiteSpace: Char = ' ', config: PanguTextConfig = globalConfig): CharSequence {
|
fun format(text: CharSequence, whiteSpace: Char = ' ', config: PanguTextConfig = globalConfig): CharSequence {
|
||||||
if (!config.isEnabled) return text
|
if (!config.isEnabled) return text
|
||||||
|
|
||||||
// In any case, always perform a cleanup operation before accepting text.
|
// In any case, always perform a cleanup operation before accepting text.
|
||||||
val processed = text.clearSpans()
|
val processed = text.clearSpans()
|
||||||
val patterns = config.excludePatterns.toTypedArray()
|
val patterns = config.excludePatterns.toTypedArray()
|
||||||
|
|
||||||
return if ((config.isProcessedSpanned || text !is Spanned) && text.isNotBlank() && text.length > 1)
|
return if ((config.isProcessedSpanned || text !is Spanned) && text.isNotBlank() && text.length > 1)
|
||||||
PanguPatterns.matchAndReplace(processed, whiteSpace, *patterns)
|
PanguPatterns.matchAndReplace(processed, whiteSpace, *patterns)
|
||||||
else processed
|
else processed
|
||||||
@@ -136,6 +139,7 @@ object PanguText {
|
|||||||
whiteSpace: Char = PH
|
whiteSpace: Char = PH
|
||||||
): CharSequence {
|
): CharSequence {
|
||||||
val builder = SpannableStringBuilder(formatted)
|
val builder = SpannableStringBuilder(formatted)
|
||||||
|
|
||||||
formatted.forEachIndexed { index, c ->
|
formatted.forEachIndexed { index, c ->
|
||||||
// Add spacing to the previous character.
|
// Add spacing to the previous character.
|
||||||
if (c == whiteSpace && index in 0..formatted.lastIndex) {
|
if (c == whiteSpace && index in 0..formatted.lastIndex) {
|
||||||
@@ -143,20 +147,26 @@ object PanguText {
|
|||||||
builder.setSpan(span, index - 1, index, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
builder.setSpan(span, index - 1, index, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the placeholder character.
|
// Delete the placeholder character.
|
||||||
for (i in (builder.length - 1) downTo 0) {
|
for (i in (builder.length - 1) downTo 0) {
|
||||||
if (builder[i] == whiteSpace) builder.delete(i, i + 1)
|
if (builder[i] == whiteSpace) builder.delete(i, i + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the [PanguMarginSpan.Placeholder] subscript in [builder] and use [PanguMarginSpan] to set it to [original].
|
// 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 builderSpans = builder.getSpans(0, builder.length, classOf<PanguMarginSpan.Placeholder>())
|
||||||
val spannable = if (this !is Spannable) SpannableString(this) else this
|
val spannable = if (this !is Spannable) SpannableString(this) else this
|
||||||
|
|
||||||
// Add new [PanguMarginSpan].
|
// Add new [PanguMarginSpan].
|
||||||
builderSpans.forEach {
|
builderSpans.forEach {
|
||||||
val start = builder.getSpanStart(it)
|
val start = builder.getSpanStart(it)
|
||||||
val end = builder.getSpanEnd(it)
|
val end = builder.getSpanEnd(it)
|
||||||
|
|
||||||
val span = PanguMarginSpan.create(resources, textSize, config.cjkSpacingRatio)
|
val span = PanguMarginSpan.create(resources, textSize, config.cjkSpacingRatio)
|
||||||
spannable.setSpan(span, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(span, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||||
}; builder.clear()
|
}
|
||||||
|
|
||||||
|
builder.clear()
|
||||||
return spannable
|
return spannable
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,12 +180,16 @@ object PanguText {
|
|||||||
*/
|
*/
|
||||||
private fun CharSequence.clearSpans(): CharSequence {
|
private fun CharSequence.clearSpans(): CharSequence {
|
||||||
if (this !is Spannable || isBlank() || !hasSpan<PanguMarginSpan>()) return this
|
if (this !is Spannable || isBlank() || !hasSpan<PanguMarginSpan>()) return this
|
||||||
|
|
||||||
getSpans(0, length, classOf<PanguMarginSpan>()).forEach { span ->
|
getSpans(0, length, classOf<PanguMarginSpan>()).forEach { span ->
|
||||||
val start = getSpanStart(span)
|
val start = getSpanStart(span)
|
||||||
val end = getSpanEnd(span)
|
val end = getSpanEnd(span)
|
||||||
|
|
||||||
// Clear the [PanguMarginSpan].
|
// Clear the [PanguMarginSpan].
|
||||||
if (start < length && end > 0) removeSpan(span)
|
if (start < length && end > 0) removeSpan(span)
|
||||||
}; return this
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,6 +200,7 @@ object PanguText {
|
|||||||
private inline fun <reified T : CharacterStyle> CharSequence.hasSpan(): Boolean {
|
private inline fun <reified T : CharacterStyle> CharSequence.hasSpan(): Boolean {
|
||||||
val spannable = this as? Spanned ?: return false
|
val spannable = this as? Spanned ?: return false
|
||||||
val spans = spannable.getSpans(0, spannable.length, classOf<T>())
|
val spans = spannable.getSpans(0, spannable.length, classOf<T>())
|
||||||
|
|
||||||
return spans.isNotEmpty()
|
return spans.isNotEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -76,18 +76,24 @@ internal class PanguMarginSpan(@Px val margin: Int) : ReplacementSpan() {
|
|||||||
// Get background color.
|
// Get background color.
|
||||||
val color = span.backgroundColor
|
val color = span.backgroundColor
|
||||||
val originalColor = paint.color
|
val originalColor = paint.color
|
||||||
|
|
||||||
// Save the current [paint] color.
|
// Save the current [paint] color.
|
||||||
paint.color = color
|
paint.color = color
|
||||||
|
|
||||||
// Get the width of the text.
|
// Get the width of the text.
|
||||||
val textWidth = paint.measureText(text, start, end)
|
val textWidth = paint.measureText(text, start, end)
|
||||||
|
|
||||||
// Draw background rectangle.
|
// Draw background rectangle.
|
||||||
canvas.drawRect(x, top.toFloat(), x + textWidth + margin, bottom.toFloat(), paint)
|
canvas.drawRect(x, top.toFloat(), x + textWidth + margin, bottom.toFloat(), paint)
|
||||||
|
|
||||||
// Restore original color.
|
// Restore original color.
|
||||||
paint.color = originalColor
|
paint.color = originalColor
|
||||||
}
|
}
|
||||||
span is CharacterStyle && paint is TextPaint -> span.updateDrawState(paint)
|
span is CharacterStyle && paint is TextPaint -> span.updateDrawState(paint)
|
||||||
}
|
}
|
||||||
}; text?.let { canvas.drawText(it, start, end, x, y.toFloat(), paint) }
|
}
|
||||||
|
|
||||||
|
text?.let { canvas.drawText(it, start, end, x, y.toFloat(), paint) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -25,7 +25,7 @@ import android.text.Editable
|
|||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.highcapable.betterandroid.system.extension.tool.SystemVersion
|
import com.highcapable.betterandroid.system.extension.tool.AndroidVersion
|
||||||
import com.highcapable.kavaref.KavaRef.Companion.asResolver
|
import com.highcapable.kavaref.KavaRef.Companion.asResolver
|
||||||
import com.highcapable.pangutext.android.PanguText
|
import com.highcapable.pangutext.android.PanguText
|
||||||
import com.highcapable.pangutext.android.PanguTextConfig
|
import com.highcapable.pangutext.android.PanguTextConfig
|
||||||
@@ -56,21 +56,24 @@ class PanguTextWatcher internal constructor(private val base: TextView, private
|
|||||||
*/
|
*/
|
||||||
private val isAutoRemeasureText
|
private val isAutoRemeasureText
|
||||||
get() = config.isAutoRemeasureText && base !is EditText && (base.maxLines == 1 ||
|
get() = config.isAutoRemeasureText && base !is EditText && (base.maxLines == 1 ||
|
||||||
SystemVersion.require(SystemVersion.Q, base.maxLines == 1) { base.isSingleLine })
|
AndroidVersion.require(AndroidVersion.Q, base.maxLines == 1) { base.isSingleLine })
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable?) {
|
override fun afterTextChanged(editable: Editable?) {
|
||||||
editable?.let { PanguText.format(base.resources, base.textSize, it, config) }
|
editable?.let { PanguText.format(base.resources, base.textSize, it, config) }
|
||||||
if (!isAutoRemeasureText) return
|
if (!isAutoRemeasureText) return
|
||||||
|
|
||||||
val currentWatchers = mutableListOf<TextWatcher>()
|
val currentWatchers = mutableListOf<TextWatcher>()
|
||||||
textWatchers?.also {
|
textWatchers?.also {
|
||||||
currentWatchers.addAll(it)
|
currentWatchers.addAll(it)
|
||||||
// Avoid triggering events again during processing.
|
// Avoid triggering events again during processing.
|
||||||
it.clear()
|
it.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the text to trigger remeasurement.
|
// Reset the text to trigger remeasurement.
|
||||||
base.text = editable
|
base.text = editable
|
||||||
// Re-add to continue listening to text changes.
|
// Re-add to continue listening to text changes.
|
||||||
textWatchers?.addAll(currentWatchers)
|
textWatchers?.addAll(currentWatchers)
|
||||||
|
|
||||||
currentWatchers.clear()
|
currentWatchers.clear()
|
||||||
}
|
}
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
@@ -57,6 +57,7 @@ fun PanguTextConfig(copyFromGlobal: Boolean = true, body: PanguTextConfig.() ->
|
|||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun TextView.injectPanguText(injectHint: Boolean = true, config: PanguTextConfig = PanguText.globalConfig) {
|
fun TextView.injectPanguText(injectHint: Boolean = true, config: PanguTextConfig = PanguText.globalConfig) {
|
||||||
if (!config.isEnabled) return
|
if (!config.isEnabled) return
|
||||||
|
|
||||||
setTextWithPangu(this.text, config)
|
setTextWithPangu(this.text, config)
|
||||||
if (injectHint) setHintWithPangu(this.hint, config)
|
if (injectHint) setHintWithPangu(this.hint, config)
|
||||||
}
|
}
|
||||||
@@ -75,11 +76,15 @@ fun TextView.injectPanguText(injectHint: Boolean = true, config: PanguTextConfig
|
|||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun TextView.injectRealTimePanguText(injectHint: Boolean = true, config: PanguTextConfig = PanguText.globalConfig) {
|
fun TextView.injectRealTimePanguText(injectHint: Boolean = true, config: PanguTextConfig = PanguText.globalConfig) {
|
||||||
if (!config.isEnabled) return
|
if (!config.isEnabled) return
|
||||||
|
|
||||||
val observerKey = R.id.tag_inject_real_time_pangu_text
|
val observerKey = R.id.tag_inject_real_time_pangu_text
|
||||||
val isRepeated = getTag<Boolean>(observerKey) == true
|
val isRepeated = getTag<Boolean>(observerKey) == true
|
||||||
|
|
||||||
// It will no longer be executed if it exceeds one time.
|
// It will no longer be executed if it exceeds one time.
|
||||||
if (isRepeated) return
|
if (isRepeated) return
|
||||||
|
|
||||||
injectPanguText(injectHint, config)
|
injectPanguText(injectHint, config)
|
||||||
|
|
||||||
var currentHint = this.hint
|
var currentHint = this.hint
|
||||||
val textWatcher = PanguTextWatcher(base = this, config)
|
val textWatcher = PanguTextWatcher(base = this, config)
|
||||||
val listener = ViewTreeObserver.OnGlobalLayoutListener {
|
val listener = ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
@@ -88,13 +93,17 @@ fun TextView.injectRealTimePanguText(injectHint: Boolean = true, config: PanguTe
|
|||||||
self.setHintWithPangu(self.hint, config)
|
self.setHintWithPangu(self.hint, config)
|
||||||
currentHint = self.hint
|
currentHint = self.hint
|
||||||
}
|
}
|
||||||
|
|
||||||
setTag(observerKey, true)
|
setTag(observerKey, true)
|
||||||
doOnAttach {
|
doOnAttach {
|
||||||
addTextChangedListener(textWatcher)
|
addTextChangedListener(textWatcher)
|
||||||
|
|
||||||
// Add a global layout listener to monitor the hint text changes.
|
// Add a global layout listener to monitor the hint text changes.
|
||||||
if (injectHint) viewTreeObserver?.addOnGlobalLayoutListener(listener)
|
if (injectHint) viewTreeObserver?.addOnGlobalLayoutListener(listener)
|
||||||
|
|
||||||
doOnDetach {
|
doOnDetach {
|
||||||
removeTextChangedListener(textWatcher)
|
removeTextChangedListener(textWatcher)
|
||||||
|
|
||||||
// Remove the global layout listener when the view is detached.
|
// Remove the global layout listener when the view is detached.
|
||||||
if (injectHint) viewTreeObserver?.removeOnGlobalLayoutListener(listener)
|
if (injectHint) viewTreeObserver?.removeOnGlobalLayoutListener(listener)
|
||||||
setTag(observerKey, false)
|
setTag(observerKey, false)
|
||||||
@@ -112,7 +121,10 @@ fun TextView.injectRealTimePanguText(injectHint: Boolean = true, config: PanguTe
|
|||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun TextView.setTextWithPangu(text: CharSequence?, config: PanguTextConfig = PanguText.globalConfig) {
|
fun TextView.setTextWithPangu(text: CharSequence?, config: PanguTextConfig = PanguText.globalConfig) {
|
||||||
if (!config.isEnabled) return
|
if (!config.isEnabled) return
|
||||||
this.text = text?.let { PanguText.format(resources, textSize, it, config) }
|
|
||||||
|
this.text = text?.let {
|
||||||
|
PanguText.format(resources, textSize, it, config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,5 +137,8 @@ fun TextView.setTextWithPangu(text: CharSequence?, config: PanguTextConfig = Pan
|
|||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun TextView.setHintWithPangu(text: CharSequence?, config: PanguTextConfig = PanguText.globalConfig) {
|
fun TextView.setHintWithPangu(text: CharSequence?, config: PanguTextConfig = PanguText.globalConfig) {
|
||||||
if (!config.isEnabled) return
|
if (!config.isEnabled) return
|
||||||
this.hint = text?.let { PanguText.format(resources, textSize, it, config) }
|
|
||||||
|
this.hint = text?.let {
|
||||||
|
PanguText.format(resources, textSize, it, config)
|
||||||
|
}
|
||||||
}
|
}
|
@@ -41,26 +41,36 @@ import java.util.regex.Matcher
|
|||||||
internal fun CharSequence.replaceAndPreserveSpans(regex: Regex, replacement: String, vararg excludePatterns: Regex) =
|
internal fun CharSequence.replaceAndPreserveSpans(regex: Regex, replacement: String, vararg excludePatterns: Regex) =
|
||||||
runCatching {
|
runCatching {
|
||||||
val builder = SpannableStringBuilder(this)
|
val builder = SpannableStringBuilder(this)
|
||||||
|
|
||||||
val matcher = regex.toPattern().matcher(this)
|
val matcher = regex.toPattern().matcher(this)
|
||||||
val excludeMatchers = excludePatterns.map { it.toPattern().matcher(this) }
|
val excludeMatchers = excludePatterns.map { it.toPattern().matcher(this) }
|
||||||
val excludeIndexs = mutableSetOf<Pair<Int, Int>>()
|
val excludeIndexs = mutableSetOf<Pair<Int, Int>>()
|
||||||
|
|
||||||
excludeMatchers.forEach {
|
excludeMatchers.forEach {
|
||||||
while (it.find()) excludeIndexs.add(it.start() to it.end())
|
while (it.find()) excludeIndexs.add(it.start() to it.end())
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset = 0
|
var offset = 0
|
||||||
|
|
||||||
// Offset adjustment to account for changes in the text length after replacements.
|
// Offset adjustment to account for changes in the text length after replacements.
|
||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
val start = matcher.start() + offset
|
val start = matcher.start() + offset
|
||||||
val end = matcher.end() + offset
|
val end = matcher.end() + offset
|
||||||
|
|
||||||
// Skip the replacement if the matched range is excluded.
|
// 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.
|
// 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
|
if (excludeIndexs.any { it.first <= start + 1 && it.second >= end - 1 }) continue
|
||||||
|
|
||||||
// Perform the replacement.
|
// Perform the replacement.
|
||||||
val replacementText = matcher.buildReplacementText(replacement)
|
val replacementText = matcher.buildReplacementText(replacement)
|
||||||
|
|
||||||
builder.replace(start, end, replacementText)
|
builder.replace(start, end, replacementText)
|
||||||
|
|
||||||
// Adjust offset based on the length of the replacement.
|
// Adjust offset based on the length of the replacement.
|
||||||
offset += replacementText.length - (end - start)
|
offset += replacementText.length - (end - start)
|
||||||
}; builder
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Log.w(PangutextAndroidProperties.PROJECT_NAME, "Failed to replace span text content.", it)
|
Log.w(PangutextAndroidProperties.PROJECT_NAME, "Failed to replace span text content.", it)
|
||||||
}.getOrNull() ?: this
|
}.getOrNull() ?: this
|
||||||
@@ -74,23 +84,29 @@ internal fun CharSequence.replaceAndPreserveSpans(regex: Regex, replacement: Str
|
|||||||
private fun Matcher.buildReplacementText(replacement: String): String {
|
private fun Matcher.buildReplacementText(replacement: String): String {
|
||||||
val matcher = this
|
val matcher = this
|
||||||
var result = replacement
|
var result = replacement
|
||||||
|
|
||||||
// Check for group references (like $1, $2, ...).
|
// Check for group references (like $1, $2, ...).
|
||||||
val pattern = "\\$(\\d+)".toRegex()
|
val pattern = "\\$(\\d+)".toRegex()
|
||||||
result = pattern.replace(result) { matchResult ->
|
result = pattern.replace(result) { matchResult ->
|
||||||
val groupIndex = matchResult.groupValues[1].toInt()
|
val groupIndex = matchResult.groupValues[1].toInt()
|
||||||
|
|
||||||
if (groupIndex <= matcher.groupCount())
|
if (groupIndex <= matcher.groupCount())
|
||||||
matcher.group(groupIndex) ?: ""
|
matcher.group(groupIndex) ?: ""
|
||||||
else ""
|
else ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for named groups (like ${groupName}).
|
// Check for named groups (like ${groupName}).
|
||||||
val namedGroupPattern = "\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}".toRegex()
|
val namedGroupPattern = "\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}".toRegex()
|
||||||
result = namedGroupPattern.replace(result) { matchResult ->
|
result = namedGroupPattern.replace(result) { matchResult ->
|
||||||
val groupName = matchResult.groupValues[1]
|
val groupName = matchResult.groupValues[1]
|
||||||
val groupIndex = matcher.getNamedGroupIndex(groupName)
|
val groupIndex = matcher.getNamedGroupIndex(groupName)
|
||||||
|
|
||||||
if (groupIndex >= 0)
|
if (groupIndex >= 0)
|
||||||
matcher.group(groupIndex) ?: ""
|
matcher.group(groupIndex) ?: ""
|
||||||
else ""
|
else ""
|
||||||
}; return result
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,5 +121,6 @@ private fun Matcher.getNamedGroupIndex(groupName: String): Int {
|
|||||||
.firstFieldOrNull {
|
.firstFieldOrNull {
|
||||||
name = "namedGroups"
|
name = "namedGroups"
|
||||||
}?.of(this)?.getQuietly<Map<String, Int>>()
|
}?.of(this)?.getQuietly<Map<String, Int>>()
|
||||||
|
|
||||||
return namedGroups?.get(groupName) ?: -1
|
return namedGroups?.get(groupName) ?: -1
|
||||||
}
|
}
|
@@ -107,6 +107,7 @@ class PanguTextFactory2 private constructor(private val base: LayoutInflater.Fac
|
|||||||
if (original is PanguTextFactory2) return run {
|
if (original is PanguTextFactory2) return run {
|
||||||
Log.w(PangutextAndroidProperties.PROJECT_NAME, "PanguTextFactory2 was already injected.")
|
Log.w(PangutextAndroidProperties.PROJECT_NAME, "PanguTextFactory2 was already injected.")
|
||||||
}
|
}
|
||||||
|
|
||||||
val replacement = PanguTextFactory2(original)
|
val replacement = PanguTextFactory2(original)
|
||||||
if (original != null)
|
if (original != null)
|
||||||
inflater.asResolver().optional(silent = true).firstFieldOrNull {
|
inflater.asResolver().optional(silent = true).firstFieldOrNull {
|
||||||
|
@@ -19,6 +19,8 @@
|
|||||||
*
|
*
|
||||||
* This file is created by fankes on 2025/3/4.
|
* This file is created by fankes on 2025/3/4.
|
||||||
*/
|
*/
|
||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
package com.highcapable.pangutext.android.factory
|
package com.highcapable.pangutext.android.factory
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@@ -26,11 +26,11 @@ import android.util.AttributeSet
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.withStyledAttributes
|
||||||
import androidx.core.view.doOnAttach
|
import androidx.core.view.doOnAttach
|
||||||
import com.highcapable.betterandroid.ui.extension.component.base.getBooleanOrNull
|
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.getFloatOrNull
|
||||||
import com.highcapable.betterandroid.ui.extension.component.base.getStringOrNull
|
import com.highcapable.betterandroid.ui.extension.component.base.getStringOrNull
|
||||||
import com.highcapable.betterandroid.ui.extension.component.base.obtainStyledAttributes
|
|
||||||
import com.highcapable.kavaref.KavaRef.Companion.resolve
|
import com.highcapable.kavaref.KavaRef.Companion.resolve
|
||||||
import com.highcapable.kavaref.extension.classOf
|
import com.highcapable.kavaref.extension.classOf
|
||||||
import com.highcapable.kavaref.extension.isNotSubclassOf
|
import com.highcapable.kavaref.extension.isNotSubclassOf
|
||||||
@@ -66,12 +66,14 @@ internal object PanguWidget {
|
|||||||
}.toClassOrNull()?.let { viewClass ->
|
}.toClassOrNull()?.let { viewClass ->
|
||||||
// Avoid creating unnecessary components for waste.
|
// Avoid creating unnecessary components for waste.
|
||||||
if (viewClass isNotSubclassOf classOf<TextView>()) return null
|
if (viewClass isNotSubclassOf classOf<TextView>()) return null
|
||||||
|
|
||||||
val twoParams = viewClass.resolve()
|
val twoParams = viewClass.resolve()
|
||||||
.optional(silent = true)
|
.optional(silent = true)
|
||||||
.firstConstructorOrNull { parameters(Context::class, AttributeSet::class) }
|
.firstConstructorOrNull { parameters(Context::class, AttributeSet::class) }
|
||||||
val onceParam = viewClass.resolve()
|
val onceParam = viewClass.resolve()
|
||||||
.optional(silent = true)
|
.optional(silent = true)
|
||||||
.firstConstructorOrNull { parameters(Context::class) }
|
.firstConstructorOrNull { parameters(Context::class) }
|
||||||
|
|
||||||
// Catching when the attrs value initialization failed.
|
// Catching when the attrs value initialization failed.
|
||||||
runCatching { twoParams?.create(context, attrs) }.onFailure {
|
runCatching { twoParams?.create(context, attrs) }.onFailure {
|
||||||
Log.w(PangutextAndroidProperties.PROJECT_NAME, "Failed to create instance of $viewClass using (Context, AttributeSet).", it)
|
Log.w(PangutextAndroidProperties.PROJECT_NAME, "Failed to create instance of $viewClass using (Context, AttributeSet).", it)
|
||||||
@@ -81,8 +83,10 @@ internal object PanguWidget {
|
|||||||
Log.w(PangutextAndroidProperties.PROJECT_NAME, "Failed to create instance of $viewClass, this process will be ignored.", it)
|
Log.w(PangutextAndroidProperties.PROJECT_NAME, "Failed to create instance of $viewClass, this process will be ignored.", it)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore if the instance is not a [TextView].
|
// Ignore if the instance is not a [TextView].
|
||||||
if (instance !is TextView) return null
|
if (instance !is TextView) return null
|
||||||
|
|
||||||
return startInjection(instance, attrs)
|
return startInjection(instance, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,34 +103,45 @@ internal object PanguWidget {
|
|||||||
config: PanguTextConfig = PanguText.globalConfig
|
config: PanguTextConfig = PanguText.globalConfig
|
||||||
): TV {
|
): TV {
|
||||||
var sConfig = config
|
var sConfig = config
|
||||||
|
|
||||||
if (instance is PanguTextView) {
|
if (instance is PanguTextView) {
|
||||||
val configCopy = sConfig.copy()
|
val configCopy = sConfig.copy()
|
||||||
|
|
||||||
instance.configurePanguText(configCopy)
|
instance.configurePanguText(configCopy)
|
||||||
sConfig = configCopy
|
sConfig = configCopy
|
||||||
|
|
||||||
if (!sConfig.isEnabled) return instance
|
if (!sConfig.isEnabled) return instance
|
||||||
} else instance.obtainStyledAttributes(attrs, R.styleable.PanguTextHelper) {
|
} else instance.context.withStyledAttributes(attrs, R.styleable.PanguTextHelper) {
|
||||||
val isEnabled = it.getBooleanOrNull(R.styleable.PanguTextHelper_panguText_enabled)
|
val isEnabled = getBooleanOrNull(R.styleable.PanguTextHelper_panguText_enabled)
|
||||||
val isProcessedSpanned = it.getBooleanOrNull(R.styleable.PanguTextHelper_panguText_processedSpanned)
|
val isProcessedSpanned = getBooleanOrNull(R.styleable.PanguTextHelper_panguText_processedSpanned)
|
||||||
val isAutoRemeasureText = it.getBooleanOrNull(R.styleable.PanguTextHelper_panguText_autoRemeasureText)
|
val isAutoRemeasureText = getBooleanOrNull(R.styleable.PanguTextHelper_panguText_autoRemeasureText)
|
||||||
val cjkSpacingRatio = it.getFloatOrNull(R.styleable.PanguTextHelper_panguText_cjkSpacingRatio)
|
val cjkSpacingRatio = getFloatOrNull(R.styleable.PanguTextHelper_panguText_cjkSpacingRatio)
|
||||||
val excludePatterns = it.getStringOrNull(R.styleable.PanguTextHelper_panguText_excludePatterns)
|
|
||||||
|
val excludePatterns = getStringOrNull(R.styleable.PanguTextHelper_panguText_excludePatterns)
|
||||||
?.split(TEXT_REGEX_SPLITE_SYMBOL)?.mapNotNull { regex ->
|
?.split(TEXT_REGEX_SPLITE_SYMBOL)?.mapNotNull { regex ->
|
||||||
runCatching { regex.toRegex() }.onFailure { th ->
|
runCatching { regex.toRegex() }.onFailure { th ->
|
||||||
Log.e(PangutextAndroidProperties.PROJECT_NAME, "Invalid exclude pattern of $instance: $regex", th)
|
Log.e(PangutextAndroidProperties.PROJECT_NAME, "Invalid exclude pattern of $instance: $regex", th)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}?.toTypedArray() ?: emptyArray()
|
}?.toTypedArray() ?: emptyArray()
|
||||||
|
|
||||||
if (isEnabled == false) return instance
|
if (isEnabled == false) return instance
|
||||||
|
|
||||||
if (isProcessedSpanned != null || isAutoRemeasureText != null || cjkSpacingRatio != null || excludePatterns.isNotEmpty()) {
|
if (isProcessedSpanned != null || isAutoRemeasureText != null || cjkSpacingRatio != null || excludePatterns.isNotEmpty()) {
|
||||||
val configCopy = sConfig.copy()
|
val configCopy = sConfig.copy()
|
||||||
|
|
||||||
configCopy.isProcessedSpanned = isProcessedSpanned ?: sConfig.isProcessedSpanned
|
configCopy.isProcessedSpanned = isProcessedSpanned ?: sConfig.isProcessedSpanned
|
||||||
configCopy.isAutoRemeasureText = isAutoRemeasureText ?: sConfig.isAutoRemeasureText
|
configCopy.isAutoRemeasureText = isAutoRemeasureText ?: sConfig.isAutoRemeasureText
|
||||||
configCopy.cjkSpacingRatio = cjkSpacingRatio ?: sConfig.cjkSpacingRatio
|
configCopy.cjkSpacingRatio = cjkSpacingRatio ?: sConfig.cjkSpacingRatio
|
||||||
|
|
||||||
if (excludePatterns.isNotEmpty()) {
|
if (excludePatterns.isNotEmpty()) {
|
||||||
sConfig.excludePatterns.clear()
|
sConfig.excludePatterns.clear()
|
||||||
sConfig.excludePatterns.addAll(excludePatterns)
|
sConfig.excludePatterns.addAll(excludePatterns)
|
||||||
}; sConfig = configCopy
|
}
|
||||||
|
|
||||||
|
sConfig = configCopy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (instance.javaClass.name) {
|
when (instance.javaClass.name) {
|
||||||
// Specialize those components because loading "hint" style after [doOnAttachRepeatable] causes problems.
|
// Specialize those components because loading "hint" style after [doOnAttachRepeatable] causes problems.
|
||||||
"com.google.android.material.textfield.TextInputEditText",
|
"com.google.android.material.textfield.TextInputEditText",
|
||||||
@@ -137,12 +152,15 @@ internal object PanguWidget {
|
|||||||
else -> instance.doOnAttachRepeatable(sConfig) {
|
else -> instance.doOnAttachRepeatable(sConfig) {
|
||||||
it.injectRealTimePanguText(config = sConfig)
|
it.injectRealTimePanguText(config = sConfig)
|
||||||
}
|
}
|
||||||
}; return instance
|
}
|
||||||
|
|
||||||
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Copied from [View.doOnAttach]. */
|
/** Copied from [View.doOnAttach]. */
|
||||||
private inline fun <reified V : View> V.doOnAttachRepeatable(config: PanguTextConfig, crossinline action: (view: V) -> Unit) {
|
private inline fun <reified V : View> V.doOnAttachRepeatable(config: PanguTextConfig, crossinline action: (view: V) -> Unit) {
|
||||||
if (!config.isEnabled) return
|
if (!config.isEnabled) return
|
||||||
|
|
||||||
if (isAttachedToWindow) action(this)
|
if (isAttachedToWindow) action(this)
|
||||||
addOnAttachStateChangeListener(
|
addOnAttachStateChangeListener(
|
||||||
object : View.OnAttachStateChangeListener {
|
object : View.OnAttachStateChangeListener {
|
||||||
|
Reference in New Issue
Block a user