I'm creating a form on an android application using kotlin. Each field of the form is associated with a livedata, to validate or update the data instantly. To obtain this result, I used a textwatcher, which updates the associated livedata at each change of the text. The problem is that with each update, it places the cursor at the start of the field making it impossible to write continuously.
I paste here the intresting part of the code:
Activity
viewModel.name.observe(lifecycleOwner, Observer { nameValue->
binding.nameEditText.setText(
nameValue?.toString()
)
binding.nameTextInputLayout.error =
viewModel.nameError.value
})
viewModel.price.observe(lifecycleOwner, Observer { priceValue->
binding.priceEditText.setText(
price?.toString()
)
binding.priceTextInputLayout.error =
viewModel.priceError.value
})
binding.nameEditText.addTextChangedListener(
TextFieldValidation(
binding.nameEditText
)
)
binding.priceEditText.addTextChangedListener(
TextFieldValidation(
binding.priceEditText
)
)
inner class TextFieldValidation(private val view: View) : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
when (view.id) {
R.id.nameEditText-> {
viewModel.onNameChanged(s.toString())
viewModel.isNameValid()
}
R.id.priceEditText-> {
viewModel.onPriceChanged(s.toString())
viewModel.isPriceValid()
}
}
}
ViewModel
var name = MutableLiveData<String>()
var price = MutableLiveData<Double>()
var nameError = MutableLiveData<String>()
var priceError = MutableLiveData<String>()
fun onNameChanged(newValue: String?) {
if ((name.value != newValue)) {
name.value = newValue
}
}
fun onPriceChanged(newValue: Double?) {
if ((price.value != newValue)) {
price.value = newValue
}
}
fun isNameValid() : Boolean {
return if ((name.value == null) || (name.value == "")) {
nameError.value = "Error"
false
} else {
nameError.value = ""
true
}
}
fun isPriceValid() : Boolean {
return if ((price.value == null) || (price.value == "")) {
priceError.value = "Error"
false
} else {
priceError.value = ""
true
}
}
XML
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/nameTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="#+id/nameEditText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:backgroundTint="#color/white"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/priceTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="#+id/priceEditText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:backgroundTint="#color/white"
android:inputType="numberDecimal" />
</com.google.android.material.textfield.TextInputLayout>
I tried using 'mEdittext.setSelection (mEdittext.length ());' but it doesn't work well, because if I make changes in the middle of the string, it brings the cursor to the end. And even in double fields, it doesn't behave correctly.
I need to have the cursor always at the exact position? Either in case of adding, deleting or writing in the middle of the string. And both in the case of a string and in the case of a double.
Could someone help me?
Thank you for your patience and help!
This is happening, because in your ViewModel observer you are setting nameValue and priceValue back to the EditText where they just came from. You could add a check and not set unchanged values back to the EditText:
if ((nameValue?.toString() == binding.nameEditText.getText().toString()) == false) {
binding.nameEditText.setText(nameValue?.toString())
}
I also had the same problem that the cursor moved to the beginning, searching I found a first solution that served me to a certain extent allowing me to write a message in a row, which would be the following
viewModel.name.observe(lifecycleOwner, Observer { nameValue->
binding.nameEditText.setText(
nameValue?.toString()
)
binding.nameEditText.setSelection(
nameValue?.toString().length
)
})
I thought I had already solved it, but another problem arose and that is that if I wanted to edit any part of the center after typing a character the cursor would move to the end, until I found the correct solution https://stackoverflow.com/a/65794745/15798571
viewModel.name.observe(lifecycleOwner, Observer { nameValue->
val selection = binding.nameEditText.selectionEnd
val length = nameValue.length
binding.nameEditText.setText(
nameValue?.toString()
)
binding.nameEditText.setSelection(
Math.min(selection, length)
)
})
I hope it serves you as it served me
Related
Need help to put some pin code menu with a numpad input on some menus in TvSettings(https://github.com/LineageOS/android_packages_apps_TvSettings) like
Display, Apps, Developer Options.
If someone enters the wrong pin, the menus don't open.
Also put a build.prop device.model check inside the code so it asks for a pin if the app is used on another device.
Reference
https://blog.csdn.net/zcyxiaxi/article/details/119444898
I've created PIN input for user login before.
I've used this 3rd party PIN view: https://github.com/cren90/PinLockView.
Here's some code that can put you on the right mind:
PinFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
setUpPinView()
}
private fun setUpPinView() {
binding.pinLockView.attachIndicator(binding.indicatorDots)
binding.pinLockView.pinLockListener = object : PinLockListener {
override fun onPinSubmit(pin: String) {
Logger.d("Pin submit: $pin")
mPinFragmentListener!!.onPinSubmit(pin)
}
override fun onComplete(pin: String?) {
Logger.d("Pin complete: $pin")
if (numberOfTimesPinComplete == 0) {
numberOfTimesPinComplete++
mPinFragmentListener!!.onPinComplete(pin)
} else {
// empty input if pin was complete once before
binding.pinLockView.pin = ""
numberOfTimesPinComplete = 0
}
}
override fun onEmpty() {
Logger.d("Pin empty")
numberOfTimesPinComplete = 0
}
override fun onPinChange(pinLength: Int, intermediatePin: String?) {
Logger.d("Pin changed, new length $pinLength with intermediate pin $intermediatePin")
}
}
}
IntroActivity.kt:
override fun onPinComplete(pin: String?) {
Logger.i("IntroActivity onPinComplete()")
login(pin)
}
override fun onPinSubmit(pin: String?) {
Logger.i("IntroActivity onPinSubmit()")
if (pin.isNullOrEmpty() || pin.length < 4 || pin.length > 4) {
ToastUtils.updateError(this, getString(R.string.pin_incorrect_length))
} else {
login(pin)
}
}
fargment_pin.xml:
<com.cren90.pinlockview.Indicator
android:id="#+id/indicatorDots"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:background="#color/primaryColor"
android:textAlignment="center"
app:indicatorEmptyBackground="#drawable/indicator_empty"
app:indicatorFilledBackground="#drawable/indicator_filled"
app:indicatorSpacing="13dp"
app:indicatorTextSize="24sp"
app:indicatorTint="#color/white"
app:indicatorType="fillWithValue"
app:layout_constraintBottom_toTopOf="#+id/pinLockView"
app:layout_constraintEnd_toEndOf="#+id/pinLockView"
app:layout_constraintStart_toStartOf="#+id/pinLockView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread" />
<com.cren90.pinlockview.PinLockView
android:id="#+id/pinLockView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:keypadButtonSize="65dp"
app:keypadDeleteButtonTintColor="#color/primaryColor"
app:keypadDeleteButtonDrawable="#drawable/ic_backspace_24dp"
app:keypadDeleteButtonSize="20dp"
app:keypadHorizontalSpacing="24dp"
app:keypadShowSubmitButton="true"
app:keypadSubmitButtonTintColor="#color/secondaryColor"
app:keypadTextColor="#color/black"
app:keypadTextSize="#dimen/key_text_size"
app:keypadVerticalSpacing="18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/indicatorDots"
app:pinLength="4" />
I am learning kotlin flow in android. I want to basically instant search in my list and filter to show in reyclerview. I searched in google and found this amazing medium post. This post is basically search from google. I want to search item in list and show in reyclerview. Can someone guide me how can I start this. I am explanning in more detail
Suppose I have one SearchBox and one Reyclerview which one item abc one, abc two, xyz one, xyz two... etc.
main image when all data is combine
Scenario 1
when I start typing in SearchBox and enter small a or capital A I want to show only two item matching in recyclerview, look like this
Scenario 2
when I enter any wrong text in SearchBox I want to basically show a text message that not found, look like this
Any guidance would be great. Thanks
I am adding my piece of code
ExploreViewModel.kt
class ExploreViewModel(private var list: ArrayList<Category>) : BaseViewModel() {
val filteredTopics = MutableStateFlow<List<opics>>(emptyList())
var topicSelected: TopicsArea? = TopicsArea.ALL
set(value) {
field = value
handleTopicSelection(field ?: TopicsArea.ALL)
}
private fun handleTopicSelection(value: TopicsArea) {
if (value == TopicsArea.ALL) {
filterAllCategories(true)
} else {
filteredTopics.value = list.firstOrNull { it.topics != null && it.title == value.title }
?.topics?.sortedBy { topic -> topic.title }.orEmpty()
}
}
fun filterAllCategories(isAllCategory: Boolean) {
if (isAllCategory && topicSelected == TopicsArea.ALL && !isFirstItemIsAllCategory()) {
list.add(0, code = TopicsArea.ALL.categoryCode))
} else if (isFirstItemIsAllCategory()) {
list.removeAt(0)
}
filteredTopics.value = list.flatMap { it.topics!! }.distinctBy { topic -> topic.title }.sortedBy { topic -> topic.title }
}
private fun isFirstItemIsAllCategory() = list.firstOrNull()?.code == TopicsArea.ALL
}
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.SearchView
android:id="#+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="16dp"
app:closeIcon="#drawable/ic_cancel"
app:layout_constraintBottom_toTopOf="#+id/exploreScroll"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed" />
<HorizontalScrollView
android:id="#+id/exploreScroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/searchView">
<com.google.android.material.chip.ChipGroup
android:id="#+id/exploreChips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipSpacingHorizontal="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:singleLine="true"
app:singleSelection="true" />
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/exploreList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="20dp"
android:paddingTop="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_default="wrap"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/exploreScroll" />
</androidx.constraintlayout.widget.ConstraintLayout>
Category.kt
#Parcelize
data class Category(
val id: String? = null,
val title: String? = null,
val code: String? = null,
val topics: List<Topics>? = null,
) : Parcelable
Topics.kt
#Parcelize
data class Topics(
val id: String? = null,
val title: String? = null
) : Parcelable
Dummy data and coming from server
fun categoriesList() = listOf(
Categories("21", "physical", listOf(Topics("1", "Abc one"), Topics("2", "Abc Two"))),
Categories("2211", "mind", listOf(Topics("1", "xyz one"), Topics("2", "xyz two"))),
Categories("22131", "motorized", listOf(Topics("1", "xyz three"), Topics("2", "xyz four"))),
)
In my view model list is holding above dummy data. And In my recyclerview I am passing the whole object and I am doing flatMap to combine all data into list. Make sure In recyclerview is using Topic and using title property. In Image Abc one, Abc two is holding in Topic. Thanks
After #Tenfour04 suggestion I will go to A2 suggestion because I have already data which converted into flow and passing in my adapter. I am adding my activity code as well.
ExploreActivity.kt
class ExploreActivity : AppCompatActivity() {
private val binding by lazy { ExploreLayoutBinding.inflate(layoutInflater) }
val viewModel by viewModel<ExploreViewModel> {
val list = intent?.getParcelableArrayListExtra(LIST_KEY) ?: emptyList<Category>()
parametersOf(list)
}
var exploreAdapter = ExploreAdapter { topic -> handleNextActivity(topic) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setupView()
}
fun setupView() {
setupSearchView()
setupFilteredTopic()
setupExploreAdapter()
}
private fun setupFilteredTopic() {
lifecycleScope.launchWhenCreated {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.filteredTopics.collect { filteredTopicsList ->
exploreAdapter.submitList(filteredTopicsList)
}
}
}
}
fun setupSearchView() {
binding.searchView.apply {
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?) = false
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
})
}
}
fun setupExploreAdapter() {
with(binding.exploreList) {
adapter = exploreAdapter
}
}
}
UPDATE 2
ExploreViewModel.kt
val filteredCategories = query
.debounce(200) // low debounce because we are just filtering local data
.distinctUntilChanged()
.combine(filteredTopics) { queryText, categoriesList ->
val criteria = queryText.lowercase()
if (criteria.isEmpty()) {
return#combine filteredTopics
} else {
categoriesList.filter { category -> category.title?.lowercase()?.let { criteria.contains(it) } == true }
}
}
I am getting error when I set in adapter
fixed
filteredTopics.value
The tutorial you linked has a Flow produced by the SearchView. If you want to keep the search functionality in your ViewModel, you can put a MutableStateFlow in your ViewModel that will be updated by the SearchView indirectly. You can expose a property for updating the query.
There are two different ways this could be done, depending on whether you (A) already have a complete list of your data that you want to query quickly or (B) you want to query a server or your database every time your query text changes.
And then even (A) can be broken up into: (A1) you have a static plain old List, or (A2) your source List comes from a Flow, such as a returned Room flow that is not based on query parameters.
All code below is in the ViewModel class.
A1:
private val allCategories = categoriesList()
private val query = MutableStateFlow("")
// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
get() = query.value
set(value) { query.value = value }
// This is the flow that should be observed for the updated list that
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
.debounce(200) // low debounce because we are just filtering local data
.distinctUntilChanged()
.map {
val criteria = it.lowercase()
allCategories.filter { category -> criteria in category.title.lowercase }
}
A2:
In this example I put a simple placeholder flow for the upstream server query. This could be any flow.
private val allCategories = flow {
categoriesList()
}
private val query = MutableStateFlow("")
// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
get() = query.value
set(value) { query.value = value }
// This is the flow that should be observed for the updated list that
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
.debounce(200) // low debounce because we are just filtering local data
.distinctUntilChanged()
.combine(allCategories) { queryText, categoriesList ->
val criteria = queryText.lowercase()
categoriesList.filter { category -> criteria in category.title.lowercase }
}
B
private val query = MutableStateFlow("")
// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
get() = query.value
set(value) { query.value = value }
// This is the flow that should be observed for the updated list that
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
.debounce(500) // maybe bigger to avoid too many queries
.distinctUntilChanged()
.map {
val criteria = it.lowercase()
categoriesList(criteria) // up to you to implement this depending on source
}
I faced the following problem: I need to implement the solution for the case when the phone should be entered to the EditText. This phone should have non-removable part and last four numbers should be filled with underscore at the beginning and then when user typing them underscores should be changed to numbers, like:
+12345____ -> typing 6 -> +123456___
I implemented the non-removable part. Here's the way how I did it:
binding.etPhoneNumber.filters = arrayOf(InputFilter.LengthFilter(args.phoneNumber?.length ?: 0))
binding.etPhoneNumber.doAfterTextChanged {
val symbolsLeft = it?.toString()?.length ?: 0
if (symbolsLeft < phoneNumberUi.length) {
binding.etPhoneNumber.setText(phoneNumberUi)
binding.etPhoneNumber.setSelection(symbolsLeft + 1)
}
}
But now I do not understand, how to handle the logic with underscores. I tried to append the underscores in doAfterTextChanged, like if ((args.phoneNumber?.length ?: 0) > (it?.toString()?.length ?: 0)) append n underscores, where n is the difference between length, but in this case I cannot add new symbols as EditText is filled and underscores are not removed. So, how can I solve the problem? Thanks in advance for any help!
You can remove the LengthFilter and check the length in doAfterTextChanged :
val phoneNumberUi = "+12345"
val length = 10
binding.etPhoneNumber.doAfterTextChanged {
when {
it == null -> {
}
// missing / incomplete prefix
it.length < phoneNumberUi.length -> {
it.replace(0, it.length, phoneNumberUi)
}
// prefix was edited
!it.startsWith(phoneNumberUi) -> {
it.replace(0, phoneNumberUi.length, phoneNumberUi)
}
// too short
it.length < length -> {
it.append("_".repeat(length - it.length))
}
// too long
it.length > length -> {
it.replace(length, it.length, "")
}
// set the cursor at the first _
it.indexOf("_") >= 0 -> {
binding.etPhoneNumber.setSelection(it.indexOf("_"))
}
}
}
Note : This uses a when because every change triggers immediately a recursive call to doAfterTextChanged
This approach has the following conditional branches
The place where the user adds their input (non-removable part or the changeable part)
The entered char (number or backspace)
And works by getting the entered char (number/backspace) within the onTextChanged() and its index (second parameter), and set the new EditText value upon the values of both of them.
Also the value of the EditText is tracked by currentText variable. So that we can only replace one character at a time which is the input done by the user to avoid the burden of manipulating the entire text.
You can find the rest of the explanation by the below comments through the code:
attachTextWatcher(findViewById(R.id.edittext))
fun attachTextWatcher(editText: EditText) {
// set the cursor to the first underscore
editText.setSelection(editText.text.indexOf("_"))
var currentText = editText.text.toString() // which is "+12345____"
val watcher: TextWatcher = object : TextWatcher {
override fun onTextChanged(
s: CharSequence,
newCharIndex: Int, // "newCharIndex" is the index of the new entered char
before: Int,
count: Int
) {
// New entered char by the user that triggers the TextWatcher callbacks
val newChar = s.subSequence(newCharIndex, newCharIndex + count).toString().trim()
/* Stop the listener in order to programmatically
change the EditText Without triggering the TextWatcher*/
editText.removeTextChangedListener(this)
// Setting the new text of the EditText upon examining the user input
currentText =
if (newChar.isEmpty()) { // User entered backspace to delete a char
if (newCharIndex in 0..5) { // The backspace is pressed in the non-removable part
"+12345" + currentText.substring(6)
} else { // The backspace is pressed in the changeable part
val sb = StringBuilder(currentText)
// replace the the number at which backspace pressed with underscore
sb.setCharAt(newCharIndex, '_')
sb.toString()
}
} else { // User entered a number
if (newCharIndex in 0..5) { // The number is entered in the non-removable part
// replace the first underscore with the entered number
val sb = StringBuilder(currentText)
sb.setCharAt(sb.indexOf("_"), newChar[0])
sb.toString()
} else { // The number is entered in the changeable part
if (newCharIndex < 10) { // Avoid ArrayOutOfBoundsException as the number length should not exceed 10
val sb = StringBuilder(currentText)
// replace the the number at which the number is entered with the new number
sb.setCharAt(newCharIndex, newChar[0])
sb.toString()
} else currentText
}
}
// Set the adjusted text to the EditText
editText.setText(currentText)
// Set the current cursor place
if (editText.text.contains("_"))
editText.setSelection(editText.text.indexOf("_"))
else
editText.setSelection(editText.text.length)
// Re-add the listener, so that the EditText can intercept the number by the user
editText.addTextChangedListener(this)
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}
override fun afterTextChanged(s: Editable) {
}
}
editText.addTextChangedListener(watcher)
}
This is the layout I'm testing with:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="#+id/edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:maxLength="11"
android:text="+12345____"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Note: make sure to survive the value of the currentText on configuration change.
preview
I think that PhúcNguyễn had a good idea to marry a TextView with an EditText to produce what you are looking for. You could place these as separate fields in a layout or place them in a composite view. Either way the effect will be the same and you could achieve what you want.
You have already figured out how to handle the static text at the beginning of the field. What I am presenting below is how to handle the underscores so characters that are entered appear to overwrite the underscores.
For the demo, I have place a TextView with the static text beside a custom EditText. It is the custom EditText that is really of any interest. With the custom view, the onDraw() function is overridden to write the underscores as part of the background. Although these underscores will appear like any other character in the field, they cannot be selected, deleted, skipped over or manipulated in any way except, as the user types, the underscores are overwritten one-by-one. The end padding of the custom view is manipulated to provide room for the underscores and text.
Here is the custom view:
EditTextFillInBlanks.kt
class EditTextFillInBlanks #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : androidx.appcompat.widget.AppCompatEditText(context, attrs, defStyleAttr) {
// Right padding before we manipulate it
private var mBaseRightPadding = 0
// Width of text that has been entered
private var mTextWidth = 0f
// Mad length of data that can be entered in characters
private var mMaxLength = 0
// The blanks (underscores) that we will show
private lateinit var mBlanks: String
// MeasureSpec for measuring width of entered characters.
private val mUnspecifiedWidthHeight = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
init {
mBaseRightPadding = paddingRight
doOnTextChanged { text, _, _, _ ->
measure(mUnspecifiedWidthHeight, mUnspecifiedWidthHeight)
mTextWidth = measuredWidth.toFloat() - paddingStart - paddingEnd
updatePaddingForBlanks(text)
}
setText("", BufferType.EDITABLE)
}
/*
Make sure that the end padding is sufficient to hold the blanks that we are showing.
The blanks (underscores) are written into the expanded padding.
*/
private fun updatePaddingForBlanks(text: CharSequence?) {
if (mMaxLength <= 0) {
mMaxLength = determineMaxLen()
check(mMaxLength > 0) { "Maximum length must be > 0" }
}
text?.apply {
val blanksCount = max(0, mMaxLength - length)
mBlanks = "_".repeat(blanksCount).apply {
updatePadding(right = mBaseRightPadding + paint.measureText(this).toInt())
}
}
}
/*
Draw the underscores on the canvas. They will appear as characters in the field but
cannot be manipulated by the user.
*/
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (mBlanks.isNotEmpty()) {
canvas?.withSave {
drawText(mBlanks, paddingStart + mTextWidth, baseline.toFloat(), paint)
}
}
}
fun setMaxLen(maxLen: Int) {
mMaxLength = maxLen
}
private fun determineMaxLen(): Int {
// Before Lollipop, we can't get max for InputFilter.LengthFilter
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return 0
return filters.firstOrNull { it is InputFilter.LengthFilter }
?.let {
it as InputFilter.LengthFilter
it.max
} ?: 0
}
}
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/holo_blue_light"
tools:context=".MainActivity">
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/white"
android:paddingStart="8dp"
android:paddingTop="8dp"
android:text="+12345"
android:textColor="#android:color/black"
android:textSize="36sp"
app:layout_constraintBaseline_toBaselineOf="#id/editableSuffix"
app:layout_constraintEnd_toStartOf="#+id/editableSuffix"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="#+id/guideline2" />
<com.example.edittextwithblanks.EditTextFillInBlanks
android:id="#+id/editableSuffix"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/edittext_background"
android:inputType="number"
android:maxLength="#integer/blankFillLen"
android:paddingTop="8dp"
android:paddingEnd="8dp"
android:textColor="#android:color/black"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="#+id/textView"
app:layout_constraintTop_toTopOf="parent"
tools:text="____">
<requestFocus />
</com.example.edittextwithblanks.EditTextFillInBlanks>
<androidx.constraintlayout.widget.Guideline
android:id="#+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="92dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val mStaticStart = "+12345"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (BuildConfig.VERSION_CODE < Build.VERSION_CODES.P) {
val maxLen = resources.getInteger(R.integer.blankFillLen)
findViewById<EditTextFillInBlanks>(R.id.editableSuffix).setMaxLen(maxLen)
}
}
}
It is likely that you can incorporate your static text handling into the custom view for a complete solution.
I developed an android app that requires 3 cascading dropdownlists using TextInputLayouts and AutoComplete views. I am pulling the data from SQL and thru okhttp I receive the response and I array the results. The problem is that when I select an item from the list, it does not place the selected item in the view. Instead, it is placing some kind of hex string. I did not wire up the other two dropdownlists, so nothing is tied into it.
e.g.: com.testapp.testappmobilekotlin.model.VendorResponse#a3014fb
This is what is displayed when I select an option, "HVAC Supply"
Below is a snippet from the activity. I am assuming that my base adapter and api calls are fine since it is populating the array and the list as designed.
private fun loadVendorList() {
val sharedPreferences: SharedPreferences =
getSharedPreferences("sharedPrefs", Context.MODE_PRIVATE)
val tenantId: String? = sharedPreferences.getString("tenantId_KEY", null)
val token: String? = sharedPreferences.getString("accessToken_KEY", null)
// make API call
val call = checkStockService.getVendorList(tenantId,
"Bearer $token",
tenantId?.toInt(),
"application/json"
)
call.enqueue(object : Callback<VendorResult> {
override fun onResponse(
call: Call<VendorResult>,
response: Response<VendorResult>
) {
if (response.isSuccessful) {
vendorItemsArrayList = response.body()!!.result as ArrayList<VendorResponse>?
// initialize adapter
vendorAdapter = VendorRequestBaseAdapter(
this#CheckStockActivity,
R.layout.dropdown_item,
vendorItemsArrayList
)
val editTextFilledExposedDropdown =
findViewById<AutoCompleteTextView>(R.id.selectVendor)
editTextFilledExposedDropdown.setAdapter(vendorAdapter)
}
}
override fun onFailure(call: Call<VendorResult>, t: Throwable) {
Toast.makeText(this#CheckStockActivity, "error :(", Toast.LENGTH_SHORT).show()
}
})
}
Here is my XML layout where I house the TextInputLayout and AutoCompleteTextView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/ll_dropdown_search">
<com.google.android.material.textfield.TextInputLayout
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="25dp"
android:layout_marginEnd="25dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<AutoCompleteTextView
android:id="#+id/selectVendor"
android:layout_width="284dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="Select Vendor"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
Here is my TextView
<TextView
android:id="#+id/vendorName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:padding="10dp"
android:textColor="#color/black"
android:textStyle="normal"
android:textSize="15sp"
xmlns:android="http://schemas.android.com/apk/res/android"
/>
Below is what happens when I select, let's say, "HVAC Supply" From the list. Any guidance would be more than welcomed! Thanks.
I'm here to answer my own question with the hope that it will help anyone else out there that runs into this problem. Using Kotlin, I realized that I have to set up an "OnItemClickListener" on the AutoCompleteTextView. So, using my model, I coded the following:
val selectVendorDropDown =
findViewById<AutoCompleteTextView>(R.id.selectVendor)
selectVendorDropDown.setAdapter(vendorAdapter)
selectVendorDropDown.onItemClickListener =
AdapterView.OnItemClickListener { parent, view, position, id ->
val selectedItem = parent.adapter.getItem(position) as VendorResponse?
if (selectedItem != null) {
selectVendorDropDown.setText(selectedItem.vendorName)
}
}
This snippet of code, now allows the selected item to be displayed in the textview of whichever view you are using.
Your adapter must have implemented the filterable interface, so you can rewrite convertresulttostring to solve this problem.
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(charSequence: CharSequence): FilterResults {
val charString = charSequence.toString()
val filterResults = FilterResults()
filterResults.values = charString
return filterResults
}
override fun publishResults(
charSequence: CharSequence,
filterResults: FilterResults
) {
}
override fun convertResultToString(resultValue: Any): CharSequence? {
return (resultValue as Selector).Title
}
}
}
I'm trying to make a number look like this
932-874838/9
I did this with my EditText to append the - and / after some spaces
editText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(text: Editable?) {
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onValueChange(s.toString())
}
})
}
private fun onValueChange(value: String) {
mNumberTxtView.text = value
if (value.length == 3) {
mNumberTxtView.append("-")
}
if (value.length == 10) {
mNumberTxtView.append("/")
}
}
When I'm typing like
932
it automatically appends the - , and that works, but after it appends the - and if I type another number it replaces the - with that number instead of continuing, so it becomes 932- at first but when trying to put another number,
9328
it gets replaced like that removing the appended -
I think the problem is inside the onValueChange() method
onValueChange should be like this:
var test: StringBuilder = StringBuilder()
var lastValue: String = ""
fun onValueChange(value: String) {
if(lastValue.length > value.length) {
test.deleteCharAt(test.lastIndex)
if(test.length == 3 || test.length == 10) {
test.deleteCharAt(test.lastIndex)
}
} else {
test.append(value.last())
if (test.length == 3) {
test.append("-")
} else if (test.length == 10) {
test.append("/")
}
}
lastValue = value
textView.text = test
}
Try this, instead.
private fun onValueChange(value: String) {
if (value.length == 3) {
mNumberTxtView.text = "${value}_"
} else if (value.length == 10) {
mNumberTxtView.text = "${value}/"
}
}
Let me know if this works.
(The curly brackets around "value" in the strings may not be necessary. I'm still getting used to Kotlin's way of handling string concatenation.)
Edited to remove redundant and potentially loop-causing part.
You should not change text in beforeTextChanged and afterTextChanged to prevent re-call of those methods by TextWatcher. Make changes in afterTextChanged.
But be careful not to get yourself into an infinite loop, because any changes you make will cause this method to be called again recursively.
So set invoke of onValueChanged into afterTextChanged method
with removal of mNumberTxtView.text = value