fix: TokenAutoComplete as lib

This commit is contained in:
J-Jamet
2023-06-18 10:43:07 +02:00
parent 65d7fdbf7e
commit c3e4504a1a
12 changed files with 1 additions and 2136 deletions

View File

@@ -112,7 +112,7 @@ dependencies {
implementation "com.google.android.material:material:$android_material_version"
// Token auto complete
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed
// implementation "com.splitwise:tokenautocomplete:4.0.0-beta04"
implementation "com.splitwise:tokenautocomplete:4.0.0-beta05"
// Database
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"

View File

@@ -1,72 +0,0 @@
package com.tokenautocomplete
import android.annotation.SuppressLint
import android.text.SpannableString
import android.text.Spanned
import android.text.TextUtils
import kotlinx.parcelize.Parcelize
import java.util.*
/**
* Tokenizer with configurable array of characters to tokenize on.
*
* Created on 2/3/15.
* @author mgod
*/
@Parcelize
@SuppressLint("ParcelCreator")
open class CharacterTokenizer(private val splitChar: List<Char>, private val tokenTerminator: String) : Tokenizer {
override fun containsTokenTerminator(charSequence: CharSequence): Boolean {
for (element in charSequence) {
if (splitChar.contains(element)) {
return true
}
}
return false
}
override fun findTokenRanges(charSequence: CharSequence, start: Int, end: Int): List<Range> {
val result = ArrayList<Range>()
if (start == end) {
//Can't have a 0 length token
return result
}
var tokenStart = start
for (cursor in start until end) {
val character = charSequence[cursor]
//Avoid including leading whitespace, tokenStart will match the cursor as long as we're at the start
if (tokenStart == cursor && Character.isWhitespace(character)) {
tokenStart = cursor + 1
}
//Either this is a split character, or we contain some content and are at the end of input
if (splitChar.contains(character) || cursor == end - 1) {
val hasTokenContent = //There is token content befor the current character
cursor > tokenStart || //If the current single character is valid token content, not a split char or whitespace
cursor == tokenStart && !splitChar.contains(character)
if (hasTokenContent) {
//There is some token content
//Add one to range end as the end of the ranges is not inclusive
result.add(Range(tokenStart, cursor + 1))
}
tokenStart = cursor + 1
}
}
return result
}
override fun wrapTokenValue(unwrappedTokenValue: CharSequence): CharSequence {
val wrappedText: CharSequence = unwrappedTokenValue.toString() + tokenTerminator
return if (unwrappedTokenValue is Spanned) {
val sp = SpannableString(wrappedText)
TextUtils.copySpansFrom(
unwrappedTokenValue, 0, unwrappedTokenValue.length,
Any::class.java, sp, 0
)
sp
} else {
wrappedText
}
}
}

View File

@@ -1,33 +0,0 @@
package com.tokenautocomplete
import android.text.Layout
import android.text.TextPaint
import android.text.style.CharacterStyle
import java.util.*
/**
* Span that displays +[count]
*
* Created on 2/3/15.
* @author mgod
*/
class CountSpan : CharacterStyle() {
var countText = ""
private set
override fun updateDrawState(textPaint: TextPaint) {
//Do nothing, we are using this span as a location marker
}
fun setCount(c: Int) {
countText = if (c > 0) {
String.format(Locale.getDefault(), " +%d", c)
} else {
""
}
}
fun getCountTextWidthForPaint(paint: TextPaint?): Float {
return Layout.getDesiredWidth(countText, 0, countText.length, paint)
}
}

View File

@@ -1,18 +0,0 @@
package com.tokenautocomplete
import android.text.TextPaint
import android.text.style.MetricAffectingSpan
/**
* Invisible MetricAffectingSpan that will trigger a redraw when it is being added to or removed from an Editable.
*
* @see TokenCompleteTextView.redrawTokens
*/
internal class DummySpan private constructor() : MetricAffectingSpan() {
override fun updateMeasureState(textPaint: TextPaint) {}
override fun updateDrawState(tp: TextPaint) {}
companion object {
val INSTANCE = DummySpan()
}
}

View File

@@ -1,139 +0,0 @@
package com.tokenautocomplete
import android.content.Context
import android.widget.ArrayAdapter
import android.widget.Filter
import java.util.*
/**
* Simplified custom filtered ArrayAdapter
* override keepObject with your test for filtering
*
*
* Based on gist [
* FilteredArrayAdapter](https://gist.github.com/tobiasschuerg/3554252/raw/30634bf9341311ac6ad6739ef094222fc5f07fa8/FilteredArrayAdapter.java) by Tobias Schürg
*
*
* Created on 9/17/13.
* @author mgod
*/
abstract class FilteredArrayAdapter<T>
/**
* Constructor
*
* @param context The current context.
* @param resource The resource ID for a layout file containing a layout to use when
* instantiating views.
* @param textViewResourceId The id of the TextView within the layout resource to be populated
* @param objects The objects to represent in the ListView.
*/(
context: Context,
resource: Int,
textViewResourceId: Int,
objects: List<T>
) : ArrayAdapter<T>(
context, resource, textViewResourceId, ArrayList(objects)
) {
private val originalObjects: List<T> = objects
private var filter: Filter? = null
/**
* Constructor
*
* @param context The current context.
* @param resource The resource ID for a layout file containing a TextView to use when
* instantiating views.
* @param objects The objects to represent in the ListView.
*/
constructor(context: Context, resource: Int, objects: Array<T>) : this(
context,
resource,
0,
objects
)
/**
* Constructor
*
* @param context The current context.
* @param resource The resource ID for a layout file containing a layout to use when
* instantiating views.
* @param textViewResourceId The id of the TextView within the layout resource to be populated
* @param objects The objects to represent in the ListView.
*/
constructor(
context: Context,
resource: Int,
textViewResourceId: Int,
objects: Array<T>
) : this(context, resource, textViewResourceId, ArrayList<T>(listOf(*objects)))
/**
* Constructor
*
* @param context The current context.
* @param resource The resource ID for a layout file containing a TextView to use when
* instantiating views.
* @param objects The objects to represent in the ListView.
*/
@Suppress("unused")
constructor(context: Context, resource: Int, objects: List<T>) : this(
context,
resource,
0,
objects
)
override fun getFilter(): Filter {
if (filter == null) filter = AppFilter()
return filter!!
}
/**
* Filter method used by the adapter. Return true if the object should remain in the list
*
* @param obj object we are checking for inclusion in the adapter
* @param mask current text in the edit text we are completing against
* @return true if we should keep the item in the adapter
*/
protected abstract fun keepObject(obj: T, mask: String?): Boolean
/**
* Class for filtering Adapter, relies on keepObject in FilteredArrayAdapter
*
* based on gist by Tobias Schürg
* in turn inspired by inspired by Alxandr
* (http://stackoverflow.com/a/2726348/570168)
*/
private inner class AppFilter : Filter() {
override fun performFiltering(chars: CharSequence?): FilterResults {
val sourceObjects = ArrayList(originalObjects)
val result = FilterResults()
if (chars != null && chars.isNotEmpty()) {
val mask = chars.toString()
val keptObjects = ArrayList<T>()
for (sourceObject in sourceObjects) {
if (keepObject(sourceObject, mask)) keptObjects.add(sourceObject)
}
result.count = keptObjects.size
result.values = keptObjects
} else {
// add all objects
result.values = sourceObjects
result.count = sourceObjects.size
}
return result
}
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
clear()
if (results.count > 0) {
@Suppress("unchecked_cast")
this@FilteredArrayAdapter.addAll(results.values as Collection<T>)
notifyDataSetChanged()
} else {
notifyDataSetInvalidated()
}
}
}
}

