From 044a4f7575c8bc94fa6e3930633af1d01892ba03 Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 9 Jan 2020 21:22:58 -0500 Subject: [PATCH 1/4] Split general settings into general and library --- .../ui/setting/SettingsGeneralController.kt | 184 ---------------- .../ui/setting/SettingsLibraryController.kt | 199 ++++++++++++++++++ .../ui/setting/SettingsMainController.kt | 6 + app/src/main/res/values/strings.xml | 19 +- 4 files changed, 216 insertions(+), 192 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt index 8698202381..c078bdd805 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt @@ -1,29 +1,12 @@ package eu.kanade.tachiyomi.ui.setting -import android.app.Dialog -import android.os.Bundle -import android.os.Handler import androidx.preference.PreferenceScreen -import android.view.View -import com.afollestad.materialdialogs.MaterialDialog import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.library.LibraryUpdateJob -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.util.LocaleHelper -import kotlinx.android.synthetic.main.pref_library_columns.view.* -import rx.Observable -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys class SettingsGeneralController : SettingsController() { - private val db: DatabaseHelper = Injekt.get() - override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { titleRes = R.string.pref_category_general @@ -64,30 +47,6 @@ class SettingsGeneralController : SettingsController() { true } } - preference { - titleRes = R.string.pref_library_columns - onClick { - LibraryColumnsDialog().showDialog(router) - } - - fun getColumnValue(value: Int): String { - return if (value == 0) - context.getString(R.string.default_columns) - else - value.toString() - } - - Observable.combineLatest( - preferences.portraitColumns().asObservable(), - preferences.landscapeColumns().asObservable() - ) { portraitCols, landscapeCols -> Pair(portraitCols, landscapeCols) } - .subscribeUntilDestroy { (portraitCols, landscapeCols) -> - val portrait = getColumnValue(portraitCols) - val landscape = getColumnValue(landscapeCols) - summary = "${context.getString(R.string.portrait)}: $portrait, " + - "${context.getString(R.string.landscape)}: $landscape" - } - } intListPreference { key = Keys.startScreen titleRes = R.string.pref_start_screen @@ -97,149 +56,6 @@ class SettingsGeneralController : SettingsController() { defaultValue = "1" summary = "%s" } - intListPreference { - key = Keys.libraryUpdateInterval - titleRes = R.string.pref_library_update_interval - entriesRes = arrayOf(R.string.update_never, R.string.update_1hour, - R.string.update_2hour, R.string.update_3hour, R.string.update_6hour, - R.string.update_12hour, R.string.update_24hour, R.string.update_48hour) - entryValues = arrayOf("0", "1", "2", "3", "6", "12", "24", "48") - defaultValue = "0" - summary = "%s" - - onChange { newValue -> - // Always cancel the previous task, it seems that sometimes they are not updated. - LibraryUpdateJob.cancelTask() - - val interval = (newValue as String).toInt() - if (interval > 0) { - LibraryUpdateJob.setupTask(interval) - } - true - } - } - multiSelectListPreference { - key = Keys.libraryUpdateRestriction - titleRes = R.string.pref_library_update_restriction - entriesRes = arrayOf(R.string.wifi, R.string.charging) - entryValues = arrayOf("wifi", "ac") - summaryRes = R.string.pref_library_update_restriction_summary - - preferences.libraryUpdateInterval().asObservable() - .subscribeUntilDestroy { isVisible = it > 0 } - - onChange { - // Post to event looper to allow the preference to be updated. - Handler().post { LibraryUpdateJob.setupTask() } - true - } - } - switchPreference { - key = Keys.updateOnlyNonCompleted - titleRes = R.string.pref_update_only_non_completed - defaultValue = false - } - - val dbCategories = db.getCategories().executeAsBlocking() - val categories = listOf(Category.createDefault()) + dbCategories - - multiSelectListPreference { - key = Keys.libraryUpdateCategories - titleRes = R.string.pref_library_update_categories - entries = categories.map { it.name }.toTypedArray() - entryValues = categories.map { it.id.toString() }.toTypedArray() - preferences.libraryUpdateCategories().asObservable() - .subscribeUntilDestroy { - val selectedCategories = it - .mapNotNull { id -> categories.find { it.id == id.toInt() } } - .sortedBy { it.order } - - summary = if (selectedCategories.isEmpty()) - context.getString(R.string.all) - else - selectedCategories.joinToString { it.name } - } - } - intListPreference{ - key = Keys.libraryUpdatePrioritization - titleRes = R.string.pref_library_update_prioritization - // The following arrays are to be lined up with the list rankingScheme in: - // ../../data/library/LibraryUpdateRanker.kt - entriesRes = arrayOf( - R.string.action_sort_alpha, - R.string.action_sort_last_updated - ) - entryValues = arrayOf( - "0", - "1" - ) - defaultValue = "0" - summaryRes = R.string.pref_library_update_prioritization_summary - } - intListPreference { - key = Keys.defaultCategory - titleRes = R.string.default_category - - val selectedCategory = categories.find { it.id == preferences.defaultCategory() } - entries = arrayOf(context.getString(R.string.default_category_summary)) + - categories.map { it.name }.toTypedArray() - entryValues = arrayOf("-1") + categories.map { it.id.toString() }.toTypedArray() - defaultValue = "-1" - summary = selectedCategory?.name ?: context.getString(R.string.default_category_summary) - - onChange { newValue -> - summary = categories.find { - it.id == (newValue as String).toInt() - }?.name ?: context.getString(R.string.default_category_summary) - true - } - } - } - - class LibraryColumnsDialog : DialogController() { - - private val preferences: PreferencesHelper = Injekt.get() - - private var portrait = preferences.portraitColumns().getOrDefault() - private var landscape = preferences.landscapeColumns().getOrDefault() - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val dialog = MaterialDialog.Builder(activity!!) - .title(R.string.pref_library_columns) - .customView(R.layout.pref_library_columns, false) - .positiveText(android.R.string.ok) - .negativeText(android.R.string.cancel) - .onPositive { _, _ -> - preferences.portraitColumns().set(portrait) - preferences.landscapeColumns().set(landscape) - } - .build() - - onViewCreated(dialog.view) - return dialog - } - - fun onViewCreated(view: View) { - with(view.portrait_columns) { - displayedValues = arrayOf(context.getString(R.string.default_columns)) + - IntRange(1, 10).map(Int::toString) - value = portrait - - setOnValueChangedListener { _, _, newValue -> - portrait = newValue - } - } - with(view.landscape_columns) { - displayedValues = arrayOf(context.getString(R.string.default_columns)) + - IntRange(1, 10).map(Int::toString) - value = landscape - - setOnValueChangedListener { _, _, newValue -> - landscape = newValue - } - } - } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt new file mode 100644 index 0000000000..eb63229387 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt @@ -0,0 +1,199 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.app.Dialog +import android.os.Bundle +import android.os.Handler +import android.view.View +import androidx.preference.PreferenceScreen +import com.afollestad.materialdialogs.MaterialDialog +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.library.LibraryUpdateJob +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import kotlinx.android.synthetic.main.pref_library_columns.view.landscape_columns +import kotlinx.android.synthetic.main.pref_library_columns.view.portrait_columns +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys + +class SettingsLibraryController : SettingsController() { + + private val db: DatabaseHelper = Injekt.get() + + override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { + titleRes = R.string.pref_category_library + + preference { + titleRes = R.string.pref_library_columns + onClick { + LibraryColumnsDialog().showDialog(router) + } + + fun getColumnValue(value: Int): String { + return if (value == 0) + context.getString(R.string.default_columns) + else + value.toString() + } + + Observable.combineLatest( + preferences.portraitColumns().asObservable(), + preferences.landscapeColumns().asObservable() + ) { portraitCols, landscapeCols -> Pair(portraitCols, landscapeCols) } + .subscribeUntilDestroy { (portraitCols, landscapeCols) -> + val portrait = getColumnValue(portraitCols) + val landscape = getColumnValue(landscapeCols) + summary = "${context.getString(R.string.portrait)}: $portrait, " + + "${context.getString(R.string.landscape)}: $landscape" + } + } + intListPreference { + key = Keys.libraryUpdateInterval + titleRes = R.string.pref_library_update_interval + entriesRes = arrayOf(R.string.update_never, R.string.update_1hour, + R.string.update_2hour, R.string.update_3hour, R.string.update_6hour, + R.string.update_12hour, R.string.update_24hour, R.string.update_48hour) + entryValues = arrayOf("0", "1", "2", "3", "6", "12", "24", "48") + defaultValue = "0" + summary = "%s" + + onChange { newValue -> + // Always cancel the previous task, it seems that sometimes they are not updated. + LibraryUpdateJob.cancelTask() + + val interval = (newValue as String).toInt() + if (interval > 0) { + LibraryUpdateJob.setupTask(interval) + } + true + } + } + multiSelectListPreference { + key = Keys.libraryUpdateRestriction + titleRes = R.string.pref_library_update_restriction + entriesRes = arrayOf(R.string.wifi, R.string.charging) + entryValues = arrayOf("wifi", "ac") + summaryRes = R.string.pref_library_update_restriction_summary + + preferences.libraryUpdateInterval().asObservable() + .subscribeUntilDestroy { isVisible = it > 0 } + + onChange { + // Post to event looper to allow the preference to be updated. + Handler().post { LibraryUpdateJob.setupTask() } + true + } + } + switchPreference { + key = Keys.updateOnlyNonCompleted + titleRes = R.string.pref_update_only_non_completed + defaultValue = false + } + + val dbCategories = db.getCategories().executeAsBlocking() + val categories = listOf(Category.createDefault()) + dbCategories + + multiSelectListPreference { + key = Keys.libraryUpdateCategories + titleRes = R.string.pref_library_update_categories + entries = categories.map { it.name }.toTypedArray() + entryValues = categories.map { it.id.toString() }.toTypedArray() + preferences.libraryUpdateCategories().asObservable() + .subscribeUntilDestroy { + val selectedCategories = it + .mapNotNull { id -> categories.find { it.id == id.toInt() } } + .sortedBy { it.order } + + summary = if (selectedCategories.isEmpty()) + context.getString(R.string.all) + else + selectedCategories.joinToString { it.name } + } + } + intListPreference{ + key = Keys.libraryUpdatePrioritization + titleRes = R.string.pref_library_update_prioritization + // The following arrays are to be lined up with the list rankingScheme in: + // ../../data/library/LibraryUpdateRanker.kt + entriesRes = arrayOf( + R.string.action_sort_alpha, + R.string.action_sort_last_updated + ) + entryValues = arrayOf( + "0", + "1" + ) + defaultValue = "0" + summaryRes = R.string.pref_library_update_prioritization_summary + } + intListPreference { + key = Keys.defaultCategory + titleRes = R.string.default_category + + val selectedCategory = categories.find { it.id == preferences.defaultCategory() } + entries = arrayOf(context.getString(R.string.default_category_summary)) + + categories.map { it.name }.toTypedArray() + entryValues = arrayOf("-1") + categories.map { it.id.toString() }.toTypedArray() + defaultValue = "-1" + summary = selectedCategory?.name ?: context.getString(R.string.default_category_summary) + + onChange { newValue -> + summary = categories.find { + it.id == (newValue as String).toInt() + }?.name ?: context.getString(R.string.default_category_summary) + true + } + } + } + + class LibraryColumnsDialog : DialogController() { + + private val preferences: PreferencesHelper = Injekt.get() + + private var portrait = preferences.portraitColumns().getOrDefault() + private var landscape = preferences.landscapeColumns().getOrDefault() + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val dialog = MaterialDialog.Builder(activity!!) + .title(R.string.pref_library_columns) + .customView(R.layout.pref_library_columns, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { _, _ -> + preferences.portraitColumns().set(portrait) + preferences.landscapeColumns().set(landscape) + } + .build() + + onViewCreated(dialog.view) + return dialog + } + + fun onViewCreated(view: View) { + with(view.portrait_columns) { + displayedValues = arrayOf(context.getString(R.string.default_columns)) + + IntRange(1, 10).map(Int::toString) + value = portrait + + setOnValueChangedListener { _, _, newValue -> + portrait = newValue + } + } + with(view.landscape_columns) { + displayedValues = arrayOf(context.getString(R.string.default_columns)) + + IntRange(1, 10).map(Int::toString) + value = landscape + + setOnValueChangedListener { _, _, newValue -> + landscape = newValue + } + } + } + + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt index f465fe22bb..e13e5cb55d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt @@ -17,6 +17,12 @@ class SettingsMainController : SettingsController() { titleRes = R.string.pref_category_general onClick { navigateTo(SettingsGeneralController()) } } + preference { + iconRes = R.drawable.ic_book_black_24dp + iconTint = tintColor + titleRes = R.string.pref_category_library + onClick { navigateTo(SettingsLibraryController()) } + } preference { iconRes = R.drawable.ic_chrome_reader_mode_black_24dp iconTint = tintColor diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1db775856e..cbbe837623 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,6 +109,7 @@ General + Library Reader Downloads Sources @@ -117,6 +118,16 @@ About + App theme + Light + Dark + AMOLED dark + Dark blue + Start screen + Language + System default + + Library manga per row Portrait Landscape @@ -143,14 +154,6 @@ Only update ongoing manga Sync chapters after reading Confirm before updating - App theme - Light - Dark - AMOLED dark - Dark blue - Start screen - Language - System default Default category Always ask From 5f2aaeac575548a1984bb256c0150b7fcb0e2f2d Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 9 Jan 2020 21:32:30 -0500 Subject: [PATCH 2/4] Remove reflection to get TextView color field (closes #2469) --- .../tachiyomi/ui/reader/PageIndicatorTextView.kt | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt index a938039720..c467ce0db0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt @@ -24,12 +24,12 @@ class PageIndicatorTextView( private val strokeColor = Color.rgb(45, 45, 45) override fun onDraw(canvas: Canvas) { - textColorField.set(this, strokeColor) + setTextColor(strokeColor) paint.strokeWidth = 4f paint.style = Paint.Style.STROKE super.onDraw(canvas) - textColorField.set(this, fillColor) + setTextColor(fillColor) paint.strokeWidth = 0f paint.style = Paint.Style.FILL super.onDraw(canvas) @@ -50,12 +50,4 @@ class PageIndicatorTextView( super.setText(finalText, TextView.BufferType.SPANNABLE) } - - private companion object { - // We need to use reflection to set the text color instead of using [setTextColor], - // otherwise the view is invalidated inside [onDraw] and there's an infinite loop - val textColorField = TextView::class.java.getDeclaredField("mCurTextColor").apply { - isAccessible = true - }!! - } } From f715478070393f57d847b010f941cbd2d87adece Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 9 Jan 2020 21:51:34 -0500 Subject: [PATCH 3/4] Revert "Remove reflection to get TextView color field (closes #2469)" This reverts commit 5f2aaeac575548a1984bb256c0150b7fcb0e2f2d. --- .../tachiyomi/ui/reader/PageIndicatorTextView.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt index c467ce0db0..a938039720 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt @@ -24,12 +24,12 @@ class PageIndicatorTextView( private val strokeColor = Color.rgb(45, 45, 45) override fun onDraw(canvas: Canvas) { - setTextColor(strokeColor) + textColorField.set(this, strokeColor) paint.strokeWidth = 4f paint.style = Paint.Style.STROKE super.onDraw(canvas) - setTextColor(fillColor) + textColorField.set(this, fillColor) paint.strokeWidth = 0f paint.style = Paint.Style.FILL super.onDraw(canvas) @@ -50,4 +50,12 @@ class PageIndicatorTextView( super.setText(finalText, TextView.BufferType.SPANNABLE) } + + private companion object { + // We need to use reflection to set the text color instead of using [setTextColor], + // otherwise the view is invalidated inside [onDraw] and there's an infinite loop + val textColorField = TextView::class.java.getDeclaredField("mCurTextColor").apply { + isAccessible = true + }!! + } } From 745f8d32b58d2f1132493c4e70a3701b065c69bf Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 9 Jan 2020 22:13:25 -0500 Subject: [PATCH 4/4] Use OutlineSpan approach from CarlosEsco/Neko to avoid infinite redraws Based on work by @arsonistAnt: https://github.com/CarlosEsco/Neko/commit/1876f850f6177b756ef623c9baedd65241a424d5 --- .../ui/reader/PageIndicatorTextView.kt | 42 ++++++-------- .../eu/kanade/tachiyomi/widget/OutlineSpan.kt | 57 +++++++++++++++++++ 2 files changed, 75 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt index a938039720..de9868b127 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/PageIndicatorTextView.kt @@ -2,15 +2,14 @@ package eu.kanade.tachiyomi.ui.reader import android.annotation.SuppressLint import android.content.Context -import android.graphics.Canvas import android.graphics.Color -import android.graphics.Paint -import androidx.appcompat.widget.AppCompatTextView import android.text.Spannable import android.text.SpannableString import android.text.style.ScaleXSpan import android.util.AttributeSet import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView +import eu.kanade.tachiyomi.widget.OutlineSpan /** * Page indicator found at the bottom of the reader @@ -20,19 +19,8 @@ class PageIndicatorTextView( attrs: AttributeSet? = null ) : AppCompatTextView(context, attrs) { - private val fillColor = Color.rgb(235, 235, 235) - private val strokeColor = Color.rgb(45, 45, 45) - - override fun onDraw(canvas: Canvas) { - textColorField.set(this, strokeColor) - paint.strokeWidth = 4f - paint.style = Paint.Style.STROKE - super.onDraw(canvas) - - textColorField.set(this, fillColor) - paint.strokeWidth = 0f - paint.style = Paint.Style.FILL - super.onDraw(canvas) + init { + setTextColor(fillColor) } @SuppressLint("SetTextI18n") @@ -42,20 +30,26 @@ class PageIndicatorTextView( val currText = " $text " // Also add a bit of spacing between each character, as the stroke overlaps them - val finalText = SpannableString(currText.asIterable().joinToString("\u00A0")) + val finalText = SpannableString(currText.asIterable().joinToString("\u00A0")).apply { + // Apply text outline + setSpan(spanOutline, 1, length-1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - for (i in 1..finalText.lastIndex step 2) { - finalText.setSpan(ScaleXSpan(0.1f), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + for (i in 1..lastIndex step 2) { + setSpan(ScaleXSpan(0.2f), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } } super.setText(finalText, TextView.BufferType.SPANNABLE) } private companion object { - // We need to use reflection to set the text color instead of using [setTextColor], - // otherwise the view is invalidated inside [onDraw] and there's an infinite loop - val textColorField = TextView::class.java.getDeclaredField("mCurTextColor").apply { - isAccessible = true - }!! + private val fillColor = Color.rgb(235, 235, 235) + private val strokeColor = Color.rgb(45, 45, 45) + + // A span object with text outlining properties + val spanOutline = OutlineSpan( + strokeColor = strokeColor, + strokeWidth = 4f + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt new file mode 100644 index 0000000000..79b2057778 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/OutlineSpan.kt @@ -0,0 +1,57 @@ +package eu.kanade.tachiyomi.widget + +import android.graphics.Canvas +import android.graphics.Paint +import android.text.style.ReplacementSpan +import androidx.annotation.ColorInt +import androidx.annotation.Dimension + +/** + * Source: https://github.com/santaevpavel + * + * A class that draws the outlines of a text when given a stroke color and stroke width. + */ +class OutlineSpan( + @ColorInt private val strokeColor: Int, + @Dimension private val strokeWidth: Float +) : ReplacementSpan() { + + override fun getSize( + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fm: Paint.FontMetricsInt? + ): Int { + return paint.measureText(text.toString().substring(start until end)).toInt() + } + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint + ) { + val originTextColor = paint.color + + paint.apply { + color = strokeColor + style = Paint.Style.STROKE + this.strokeWidth = this@OutlineSpan.strokeWidth + } + canvas.drawText(text, start, end, x, y.toFloat(), paint) + + paint.apply { + color = originTextColor + style = Paint.Style.FILL + } + + canvas.drawText(text, start, end, x, y.toFloat(), paint) + } + +}