mirror of
https://github.com/BetterAndroid/PanguText.git
synced 2025-09-01 08:15:21 +08:00
refactor: merge to BetterAndroid new usage
This commit is contained in:
@@ -42,6 +42,7 @@ android {
|
||||
dependencies {
|
||||
implementation(projects.pangutextAndroid)
|
||||
implementation(com.highcapable.betterandroid.ui.component)
|
||||
implementation(com.highcapable.betterandroid.ui.component.adapter)
|
||||
implementation(com.highcapable.betterandroid.ui.extension)
|
||||
implementation(com.highcapable.betterandroid.system.extension)
|
||||
implementation(androidx.core.core.ktx)
|
||||
|
@@ -35,6 +35,7 @@ class ListActivity : BaseActivity<ActivityListBinding>() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding.recyclerView.bindAdapter<String> {
|
||||
onBindData { listData }
|
||||
onBindItemView<AdapterListBinding> { binding, text, _ ->
|
||||
|
@@ -46,9 +46,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding.root.handleOnWindowInsetsChanged(animated = true) { linearLayout, insetsWrapper ->
|
||||
linearLayout.setInsetsPadding(insetsWrapper.safeDrawing)
|
||||
}
|
||||
|
||||
listOf(
|
||||
binding.textViewPanguText,
|
||||
binding.textViewPanguTextCjkSpacingRatio,
|
||||
|
@@ -32,6 +32,7 @@ open class BaseActivity<VB : ViewBinding> : AppBindingActivity<VB>() {
|
||||
override fun onPrepareContentView(savedInstanceState: Bundle?): LayoutInflater {
|
||||
val inflater = super.onPrepareContentView(savedInstanceState)
|
||||
PanguTextFactory2.inject(inflater)
|
||||
|
||||
return inflater
|
||||
}
|
||||
}
|
@@ -31,11 +31,13 @@ plugins:
|
||||
libraries:
|
||||
com.highcapable.betterandroid:
|
||||
ui-component:
|
||||
version: 1.0.7
|
||||
version: 1.0.8
|
||||
ui-component-adapter:
|
||||
version: 1.0.0
|
||||
ui-extension:
|
||||
version: 1.0.6
|
||||
version: 1.0.7
|
||||
system-extension:
|
||||
version: 1.0.2
|
||||
version: 1.0.3
|
||||
com.highcapable.kavaref:
|
||||
kavaref-core:
|
||||
version: 1.0.1
|
||||
|
@@ -88,6 +88,7 @@ object PanguText {
|
||||
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)
|
||||
}
|
||||
@@ -110,9 +111,11 @@ object PanguText {
|
||||
@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
|
||||
@@ -136,6 +139,7 @@ object PanguText {
|
||||
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) {
|
||||
@@ -143,20 +147,26 @@ object PanguText {
|
||||
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()
|
||||
}
|
||||
|
||||
builder.clear()
|
||||
return spannable
|
||||
}
|
||||
|
||||
@@ -170,12 +180,16 @@ object PanguText {
|
||||
*/
|
||||
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)
|
||||
val end = getSpanEnd(span)
|
||||
|
||||
// Clear the [PanguMarginSpan].
|
||||
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 {
|
||||
val spannable = this as? Spanned ?: return false
|
||||
val spans = spannable.getSpans(0, spannable.length, classOf<T>())
|
||||
|
||||
return spans.isNotEmpty()
|
||||
}
|
||||
}
|
@@ -76,18 +76,24 @@ internal class PanguMarginSpan(@Px val margin: Int) : ReplacementSpan() {
|
||||
// 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) }
|
||||
}
|
||||
|
||||
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.widget.EditText
|
||||
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.pangutext.android.PanguText
|
||||
import com.highcapable.pangutext.android.PanguTextConfig
|
||||
@@ -56,21 +56,24 @@ class PanguTextWatcher internal constructor(private val base: TextView, private
|
||||
*/
|
||||
private val isAutoRemeasureText
|
||||
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?) {
|
||||
editable?.let { PanguText.format(base.resources, base.textSize, it, config) }
|
||||
if (!isAutoRemeasureText) return
|
||||
|
||||
val currentWatchers = mutableListOf<TextWatcher>()
|
||||
textWatchers?.also {
|
||||
currentWatchers.addAll(it)
|
||||
// Avoid triggering events again during processing.
|
||||
it.clear()
|
||||
}
|
||||
|
||||
// Reset the text to trigger remeasurement.
|
||||
base.text = editable
|
||||
// Re-add to continue listening to text changes.
|
||||
textWatchers?.addAll(currentWatchers)
|
||||
|
||||
currentWatchers.clear()
|
||||
}
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
|
@@ -57,6 +57,7 @@ fun PanguTextConfig(copyFromGlobal: Boolean = true, body: PanguTextConfig.() ->
|
||||
@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)
|
||||
}
|
||||
@@ -75,11 +76,15 @@ fun TextView.injectPanguText(injectHint: Boolean = true, config: PanguTextConfig
|
||||
@JvmOverloads
|
||||
fun TextView.injectRealTimePanguText(injectHint: Boolean = true, config: PanguTextConfig = PanguText.globalConfig) {
|
||||
if (!config.isEnabled) return
|
||||
|
||||
val observerKey = R.id.tag_inject_real_time_pangu_text
|
||||
val isRepeated = getTag<Boolean>(observerKey) == true
|
||||
|
||||
// It will no longer be executed if it exceeds one time.
|
||||
if (isRepeated) return
|
||||
|
||||
injectPanguText(injectHint, config)
|
||||
|
||||
var currentHint = this.hint
|
||||
val textWatcher = PanguTextWatcher(base = this, config)
|
||||
val listener = ViewTreeObserver.OnGlobalLayoutListener {
|
||||
@@ -88,13 +93,17 @@ fun TextView.injectRealTimePanguText(injectHint: Boolean = true, config: PanguTe
|
||||
self.setHintWithPangu(self.hint, config)
|
||||
currentHint = self.hint
|
||||
}
|
||||
|
||||
setTag(observerKey, true)
|
||||
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)
|
||||
@@ -112,7 +121,10 @@ fun TextView.injectRealTimePanguText(injectHint: Boolean = true, config: PanguTe
|
||||
@JvmOverloads
|
||||
fun TextView.setTextWithPangu(text: CharSequence?, config: PanguTextConfig = PanguText.globalConfig) {
|
||||
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
|
||||
fun TextView.setHintWithPangu(text: CharSequence?, config: PanguTextConfig = PanguText.globalConfig) {
|
||||
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) =
|
||||
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
|
||||
}
|
||||
|
||||
builder
|
||||
}.onFailure {
|
||||
Log.w(PangutextAndroidProperties.PROJECT_NAME, "Failed to replace span text content.", it)
|
||||
}.getOrNull() ?: this
|
||||
@@ -74,23 +84,29 @@ internal fun CharSequence.replaceAndPreserveSpans(regex: Regex, replacement: Str
|
||||
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
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,5 +121,6 @@ private fun Matcher.getNamedGroupIndex(groupName: String): Int {
|
||||
.firstFieldOrNull {
|
||||
name = "namedGroups"
|
||||
}?.of(this)?.getQuietly<Map<String, Int>>()
|
||||
|
||||
return namedGroups?.get(groupName) ?: -1
|
||||
}
|
@@ -107,6 +107,7 @@ class PanguTextFactory2 private constructor(private val base: LayoutInflater.Fac
|
||||
if (original is PanguTextFactory2) return run {
|
||||
Log.w(PangutextAndroidProperties.PROJECT_NAME, "PanguTextFactory2 was already injected.")
|
||||
}
|
||||
|
||||
val replacement = PanguTextFactory2(original)
|
||||
if (original != null)
|
||||
inflater.asResolver().optional(silent = true).firstFieldOrNull {
|
||||
|
@@ -19,6 +19,8 @@
|
||||
*
|
||||
* This file is created by fankes on 2025/3/4.
|
||||
*/
|
||||
@file:Suppress("unused")
|
||||
|
||||
package com.highcapable.pangutext.android.factory
|
||||
|
||||
import android.view.View
|
||||
|
@@ -26,11 +26,11 @@ import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.withStyledAttributes
|
||||
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.kavaref.KavaRef.Companion.resolve
|
||||
import com.highcapable.kavaref.extension.classOf
|
||||
import com.highcapable.kavaref.extension.isNotSubclassOf
|
||||
@@ -66,12 +66,14 @@ internal object PanguWidget {
|
||||
}.toClassOrNull()?.let { viewClass ->
|
||||
// Avoid creating unnecessary components for waste.
|
||||
if (viewClass isNotSubclassOf classOf<TextView>()) return null
|
||||
|
||||
val twoParams = viewClass.resolve()
|
||||
.optional(silent = true)
|
||||
.firstConstructorOrNull { parameters(Context::class, AttributeSet::class) }
|
||||
val onceParam = viewClass.resolve()
|
||||
.optional(silent = true)
|
||||
.firstConstructorOrNull { parameters(Context::class) }
|
||||
|
||||
// Catching when the attrs value initialization failed.
|
||||
runCatching { twoParams?.create(context, attrs) }.onFailure {
|
||||
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)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
// Ignore if the instance is not a [TextView].
|
||||
if (instance !is TextView) return null
|
||||
|
||||
return startInjection(instance, attrs)
|
||||
}
|
||||
|
||||
@@ -99,34 +103,45 @@ internal object PanguWidget {
|
||||
config: PanguTextConfig = PanguText.globalConfig
|
||||
): TV {
|
||||
var sConfig = config
|
||||
|
||||
if (instance is PanguTextView) {
|
||||
val configCopy = sConfig.copy()
|
||||
|
||||
instance.configurePanguText(configCopy)
|
||||
sConfig = configCopy
|
||||
|
||||
if (!sConfig.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 isAutoRemeasureText = it.getBooleanOrNull(R.styleable.PanguTextHelper_panguText_autoRemeasureText)
|
||||
val cjkSpacingRatio = it.getFloatOrNull(R.styleable.PanguTextHelper_panguText_cjkSpacingRatio)
|
||||
val excludePatterns = it.getStringOrNull(R.styleable.PanguTextHelper_panguText_excludePatterns)
|
||||
} else instance.context.withStyledAttributes(attrs, R.styleable.PanguTextHelper) {
|
||||
val isEnabled = getBooleanOrNull(R.styleable.PanguTextHelper_panguText_enabled)
|
||||
val isProcessedSpanned = getBooleanOrNull(R.styleable.PanguTextHelper_panguText_processedSpanned)
|
||||
val isAutoRemeasureText = getBooleanOrNull(R.styleable.PanguTextHelper_panguText_autoRemeasureText)
|
||||
val cjkSpacingRatio = getFloatOrNull(R.styleable.PanguTextHelper_panguText_cjkSpacingRatio)
|
||||
|
||||
val excludePatterns = 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 || isAutoRemeasureText != null || cjkSpacingRatio != null || excludePatterns.isNotEmpty()) {
|
||||
val configCopy = sConfig.copy()
|
||||
|
||||
configCopy.isProcessedSpanned = isProcessedSpanned ?: sConfig.isProcessedSpanned
|
||||
configCopy.isAutoRemeasureText = isAutoRemeasureText ?: sConfig.isAutoRemeasureText
|
||||
configCopy.cjkSpacingRatio = cjkSpacingRatio ?: sConfig.cjkSpacingRatio
|
||||
|
||||
if (excludePatterns.isNotEmpty()) {
|
||||
sConfig.excludePatterns.clear()
|
||||
sConfig.excludePatterns.addAll(excludePatterns)
|
||||
}; sConfig = configCopy
|
||||
}
|
||||
|
||||
sConfig = configCopy
|
||||
}
|
||||
}
|
||||
|
||||
when (instance.javaClass.name) {
|
||||
// Specialize those components because loading "hint" style after [doOnAttachRepeatable] causes problems.
|
||||
"com.google.android.material.textfield.TextInputEditText",
|
||||
@@ -137,12 +152,15 @@ internal object PanguWidget {
|
||||
else -> instance.doOnAttachRepeatable(sConfig) {
|
||||
it.injectRealTimePanguText(config = sConfig)
|
||||
}
|
||||
}; return instance
|
||||
}
|
||||
|
||||
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 {
|
||||
|
Reference in New Issue
Block a user