View File

@@ -1,18 +0,0 @@
package com.tokenautocomplete
import android.content.res.ColorStateList
import android.text.style.TextAppearanceSpan
/**
* Subclass of TextAppearanceSpan just to work with how Spans get detected
*
* Created on 2/3/15.
* @author mgod
*/
internal class HintSpan(
family: String?,
style: Int,
size: Int,
color: ColorStateList?,
linkColor: ColorStateList?
) : TextAppearanceSpan(family, style, size, color, linkColor)

View File

@@ -1,41 +0,0 @@
package com.tokenautocomplete
import java.util.*
class Range(start: Int, end: Int) {
@JvmField
val start: Int
@JvmField
val end: Int
fun length(): Int {
return end - start
}
override fun equals(other: Any?): Boolean {
if (null == other || other !is Range) {
return false
}
return other.start == start && other.end == end
}
override fun toString(): String {
return String.format(Locale.US, "[%d..%d]", start, end)
}
override fun hashCode(): Int {
var result = start
result = 31 * result + end
return result
}
init {
require(start <= end) {
String.format(
Locale.ENGLISH,
"Start (%d) cannot be greater than end (%d)", start, end
)
}
this.start = start
this.end = end
}
}

View File

@@ -1,69 +0,0 @@
package com.tokenautocomplete
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.TextPaint
import android.text.TextUtils
import com.tokenautocomplete.TokenCompleteTextView.TokenImageSpan
internal object SpanUtils {
@JvmStatic
fun ellipsizeWithSpans(
prefix: CharSequence?, countSpan: CountSpan?,
tokenCount: Int, paint: TextPaint,
originalText: CharSequence, maxWidth: Float
): Spanned? {
var countWidth = 0f
if (countSpan != null) {
//Assume the largest possible number of items for measurement
countSpan.setCount(tokenCount)
countWidth = countSpan.getCountTextWidthForPaint(paint)
}
val ellipsizeCallback = EllipsizeCallback()
val tempEllipsized = TextUtils.ellipsize(
originalText, paint, maxWidth - countWidth,
TextUtils.TruncateAt.END, false, ellipsizeCallback
)
val ellipsized = SpannableStringBuilder(tempEllipsized)
if (tempEllipsized is Spanned) {
TextUtils.copySpansFrom(
tempEllipsized,
0,
tempEllipsized.length,
Any::class.java,
ellipsized,
0
)
}
if (prefix != null && prefix.length > ellipsizeCallback.start) {
//We ellipsized part of the prefix, so put it back
ellipsized.replace(0, ellipsizeCallback.start, prefix)
ellipsizeCallback.end = ellipsizeCallback.end + prefix.length - ellipsizeCallback.start
ellipsizeCallback.start = prefix.length
}
if (ellipsizeCallback.start != ellipsizeCallback.end) {
if (countSpan != null) {
val visibleCount =
ellipsized.getSpans(0, ellipsized.length, TokenImageSpan::class.java).size
countSpan.setCount(tokenCount - visibleCount)
ellipsized.replace(ellipsizeCallback.start, ellipsized.length, countSpan.countText)
ellipsized.setSpan(
countSpan, ellipsizeCallback.start, ellipsized.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
return ellipsized
}
//No ellipses necessary
return null
}
private class EllipsizeCallback : TextUtils.EllipsizeCallback {
var start = 0
var end = 0
override fun ellipsized(ellipsedStart: Int, ellipsedEnd: Int) {
start = ellipsedStart
end = ellipsedEnd
}
}
}

View File

@@ -1,65 +0,0 @@
package com.tokenautocomplete
import android.annotation.SuppressLint
import kotlinx.parcelize.Parcelize
import java.util.*
@Parcelize
@SuppressLint("ParcelCreator")
open class TagTokenizer constructor(private val tagPrefixes: List<Char>) : Tokenizer {
internal constructor() : this(listOf<Char>('@', '#'))
@Suppress("MemberVisibilityCanBePrivate")
protected fun isTokenTerminator(character: Char): Boolean {
//Allow letters, numbers and underscores
return !Character.isLetterOrDigit(character) && character != '_'
}
override fun containsTokenTerminator(charSequence: CharSequence): Boolean {
for (element in charSequence) {
if (isTokenTerminator(element)) {
return true
}
}
return false
}
override fun findTokenRanges(charSequence: CharSequence, start: Int, end: Int): List<Range> {
val result = ArrayList<Range>()
if (start == end) {
//Can't have a 0 length token
return result
}
var tokenStart = Int.MAX_VALUE
for (cursor in start until end) {
val character = charSequence[cursor]
//Either this is a terminator, or we contain some content and are at the end of input
if (isTokenTerminator(character)) {
//Is there some token content? Might just be two terminators in a row
if (cursor - 1 > tokenStart) {
result.add(Range(tokenStart, cursor))
}
//mark that we don't have a candidate token start any more
tokenStart = Int.MAX_VALUE
}
//Set tokenStart when we hit a tag prefix
if (tagPrefixes.contains(character)) {
tokenStart = cursor
}
}
if (end > tokenStart) {
//There was unterminated text after a start of token
result.add(Range(tokenStart, end))
}
return result
}
override fun wrapTokenValue(unwrappedTokenValue: CharSequence): CharSequence {
return unwrappedTokenValue
}
}

View File

@@ -1,34 +0,0 @@
package com.tokenautocomplete
import android.os.Parcelable
interface Tokenizer : Parcelable {
/**
* Find all ranges that can be tokenized. This system should detect possible tokens
* both with and without having had wrapTokenValue called on the token string representation
*
* @param charSequence the string to search in
* @param start where the tokenizer should start looking for tokens
* @param end where the tokenizer should stop looking for tokens
* @return all ranges of characters that are valid tokens
*/
fun findTokenRanges(charSequence: CharSequence, start: Int, end: Int): List<Range>
/**
* Return a complete string representation of the token. Often used to add commas after email
* addresses when creating tokens
*
* This value must NOT include any leading or trailing whitespace
*
* @param unwrappedTokenValue the value to wrap
* @return the token value with any expected delimiter characters
*/
fun wrapTokenValue(unwrappedTokenValue: CharSequence): CharSequence
/**
* Return true if there is a character in the charSequence that should trigger token detection
* @param charSequence source text to look at
* @return true if charSequence contains a value that should end a token
*/
fun containsTokenTerminator(charSequence: CharSequence): Boolean
}

View File

@@ -1,76 +0,0 @@
package com.tokenautocomplete
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Paint.FontMetricsInt
import android.text.style.ReplacementSpan
import android.view.View
import android.view.ViewGroup
import androidx.annotation.IntRange
/**
* Span that holds a view it draws when rendering
*
* Created on 2/3/15.
* @author mgod
*/
open class ViewSpan(var view: View, private val layout: Layout) : ReplacementSpan() {
private var cachedMaxWidth = -1
private fun prepView() {
if (layout.maxViewSpanWidth != cachedMaxWidth || view.isLayoutRequested) {
cachedMaxWidth = layout.maxViewSpanWidth
var spec = View.MeasureSpec.AT_MOST
if (cachedMaxWidth == 0) {
//If the width is 0, allow the view to choose it's own content size
spec = View.MeasureSpec.UNSPECIFIED
}
val widthSpec = View.MeasureSpec.makeMeasureSpec(cachedMaxWidth, spec)
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
view.measure(widthSpec, heightSpec)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
}
override fun draw(
canvas: Canvas, text: CharSequence, @IntRange(from = 0) start: Int,
@IntRange(from = 0) end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint
) {
prepView()
canvas.save()
canvas.translate(x, top.toFloat())
view.draw(canvas)
canvas.restore()
}
override fun getSize(
paint: Paint, charSequence: CharSequence, @IntRange(from = 0) start: Int,
@IntRange(from = 0) end: Int, fontMetricsInt: FontMetricsInt?
): Int {
prepView()
if (fontMetricsInt != null) {
//We need to make sure the layout allots enough space for the view
val height = view.measuredHeight
var adjustedBaseline = view.baseline
//-1 means the view doesn't support baseline alignment, so align bottom to font baseline
if (adjustedBaseline == -1) {
adjustedBaseline = height
}
fontMetricsInt.top = -adjustedBaseline
fontMetricsInt.ascent = fontMetricsInt.top
fontMetricsInt.bottom = height - adjustedBaseline
fontMetricsInt.descent = fontMetricsInt.bottom
}
return view.right
}
interface Layout {
val maxViewSpanWidth: Int
}
init {
view.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
}