Compare commits
2 Commits
master
...
feature/ba
| Author | SHA1 | Date |
|---|---|---|
|
|
dbd0587438 | |
|
|
4baf118cc2 |
|
|
@ -12,7 +12,7 @@ dependencies {
|
||||||
constraints {
|
constraints {
|
||||||
implementation("androidx.appcompat:appcompat") {
|
implementation("androidx.appcompat:appcompat") {
|
||||||
version {
|
version {
|
||||||
require '1.7.0-alpha03'
|
require '1.0.2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,21 +25,9 @@ abstract class KeyboardResizeableViewController<TActivity : BaseActivity, TState
|
||||||
lifecycle.addObserver(activity.keyboardBehaviorDetector as LifecycleObserver)
|
lifecycle.addObserver(activity.keyboardBehaviorDetector as LifecycleObserver)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val onKeyboardHideListener = {
|
|
||||||
if (isKeyboardVisible) {
|
|
||||||
onKeyboardHide()
|
|
||||||
}
|
|
||||||
isKeyboardVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private val onKeyboardShowListener = { diff: Int ->
|
|
||||||
onKeyboardShow(diff)
|
|
||||||
isKeyboardVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isKeyboardVisible: Boolean = false
|
private var isKeyboardVisible: Boolean = false
|
||||||
|
|
||||||
private val keyboardHidingOnBackPressedListener = OnBackPressedListener {
|
private val keyboardHideListener = OnBackPressedListener {
|
||||||
if (isKeyboardVisible) {
|
if (isKeyboardVisible) {
|
||||||
UiUtils.OfViews.hideSoftInput(activity)
|
UiUtils.OfViews.hideSoftInput(activity)
|
||||||
true
|
true
|
||||||
|
|
@ -58,31 +46,39 @@ abstract class KeyboardResizeableViewController<TActivity : BaseActivity, TState
|
||||||
isHideKeyboardOnBackEnabled = true
|
isHideKeyboardOnBackEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
activity.keyboardBehaviorDetector?.apply {
|
|
||||||
addOnHideListener(onKeyboardHideListener)
|
|
||||||
addOnShowListener(onKeyboardShowListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
if (isHideKeyboardOnBackEnabled) activity.addOnBackPressedListener(keyboardHidingOnBackPressedListener)
|
if (isHideKeyboardOnBackEnabled) activity.addOnBackPressedListener(keyboardHideListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
notifyKeyboardHidden()
|
notifyKeyboardHidden()
|
||||||
if (isHideKeyboardOnBackEnabled) activity.removeOnBackPressedListener(keyboardHidingOnBackPressedListener)
|
if (isHideKeyboardOnBackEnabled) activity.removeOnBackPressedListener(keyboardHideListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
activity.keyboardBehaviorDetector?.apply {
|
||||||
|
keyboardHideListener = {
|
||||||
|
if (isKeyboardVisible) {
|
||||||
|
onKeyboardHide()
|
||||||
|
}
|
||||||
|
isKeyboardVisible = false
|
||||||
|
}
|
||||||
|
keyboardShowListener = { diff ->
|
||||||
|
onKeyboardShow(diff)
|
||||||
|
isKeyboardVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
activity.keyboardBehaviorDetector?.apply {
|
activity.keyboardBehaviorDetector?.apply {
|
||||||
removeOnHideListener(onKeyboardHideListener)
|
keyboardHideListener = null
|
||||||
removeOnShowListener(onKeyboardShowListener)
|
keyboardShowListener = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,4 +86,5 @@ abstract class KeyboardResizeableViewController<TActivity : BaseActivity, TState
|
||||||
if (isKeyboardVisible) onKeyboardHide()
|
if (isKeyboardVisible) onKeyboardHide()
|
||||||
isKeyboardVisible = false
|
isKeyboardVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,13 +69,12 @@ open class ViewController<TActivity : FragmentActivity, TState : Parcelable>(
|
||||||
|
|
||||||
val view: View = creationContext.inflater.inflate(layoutRes, creationContext.container, false)
|
val view: View = creationContext.inflater.inflate(layoutRes, creationContext.container, false)
|
||||||
|
|
||||||
override val lifecycle: Lifecycle
|
|
||||||
get() = fragment.viewLifecycleOwner.lifecycle
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
lifecycle.addObserver(LifecycleLoggingObserver(this))
|
lifecycle.addObserver(LifecycleLoggingObserver(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLifecycle(): Lifecycle = fragment.viewLifecycleOwner.lifecycle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look for a child view with the given id. If this view has the given id, return this view.
|
* Look for a child view with the given id. If this view has the given id, return this view.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -71,19 +71,6 @@ open class DelegationListAdapter<TItem>(config: AsyncDifferConfig<TItem>) : Recy
|
||||||
*/
|
*/
|
||||||
fun submitList(list: List<TItem>) = differ.submitList(list)
|
fun submitList(list: List<TItem>) = differ.submitList(list)
|
||||||
|
|
||||||
/**
|
|
||||||
* Submits a new list to be diffed, and displayed.
|
|
||||||
*
|
|
||||||
* The commit callback can be used to know when the List is committed, but note that it
|
|
||||||
* may not be executed. If List B is submitted immediately after List A, and is
|
|
||||||
* committed directly, the callback associated with List A will not be run.
|
|
||||||
*
|
|
||||||
* @param newList The new List.
|
|
||||||
* @param commitCallback Optional runnable that is executed when the List is committed, if
|
|
||||||
* it is committed.
|
|
||||||
*/
|
|
||||||
fun submitList(list: List<TItem>?, commitCallback: (() -> Unit)?) = differ.submitList(list, commitCallback)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current List - any diffing to present this list has already been computed and
|
* Get the current List - any diffing to present this list has already been computed and
|
||||||
* dispatched via the ListUpdateCallback.
|
* dispatched via the ListUpdateCallback.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ apply from: "../android-configs/lib-config.gradle"
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 23
|
||||||
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +18,8 @@ dependencies {
|
||||||
|
|
||||||
implementation "com.google.android.material:material"
|
implementation "com.google.android.material:material"
|
||||||
implementation "androidx.core:core-ktx"
|
implementation "androidx.core:core-ktx"
|
||||||
|
implementation "com.redmadrobot:inputmask:3.4.4"
|
||||||
|
implementation "net.danlew:android.joda:2.10.3"
|
||||||
|
|
||||||
constraints {
|
constraints {
|
||||||
implementation("com.google.android.material:material") {
|
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
|
||||||
|
)
|
||||||
|
|
@ -36,20 +36,17 @@ class LoadingContentView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateView(state: State) {
|
private fun updateView(state: State) {
|
||||||
when (state) {
|
if (state == State.ShowContent) {
|
||||||
State.ShowContent -> {
|
|
||||||
getChildAt(childCount - 1)?.let { showChild(it.id) }
|
getChildAt(childCount - 1)?.let { showChild(it.id) }
|
||||||
}
|
} else {
|
||||||
|
when (state) {
|
||||||
is State.Stub -> {
|
is State.Stub -> {
|
||||||
setStubText(state.stubText)
|
setStubText(state.stubText)
|
||||||
showChild(R.id.text_stub)
|
showChild(R.id.text_stub)
|
||||||
}
|
}
|
||||||
|
is State.Loading -> {
|
||||||
State.Loading -> {
|
|
||||||
showChild(R.id.progress_bar)
|
showChild(R.id.progress_bar)
|
||||||
}
|
}
|
||||||
|
|
||||||
is State.Error -> {
|
is State.Error -> {
|
||||||
binding.apply {
|
binding.apply {
|
||||||
errorText.text = state.errorText
|
errorText.text = state.errorText
|
||||||
|
|
@ -60,6 +57,7 @@ class LoadingContentView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sealed class State {
|
sealed class State {
|
||||||
object ShowContent : State()
|
object ShowContent : State()
|
||||||
|
|
|
||||||
|
|
@ -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"/>
|
<attr name="isUnderlineText"/>
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
|
<declare-styleable name="MaskedHintEditText">
|
||||||
|
<attr name="maskedHint" format="string" />
|
||||||
|
<attr name="maskedHintColor" format="color" />
|
||||||
|
</declare-styleable>
|
||||||
|
|
||||||
</resources>
|
</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