feature: add BaseTextInputView from MIR #4
|
|
@ -2,6 +2,10 @@ apply from: "../android-configs/lib-config.gradle"
|
|||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
|
@ -14,6 +18,8 @@ dependencies {
|
|||
|
||||
implementation "com.google.android.material:material"
|
||||
implementation "androidx.core:core-ktx"
|
||||
implementation "com.redmadrobot:inputmask:3.4.4"
|
||||
implementation "net.danlew:android.joda:2.10.3"
|
||||
|
||||
constraints {
|
||||
implementation("com.google.android.material:material") {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package ru.touchin.roboswag.views.input
|
||||
|
||||
import android.view.MotionEvent
|
||||
|
||||
typealias TouchListener = (event: MotionEvent) -> Unit
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.roboswag.views.input
|
||||
|
||||
open class BaseListenersContainer<E>(protected val listeners: MutableList<E> = mutableListOf()) {
|
||||
|
||||
fun add(listener: E): Boolean = listeners.add(listener)
|
||||
|
||||
fun remove(listener: E): Boolean = listeners.remove(listener)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
package ru.touchin.roboswag.views.input
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.graphics.Color
|
||||
import android.os.Parcelable
|
||||
import android.text.InputType
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import ru.touchin.roboswag.views.R
|
||||
import ru.touchin.roboswag.views.input.delegates.InputEventDelegate
|
||||
import ru.touchin.roboswag.views.input.delegates.bottom_hint.BottomHintState
|
||||
import ru.touchin.roboswag.views.input.delegates.bottom_hint.InputSupportHintDelegate
|
||||
import ru.touchin.roboswag.views.input.delegates.bottom_hint.ShowSupportHintMode
|
||||
import ru.touchin.roboswag.views.input.delegates.edit_text.InputEditorDelegate
|
||||
import ru.touchin.roboswag.views.input.delegates.edit_text.InputEditorDelegateImpl
|
||||
import ru.touchin.roboswag.views.input.delegates.validators.InputValidatorDelegate
|
||||
import ru.touchin.roboswag.views.input.delegates.validators.InputValidatorDelegateImpl
|
||||
import ru.touchin.roboswag.views.input.delegates.validators.ValidatorTypes
|
||||
import ru.touchin.roboswag.views.input.utils.MaskConstants
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.Validator
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.implementation.CyrillicValidator
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.implementation.DateFormatValidator
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.implementation.EmailValidator
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.implementation.EmptyValidator
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.implementation.LatinValidator
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.implementation.NumberValidator
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.implementation.PhoneValidator
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.implementation.TimeValidator
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
open class BaseTextInputView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : LinearLayout(context, attrs, defStyleAttr), InputValidatorDelegate by InputValidatorDelegateImpl(),
|
||||
InputEditorDelegate by InputEditorDelegateImpl() {
|
||||
|
||||
companion object {
|
||||
private const val NO_RESOURCE = -1
|
||||
|
||||
private const val MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT = 5000
|
||||
private const val ONE_LINE_EDIT_TEXT = 1
|
||||
private const val NONE = 0
|
||||
}
|
||||
|
||||
private val eventDelegate = InputEventDelegate()
|
||||
|
||||
init {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_base_input_text, this, true)
|
||||
initDelegates()
|
||||
|
||||
context.withStyledAttributes(attrs, R.styleable.BaseTextInputView, defStyleAttr, 0) {
|
||||
setupMask()
|
||||
setupEditOptions()
|
||||
setupEditTextPadding()
|
||||
setupHint()
|
||||
setupSupportHint()
|
||||
val errorMessageId = setupError()
|
||||
setupText()
|
||||
|
||||
setMaskFromAttr(getInt(R.styleable.BaseTextInputView_mask, MaskTypes.NONE.ordinal))
|
||||
|
||||
setValidator(
|
||||
createValidatorFromAttr(
|
||||
index = getInt(R.styleable.BaseTextInputView_validator, NONE),
|
||||
errorId = errorMessageId
|
||||
)
|
||||
)
|
||||
setValidatorMode(getInt(R.styleable.BaseTextInputView_validatorMode, InputValidatorDelegate.FLAG_NONE))
|
||||
}
|
||||
}
|
||||
|
||||
private fun TypedArray.setupText() {
|
||||
setText(getString(R.styleable.BaseTextInputView_editText))
|
||||
setTextColor(getColor(R.styleable.BaseTextInputView_editTextColor, Color.BLACK))
|
||||
val appearance = getResourceId(R.styleable.BaseTextInputView_editTextAppearance, NO_RESOURCE)
|
||||
setTextAppearance(if (appearance == NO_RESOURCE) null else appearance)
|
||||
setEditTextBackgroundTint(getColor(R.styleable.BaseTextInputView_editTextBackgroundTint, getCurrentHintTextColor()))
|
||||
}
|
||||
|
||||
private fun TypedArray.setupError(): Int {
|
||||
setSupportHintEnable(getBoolean(R.styleable.BaseTextInputView_errorEnabled, false))
|
||||
val errorMessageId = getResourceId(R.styleable.BaseTextInputView_error, NO_RESOURCE)
|
||||
|
||||
showErrorHintText(if (errorMessageId == NO_RESOURCE) null else context.getString(errorMessageId))
|
||||
setupErrorColor(getColor(R.styleable.BaseTextInputView_errorTextColor, InputSupportHintDelegate.DEFAULT_ERROR_COLOR))
|
||||
setSupportHintAppearance(getResourceId(R.styleable.BaseTextInputView_errorTextAppearance, NO_RESOURCE))
|
||||
isFloating(getBoolean(R.styleable.BaseTextInputView_floatError, true))
|
||||
|
||||
return errorMessageId
|
||||
}
|
||||
|
||||
private fun TypedArray.setupSupportHint() {
|
||||
setInformationHintText(getString(R.styleable.BaseTextInputView_informationHint))
|
||||
val index = getInt(R.styleable.BaseTextInputView_showInformationHintMode, ShowSupportHintMode.PERMANENT.ordinal)
|
||||
val informationHintColor = getColor(R.styleable.BaseTextInputView_informationHintTextColor, InputSupportHintDelegate.DEFAULT_HINT_COLOR)
|
||||
setupInformationColor(informationHintColor)
|
||||
val mode = ShowSupportHintMode.values()[index]
|
||||
setSupportHintMode(mode)
|
||||
}
|
||||
|
||||
// TODO Сделайть ренейминг на label
|
||||
private fun TypedArray.setupHint() {
|
||||
setHint(getString(R.styleable.BaseTextInputView_hint))
|
||||
val colorHint = getColor(R.styleable.BaseTextInputView_hintTextColor, InputSupportHintDelegate.DEFAULT_HINT_COLOR)
|
||||
setHintColor(colorHint)
|
||||
val appearance = getResourceId(R.styleable.BaseTextInputView_hintTextAppearance, NO_RESOURCE)
|
||||
setHintAppearance(if (appearance == NO_RESOURCE) null else appearance)
|
||||
}
|
||||
|
||||
private fun TypedArray.setupEditTextPadding() {
|
||||
val editTextPadding = getDimensionPixelSize(R.styleable.BaseTextInputView_editTextPadding, NO_RESOURCE)
|
||||
val editTextPaddingStart = getDimensionPixelSize(R.styleable.BaseTextInputView_editTextPaddingStart, getEditTextPaddingStart())
|
||||
val editTextPaddingEnd = getDimensionPixelSize(R.styleable.BaseTextInputView_editTextPaddingEnd, getEditTextPaddingEnd())
|
||||
val editTextPaddingTop = getDimensionPixelSize(R.styleable.BaseTextInputView_editTextPaddingTop, getEditTextPaddingTop())
|
||||
val editTextPaddingBottom = getDimensionPixelSize(R.styleable.BaseTextInputView_editTextPaddingBottom, getEditTextPaddingBottom())
|
||||
|
||||
if (editTextPadding == NO_RESOURCE) {
|
||||
setEditTextPadding(
|
||||
start = editTextPaddingStart,
|
||||
end = editTextPaddingEnd,
|
||||
top = editTextPaddingTop,
|
||||
bottom = editTextPaddingBottom
|
||||
)
|
||||
} else {
|
||||
setEditTextPadding(editTextPadding)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TypedArray.setupEditOptions() {
|
||||
setImeOptions(getInt(R.styleable.BaseTextInputView_imeOptions, EditorInfo.IME_NULL))
|
||||
setInputType(getInt(R.styleable.BaseTextInputView_inputType, InputType.TYPE_CLASS_TEXT))
|
||||
setDigits(getString(R.styleable.BaseTextInputView_digits).orEmpty())
|
||||
setDrawableEnd(getDrawable(R.styleable.BaseTextInputView_drawableEnd))
|
||||
setMaxLength(getInt(R.styleable.BaseTextInputView_maxLength, MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT))
|
||||
setMaxLines(getInt(R.styleable.BaseTextInputView_maxLines, ONE_LINE_EDIT_TEXT))
|
||||
|
||||
isFocusable = getBoolean(R.styleable.BaseTextInputView_editTextFocusable, false)
|
||||
isFocusableInTouchMode = getBoolean(R.styleable.BaseTextInputView_editTextFocusableInTouchMode, true)
|
||||
isLongClickable = getBoolean(R.styleable.BaseTextInputView_editTextLongClickable, true)
|
||||
}
|
||||
|
||||
private fun TypedArray.setupMask() {
|
||||
setMaskedHint(getString(R.styleable.BaseTextInputView_editTextMaskedHint).orEmpty())
|
||||
setMaskedHintColor(getColor(R.styleable.BaseTextInputView_editTextMaskedHintColor, Color.BLACK))
|
||||
}
|
||||
|
||||
private fun createValidatorFromAttr(index: Int, @StringRes errorId: Int): Validator? {
|
||||
val validatorType = ValidatorTypes.values()[index]
|
||||
|
||||
return when (validatorType) {
|
||||
ValidatorTypes.NONE -> null
|
||||
ValidatorTypes.CYRILLIC -> CyrillicValidator(errorId)
|
||||
ValidatorTypes.PHONE -> PhoneValidator(errorId)
|
||||
ValidatorTypes.DATE -> DateFormatValidator(errorId)
|
||||
ValidatorTypes.TIME -> TimeValidator(errorId)
|
||||
ValidatorTypes.EMPTY -> EmptyValidator(errorId)
|
||||
ValidatorTypes.EMAIL -> EmailValidator(errorId)
|
||||
ValidatorTypes.LATIN -> LatinValidator(errorId)
|
||||
ValidatorTypes.NUMBER -> NumberValidator(errorId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMaskFromAttr(index: Int) {
|
||||
val maskType = MaskTypes.values()[index]
|
||||
|
||||
val format = when (maskType) {
|
||||
MaskTypes.NONE -> null
|
||||
MaskTypes.PHONE -> MaskConstants.PHONE_MASK
|
||||
MaskTypes.DATE -> MaskConstants.DATE_MASK
|
||||
MaskTypes.BANK_CARD -> MaskConstants.BANK_CARD
|
||||
MaskTypes.MID -> MaskConstants.MID
|
||||
}
|
||||
|
||||
when (format != null) {
|
||||
true -> setupMask(format, false)
|
||||
false -> setupMask(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initDelegates() {
|
||||
bindWithBaseTextInputView(this)
|
||||
bindWithEditorDelegate(this)
|
||||
eventDelegate.initEvents(validatorDelegate = this, editorDelegate = this)
|
||||
}
|
||||
|
||||
@Suppress("detekt.UnnecessaryApply")
|
||||
override fun onSaveInstanceState(): Parcelable? = super.onSaveInstanceState()?.let { superState ->
|
||||
BaseTextInputViewSavedState(superState).apply {
|
||||
saveData(
|
||||
BaseTextInputViewSavedState.Data(
|
||||
text = getExtractedText(),
|
||||
supportHintText = getSupportHintText(),
|
||||
bottomHintState = getSupportHintState()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable?) {
|
||||
if (state is BaseTextInputViewSavedState) {
|
||||
super.onRestoreInstanceState(state.superState)
|
||||
|
||||
val restoreData = state.restoreData()
|
||||
setText(text = restoreData.text, withValidate = false)
|
||||
|
||||
when (restoreData.bottomHintState) {
|
||||
BottomHintState.INFORMATION -> showInformationHint(restoreData.supportHintText)
|
||||
BottomHintState.ERROR -> showErrorHintText(restoreData.supportHintText)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
} else {
|
||||
super.onRestoreInstanceState(state)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("detekt.LabeledExpression", "ClickableViewAccessibility")
|
||||
fun onSimpleTouchActionUp(action: () -> Unit) = onTouchActionUp {
|
||||
action.invoke()
|
||||
|
||||
return@onTouchActionUp true
|
||||
}
|
||||
|
||||
@Suppress("detekt.LabeledExpression", "ClickableViewAccessibility")
|
||||
fun onTouchActionUp(action: (MotionEvent) -> Boolean) {
|
||||
setOnTouchListener { _, event ->
|
||||
return@setOnTouchListener when (event.action) {
|
||||
MotionEvent.ACTION_UP -> action.invoke(event)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setText(text: String?, withValidate: Boolean) = when (withValidate) {
|
||||
true -> setText(text).also { manualValidate() }
|
||||
false -> actionWithDisableValidate { setText(text) }
|
||||
}
|
||||
|
||||
fun setTextColorId(@ColorRes colorId: Int) = setTextColor(context.getColor(colorId))
|
||||
|
||||
private enum class MaskTypes {
|
||||
NONE,
|
||||
PHONE,
|
||||
DATE,
|
||||
BANK_CARD,
|
||||
MID
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package ru.touchin.roboswag.views.input
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import ru.touchin.roboswag.views.input.delegates.bottom_hint.BottomHintState
|
||||
|
||||
class BaseTextInputViewSavedState : View.BaseSavedState {
|
||||
|
||||
private var savedState = Data()
|
||||
|
||||
fun saveData(newState: Data) {
|
||||
savedState = newState
|
||||
}
|
||||
|
||||
fun restoreData(): Data = savedState
|
||||
|
||||
constructor(parcel: Parcel) : super(parcel) {
|
||||
savedState = savedState.copy(
|
||||
text = parcel.readString().orEmpty(),
|
||||
supportHintText = parcel.readString(),
|
||||
bottomHintState = getSupportHintState(parcel)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSupportHintState(parcel: Parcel) = parcel.readString()
|
||||
?.let(BottomHintState::valueOf)
|
||||
?: BottomHintState.INFORMATION
|
||||
|
||||
constructor(superState: Parcelable) : super(superState)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
super.writeToParcel(parcel, flags)
|
||||
parcel.writeString(savedState.text)
|
||||
parcel.writeString(savedState.supportHintText)
|
||||
parcel.writeString(savedState.bottomHintState.name)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmField
|
||||
val CREATOR = object : Parcelable.Creator<BaseTextInputViewSavedState> {
|
||||
|
||||
override fun createFromParcel(parcel: Parcel): BaseTextInputViewSavedState = BaseTextInputViewSavedState(parcel)
|
||||
|
||||
override fun newArray(size: Int): Array<BaseTextInputViewSavedState?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
data class Data(
|
||||
val text: String = "",
|
||||
val supportHintText: String? = "",
|
||||
val bottomHintState: BottomHintState = BottomHintState.INFORMATION
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package ru.touchin.roboswag.views.input
|
||||
|
||||
import android.view.View
|
||||
|
||||
class FocusChangeListenerContainer : BaseListenersContainer<View.OnFocusChangeListener>(), View.OnFocusChangeListener {
|
||||
|
||||
override fun onFocusChange(v: View?, hasFocus: Boolean) {
|
||||
for (i in listeners.indices) {
|
||||
listeners[i].onFocusChange(v, hasFocus)
|
||||
}
|
||||
}
|
||||
|
||||
fun add(listener: (Boolean) -> Unit): Boolean = listeners.add(
|
||||
View.OnFocusChangeListener { _, hasFocus -> listener.invoke(hasFocus) }
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.roboswag.views.input
|
||||
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
|
||||
interface GlobalEditTextEventListener : View.OnFocusChangeListener, TextWatcher
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package ru.touchin.roboswag.views.input
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextWatcher
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.widget.EditText
|
||||
import androidx.annotation.ColorInt
|
||||
import com.redmadrobot.inputmask.MaskedTextChangedListener
|
||||
import com.redmadrobot.inputmask.helper.Mask
|
||||
import com.redmadrobot.inputmask.model.CaretString
|
||||
|
||||
class HintedMaskedEditTextListener(
|
||||
format: String,
|
||||
field: EditText,
|
||||
autocomplete: Boolean,
|
||||
valueListener: ValueListener? = null,
|
||||
listener: TextWatcher? = null
|
||||
) : MaskedTextChangedListener(format, field = field, autocomplete = autocomplete, valueListener = valueListener, listener = listener) {
|
||||
|
||||
lateinit var hint: String
|
||||
|
||||
@ColorInt
|
||||
var hintColor: Int = field.context.getColor(android.R.color.darker_gray)
|
||||
|
||||
override fun afterTextChanged(edit: Editable?) {
|
||||
field.get()?.removeTextChangedListener(this)
|
||||
|
||||
edit?.replace(0, edit.length, afterText + getCurrentHint())
|
||||
|
||||
if (hint.isNotEmpty()) {
|
||||
edit?.setSpan(ForegroundColorSpan(hintColor), afterText.length, hint.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
|
||||
field.get()?.setSelection(this.caretPosition)
|
||||
|
||||
field.get()?.addTextChangedListener(this)
|
||||
listener?.afterTextChanged(edit)
|
||||
}
|
||||
|
||||
// Point deletion logic is used to auto removing delimiter and change caret position
|
||||
// Ex: 12.34.|5678 -> 12.3|5.678y, not 12.34.|5678 -> 12.34|.5678
|
||||
override fun onTextChanged(text: CharSequence, cursorPosition: Int, before: Int, count: Int) {
|
||||
val isDeletion: Boolean = before > 0 && count == 0
|
||||
val isPointDeletion: Boolean = isDeletion && getMaskDotsDelimiterIndex().contains(cursorPosition)
|
||||
val pointDeletionString = if (isPointDeletion) StringBuilder(text).apply { deleteCharAt(cursorPosition - 1) }.toString() else ""
|
||||
|
||||
val result: Mask.Result =
|
||||
this.mask.apply(
|
||||
CaretString(
|
||||
if (isPointDeletion) pointDeletionString else text.toString(),
|
||||
when {
|
||||
isPointDeletion -> cursorPosition - 1
|
||||
isDeletion -> cursorPosition
|
||||
else -> cursorPosition + count
|
||||
}
|
||||
),
|
||||
this.autocomplete && !isDeletion
|
||||
)
|
||||
this.afterText = result.formattedText.string
|
||||
this.caretPosition = when {
|
||||
isPointDeletion -> cursorPosition - 1
|
||||
isDeletion -> cursorPosition
|
||||
else -> result.formattedText.caretPosition
|
||||
}
|
||||
this.valueListener?.onTextChanged(result.complete, result.extractedValue)
|
||||
listener?.onTextChanged(result.extractedValue, cursorPosition, before, count)
|
||||
}
|
||||
|
||||
private fun getCurrentHint(): String = if (hint.isNotEmpty()) {
|
||||
hint.substring(afterText.length, hint.length)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
private fun getMaskDotsDelimiterIndex(): List<Int> {
|
||||
val indexDelimiterList = mutableListOf<Int>()
|
||||
|
||||
hint.forEachIndexed { index, char ->
|
||||
if (char == '.') indexDelimiterList.add(index)
|
||||
}
|
||||
|
||||
return indexDelimiterList
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package ru.touchin.roboswag.views.input
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.text.TextWatcher
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import ru.touchin.roboswag.views.R
|
||||
|
||||
class MaskedHintEditText @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : TextInputEditText(context, attrs, defStyleAttr) {
|
||||
|
||||
private var maskedEditTextListener: HintedMaskedEditTextListener? = null
|
||||
|
||||
lateinit var maskedHint: String
|
||||
|
||||
@ColorInt
|
||||
private var maskedHintColor: Int = context.getColor(android.R.color.darker_gray)
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(attrs, R.styleable.MaskedHintEditText, defStyleAttr, 0) {
|
||||
maskedHint = getString(R.styleable.MaskedHintEditText_maskedHint).orEmpty()
|
||||
maskedHintColor = getColor(R.styleable.MaskedHintEditText_maskedHintColor, maskedHintColor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addTextChangedListener(watcher: TextWatcher) {
|
||||
val textWatcher = watcher as? HintedMaskedEditTextListener
|
||||
|
||||
textWatcher?.let {
|
||||
removeTextChangedListener(maskedEditTextListener)
|
||||
maskedEditTextListener = textWatcher
|
||||
textWatcher.hint = maskedHint
|
||||
textWatcher.hintColor = maskedHintColor
|
||||
}
|
||||
|
||||
super.addTextChangedListener(watcher)
|
||||
}
|
||||
|
||||
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
||||
super.onSelectionChanged(selStart, selEnd)
|
||||
|
||||
val inputStringLength = maskedEditTextListener?.afterText?.length ?: selEnd
|
||||
|
||||
if (selStart > inputStringLength) {
|
||||
setSelection(inputStringLength)
|
||||
}
|
||||
}
|
||||
|
||||
fun getRawText(): String = maskedEditTextListener?.afterText ?: text?.toString().orEmpty()
|
||||
|
||||
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
|
||||
super.onFocusChanged(focused, direction, previouslyFocusedRect)
|
||||
maskedEditTextListener?.let {
|
||||
when {
|
||||
focused && getRawText().isEmpty() -> maskedEditTextListener?.hint = maskedHint
|
||||
!focused && getRawText().isEmpty() -> maskedEditTextListener?.hint = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setMaskedHintColorId(@ColorRes colorId: Int) {
|
||||
maskedHintColor = context.getColor(colorId)
|
||||
}
|
||||
|
||||
fun setMaskedHintColor(color: Int) {
|
||||
maskedHintColor = color
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package ru.touchin.roboswag.views.input
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
|
||||
class TextWatcherContainer : BaseListenersContainer<TextWatcher>(), TextWatcher {
|
||||
|
||||
override fun beforeTextChanged(text: CharSequence?, start: Int, count: Int, after: Int) =
|
||||
listeners.forEach { textWatcher -> textWatcher.beforeTextChanged(text, start, count, after) }
|
||||
|
||||
override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) =
|
||||
listeners.forEach { textWatcher -> textWatcher.onTextChanged(text, start, before, count) }
|
||||
|
||||
override fun afterTextChanged(text: Editable?) =
|
||||
listeners.forEach { textWatcher -> textWatcher.afterTextChanged(text) }
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package ru.touchin.roboswag.views.input
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
|
||||
class TouchEventListenerContainer : BaseListenersContainer<TouchListener>(), View.OnTouchListener {
|
||||
|
||||
private var mainTouchEventListener: View.OnTouchListener? = null
|
||||
|
||||
fun setMainTouchEventListener(listener: View.OnTouchListener?) {
|
||||
mainTouchEventListener = listener
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(v: View?, event: MotionEvent): Boolean = mainTouchEventListener
|
||||
?.onTouch(v, event).also { listeners.forEach { it.invoke(event) } }
|
||||
?: false
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package ru.touchin.roboswag.views.input.delegates
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import ru.touchin.roboswag.views.input.GlobalEditTextEventListener
|
||||
import ru.touchin.roboswag.views.input.delegates.edit_text.InputEditorDelegate
|
||||
import ru.touchin.roboswag.views.input.delegates.validators.InputValidatorDelegate
|
||||
import ru.touchin.roboswag.views.input.delegates.validators.InputValidatorDelegate.Companion.FLAG_AFTER_TEXT_CHANGED
|
||||
import ru.touchin.roboswag.views.input.delegates.validators.InputValidatorDelegate.Companion.FLAG_BEFORE_TEXT_CHANGED
|
||||
import ru.touchin.roboswag.views.input.delegates.validators.InputValidatorDelegate.Companion.FLAG_HAS_FOCUS
|
||||
import ru.touchin.roboswag.views.input.delegates.validators.InputValidatorDelegate.Companion.FLAG_LOSS_FOCUS
|
||||
import ru.touchin.roboswag.views.input.delegates.validators.InputValidatorDelegate.Companion.FLAG_ON_TEXT_CHANGED
|
||||
|
||||
class InputEventDelegate {
|
||||
|
||||
// TODO перекомпановать инпут: InputEventDelegate должен отвечает только за поставку событий,
|
||||
// "handle" должен быть в другом месте
|
||||
|
||||
private lateinit var globalEditTextEventListener: GlobalEditTextEventListener
|
||||
|
||||
fun initEvents(validatorDelegate: InputValidatorDelegate, editorDelegate: InputEditorDelegate) {
|
||||
setupGlobalEventListener(validatorDelegate)
|
||||
bindWithEditDelegateEvents(editorDelegate)
|
||||
}
|
||||
|
||||
private fun bindWithEditDelegateEvents(editorDelegate: InputEditorDelegate) {
|
||||
editorDelegate.setOnFocusChangeListener(globalEditTextEventListener)
|
||||
|
||||
editorDelegate.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(text: CharSequence?, start: Int, count: Int, after: Int) =
|
||||
globalEditTextEventListener.beforeTextChanged(text, start, count, after)
|
||||
|
||||
override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) =
|
||||
globalEditTextEventListener.onTextChanged(text, start, before, count)
|
||||
|
||||
override fun afterTextChanged(text: Editable?) =
|
||||
globalEditTextEventListener.afterTextChanged(text)
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupGlobalEventListener(validatorDelegate: InputValidatorDelegate) {
|
||||
globalEditTextEventListener = object : GlobalEditTextEventListener {
|
||||
override fun onFocusChange(v: View?, hasFocus: Boolean) = when (hasFocus) {
|
||||
true -> handleEditEvent(FLAG_HAS_FOCUS, validatorDelegate)
|
||||
false -> handleEditEvent(FLAG_LOSS_FOCUS, validatorDelegate)
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) =
|
||||
handleEditEvent(FLAG_BEFORE_TEXT_CHANGED, validatorDelegate)
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) =
|
||||
handleEditEvent(FLAG_ON_TEXT_CHANGED, validatorDelegate)
|
||||
|
||||
override fun afterTextChanged(s: Editable?) =
|
||||
handleEditEvent(FLAG_AFTER_TEXT_CHANGED, validatorDelegate)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEditEvent(event: Int, validateDelegate: InputValidatorDelegate) {
|
||||
when (event and validateDelegate.getValidateModeFlag()) {
|
||||
FLAG_HAS_FOCUS,
|
||||
FLAG_LOSS_FOCUS,
|
||||
FLAG_BEFORE_TEXT_CHANGED,
|
||||
FLAG_ON_TEXT_CHANGED,
|
||||
FLAG_AFTER_TEXT_CHANGED -> {
|
||||
validateDelegate.validate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.roboswag.views.input.delegates.bottom_hint
|
||||
|
||||
enum class BottomHintState {
|
||||
ERROR,
|
||||
INFORMATION,
|
||||
EMPTY
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package ru.touchin.roboswag.views.input.delegates.bottom_hint
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.StyleRes
|
||||
|
||||
@Suppress("detekt.TooManyFunctions")
|
||||
interface InputSupportHintDelegate {
|
||||
|
||||
companion object {
|
||||
// TODO Заменить на использование цветов из ресурсов
|
||||
const val DEFAULT_ERROR_COLOR = Color.RED
|
||||
const val DEFAULT_HINT_COLOR = Color.LTGRAY
|
||||
}
|
||||
|
||||
fun showSupportHint()
|
||||
fun hideSupportHint()
|
||||
fun supportHintIsShow(): Boolean
|
||||
fun isFloating(isFloating: Boolean)
|
||||
fun setSupportHintEnable(isEnable: Boolean)
|
||||
fun setSupportHintMode(mode: ShowSupportHintMode)
|
||||
fun getSupportHintState(): BottomHintState
|
||||
fun hasError(): Boolean
|
||||
|
||||
fun setSupportHintText(text: String?)
|
||||
fun setSupportHintText(text: String?, state: BottomHintState)
|
||||
fun setSupportHintText(@StringRes textId: Int)
|
||||
fun setSupportHintTextColor(color: Int)
|
||||
fun setSupportHintTextColorId(@ColorRes colorId: Int)
|
||||
fun setSupportHintStartDrawable(@DrawableRes drawableId: Int)
|
||||
fun setSupportHintStartDrawable(drawable: Drawable?)
|
||||
fun setSupportHintStartDrawable(drawable: Drawable?, sizePx: Int?, gravityInt: Int?, marginPx: Int?)
|
||||
fun clearSupportHintStartDrawable()
|
||||
fun getSupportHintText(): String?
|
||||
|
||||
fun showInformationHint()
|
||||
fun showInformationHint(text: String?)
|
||||
fun setInformationHintText(text: String?)
|
||||
fun setupInformationColor(color: Int)
|
||||
fun setupInformationColorId(@ColorRes colorId: Int)
|
||||
|
||||
fun showErrorHintText(errorText: String?)
|
||||
fun showErrorHintText(@StringRes errorText: Int)
|
||||
fun setupErrorColor(color: Int)
|
||||
|
||||
fun setErrorColorId(@ColorRes colorId: Int)
|
||||
fun setSupportHintAppearance(@StyleRes style: Int?)
|
||||
}
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
package ru.touchin.roboswag.views.input.delegates.bottom_hint
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.setMargins
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import ru.touchin.roboswag.views.R
|
||||
import ru.touchin.roboswag.views.input.delegates.edit_text.InputEditorDelegateImpl
|
||||
|
||||
@Suppress("detekt.TooManyFunctions")
|
||||
class InputSupportHintDelegateImpl : InputSupportHintDelegate {
|
||||
|
||||
private companion object {
|
||||
const val UNDERLINE_FOCUS_HEIGHT = 2.0F
|
||||
const val UNDERLINE_DEFAULT_HEIGHT = 1.0F
|
||||
}
|
||||
|
||||
private lateinit var textInputLayout: TextInputLayout
|
||||
private lateinit var supportHintTextView: TextView
|
||||
private lateinit var supportHintStartImage: ImageView
|
||||
private lateinit var underline: View
|
||||
private var informationHintColor: Int = InputSupportHintDelegate.DEFAULT_HINT_COLOR
|
||||
private var errorHintColor: Int = InputSupportHintDelegate.DEFAULT_ERROR_COLOR
|
||||
private var showSupportHintMode: ShowSupportHintMode = ShowSupportHintMode.PERMANENT
|
||||
private var informationHintText: String? = null
|
||||
private var supportHintIsShow = false
|
||||
private var isFloating = true
|
||||
|
||||
private var currentState: BottomHintState = BottomHintState.INFORMATION
|
||||
set(value) {
|
||||
field = value
|
||||
updateUnderlineColor(value, textInputLayout.editText?.hasFocus() ?: false)
|
||||
}
|
||||
|
||||
fun init(
|
||||
textInputView: TextInputLayout,
|
||||
underline: View,
|
||||
inputEditorDelegateImpl: InputEditorDelegateImpl,
|
||||
supportHintTextView: TextView,
|
||||
supportHintStartImage: ImageView
|
||||
) {
|
||||
this.textInputLayout = textInputView
|
||||
this.underline = underline
|
||||
this.supportHintTextView = supportHintTextView
|
||||
this.supportHintStartImage = supportHintStartImage
|
||||
|
||||
setupUnderline(inputEditorDelegateImpl)
|
||||
}
|
||||
|
||||
private fun setupUnderline(inputEditorDelegateImpl: InputEditorDelegateImpl) {
|
||||
inputEditorDelegateImpl.setOnFocusChangeListener(View.OnFocusChangeListener { _, hasFocus ->
|
||||
when (hasFocus) {
|
||||
true -> underline.scaleY = UNDERLINE_FOCUS_HEIGHT
|
||||
false -> underline.scaleY = UNDERLINE_DEFAULT_HEIGHT
|
||||
}
|
||||
updateUnderlineColor(currentState, hasFocus)
|
||||
})
|
||||
}
|
||||
|
||||
override fun showSupportHint() {
|
||||
supportHintTextView.isVisible = true
|
||||
}
|
||||
|
||||
override fun hideSupportHint() {
|
||||
supportHintTextView.text = null
|
||||
currentState = BottomHintState.EMPTY
|
||||
|
||||
if (isFloating) {
|
||||
supportHintTextView.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun supportHintIsShow(): Boolean = supportHintIsShow
|
||||
|
||||
override fun isFloating(isFloating: Boolean) {
|
||||
this.isFloating = isFloating
|
||||
}
|
||||
|
||||
override fun setSupportHintEnable(isEnable: Boolean) {
|
||||
supportHintTextView.isVisible = isEnable
|
||||
supportHintStartImage.isVisible = isEnable && supportHintStartImage.drawable != null
|
||||
}
|
||||
|
||||
override fun setSupportHintMode(mode: ShowSupportHintMode) {
|
||||
showSupportHintMode = mode
|
||||
}
|
||||
|
||||
override fun setSupportHintText(text: String?) {
|
||||
if (text.isNullOrEmpty()) {
|
||||
hideSupportHint()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val color = when (currentState) {
|
||||
BottomHintState.ERROR -> errorHintColor
|
||||
BottomHintState.INFORMATION -> informationHintColor
|
||||
BottomHintState.EMPTY -> return
|
||||
}
|
||||
|
||||
supportHintTextView.setTextColor(ColorStateList.valueOf(color))
|
||||
|
||||
if (supportHintTextView.isVisible || isFloating) {
|
||||
|
||||
supportHintTextView.isVisible = isFloating
|
||||
|
||||
if (supportHintTextView.text.toString() != text) {
|
||||
// Чтобы не было мельканий
|
||||
supportHintTextView.text = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setSupportHintText(text: String?, state: BottomHintState) {
|
||||
currentState = state
|
||||
updateUnderlineColor(currentState, textInputLayout.editText?.hasFocus() ?: false)
|
||||
setSupportHintText(text)
|
||||
}
|
||||
|
||||
override fun setSupportHintTextColor(color: Int) {
|
||||
val colorList = ColorStateList.valueOf(color)
|
||||
supportHintTextView.setTextColor(colorList)
|
||||
supportHintStartImage.imageTintList = colorList
|
||||
}
|
||||
|
||||
override fun setSupportHintTextColorId(colorId: Int) =
|
||||
supportHintTextView.setTextColor(ColorStateList.valueOf(context().getColor(colorId)))
|
||||
|
||||
override fun setSupportHintStartDrawable(drawableId: Int) =
|
||||
setSupportHintStartDrawable(ContextCompat.getDrawable(context(), drawableId))
|
||||
|
||||
override fun setSupportHintStartDrawable(drawable: Drawable?) =
|
||||
setSupportHintStartDrawable(drawable, null, null, null)
|
||||
|
||||
override fun setSupportHintStartDrawable(drawable: Drawable?, sizePx: Int?, gravityInt: Int?, marginPx: Int?) {
|
||||
supportHintStartImage.setImageDrawable(drawable)
|
||||
supportHintStartImage.isVisible = supportHintTextView.isVisible && drawable != null
|
||||
|
||||
val layoutParams = supportHintStartImage.layoutParams as? LinearLayout.LayoutParams ?: return
|
||||
|
||||
sizePx?.let { size ->
|
||||
layoutParams.width = size
|
||||
layoutParams.height = size
|
||||
}
|
||||
gravityInt?.let { gravity ->
|
||||
layoutParams.gravity = gravity
|
||||
}
|
||||
marginPx?.let { margin ->
|
||||
layoutParams.setMargins(margin)
|
||||
}
|
||||
|
||||
supportHintStartImage.layoutParams = layoutParams
|
||||
supportHintStartImage.requestLayout()
|
||||
}
|
||||
|
||||
override fun clearSupportHintStartDrawable() {
|
||||
supportHintStartImage.setImageDrawable(null)
|
||||
supportHintStartImage.isVisible = false
|
||||
}
|
||||
|
||||
private fun context(): Context = textInputLayout.context
|
||||
|
||||
private fun updateUnderlineColor(currentState: BottomHintState, hasFocus: Boolean) {
|
||||
val color = when {
|
||||
currentState == BottomHintState.ERROR -> errorHintColor
|
||||
hasFocus -> context().getColor(android.R.color.darker_gray)
|
||||
else -> informationHintColor
|
||||
}
|
||||
|
||||
underline.background = ColorDrawable(color)
|
||||
}
|
||||
|
||||
override fun getSupportHintState(): BottomHintState = currentState
|
||||
|
||||
override fun hasError(): Boolean = currentState == BottomHintState.ERROR
|
||||
|
||||
override fun setSupportHintText(@StringRes textId: Int) = setSupportHintText(context().getString(textId))
|
||||
|
||||
override fun getSupportHintText(): String? = supportHintTextView.text?.toString()
|
||||
|
||||
override fun setInformationHintText(text: String?) {
|
||||
informationHintText = text
|
||||
}
|
||||
|
||||
override fun setupInformationColor(color: Int) {
|
||||
informationHintColor = color
|
||||
}
|
||||
|
||||
override fun setupInformationColorId(colorId: Int) = setupInformationColor(context().getColor(colorId))
|
||||
|
||||
override fun showInformationHint(text: String?) {
|
||||
informationHintText = text
|
||||
showInformationHint()
|
||||
}
|
||||
|
||||
override fun showInformationHint() = setSupportHintText(informationHintText, BottomHintState.INFORMATION)
|
||||
|
||||
override fun showErrorHintText(errorText: String?) = setSupportHintText(errorText, BottomHintState.ERROR)
|
||||
|
||||
override fun showErrorHintText(errorText: Int) = showErrorHintText(context().getString(errorText))
|
||||
|
||||
override fun setupErrorColor(color: Int) {
|
||||
errorHintColor = color
|
||||
}
|
||||
|
||||
override fun setErrorColorId(colorId: Int) = setupErrorColor(context().getColor(colorId))
|
||||
|
||||
override fun setSupportHintAppearance(style: Int?) {
|
||||
if (style == null) return
|
||||
supportHintTextView.setTextAppearance(style)
|
||||
}
|
||||
|
||||
fun clearCurrentError() {
|
||||
val currentText = textInputLayout.editText?.text?.toString().orEmpty()
|
||||
|
||||
when {
|
||||
informationHintText.isNullOrEmpty() -> hideSupportHint()
|
||||
showSupportHintMode == ShowSupportHintMode.PERMANENT -> showInformationHint()
|
||||
showSupportHintMode == ShowSupportHintMode.ON_EMPTY && currentText.isEmpty() -> showInformationHint()
|
||||
else -> hideSupportHint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.roboswag.views.input.delegates.bottom_hint
|
||||
|
||||
enum class ShowSupportHintMode {
|
||||
PERMANENT,
|
||||
ON_EMPTY
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package ru.touchin.roboswag.views.input.delegates.edit_text
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.InputFilter
|
||||
import android.text.TextWatcher
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StyleRes
|
||||
import ru.touchin.roboswag.views.input.BaseTextInputView
|
||||
import ru.touchin.roboswag.views.input.HintedMaskedEditTextListener
|
||||
import ru.touchin.roboswag.views.input.TouchListener
|
||||
import ru.touchin.roboswag.views.input.delegates.bottom_hint.InputSupportHintDelegate
|
||||
|
||||
@Suppress("detekt.TooManyFunctions")
|
||||
interface InputEditorDelegate : InputSupportHintDelegate {
|
||||
|
||||
fun bindWithBaseTextInputView(baseTextInputView: BaseTextInputView)
|
||||
fun getEditText(): EditText
|
||||
|
||||
fun getExtractedText(): String
|
||||
fun getRawText(): String
|
||||
fun setText(text: String?)
|
||||
fun getSelectionStart(): Int
|
||||
fun setSelectionStart(index: Int)
|
||||
fun setSelectorToEnd()
|
||||
|
||||
fun setHint(text: String?)
|
||||
fun setHintColor(color: Int)
|
||||
fun setHintColorId(@ColorRes colorId: Int)
|
||||
fun setHintAppearance(@StyleRes style: Int?)
|
||||
|
||||
fun getCurrentHintTextColor(): Int
|
||||
|
||||
fun setFilters(vararg filters: InputFilter)
|
||||
fun setLongClickable(longClickable: Boolean)
|
||||
fun setEnabled(enabled: Boolean)
|
||||
fun setOnEditorActionListener(listener: TextView.OnEditorActionListener)
|
||||
fun setOnTouchListener(listener: View.OnTouchListener?)
|
||||
fun setSupportTouchListener(listener: TouchListener)
|
||||
|
||||
fun isEndDrawableIconClicked(event: MotionEvent): Boolean
|
||||
fun setFocusable(focusable: Boolean)
|
||||
fun setFocusableInTouchMode(focusable: Boolean)
|
||||
fun hasFocus(): Boolean
|
||||
fun clearFocus()
|
||||
fun setOnIconClickListener(listener: (() -> Unit)?)
|
||||
fun requestFocus(direction: Int, previouslyFocusedRect: Rect?): Boolean
|
||||
fun setOnFocusChangeListener(listener: View.OnFocusChangeListener?)
|
||||
fun removeOnFocusChangeListener(listener: View.OnFocusChangeListener)
|
||||
|
||||
fun setTextColor(color: Int)
|
||||
fun setTextAppearance(@StyleRes style: Int?)
|
||||
|
||||
fun setInputType(type: Int)
|
||||
fun setImeOptions(imeOptions: Int)
|
||||
|
||||
fun setDigits(digit: String)
|
||||
|
||||
fun setMaxLines(maxLines: Int)
|
||||
fun setMaxLength(maxLength: Int)
|
||||
fun setDrawableEnd(@DrawableRes drawable: Int)
|
||||
fun setDrawableEnd(drawable: Drawable?)
|
||||
|
||||
fun setEditTextBackgroundTint(color: Int)
|
||||
fun setEditTextBackgroundTintId(@ColorRes colorId: Int)
|
||||
|
||||
fun addTextChangedListener(watcher: TextWatcher)
|
||||
fun removeTextChangedListener(watcher: TextWatcher)
|
||||
fun setupMask(format: String, autocomplete: Boolean)
|
||||
fun setupMask(maskListener: HintedMaskedEditTextListener?)
|
||||
fun setOnMaskFilledListener(listener: MaskFilledListener?)
|
||||
fun setOnMaskFilledListener(listener: (Boolean, String) -> Unit)
|
||||
fun setMaskedHint(text: String)
|
||||
fun setMaskedHintColor(color: Int)
|
||||
fun setMaskedHintColorId(@ColorRes colorId: Int)
|
||||
fun maskIsFilled(): Boolean?
|
||||
|
||||
fun setEditTextMargin(margin: Int)
|
||||
fun setEditTextMargin(start: Int, top: Int, end: Int, bottom: Int)
|
||||
|
||||
fun setEditTextPadding(start: Int, top: Int, end: Int, bottom: Int)
|
||||
fun setEditTextPadding(padding: Int)
|
||||
|
||||
fun getEditTextPaddingStart(): Int
|
||||
fun getEditTextPaddingEnd(): Int
|
||||
fun getEditTextPaddingTop(): Int
|
||||
fun getEditTextPaddingBottom(): Int
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,394 @@
|
|||
package ru.touchin.roboswag.views.input.delegates.edit_text
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.InputFilter
|
||||
import android.text.TextWatcher
|
||||
import android.text.method.DigitsKeyListener
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.StyleRes
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.redmadrobot.inputmask.MaskedTextChangedListener
|
||||
import ru.touchin.roboswag.views.input.BaseTextInputView
|
||||
import ru.touchin.roboswag.views.input.FocusChangeListenerContainer
|
||||
import ru.touchin.roboswag.views.input.HintedMaskedEditTextListener
|
||||
import ru.touchin.roboswag.views.input.MaskedHintEditText
|
||||
import ru.touchin.roboswag.views.input.TextWatcherContainer
|
||||
import ru.touchin.roboswag.views.input.TouchEventListenerContainer
|
||||
import ru.touchin.roboswag.views.input.TouchListener
|
||||
import ru.touchin.roboswag.views.input.delegates.bottom_hint.BottomHintState
|
||||
import ru.touchin.roboswag.views.input.delegates.bottom_hint.InputSupportHintDelegate
|
||||
import ru.touchin.roboswag.views.input.delegates.bottom_hint.InputSupportHintDelegateImpl
|
||||
import ru.touchin.roboswag.views.input.delegates.bottom_hint.ShowSupportHintMode
|
||||
import setMargins
|
||||
|
||||
@Suppress("detekt.TooManyFunctions")
|
||||
class InputEditorDelegateImpl : InputEditorDelegate {
|
||||
|
||||
private companion object {
|
||||
const val DRAWABLE_END = 2 // Индекс правого drawable в compoundDrawables TextView
|
||||
const val TEXT_INPUT_LAYOUT_TAG = "text_input_layout"
|
||||
const val EDIT_TEXT_TAG = "edit_text"
|
||||
const val UNDERLINE_TAG = "underline"
|
||||
const val SUPPORT_HINT_TAG = "support_hint"
|
||||
const val SUPPORT_HINT_START_IMAGE_TAG = "support_hint_start_image"
|
||||
}
|
||||
|
||||
private var hintColor: Int = InputSupportHintDelegate.DEFAULT_ERROR_COLOR
|
||||
|
||||
private lateinit var textInputLayout: TextInputLayout
|
||||
private lateinit var supportHintTextView: TextView
|
||||
private lateinit var supportHintStartImage: ImageView
|
||||
private lateinit var editText: MaskedHintEditText
|
||||
private lateinit var underline: View
|
||||
|
||||
private var extractedText: String? = null
|
||||
private var maskIsFilled: Boolean? = null
|
||||
private var maskFieldListener: MaskFilledListener? = null
|
||||
private var onIconClickListener: (() -> Unit)? = null
|
||||
|
||||
private val focusChangeListenerContainer = FocusChangeListenerContainer()
|
||||
private val textWatcherContainer = TextWatcherContainer()
|
||||
private val touchEventListenerContainer = TouchEventListenerContainer()
|
||||
|
||||
private val supportHintDelegate = InputSupportHintDelegateImpl()
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun bindWithBaseTextInputView(baseTextInputView: BaseTextInputView) {
|
||||
textInputLayout = baseTextInputView.findViewWithTag(TEXT_INPUT_LAYOUT_TAG)
|
||||
editText = baseTextInputView.findViewWithTag(EDIT_TEXT_TAG)
|
||||
underline = baseTextInputView.findViewWithTag(UNDERLINE_TAG)
|
||||
supportHintTextView = baseTextInputView.findViewWithTag(SUPPORT_HINT_TAG)
|
||||
supportHintStartImage = baseTextInputView.findViewWithTag(SUPPORT_HINT_START_IMAGE_TAG)
|
||||
|
||||
textInputLayout.id = View.generateViewId()
|
||||
editText.id = View.generateViewId()
|
||||
underline.id = View.generateViewId()
|
||||
supportHintTextView.id = View.generateViewId()
|
||||
supportHintStartImage.id = View.generateViewId()
|
||||
|
||||
supportHintDelegate.init(textInputLayout, underline, this, supportHintTextView, supportHintStartImage)
|
||||
|
||||
editText.onFocusChangeListener = focusChangeListenerContainer
|
||||
editText.setOnTouchListener(touchEventListenerContainer)
|
||||
|
||||
baseTextInputView.addValidateListener(
|
||||
onSuccess = {
|
||||
supportHintDelegate.clearCurrentError()
|
||||
},
|
||||
onFailure = { _, errorId ->
|
||||
clearSupportHintStartDrawable()
|
||||
showErrorHintText(errorId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun getEditText(): EditText = editText
|
||||
|
||||
override fun getExtractedText(): String = extractedText ?: editText.text.toString()
|
||||
|
||||
override fun getRawText(): String = editText.getRawText()
|
||||
|
||||
override fun setText(text: String?) = editText.setText(text)
|
||||
|
||||
override fun getSelectionStart(): Int = editText.selectionStart
|
||||
|
||||
override fun setSelectionStart(index: Int) = editText.setSelection(index)
|
||||
|
||||
override fun setSelectorToEnd() = editText.setSelection(getRawText().length)
|
||||
|
||||
override fun setHint(text: String?) {
|
||||
textInputLayout.hint = text
|
||||
}
|
||||
|
||||
override fun setHintColor(color: Int) {
|
||||
hintColor = color
|
||||
textInputLayout.defaultHintTextColor = ColorStateList.valueOf(color)
|
||||
}
|
||||
|
||||
override fun setHintColorId(@ColorRes colorId: Int) = setHintColor(editText.context.getColor(colorId))
|
||||
|
||||
override fun setHintAppearance(@StyleRes style: Int?) {
|
||||
if (style == null) return
|
||||
textInputLayout.setHintTextAppearance(style)
|
||||
}
|
||||
|
||||
override fun getCurrentHintTextColor(): Int = editText.currentTextColor
|
||||
|
||||
override fun setFilters(vararg filters: InputFilter) {
|
||||
editText.filters = editText.filters
|
||||
.toMutableList()
|
||||
.apply { addAll(filters) }
|
||||
.toTypedArray()
|
||||
}
|
||||
|
||||
override fun setLongClickable(longClickable: Boolean) {
|
||||
editText.isLongClickable = longClickable
|
||||
}
|
||||
|
||||
override fun setEnabled(enabled: Boolean) {
|
||||
editText.isEnabled = enabled
|
||||
}
|
||||
|
||||
override fun setOnEditorActionListener(listener: TextView.OnEditorActionListener) {
|
||||
editText.setOnEditorActionListener(listener)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun setOnTouchListener(listener: View.OnTouchListener?) {
|
||||
touchEventListenerContainer.setMainTouchEventListener(listener)
|
||||
}
|
||||
|
||||
override fun setSupportTouchListener(listener: TouchListener) {
|
||||
touchEventListenerContainer.add(listener)
|
||||
}
|
||||
|
||||
override fun setFocusable(focusable: Boolean) {
|
||||
editText.isFocusable = focusable
|
||||
}
|
||||
|
||||
override fun setFocusableInTouchMode(focusable: Boolean) {
|
||||
editText.isFocusableInTouchMode = focusable
|
||||
}
|
||||
|
||||
override fun hasFocus(): Boolean = editText.hasFocus()
|
||||
|
||||
override fun clearFocus() = editText.clearFocus()
|
||||
|
||||
override fun requestFocus(direction: Int, previouslyFocusedRect: Rect?): Boolean =
|
||||
editText.requestFocus(direction, previouslyFocusedRect)
|
||||
|
||||
@Suppress("ClickableViewAccessibility", "LabeledExpression")
|
||||
override fun setOnIconClickListener(listener: (() -> Unit)?) {
|
||||
onIconClickListener = listener
|
||||
|
||||
editText.setOnTouchListener { _, event ->
|
||||
val result = isEndDrawableIconClicked(event)
|
||||
|
||||
if (!result) {
|
||||
onIconClickListener?.invoke()
|
||||
}
|
||||
return@setOnTouchListener result
|
||||
}
|
||||
}
|
||||
|
||||
override fun isEndDrawableIconClicked(event: MotionEvent): Boolean {
|
||||
val drawableWidth = editText.compoundDrawables[DRAWABLE_END]?.bounds
|
||||
?.width()
|
||||
?: return false
|
||||
|
||||
val drawableStartX = editText.width - drawableWidth - editText.paddingEnd
|
||||
|
||||
return event.action == MotionEvent.ACTION_UP && event.rawX >= drawableStartX
|
||||
}
|
||||
|
||||
override fun setOnFocusChangeListener(listener: View.OnFocusChangeListener?) { // TODO проблема нейминга set/add
|
||||
listener?.let(focusChangeListenerContainer::add)
|
||||
}
|
||||
|
||||
override fun removeOnFocusChangeListener(listener: View.OnFocusChangeListener) {
|
||||
focusChangeListenerContainer.remove(listener)
|
||||
}
|
||||
|
||||
override fun setTextColor(color: Int) {
|
||||
editText.setTextColor(color)
|
||||
}
|
||||
|
||||
override fun setTextAppearance(@StyleRes style: Int?) {
|
||||
if (style == null) return
|
||||
editText.setTextAppearance(style)
|
||||
}
|
||||
|
||||
override fun setInputType(type: Int) {
|
||||
editText.inputType = type
|
||||
}
|
||||
|
||||
override fun setImeOptions(imeOptions: Int) {
|
||||
editText.imeOptions = imeOptions
|
||||
}
|
||||
|
||||
override fun setDigits(digit: String) {
|
||||
if (digit.isBlank()) return
|
||||
editText.keyListener = DigitsKeyListener.getInstance(digit)
|
||||
}
|
||||
|
||||
override fun setMaxLines(maxLines: Int) {
|
||||
editText.maxLines = maxLines
|
||||
}
|
||||
|
||||
override fun setMaxLength(maxLength: Int) {
|
||||
editText.filters = arrayOf(InputFilter.LengthFilter(maxLength))
|
||||
}
|
||||
|
||||
override fun setDrawableEnd(@DrawableRes drawable: Int) {
|
||||
editText.setCompoundDrawablesWithIntrinsicBounds(0, 0, drawable, 0)
|
||||
}
|
||||
|
||||
override fun setDrawableEnd(drawable: Drawable?) {
|
||||
editText.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
|
||||
}
|
||||
|
||||
override fun setEditTextBackgroundTint(color: Int) {
|
||||
editText.backgroundTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
|
||||
override fun setEditTextBackgroundTintId(@ColorRes colorId: Int) = setEditTextBackgroundTint(editText.context.getColor(colorId))
|
||||
|
||||
override fun addTextChangedListener(watcher: TextWatcher) {
|
||||
when (watcher) {
|
||||
is HintedMaskedEditTextListener -> setupMask(watcher)
|
||||
else -> textWatcherContainer.add(watcher)
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeTextChangedListener(watcher: TextWatcher) {
|
||||
when (watcher) {
|
||||
is HintedMaskedEditTextListener -> {
|
||||
editText.removeTextChangedListener(watcher)
|
||||
editText.addTextChangedListener(textWatcherContainer)
|
||||
}
|
||||
|
||||
else -> {
|
||||
textWatcherContainer.remove(watcher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupMask(format: String, autocomplete: Boolean) = setupMask(createMask(format, autocomplete))
|
||||
|
||||
private fun createMask(format: String, autocomplete: Boolean) = HintedMaskedEditTextListener(
|
||||
format = format,
|
||||
field = editText,
|
||||
autocomplete = autocomplete,
|
||||
valueListener = object : MaskedTextChangedListener.ValueListener {
|
||||
override fun onTextChanged(maskFilled: Boolean, extractedValue: String) {
|
||||
extractedText = extractedValue
|
||||
maskIsFilled = maskFilled
|
||||
maskFieldListener?.onFilled(maskFilled, extractedValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
override fun setupMask(maskListener: HintedMaskedEditTextListener?) {
|
||||
maskFieldListener = null
|
||||
extractedText = null
|
||||
|
||||
editText.removeTextChangedListener(textWatcherContainer)
|
||||
|
||||
val rootTextWatcher = when (maskListener != null) {
|
||||
true -> maskListener.apply { listener = textWatcherContainer }
|
||||
false -> textWatcherContainer
|
||||
}
|
||||
|
||||
editText.addTextChangedListener(rootTextWatcher)
|
||||
}
|
||||
|
||||
override fun setOnMaskFilledListener(listener: MaskFilledListener?) {
|
||||
maskFieldListener = listener
|
||||
}
|
||||
|
||||
override fun setOnMaskFilledListener(listener: (Boolean, String) -> Unit) {
|
||||
maskFieldListener = object : MaskFilledListener {
|
||||
override fun onFilled(maskFilled: Boolean, extractedText: String) {
|
||||
listener.invoke(maskFilled, extractedText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setMaskedHint(text: String) {
|
||||
editText.maskedHint = text
|
||||
}
|
||||
|
||||
override fun setMaskedHintColor(color: Int) = editText.setMaskedHintColor(color)
|
||||
|
||||
override fun setMaskedHintColorId(@ColorRes colorId: Int) = setMaskedHintColor(editText.context.getColor(colorId))
|
||||
|
||||
override fun maskIsFilled(): Boolean? = maskIsFilled
|
||||
|
||||
override fun setEditTextMargin(start: Int, top: Int, end: Int, bottom: Int) =
|
||||
editText.setMargins(start, top, end, bottom)
|
||||
|
||||
override fun setEditTextMargin(margin: Int) = setEditTextMargin(margin, margin, margin, margin)
|
||||
|
||||
override fun setEditTextPadding(start: Int, top: Int, end: Int, bottom: Int) =
|
||||
editText.setPadding(start, top, end, bottom)
|
||||
|
||||
override fun setEditTextPadding(padding: Int) = setEditTextPadding(padding, padding, padding, padding)
|
||||
|
||||
override fun getEditTextPaddingStart(): Int = editText.paddingStart
|
||||
|
||||
override fun getEditTextPaddingEnd(): Int = editText.paddingEnd
|
||||
|
||||
override fun getEditTextPaddingTop(): Int = editText.paddingTop
|
||||
|
||||
override fun getEditTextPaddingBottom(): Int = editText.paddingBottom
|
||||
|
||||
// START SupportHint delegate region
|
||||
override fun showSupportHint() = supportHintDelegate.showSupportHint()
|
||||
|
||||
override fun setInformationHintText(text: String?) = supportHintDelegate.setInformationHintText(text)
|
||||
|
||||
override fun setSupportHintMode(mode: ShowSupportHintMode) = supportHintDelegate.setSupportHintMode(mode)
|
||||
|
||||
override fun supportHintIsShow(): Boolean = supportHintDelegate.supportHintIsShow()
|
||||
|
||||
override fun getSupportHintText(): String? = supportHintDelegate.getSupportHintText()
|
||||
|
||||
override fun setSupportHintAppearance(@StyleRes style: Int?) = supportHintDelegate.setSupportHintAppearance(style)
|
||||
|
||||
override fun showErrorHintText(@StringRes errorText: Int) = showErrorHintText(editText.context.getString(errorText))
|
||||
|
||||
override fun showErrorHintText(errorText: String?) = supportHintDelegate.showErrorHintText(errorText)
|
||||
|
||||
override fun getSupportHintState(): BottomHintState = supportHintDelegate.getSupportHintState()
|
||||
|
||||
override fun hasError(): Boolean = supportHintDelegate.hasError()
|
||||
|
||||
override fun setSupportHintText(text: String?) = supportHintDelegate.setSupportHintText(text)
|
||||
|
||||
override fun setSupportHintText(text: String?, state: BottomHintState) = supportHintDelegate.setSupportHintText(text, state)
|
||||
|
||||
override fun setSupportHintText(textId: Int) = supportHintDelegate.setSupportHintText(textId)
|
||||
|
||||
override fun setSupportHintTextColor(color: Int) = supportHintDelegate.setSupportHintTextColor(color)
|
||||
|
||||
override fun setSupportHintTextColorId(colorId: Int) = supportHintDelegate.setSupportHintTextColorId(colorId)
|
||||
|
||||
override fun setSupportHintStartDrawable(drawableId: Int) = supportHintDelegate.setSupportHintStartDrawable(drawableId)
|
||||
|
||||
override fun setSupportHintStartDrawable(drawable: Drawable?) = supportHintDelegate.setSupportHintStartDrawable(drawable)
|
||||
|
||||
override fun setSupportHintStartDrawable(drawable: Drawable?, sizePx: Int?, gravityInt: Int?, marginPx: Int?) =
|
||||
supportHintDelegate.setSupportHintStartDrawable(drawable, sizePx, gravityInt, marginPx)
|
||||
|
||||
override fun clearSupportHintStartDrawable() = supportHintDelegate.clearSupportHintStartDrawable()
|
||||
|
||||
override fun showInformationHint() = supportHintDelegate.showInformationHint()
|
||||
|
||||
override fun showInformationHint(text: String?) = supportHintDelegate.showInformationHint(text)
|
||||
|
||||
override fun setupInformationColor(color: Int) = supportHintDelegate.setupInformationColor(color)
|
||||
|
||||
override fun setupInformationColorId(colorId: Int) = supportHintDelegate.setupInformationColor(colorId)
|
||||
|
||||
override fun setupErrorColor(color: Int) = supportHintDelegate.setupErrorColor(color)
|
||||
|
||||
override fun setErrorColorId(@ColorRes colorId: Int) = setupErrorColor(editText.context.getColor(colorId))
|
||||
|
||||
override fun isFloating(isFloating: Boolean) = supportHintDelegate.isFloating(isFloating)
|
||||
|
||||
override fun hideSupportHint() = supportHintDelegate.hideSupportHint()
|
||||
|
||||
override fun setSupportHintEnable(isEnable: Boolean) = supportHintDelegate.setSupportHintEnable(isEnable)
|
||||
// End SupportHint delegate region
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.roboswag.views.input.delegates.edit_text
|
||||
|
||||
interface MaskFilledListener {
|
||||
|
||||
fun onFilled(maskFilled: Boolean, extractedText: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package ru.touchin.roboswag.views.input.delegates.validators
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.listener.ValidatorListener
|
||||
import ru.touchin.roboswag.views.input.validatable.listener.implementation.SimpleValidatorListener
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.ValidateResult
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.Validator
|
||||
import ru.touchin.roboswag.views.input.validatable.ValidatorListenerContainer
|
||||
|
||||
abstract class BaseInputValidatorDelegate : InputValidatorDelegate {
|
||||
|
||||
protected var currentValidator: Validator? = null
|
||||
protected var validatorListeners = ValidatorListenerContainer()
|
||||
protected var needValidateFlag: Boolean = true
|
||||
protected var validateFlag: Int = InputValidatorDelegate.FLAG_NONE
|
||||
|
||||
override fun manualValidate() {
|
||||
currentValidator?.let { validator -> validate(validator = validator, isManualValidate = true) }
|
||||
}
|
||||
|
||||
override fun validate() {
|
||||
currentValidator?.let(this::validate)
|
||||
}
|
||||
|
||||
override fun validate(validator: Validator) = validate(validator = validator, isManualValidate = false)
|
||||
|
||||
abstract fun validate(validator: Validator, isManualValidate: Boolean)
|
||||
|
||||
override fun getValidateModeFlag(): Int = validateFlag
|
||||
|
||||
override fun setValidatorMode(flagMode: Int) {
|
||||
validateFlag = flagMode
|
||||
}
|
||||
|
||||
override fun hasNeedToValidateFlag(): Boolean = needValidateFlag
|
||||
|
||||
override fun setNeedToValidateFlag(isNeed: Boolean) {
|
||||
needValidateFlag = isNeed
|
||||
}
|
||||
|
||||
override fun setValidator(validator: Validator?) {
|
||||
currentValidator = validator?.apply { addListener(validatorListeners) }
|
||||
}
|
||||
|
||||
override fun addValidateListener(onSuccess: ((String) -> Unit)?, onFailure: ((String, Int) -> Unit)?) = addValidateListener(
|
||||
SimpleValidatorListener { result ->
|
||||
when (result) {
|
||||
is ValidateResult.Success -> onSuccess?.invoke(result.data)
|
||||
is ValidateResult.Error -> onFailure?.invoke(result.data, result.errorMessageId)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
override fun addValidateListener(listener: ValidatorListener) {
|
||||
validatorListeners.add(listener)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package ru.touchin.roboswag.views.input.delegates.validators
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.listener.ValidatorListener
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.Validator
|
||||
import ru.touchin.roboswag.views.input.delegates.edit_text.InputEditorDelegate
|
||||
|
||||
interface InputValidatorDelegate {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* <pre>
|
||||
* |-------|-------|-------|-------|
|
||||
* FLAG_NONE
|
||||
* 1 FLAG_HAS_FOCUS
|
||||
* 1 FLAG_LOSS_FOCUS
|
||||
* 1 FLAG_BEFORE_TEXT_CHANGED
|
||||
* 1 FLAG_ON_TEXT_CHANGED
|
||||
* 1 FLAG_AFTER_TEXT_CHANGED
|
||||
* |-------|-------|-------|-------|</pre>
|
||||
*/
|
||||
|
||||
const val FLAG_NONE = 0b0000000000 // 0
|
||||
const val FLAG_HAS_FOCUS = 0b0000000001 // 1
|
||||
const val FLAG_LOSS_FOCUS = 0b0000000010 // 2
|
||||
const val FLAG_BEFORE_TEXT_CHANGED = 0b0000000100 // 4
|
||||
const val FLAG_ON_TEXT_CHANGED = 0b0000001000 // 8
|
||||
const val FLAG_AFTER_TEXT_CHANGED = 0b0000010000 // 16
|
||||
}
|
||||
|
||||
fun bindWithEditorDelegate(inputEditorDelegate: InputEditorDelegate)
|
||||
|
||||
fun manualValidate()
|
||||
|
||||
fun validate()
|
||||
|
||||
fun validate(validator: Validator)
|
||||
|
||||
fun setNeedToValidateFlag(isNeed: Boolean)
|
||||
|
||||
fun setValidator(validator: Validator?)
|
||||
|
||||
fun setValidatorMode(flagMode: Int)
|
||||
|
||||
fun getValidateModeFlag(): Int
|
||||
|
||||
fun actionWithDisableValidate(action: () -> Unit)
|
||||
|
||||
fun addValidateListener(listener: ValidatorListener)
|
||||
|
||||
fun addSuccessOrNullValidateListener(listener: (String?) -> Unit)
|
||||
|
||||
fun addValidateListener(
|
||||
onSuccess: ((String) -> Unit)? = null,
|
||||
onFailure: ((String, Int) -> Unit)? = null
|
||||
)
|
||||
|
||||
fun hasNeedToValidateFlag(): Boolean
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package ru.touchin.roboswag.views.input.delegates.validators
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.listener.implementation.SuccessOrNullValidatorListener
|
||||
import ru.touchin.roboswag.views.input.delegates.edit_text.InputEditorDelegate
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.Validator
|
||||
|
||||
class InputValidatorDelegateImpl : BaseInputValidatorDelegate() {
|
||||
|
||||
private lateinit var inputEditorDelegate: InputEditorDelegate
|
||||
|
||||
override fun bindWithEditorDelegate(inputEditorDelegate: InputEditorDelegate) {
|
||||
this.inputEditorDelegate = inputEditorDelegate
|
||||
}
|
||||
|
||||
override fun validate(validator: Validator, isManualValidate: Boolean) {
|
||||
if (!needValidateFlag && !isManualValidate) return
|
||||
|
||||
validator.validate(text = inputEditorDelegate.getExtractedText())
|
||||
}
|
||||
|
||||
override fun addSuccessOrNullValidateListener(listener: (String?) -> Unit) = addValidateListener(
|
||||
SuccessOrNullValidatorListener { result -> listener.invoke(result) }
|
||||
)
|
||||
|
||||
override fun actionWithDisableValidate(action: () -> Unit) {
|
||||
val previousNeedValidateFlag = needValidateFlag
|
||||
needValidateFlag = false
|
||||
action.invoke()
|
||||
needValidateFlag = previousNeedValidateFlag
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package ru.touchin.roboswag.views.input.delegates.validators
|
||||
|
||||
enum class ValidatorTypes {
|
||||
NONE,
|
||||
PHONE,
|
||||
DATE,
|
||||
TIME,
|
||||
EMPTY,
|
||||
EMAIL,
|
||||
LATIN,
|
||||
CYRILLIC,
|
||||
NUMBER
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
package ru.touchin.roboswag.views.input.utils
|
||||
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.LocalDate
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* This util allows you to check the validity of the entered date as you enter each character.
|
||||
* This util checks ranges [01-31].[01-12].[1900-2099]. This means that the value "31.02.yyyy"
|
||||
* will be considered valid until the date is fully entered.
|
||||
* Use it only for date format "dd.mm.yyyy".
|
||||
**/
|
||||
object DateValidationUtils {
|
||||
private const val DAY_INDEX = 0
|
||||
private const val MONTH_INDEX = 1
|
||||
private const val YEAR_INDEX = 2
|
||||
|
||||
private const val SHORT_DATE_FORMAT = "dd.MM.yyyy"
|
||||
|
||||
// ^$ empty string - correct
|
||||
// [0-3] as first char - correct
|
||||
// (0)[1-9] is [01; 09] range, excluding "00"
|
||||
// [1-2][0-9] is [10; 20] range
|
||||
// (3)[0-1] is [30;31] range
|
||||
private const val DAY_PATTERN = "^$|[0-3]|((0)[1-9]|[1-2][0-9]|(3)[0-1])(.)"
|
||||
|
||||
// ^$ empty string - correct
|
||||
// [0-1] as first char - correct
|
||||
// (0)[1-9] is [01; 09] range, excluding "00"
|
||||
// (1)[0-2] is [10; 12] range
|
||||
private const val MONTH_PATTERN = "^$|[0-1]|((0)[1-9]|(1)[0-2])(.)"
|
||||
|
||||
// ^$ empty string - correct
|
||||
// [1-2] as first char - correct
|
||||
// (19)|(20) is [19; 20] range
|
||||
// (19)[0-9] is [190; 199] range
|
||||
// (19)[0-9][0-9] is [1900; 1999] range
|
||||
// (20)[0-9] is [200; 209] range
|
||||
// (20)[0-9][0-9] is [2000; 2099] range
|
||||
private const val YEAR_PATTERN = "^$|[1-2]|(19)|(20)|(19)[0-9]|(19)[0-9][0-9]|(20)[0-9]|(20)[0-9][0-9]"
|
||||
|
||||
// ""
|
||||
private const val MIN_DAY_STRING_PART_INDEX = 0
|
||||
|
||||
// "dd."
|
||||
private const val MAX_DAY_STRING_PART_INDEX = 3
|
||||
|
||||
// "dd.m"
|
||||
private const val MIN_MONTH_STRING_PART_INDEX = 3
|
||||
|
||||
// "dd.mm."
|
||||
private const val MAX_MONTH_STRING_PART_INDEX = 6
|
||||
|
||||
// "dd.mm.y"
|
||||
private const val MIN_YEAR_STRING_PART_INDEX = 6
|
||||
|
||||
// "dd.mm.yyyy"
|
||||
private const val MAX_YEAR_STRING_PART_INDEX = 10
|
||||
|
||||
fun isDatePreValid(dateString: String): Boolean {
|
||||
val dayStringPart = getDayStringPart(dateString)
|
||||
val monthStringPart = getMonthStringPart(dateString)
|
||||
val yearStringPart = getYearStringPart(dateString)
|
||||
|
||||
val dayPattern = DAY_PATTERN.toRegex()
|
||||
val monthPattern = MONTH_PATTERN.toRegex()
|
||||
val yearPattern = YEAR_PATTERN.toRegex()
|
||||
|
||||
val isDayValid = dayPattern.matches(dayStringPart)
|
||||
val isMonthValid = monthPattern.matches(monthStringPart)
|
||||
val isYearValid = yearPattern.matches(yearStringPart)
|
||||
|
||||
val isDateExist = isDateExist(dateString)
|
||||
|
||||
return isDayValid && isMonthValid && isYearValid && isDateExist
|
||||
|
||||
}
|
||||
|
||||
private fun getYearStringPart(dateString: String): String = if (dateString.length > MIN_YEAR_STRING_PART_INDEX) {
|
||||
dateString.substring(MIN_YEAR_STRING_PART_INDEX, minOf(MAX_YEAR_STRING_PART_INDEX, dateString.length))
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
private fun getMonthStringPart(dateString: String): String = if (dateString.length >= MIN_MONTH_STRING_PART_INDEX) {
|
||||
dateString.substring(MIN_MONTH_STRING_PART_INDEX, minOf(MAX_MONTH_STRING_PART_INDEX, dateString.length))
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
private fun getDayStringPart(dateString: String) =
|
||||
dateString.substring(MIN_DAY_STRING_PART_INDEX, minOf(MAX_DAY_STRING_PART_INDEX, dateString.length))
|
||||
|
||||
private fun isDateExist(dateString: String): Boolean {
|
||||
if (dateString.length == MAX_YEAR_STRING_PART_INDEX) {
|
||||
try {
|
||||
dateString.toShortFormatDateTime()
|
||||
} catch (exception: ParseException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun String.toShortFormatDateTime(): DateTime {
|
||||
val format = SimpleDateFormat(SHORT_DATE_FORMAT, Locale.ROOT)
|
||||
format.isLenient = false
|
||||
|
||||
// Don't use DateTime(format.parse(this)), cause it doesn't work correctly with dates that were >= 110 year ago
|
||||
return DateTime().withDate(LocalDate.fromDateFields(format.parse(this))).withTime(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
fun String.toDateShortFormatOrNull(): DateTime? {
|
||||
val dateSplit = this.split(".")
|
||||
|
||||
return try {
|
||||
if (dateSplit.size == 3) {
|
||||
val day = dateSplit[DAY_INDEX].toInt()
|
||||
val month = dateSplit[MONTH_INDEX].toInt()
|
||||
val year = dateSplit[YEAR_INDEX].toInt()
|
||||
|
||||
DateTime().withDate(year, month, day).withTime(0, 0, 0, 0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package ru.touchin.roboswag.views.input.utils
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object EmailUtil {
|
||||
|
||||
private const val EMAIL_PARTS_COUNT = 3
|
||||
private const val MAIN_EMAIL_PARTS_COUNT = 2
|
||||
|
||||
private const val LOCAL_INDEX = 0
|
||||
private const val DOMAIN_INDEX = 1
|
||||
|
||||
private const val NAME_DOMAIN_EMAIL_INDEX = 1
|
||||
private const val HOST_DOMAIN_EMAIL_INDEX = 2
|
||||
|
||||
private const val HOST_MIN_SIZE = 3 // include dot
|
||||
private const val HOST_MAX_SIZE = 25
|
||||
private const val NAME_MIN_SIZE = 3 // include @
|
||||
private const val NAME_MAX_SIZE = 64
|
||||
private const val LOCAL_MIN_SIZE = 1
|
||||
|
||||
private const val NOT_FOUND = -1
|
||||
|
||||
private const val BASE_SYMBOLS = "a-zA-Z0-9"
|
||||
private const val TWO_DOTS = ".." // TODO как-нибудь занести в регулярку проверку на вхождение двух точек
|
||||
|
||||
private const val LOCAL_PART_PATTERN: String = "^[$BASE_SYMBOLS\\+\\.\\_\\%\\-]"
|
||||
private const val DOMAIN_PART_PATTERN: String = "\\@[[^\\-.]&&$BASE_SYMBOLS\\.][$BASE_SYMBOLS\\-]"
|
||||
private const val HOST_PATTERN: String = "\\.[$BASE_SYMBOLS]"
|
||||
|
||||
private const val PRE_LOCAL_PART_EMAIL: String = "$LOCAL_PART_PATTERN+"
|
||||
private const val PRE_DOMAIN_PART_EMAIL: String = "$DOMAIN_PART_PATTERN*"
|
||||
private const val PRE_HOST_EMAIL: String = "$HOST_PATTERN*"
|
||||
|
||||
// Pattern for email validation
|
||||
// 1. local-part [a-zA-Z0-9\+\.\_\%\-]{1,64}
|
||||
// - uppercase and lowercase Latin letters A to Z and a to z
|
||||
// - digits 0 to 9
|
||||
// - printable characters - _ % + - .
|
||||
// from 1 to 64 symbols
|
||||
private const val LOCAL_PART_EMAIL: String = "$LOCAL_PART_PATTERN{1,64}"
|
||||
|
||||
// 2. domain-part [a-zA-Z0-9][a-zA-Z0-9\-]{1,64}
|
||||
// - uppercase and lowercase Latin letters A to Z and a to z
|
||||
// - digits 0 to 9 and '-' not in the beginning
|
||||
// from 2 to 64 symbols
|
||||
private const val DOMAIN_PART_EMAIL: String = "$DOMAIN_PART_PATTERN{1,63}"
|
||||
|
||||
// 3. (.[a-zA-Z0-9][a-zA-Z0-9\-]{2,24}) group repeats 1 or more times
|
||||
// - dot and uppercase and lowercase Latin letters A to Z and a to z
|
||||
// - digits 2 to 9
|
||||
// from 3 to 25 symbols (включая точку)
|
||||
private const val HOST_EMAIL: String = "$HOST_PATTERN{2,24}$"
|
||||
|
||||
val EMAIL_PATTERN_REGEX = "$LOCAL_PART_EMAIL$DOMAIN_PART_EMAIL$HOST_EMAIL".toRegex()
|
||||
|
||||
@Suppress("detekt.ReturnCount")
|
||||
fun String.emailHaveAllParts(): Boolean {
|
||||
val parts = getEmailParts(this)
|
||||
val nonNullParts = parts.filterNotNull()
|
||||
|
||||
if (nonNullParts.size != parts.size) return false
|
||||
|
||||
val map = mapOf(
|
||||
nonNullParts[LOCAL_INDEX] to PatternEmail.LOCAL,
|
||||
nonNullParts[NAME_DOMAIN_EMAIL_INDEX] to PatternEmail.DOMAIN_NAME,
|
||||
nonNullParts[HOST_DOMAIN_EMAIL_INDEX] to PatternEmail.DOMAIN_HOST
|
||||
)
|
||||
|
||||
for ((part, partPattern) in map.entries) {
|
||||
when (partPattern) {
|
||||
PatternEmail.LOCAL, PatternEmail.DOMAIN_NAME -> if (part.isEmpty()) return false
|
||||
PatternEmail.DOMAIN_HOST -> if (part.length < partPattern.minLength) return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun String.containsTwoDots(): Boolean = contains(TWO_DOTS)
|
||||
|
||||
private fun getEmailParts(email: String): Array<String?> {
|
||||
val emailParts = arrayOfNulls<String>(EMAIL_PARTS_COUNT)
|
||||
|
||||
val mainParts = email.split("@".toRegex(), MAIN_EMAIL_PARTS_COUNT)
|
||||
emailParts[LOCAL_INDEX] = mainParts.getOrNull(LOCAL_INDEX).orEmpty()
|
||||
|
||||
val domainPart = mainParts.getOrNull(DOMAIN_INDEX).orEmpty()
|
||||
val lastDotIndex = domainPart.lastIndexOf(".")
|
||||
|
||||
val nameDomainPart = if (lastDotIndex == NOT_FOUND) domainPart else domainPart.substring(0, lastDotIndex)
|
||||
emailParts[NAME_DOMAIN_EMAIL_INDEX] = if (nameDomainPart.isEmpty()) null else "@$nameDomainPart"
|
||||
|
||||
val hostDomainPart = if (lastDotIndex == NOT_FOUND) "" else domainPart.substring(lastDotIndex)
|
||||
emailParts[HOST_DOMAIN_EMAIL_INDEX] = hostDomainPart.ifEmpty { null }
|
||||
|
||||
return emailParts
|
||||
}
|
||||
|
||||
fun String.emailIsPreValid(): Boolean {
|
||||
val parts = getEmailParts(this)
|
||||
|
||||
// Проверяет с конца
|
||||
val map = mapOf(
|
||||
parts[HOST_DOMAIN_EMAIL_INDEX] to PatternEmail.DOMAIN_HOST,
|
||||
parts[NAME_DOMAIN_EMAIL_INDEX] to PatternEmail.DOMAIN_NAME,
|
||||
parts[LOCAL_INDEX] to PatternEmail.LOCAL
|
||||
)
|
||||
|
||||
var previousPart: String? = null
|
||||
for ((part, partPattern) in map.entries) {
|
||||
if (part == null) {
|
||||
if (previousPart != null) return false
|
||||
previousPart = null
|
||||
continue
|
||||
}
|
||||
|
||||
val isValidPattern = Pattern
|
||||
.compile(partPattern.pattern, Pattern.CASE_INSENSITIVE)
|
||||
.matcher(part)
|
||||
.matches()
|
||||
|
||||
if (
|
||||
!isValidPattern
|
||||
|| isTooLong(part, partPattern)
|
||||
|| isTooSmall(part, partPattern, previousPart)
|
||||
|| part.containsTwoDots()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
previousPart = part
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Последняя часть на длину провериться не может, так как нельзя найти тригера завершенности
|
||||
private fun isTooSmall(part: String, partPattern: PatternEmail, previousPart: String?): Boolean =
|
||||
partPattern != PatternEmail.DOMAIN_HOST && part.length < partPattern.minLength && previousPart != null
|
||||
|
||||
private fun isTooLong(part: String, partPattern: PatternEmail): Boolean = part.length > partPattern.maxLength
|
||||
|
||||
private enum class PatternEmail(val pattern: String, val minLength: Int, val maxLength: Int) {
|
||||
LOCAL(PRE_LOCAL_PART_EMAIL, LOCAL_MIN_SIZE, NAME_MAX_SIZE),
|
||||
DOMAIN_NAME(PRE_DOMAIN_PART_EMAIL, NAME_MIN_SIZE, NAME_MAX_SIZE),
|
||||
DOMAIN_HOST(PRE_HOST_EMAIL, HOST_MIN_SIZE, HOST_MAX_SIZE)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package ru.touchin.roboswag.views.input.utils
|
||||
|
||||
object MaskConstants {
|
||||
|
||||
const val PHONE_MASK = "+{7} [000] [000]-[00]-[00]"
|
||||
|
||||
const val PHONE_MASK_WITHOUT_INTERNATIONAL_CODE = "[000] [000]-[00]-[00]"
|
||||
|
||||
const val BANK_CARD = "[0000] [0000] [0000] [0000] [999]"
|
||||
|
||||
const val MID = "[0000] [0000] [0000] [0000]"
|
||||
|
||||
const val DATE_MASK = "[00]{.}[00]{.}[0000]"
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import android.content.res.Resources
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.core.view.marginEnd
|
||||
import androidx.core.view.marginStart
|
||||
import androidx.core.view.marginTop
|
||||
|
||||
fun View.setMargins(start: Int? = marginStart, top: Int? = marginTop, end: Int? = marginEnd, bottom: Int? = marginBottom) {
|
||||
val params = getMarginLayoutParams()
|
||||
params.setMargins(
|
||||
start?.dpToPx() ?: params.marginStart,
|
||||
top?.dpToPx() ?: params.topMargin,
|
||||
end?.dpToPx() ?: params.marginEnd,
|
||||
bottom?.dpToPx() ?: params.bottomMargin
|
||||
)
|
||||
}
|
||||
|
||||
fun View.getMarginLayoutParams(): ViewGroup.MarginLayoutParams = layoutParams as ViewGroup.MarginLayoutParams
|
||||
|
||||
fun Int.dpToPx() = (this * Resources.getSystem().displayMetrics.density).toInt()
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.roboswag.views.input.validatable
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.listener.ValidatorListener
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.ValidateResult
|
||||
import ru.touchin.roboswag.views.input.BaseListenersContainer
|
||||
|
||||
class ValidatorListenerContainer : BaseListenersContainer<ValidatorListener>(), ValidatorListener {
|
||||
|
||||
override fun onValidate(result: ValidateResult) =
|
||||
listeners.forEach { validatorListener -> validatorListener.onValidate(result) }
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package ru.touchin.roboswag.views.input.validatable
|
||||
|
||||
interface ValidatorUnit
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.listener
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.ValidateResult
|
||||
|
||||
interface ValidatorListener {
|
||||
|
||||
fun onValidate(result: ValidateResult)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.listener.implementation
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.listener.ValidatorListener
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.ValidateResult
|
||||
|
||||
open class SimpleValidatorListener(private val onValidate: (ValidateResult) -> Unit) : ValidatorListener {
|
||||
|
||||
override fun onValidate(result: ValidateResult) {
|
||||
onValidate.invoke(result)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.listener.implementation
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.listener.ValidatorListener
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.ValidateResult
|
||||
|
||||
open class SuccessOrNullValidatorListener(private val onValidate: (String?) -> Unit) : ValidatorListener {
|
||||
|
||||
override fun onValidate(result: ValidateResult) = onValidate.invoke((result as? ValidateResult.Success)?.data)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.listener.implementation
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.listener.ValidatorListener
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.ValidateResult
|
||||
|
||||
open class SuccessValidatorListener(private val onValidate: (String) -> Unit) : ValidatorListener {
|
||||
|
||||
override fun onValidate(result: ValidateResult) {
|
||||
if (result is ValidateResult.Success) {
|
||||
onValidate.invoke(result.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.mapper
|
||||
|
||||
import org.joda.time.DateTime
|
||||
import ru.touchin.roboswag.views.input.utils.DateValidationUtils.toDateShortFormatOrNull
|
||||
|
||||
class DateValidateMapper : ValidateMapper {
|
||||
|
||||
private companion object {
|
||||
|
||||
val INVALID_DATE = DateTime().withDate(1900, 1, 1).minusDays(1).millis
|
||||
}
|
||||
|
||||
override fun map(text: String): String = getMillsFromDateShort(text)
|
||||
|
||||
private fun getMillsFromDateShort(text: String): String {
|
||||
val millis = text.toDateShortFormatOrNull()
|
||||
?.millis
|
||||
?: INVALID_DATE
|
||||
|
||||
return millis.toString()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.mapper
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.ValidatorUnit
|
||||
|
||||
interface ValidateMapper : ValidatorUnit {
|
||||
|
||||
fun map(text: String): String
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.switcher
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.Validator
|
||||
|
||||
class SimpleValidatorSwitcher(private val currentValidator: () -> Validator) : ValidatorSwitcher {
|
||||
|
||||
override fun getValidator(text: String): Validator = currentValidator.invoke()
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.switcher
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.Validator
|
||||
|
||||
class TextValidatorSwitcher(private val currentValidator: (text: String) -> Validator) : ValidatorSwitcher {
|
||||
|
||||
override fun getValidator(text: String): Validator = currentValidator.invoke(text)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.switcher
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.ValidatorUnit
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.Validatable
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.ValidateResult
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.Validator
|
||||
|
||||
interface ValidatorSwitcher : ValidatorUnit, Validatable {
|
||||
|
||||
fun getValidator(text: String): Validator
|
||||
|
||||
override fun validate(text: String): ValidateResult = getValidator(text).validate(text)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.listener.ValidatorListener
|
||||
|
||||
abstract class BaseValidator : Validator {
|
||||
|
||||
protected var listeners: MutableList<ValidatorListener>? = null
|
||||
|
||||
override fun addListener(listener: ValidatorListener) {
|
||||
listeners = listeners ?: mutableListOf()
|
||||
|
||||
listeners?.add(listener)
|
||||
}
|
||||
|
||||
override fun removeListener(listener: ValidatorListener) {
|
||||
listeners?.remove(listener)
|
||||
|
||||
if (listeners.isNullOrEmpty()) {
|
||||
listeners = null
|
||||
}
|
||||
}
|
||||
|
||||
protected fun notifyAll(result: ValidateResult) = listeners?.forEach { listener ->
|
||||
listener.onValidate(result)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator
|
||||
|
||||
interface Validatable {
|
||||
|
||||
fun validate(text: String): ValidateResult
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
sealed class ValidateResult(val data: String) {
|
||||
|
||||
class Success(data: String) : ValidateResult(data)
|
||||
|
||||
class Error(data: String, @StringRes val errorMessageId: Int) : ValidateResult(data)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.ValidatorUnit
|
||||
import ru.touchin.roboswag.views.input.validatable.listener.ValidatorListener
|
||||
|
||||
interface Validator : ValidatorUnit, Validatable {
|
||||
|
||||
override fun validate(text: String): ValidateResult
|
||||
|
||||
fun addListener(listener: ValidatorListener)
|
||||
|
||||
fun removeListener(listener: ValidatorListener)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.BaseValidator
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.ValidateResult
|
||||
|
||||
open class BasePredicateValidator(
|
||||
protected val predicate: (String) -> ValidateResult
|
||||
) : BaseValidator() {
|
||||
|
||||
override fun validate(text: String): ValidateResult =
|
||||
predicate.invoke(text).also { result -> notifyAll(result) }
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.ValidateResult
|
||||
|
||||
@Suppress("detekt.LabeledExpression")
|
||||
open class CharValidator(
|
||||
private val condition: (Char) -> Boolean,
|
||||
@StringRes private val errorMessage: Int
|
||||
) : BasePredicateValidator(
|
||||
predicate = { text ->
|
||||
var result = true
|
||||
|
||||
run breaking@{
|
||||
text.forEach { char ->
|
||||
if (!condition(char)) {
|
||||
result = false
|
||||
return@breaking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (result) {
|
||||
true -> ValidateResult.Success(text)
|
||||
false -> ValidateResult.Error(text, errorMessage)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import ru.touchin.roboswag.views.input.validatable.ValidatorUnit
|
||||
import ru.touchin.roboswag.views.input.validatable.mapper.ValidateMapper
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.BaseValidator
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.Validatable
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.ValidateResult
|
||||
|
||||
open class ContainerValidator(
|
||||
private val validatorUnits: List<ValidatorUnit>
|
||||
) : BaseValidator() {
|
||||
|
||||
constructor(vararg validatorUnits: ValidatorUnit) : this(validatorUnits.toList())
|
||||
|
||||
@Suppress("detekt.LabeledExpression", "detekt.NestedBlockDepth")
|
||||
override fun validate(text: String): ValidateResult {
|
||||
var currentText = text
|
||||
|
||||
val validateResult: ValidateResult = run breaking@{
|
||||
validatorUnits.forEach { unit ->
|
||||
when (unit) {
|
||||
is ValidateMapper -> currentText = unit.map(currentText)
|
||||
is Validatable -> unit.validate(currentText).let { result ->
|
||||
when (result) {
|
||||
is ValidateResult.Error -> return@breaking ValidateResult.Error(text, result.errorMessageId)
|
||||
is ValidateResult.Success -> currentText = result.data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return@breaking ValidateResult.Success(text)
|
||||
}
|
||||
|
||||
notifyAll(validateResult)
|
||||
|
||||
return validateResult
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
class CyrillicValidator(
|
||||
@StringRes errorMessage: Int
|
||||
) : RegexValidator(
|
||||
regex = CYRILLIC_PATTERN.toRegex(),
|
||||
errorMessage = errorMessage
|
||||
) {
|
||||
private companion object {
|
||||
const val CYRILLIC_PATTERN = "^[а-яА-Я]+\$"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class DateFormatValidator(
|
||||
@StringRes errorMessage: Int
|
||||
) : PatternValidator(
|
||||
pattern = Pattern.compile("$DAY_PATTERN$MONTH_PATTERN$YEAR_PATTERN"),
|
||||
errorMessage = errorMessage
|
||||
) {
|
||||
private companion object {
|
||||
// [0-3] as first char - correct
|
||||
// (0)[1-9] is [01; 09] range, excluding "00"
|
||||
// [1-2][0-9] is [10; 20] range
|
||||
// (3)[0-1] is [30;31] range
|
||||
const val DAY_PATTERN = "[0-3]|((0)[1-9]|[1-2][0-9]|(3)[0-1])(.)"
|
||||
|
||||
// ^$ empty string - correct
|
||||
// [0-1] as first char - correct
|
||||
// (0)[1-9] is [01; 09] range, excluding "00"
|
||||
// (1)[0-2] is [10; 12] range
|
||||
const val MONTH_PATTERN = "[0-1]|((0)[1-9]|(1)[0-2])(.)"
|
||||
|
||||
// [1-2] as first char - correct
|
||||
// (19)|(20) is [19; 20] range
|
||||
// (19)[0-9] is [190; 199] range
|
||||
// (19)[0-9][0-9] is [1900; 1999] range
|
||||
// (20)[0-9] is [200; 209] range
|
||||
// (20)[0-9][0-9] is [2000; 2099] range
|
||||
const val YEAR_PATTERN = "[1-2]|(19)|(20)|(19)[0-9]|(19)[0-9][0-9]|(20)[0-9]|(20)[0-9][0-9]"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
class DiapasonLengthValidator(
|
||||
private val minLength: Int,
|
||||
private val maxLength: Int,
|
||||
@StringRes errorMessage: Int
|
||||
) : SimpleValidator(
|
||||
condition = { text -> text.length in minLength..maxLength },
|
||||
errorMessage = errorMessage
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import ru.touchin.roboswag.views.input.utils.EmailUtil
|
||||
import ru.touchin.roboswag.views.input.utils.EmailUtil.containsTwoDots
|
||||
|
||||
class EmailValidator(
|
||||
@StringRes errorMessage: Int
|
||||
) : SimpleValidator(
|
||||
condition = { email ->
|
||||
// TODO убрать containsTwoDots в регулярку
|
||||
EmailUtil.EMAIL_PATTERN_REGEX.matches(email) && !email.containsTwoDots()
|
||||
},
|
||||
errorMessage = errorMessage
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
open class EmptyValidator(
|
||||
@StringRes errorMessage: Int
|
||||
) : SimpleValidator(
|
||||
condition = { text -> text.isNotEmpty() },
|
||||
errorMessage = errorMessage
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
class LatinValidator(
|
||||
@StringRes errorMessage: Int
|
||||
) : RegexValidator(
|
||||
regex = LATIN_PATTERN.toRegex(),
|
||||
errorMessage = errorMessage
|
||||
) {
|
||||
private companion object {
|
||||
const val LATIN_PATTERN = "^[a-zA-Z]+\$"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
class LengthValidator(
|
||||
private val length: Int,
|
||||
@StringRes errorMessage: Int
|
||||
) : SimpleValidator(
|
||||
condition = { text -> text.length == length },
|
||||
errorMessage = errorMessage
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
class LowerCaseValidator(
|
||||
@StringRes errorMessage: Int
|
||||
) : CharValidator(
|
||||
condition = { char -> char.isLetter() && char.isLowerCase() },
|
||||
errorMessage = errorMessage
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.joda.time.DateTime
|
||||
|
||||
class MaxDateValidator(
|
||||
private val maxDate: Long,
|
||||
@StringRes val errorMessage: Int
|
||||
) : SimpleValidator(
|
||||
condition = { text ->
|
||||
text.toLongOrNull()
|
||||
?.let { dateInMills -> DateTime(dateInMills).isBefore(maxDate) }
|
||||
?: false
|
||||
},
|
||||
errorMessage = errorMessage
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
class MinDateValidator(
|
||||
private val minDate: Long,
|
||||
@StringRes val errorMessage: Int
|
||||
) : SimpleValidator(
|
||||
condition = { text ->
|
||||
text.toLongOrNull()
|
||||
?.let { dateInMills -> dateInMills >= minDate }
|
||||
?: false
|
||||
},
|
||||
errorMessage = errorMessage
|
||||
)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
class NameValidator(
|
||||
@StringRes errorMessage: Int
|
||||
) : SimpleValidator(
|
||||
condition = { name ->
|
||||
NAME_PATTERN.matches(name) && !MISMATCH_PATTERN.containsMatchIn(name)
|
||||
},
|
||||
errorMessage = errorMessage
|
||||
) {
|
||||
private companion object {
|
||||
// "\u0020" - пробел
|
||||
const val BASE_SYMBOLS = "а-яА-ЯёЁ"
|
||||
|
||||
val NAME_PATTERN = "^[$BASE_SYMBOLS][\\u0020\\-$BASE_SYMBOLS]*[$BASE_SYMBOLS]\$".toRegex()
|
||||
|
||||
// TODO придумать как включить всё это в одну регулярку
|
||||
// проверка на пробелы и дефисы
|
||||
val MISMATCH_PATTERN = "\\u0020\\u0020|\\u0020-|-\\u0020|--".toRegex()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
class NumberValidator(
|
||||
@StringRes errorMessage: Int
|
||||
) : RegexValidator(
|
||||
regex = NUMBER_PATTERN.toRegex(),
|
||||
errorMessage = errorMessage
|
||||
) {
|
||||
private companion object {
|
||||
const val NUMBER_PATTERN = "\\d+"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import java.util.regex.Pattern
|
||||
|
||||
open class PatternValidator(
|
||||
private val pattern: Pattern,
|
||||
@StringRes val errorMessage: Int
|
||||
) : SimpleValidator(
|
||||
condition = { text -> pattern.matcher(text).find() },
|
||||
errorMessage = errorMessage
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
class PhoneValidator(
|
||||
@StringRes errorMessage: Int
|
||||
) : RegexValidator(
|
||||
regex = PHONE_PATTERN.toRegex(),
|
||||
errorMessage = errorMessage
|
||||
) {
|
||||
private companion object {
|
||||
const val PHONE_PATTERN = "\\d{11}"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.joda.time.DateTime
|
||||
|
||||
class PostDateValidator(
|
||||
@StringRes incorrectDateRes: Int,
|
||||
@StringRes incorrectMaxDateRes: Int,
|
||||
@StringRes incorrectMinDateRes: Int,
|
||||
minDate: Long,
|
||||
maxDate: Long = DateTime.now().millis
|
||||
) : ContainerValidator(
|
||||
DateFormatValidator(incorrectDateRes),
|
||||
MaxDateValidator(
|
||||
maxDate = maxDate,
|
||||
errorMessage = incorrectMaxDateRes
|
||||
),
|
||||
MinDateValidator(
|
||||
minDate = minDate,
|
||||
errorMessage = incorrectMinDateRes
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import ru.touchin.roboswag.views.input.utils.DateValidationUtils
|
||||
|
||||
class PreDateFormatValidator(@StringRes errorMessage: Int) : SimpleValidator(
|
||||
condition = { date -> DateValidationUtils.isDatePreValid(date) },
|
||||
errorMessage = errorMessage
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
open class RegexValidator(
|
||||
private val regex: Regex,
|
||||
@StringRes val errorMessage: Int
|
||||
) : SimpleValidator(
|
||||
condition = { text -> regex.matches(text) },
|
||||
errorMessage = errorMessage
|
||||
) {
|
||||
constructor(regex: String, @StringRes errorMessage: Int) : this(regex.toRegex(), errorMessage)
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import ru.touchin.roboswag.views.input.validatable.validator.ValidateResult
|
||||
|
||||
open class SimpleValidator(
|
||||
private val condition: (String) -> Boolean,
|
||||
@StringRes private val errorMessage: Int
|
||||
) : BasePredicateValidator(
|
||||
predicate = { text ->
|
||||
when (condition.invoke(text)) {
|
||||
true -> ValidateResult.Success(text)
|
||||
false -> ValidateResult.Error(text, errorMessage)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
class TimeValidator(
|
||||
@StringRes errorMessage: Int
|
||||
) : RegexValidator(
|
||||
regex = TIME_PATTERN.toRegex(),
|
||||
errorMessage = errorMessage
|
||||
) {
|
||||
private companion object {
|
||||
const val TIME_PATTERN = "^([0-1]?[0-9]|2[0-3]):[0-5][0-9]\$"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package ru.touchin.roboswag.views.input.validatable.validator.implementation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
class UpperCaseValidator(
|
||||
@StringRes errorMessage: Int
|
||||
) : CharValidator(
|
||||
condition = { char -> char.isLetter() && char.isUpperCase() },
|
||||
errorMessage = errorMessage
|
||||
)
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:parentTag="android.widget.LinearLayout">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:tag="text_input_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:errorEnabled="false"
|
||||
tools:hint="Hint"
|
||||
tools:textColorHint="@android:color/darker_gray">
|
||||
|
||||
<ru.touchin.roboswag.views.input.MaskedHintEditText
|
||||
android:tag="edit_text"
|
||||
android:textColorHighlight="@android:color/holo_green_dark"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusableInTouchMode="true"
|
||||
android:background="@null"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
tools:text="Text" />
|
||||
<!--TODO add android:textAppearance to styles-->
|
||||
|
||||
<View
|
||||
android:tag="underline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0.5dp"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:background="@android:color/darker_gray" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:tag="support_hint_start_image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="4dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
tools:src="@android:drawable/btn_plus"
|
||||
tools:tint="@android:color/darker_gray" />
|
||||
|
||||
<TextView
|
||||
android:tag="support_hint"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center"
|
||||
tools:text="Bottom hint Bottom hint Bottom hint\nBottom hint" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</merge>
|
||||
|
|
@ -75,4 +75,9 @@
|
|||
<attr name="isUnderlineText"/>
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="MaskedHintEditText">
|
||||
<attr name="maskedHint" format="string" />
|
||||
<attr name="maskedHintColor" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="BaseTextInputView">
|
||||
|
||||
<attr name="validator">
|
||||
<enum name="none" value="0" />
|
||||
<enum name="phone" value="1" />
|
||||
<enum name="date" value="2" />
|
||||
<enum name="time" value="3" />
|
||||
<enum name="empty" value="4" />
|
||||
<enum name="email" value="5" />
|
||||
<enum name="latin" value="6" />
|
||||
<enum name="cyrillic" value="7" />
|
||||
<enum name="number" value="8" />
|
||||
</attr>
|
||||
|
||||
<attr name="validatorMode">
|
||||
<flag name="none" value="0" />
|
||||
<flag name="hasFocus" value="1" />
|
||||
<flag name="lossFocus" value="2" />
|
||||
<flag name="beforeTextChanged" value="4" />
|
||||
<flag name="onTextChanged" value="8" />
|
||||
<flag name="afterTextChanged" value="16" />
|
||||
</attr>
|
||||
|
||||
<attr name="mask">
|
||||
<enum name="none" value="0" />
|
||||
<enum name="phone" value="1" />
|
||||
<enum name="date" value="2" />
|
||||
<enum name="bankCard" value="3" />
|
||||
<enum name="mid" value="4" />
|
||||
</attr>
|
||||
|
||||
<attr name="informationHint" format="string" />
|
||||
<attr name="showInformationHintMode">
|
||||
<enum name="permanent" value="0" />
|
||||
<enum name="onEmpty" value="1" />
|
||||
</attr>
|
||||
<attr name="floatError" format="boolean" />
|
||||
|
||||
<!--Аттрибуты TextInputEditText-->
|
||||
<attr name="errorEnabled" format="boolean" />
|
||||
<attr name="error" format="string" />
|
||||
<attr name="errorTextAppearance" format="reference" />
|
||||
<attr name="errorTextColor" format="reference|color" />
|
||||
|
||||
<attr name="hint" format="string" />
|
||||
<attr name="hintTextAppearance" format="reference" />
|
||||
<attr name="hintTextColor" format="reference|color" />
|
||||
<attr name="informationHintTextColor" format="reference|color" />
|
||||
|
||||
<!--Аттрибуты доставшиеся по "наследству"-->
|
||||
<attr name="editTextMaskedHint" format="string" />
|
||||
<attr name="editTextMaskedHintColor" format="color" />
|
||||
|
||||
<attr name="editTextMargin" format="dimension" />
|
||||
<attr name="editTextMarginTop" format="dimension" />
|
||||
<attr name="editTextMarginBottom" format="dimension" />
|
||||
<attr name="editTextMarginStart" format="dimension" />
|
||||
<attr name="editTextMarginEnd" format="dimension" />
|
||||
|
||||
<attr name="editTextPadding" format="dimension" />
|
||||
<attr name="editTextPaddingTop" format="dimension" />
|
||||
<attr name="editTextPaddingBottom" format="dimension" />
|
||||
<attr name="editTextPaddingStart" format="dimension" />
|
||||
<attr name="editTextPaddingEnd" format="dimension" />
|
||||
|
||||
<attr name="imeOptions">
|
||||
<flag name="normal" value="0x00000000" />
|
||||
<flag name="actionUnspecified" value="0x00000000" />
|
||||
<flag name="actionNone" value="0x00000001" />
|
||||
<flag name="actionGo" value="0x00000002" />
|
||||
<flag name="actionSearch" value="0x00000003" />
|
||||
<flag name="actionSend" value="0x00000004" />
|
||||
<flag name="actionNext" value="0x00000005" />
|
||||
<flag name="actionDone" value="0x00000006" />
|
||||
<flag name="actionPrevious" value="0x00000007" />
|
||||
<flag name="flagNoPersonalizedLearning" value="0x1000000" />
|
||||
<flag name="flagNoFullscreen" value="0x2000000" />
|
||||
<flag name="flagNavigatePrevious" value="0x4000000" />
|
||||
<flag name="flagNavigateNext" value="0x8000000" />
|
||||
<flag name="flagNoExtractUi" value="0x10000000" />
|
||||
<flag name="flagNoAccessoryAction" value="0x20000000" />
|
||||
<flag name="flagNoEnterAction" value="0x40000000" />
|
||||
<flag name="flagForceAscii" value="0x80000000" />
|
||||
</attr>
|
||||
|
||||
<attr name="inputType">
|
||||
<flag name="none" value="0x00000000" />
|
||||
<flag name="text" value="0x00000001" />
|
||||
<flag name="textCapCharacters" value="0x00001001" />
|
||||
<flag name="textCapWords" value="0x00002001" />
|
||||
<flag name="textCapSentences" value="0x00004001" />
|
||||
<flag name="textAutoCorrect" value="0x00008001" />
|
||||
<flag name="textAutoComplete" value="0x00010001" />
|
||||
<flag name="textMultiLine" value="0x00020001" />
|
||||
<flag name="textImeMultiLine" value="0x00040001" />
|
||||
<flag name="textNoSuggestions" value="0x00080001" />
|
||||
<flag name="textUri" value="0x00000011" />
|
||||
<flag name="textEmailAddress" value="0x00000021" />
|
||||
<flag name="textEmailSubject" value="0x00000031" />
|
||||
<flag name="textShortMessage" value="0x00000041" />
|
||||
<flag name="textLongMessage" value="0x00000051" />
|
||||
<flag name="textPersonName" value="0x00000061" />
|
||||
<flag name="textPostalAddress" value="0x00000071" />
|
||||
<flag name="textPassword" value="0x00000081" />
|
||||
<flag name="textVisiblePassword" value="0x00000091" />
|
||||
<flag name="textWebEditText" value="0x000000a1" />
|
||||
<flag name="textFilter" value="0x000000b1" />
|
||||
<flag name="textPhonetic" value="0x000000c1" />
|
||||
<flag name="textWebEmailAddress" value="0x000000d1" />
|
||||
<flag name="textWebPassword" value="0x000000e1" />
|
||||
<flag name="number" value="0x00000002" />
|
||||
<flag name="numberSigned" value="0x00001002" />
|
||||
<flag name="numberDecimal" value="0x00002002" />
|
||||
<flag name="numberPassword" value="0x00000012" />
|
||||
<flag name="phone" value="0x00000003" />
|
||||
<flag name="datetime" value="0x00000004" />
|
||||
<flag name="date" value="0x00000014" />
|
||||
<flag name="time" value="0x00000024" />
|
||||
</attr>
|
||||
|
||||
<attr name="digits" format="string" />
|
||||
<attr name="drawableEnd" format="reference|color" />
|
||||
<attr name="editTextFocusable" format="boolean" />
|
||||
<attr name="editTextFocusableInTouchMode" format="boolean" />
|
||||
<attr name="editTextLongClickable" format="boolean" />
|
||||
<attr name="editText" format="string" localization="suggested" />
|
||||
<attr name="editTextColor" format="reference|color" />
|
||||
<attr name="editTextAppearance" format="reference" />
|
||||
|
||||
<attr name="maxLength" format="integer" min="0" />
|
||||
<attr name="maxLines" format="integer" min="0" />
|
||||
<attr name="editTextBackgroundTint" format="reference|color" />
|
||||
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
Loading…
Reference in New Issue