Kotlin - Android: Two-way databinding custom property on custom view - android

I've created a custom view for selecting days of the week which results is a string. I'd like to use it with two-way data binding.
<?xml version="1.0" encoding="utf-8"?><com.google.android.material.textfield.TextInputLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="#+id/daypicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ToggleButton
android:id="#+id/tMon"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Mon"
android:textOn="#string/Mon" />
<ToggleButton
android:id="#+id/tTue"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Tue"
android:textOn="#string/Tue" />
<ToggleButton
android:id="#+id/tWed"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Wed"
android:textOn="#string/Wed" />
<ToggleButton
android:id="#+id/tThu"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Thu"
android:textOn="#string/Thu" />
<ToggleButton
android:id="#+id/tFri"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Fri"
android:textOn="#string/Fri" />
<ToggleButton
android:id="#+id/tSat"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Sat"
android:textOn="#string/Sat" />
<ToggleButton
android:id="#+id/tSun"
style="#style/toggleButton"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:background="#drawable/toggle_bg"
android:textOff="#string/Sun"
android:textOn="#string/Sun" />
</LinearLayout></com.google.android.material.textfield.TextInputLayout>
And class to service:
class DayPicker : TextInputLayout {
var days: MutableSet<DayOfWeek> = HashSet()
private lateinit var tMon: ToggleButton
private lateinit var tTue: ToggleButton
private lateinit var tWed: ToggleButton
private lateinit var tThu: ToggleButton
private lateinit var tFri: ToggleButton
private lateinit var tSat: ToggleButton
private lateinit var tSun: ToggleButton
var mContext: Context? = null
constructor(context: Context) : super(context) {
mContext = context
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
mContext = context
initControl(context)
initDays()
initListeners()
}
private fun initListeners() {
initListener(tMon, DayOfWeek.MONDAY)
initListener(tTue, DayOfWeek.TUESDAY)
initListener(tWed, DayOfWeek.WEDNESDAY)
initListener(tThu, DayOfWeek.THURSDAY)
initListener(tFri, DayOfWeek.FRIDAY)
initListener(tSat, DayOfWeek.SATURDAY)
initListener(tSun, DayOfWeek.SUNDAY)
}
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
context,
attrs,
defStyle
) {
mContext = context
}
#BindingAdapter("selectedDays")
fun setSelectedDays(dayPicker: DayPicker, selectedDays: String?) {
days = (selectedDays?.split(",")?.map { id -> DayOfWeek.of(Integer.parseInt(id)) }?.toSet()
?: HashSet()) as MutableSet<DayOfWeek>
}
#InverseBindingAdapter(attribute = "selectedDays")
fun getSelectedDays(dayPicker: DayPicker): String {
if (days.isEmpty()) {
this.error = "emptyy"
}
return days.map { x -> x.value }.joinToString(",")
}
#BindingAdapter("selectedDaysAttrChanged")
fun setSelectedDaysChangedListener(dayPicker: DayPicker, listener: InverseBindingListener) {
listener.onChange()
}
/**
* Load component XML layout
*/
private fun initControl(context: Context) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.daypicker, this, true)
// layout is inflated, assign local variables to components
tMon = findViewById(R.id.tMon)!!
tTue = findViewById(R.id.tTue)!!
tWed = findViewById(R.id.tWed)!!
tThu = findViewById(R.id.tThu)!!
tFri = findViewById(R.id.tFri)!!
tSat = findViewById(R.id.tSat)!!
tSun = findViewById(R.id.tSun)!!
}
fun initDays() {
this.days.forEach { day ->
if (day == DayOfWeek.MONDAY) {
tMon.isChecked = true
} else if (day == DayOfWeek.TUESDAY) {
tTue.isChecked = true
} else if (day == DayOfWeek.WEDNESDAY) {
tWed.isChecked = true
} else if (day == DayOfWeek.THURSDAY) {
tThu.isChecked = true
} else if (day == DayOfWeek.FRIDAY) {
tFri.isChecked = true
} else if (day == DayOfWeek.SATURDAY) {
tSat.isChecked = true
} else if (day == DayOfWeek.SUNDAY) {
tSun.isChecked = true
}
}
}
fun initListener(button: ToggleButton, day: DayOfWeek) {
button.setOnClickListener {
if (button.isChecked) {
days.add(day)
} else {
days.remove(day)
}
}
}
}
When I use it in activity/fragment:
<.....textview.DayPicker
android:id="#+id/daypicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:visibleOrGone="#{viewModel.isSelectedDays().ld}"
app:selectedDays="#={viewModel.treatment.selected_days}"/>
I receive an error:
.../androidApp/build/generated/source/kapt/debug/com/package/android/databinding/FragmentAddStep2BindingImpl.java:41:
error: expected
java.lang.String callbackArg_0 = mBindingComponent.null.getSelectedDays(daypicker);
This happens when I use two-way data binding: 'app:selectedDays="#={viewModel.treatment.selected_days}"'
I think that is something wrong with #InverseBindingAdapter but I don't know where.
I tried to look for a solution, but unfortunately I couldn't find it. I don't know what should I do to not have null in mBindingComponent object.

I was just doing a workaround and I found solution.
I changed two way data binding to binding and I want in action manually get data from picker and replace in ViewModels data object.
When I do that and I run app in this fragment I got error:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.package.android, PID: 12221
java.lang.IllegalStateException: Required DataBindingComponent is null in class FragmentAddStep2BindingImpl. A BindingAdapter in
com.package.utils.textview.DayPicker is not static and requires an
object to use, retrieved from the DataBindingComponent. If you don't
use an inflation method taking a DataBindingComponent, use
DataBindingUtil.setDefaultComponent or make all BindingAdapter methods
static.
at androidx.databinding.ViewDataBinding.ensureBindingComponentIsNotNull(ViewDataBinding.java:709)
at com.package.android.databinding.FragmentAddStep2BindingImpl.(FragmentAddStep2BindingImpl.java:51)
at com.package.android.databinding.FragmentAddStep2BindingImpl.(FragmentAddStep2BindingImpl.java:38)
at com.package.android.DataBinderMapperImpl.getDataBinder(DataBinderMapperImpl.java:222)
After that I modify my DayPicker class and I moved adapters of class outside. And finally compilation not show error! Functions in class are not properly implement so don't suggested in.
var days: MutableSet<DayOfWeek> = HashSet()
#BindingAdapter("selectedDays")
fun setSelectedDays(dayPicker: DayPicker, selectedDays: String?) {
if(selectedDays.isNullOrEmpty()){
return
}
days = (selectedDays?.split(",")?.map { id -> DayOfWeek.of(Integer.parseInt(id)) }?.toSet()
?: HashSet()) as MutableSet<DayOfWeek>
}
#InverseBindingAdapter(attribute = "selectedDays")
fun getSelectedDays(dayPicker: DayPicker): String {
if (days.isEmpty()) {
dayPicker.error = "emptyy"
}
return days.map { x -> x.value }.joinToString(",")
}
#BindingAdapter("selectedDaysAttrChanged")
fun setSelectedDaysChangedListener(dayPicker: DayPicker, listener: InverseBindingListener) {
dayPicker
listener.onChange()
}
class DayPicker : TextInputLayout {
private lateinit var tMon: ToggleButton
private lateinit var tTue: ToggleButton
private lateinit var tWed: ToggleButton
private lateinit var tThu: ToggleButton
private lateinit var tFri: ToggleButton
private lateinit var tSat: ToggleButton
private lateinit var tSun: ToggleButton
var mContext: Context? = null
constructor(context: Context) : super(context) {
mContext = context
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
mContext = context
initControl(context)
initDays()
initListeners()
}
private fun initListeners() {
initListener(tMon, DayOfWeek.MONDAY)
initListener(tTue, DayOfWeek.TUESDAY)
initListener(tWed, DayOfWeek.WEDNESDAY)
initListener(tThu, DayOfWeek.THURSDAY)
initListener(tFri, DayOfWeek.FRIDAY)
initListener(tSat, DayOfWeek.SATURDAY)
initListener(tSun, DayOfWeek.SUNDAY)
}
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
context,
attrs,
defStyle
) {
mContext = context
}
/**
* Load component XML layout
*/
private fun initControl(context: Context) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.daypicker, this, true)
// layout is inflated, assign local variables to components
tMon = findViewById(R.id.tMon)!!
tTue = findViewById(R.id.tTue)!!
tWed = findViewById(R.id.tWed)!!
tThu = findViewById(R.id.tThu)!!
tFri = findViewById(R.id.tFri)!!
tSat = findViewById(R.id.tSat)!!
tSun = findViewById(R.id.tSun)!!
}
fun initDays() {
days.forEach { day ->
if (day == DayOfWeek.MONDAY) {
tMon.isChecked = true
} else if (day == DayOfWeek.TUESDAY) {
tTue.isChecked = true
} else if (day == DayOfWeek.WEDNESDAY) {
tWed.isChecked = true
} else if (day == DayOfWeek.THURSDAY) {
tThu.isChecked = true
} else if (day == DayOfWeek.FRIDAY) {
tFri.isChecked = true
} else if (day == DayOfWeek.SATURDAY) {
tSat.isChecked = true
} else if (day == DayOfWeek.SUNDAY) {
tSun.isChecked = true
}
}
}
fun initListener(button: ToggleButton, day: DayOfWeek) {
button.setOnClickListener {
if (button.isChecked) {
days.add(day)
} else {
days.remove(day)
}
}
}
}

Related

How to show Text view with animation by 3 dot one by one like progress bar

HI I m trying show progress bar with text view for some time like . we have to show text view text for 3 second in that 3 second each second we have to display one by one with one ,two and three dot.
expected :
whole textview should show 3 second
Hello . in one second
Hello.. in 2 second
Hello...in 3 second
after that it should be hide .
I have tried with handler but unable to do this .
private fun blink() {
var iCount = 0;
val handler = Handler()
Thread {
val timeToBlink = 1000
try {
Thread.sleep(timeToBlink.toLong())
} catch (e: Exception) {
}
handler.post {
if(iCount==0)
{
iCount = 1
text = "Hello."
}
else if(iCount==1){
iCount = 2
text = "Hello.."
}
else {
Toast.makeText(context,"...",Toast.LENGTH_LONG).show()
iCount=0
text = "Hello..."
}
}
}.start()
}
Please help me with this .
Try this
class MainActivity : AppCompatActivity() {
private lateinit var tvOne: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tvOne = findViewById<TextView>(R.id.tvOne)
}
override fun onResume() {
super.onResume()
tvOne.text = "Hello"
Handler(Looper.getMainLooper()).postDelayed({
tvOne.text = "Hello."
}, 1000)
Handler(Looper.getMainLooper()).postDelayed({
tvOne.text = "Hello.."
}, 2000)
Handler(Looper.getMainLooper()).postDelayed({
tvOne.text = "Hello..."
}, 3000)
Handler(Looper.getMainLooper()).postDelayed({
tvOne.visibility = View.GONE
}, 4000)
}
}
And the XML
<TextView
android:id="#+id/tvOne"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
TRY This once.
STEP 1 Create class TextAndAnimationView
class TextAndAnimationView : LinearLayout {
lateinit var textToShow: TextView
lateinit var animatedTextView: DotAnimatedTextView
constructor(context: Context) : super(context) {
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
showTextAndAnimation(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
showTextAndAnimation(context, attrs)
}
private fun showTextAndAnimation(context: Context, attrs: AttributeSet) {
inflate(context, R.layout.layout, this)
textToShow = this.findViewById(R.id.text_to_show)
animatedTextView = this.findViewById(R.id.progress_dots_txt)
val ta = context.obtainStyledAttributes(attrs, R.styleable.TextAndAnimationView, 0, 0)
try {
val text = ta.getText(R.styleable.TextAndAnimationView_setText)
val textHint = ta.getText(R.styleable.TextAndAnimationView_setTextHint)
val color = ta.getInt(R.styleable.TextAndAnimationView_setTextColor, 0)
val textSize = ta.getFloat(R.styleable.TextAndAnimationView_setTextSize, 0f)
val dotsCount = ta.getInt(R.styleable.TextAndAnimationView_numberOfDots, 0)
if (text != null)
setText(text)
if (textHint != null)
setTextHint(textHint)
if (color != 0)
setTextColor(color)
if (textSize != 0f)
setTextSize(textSize)
if (dotsCount != 0)
noOfDots(dotsCount)
} finally {
ta.recycle()
}
animatedTextView.showDotsAnimation()
}
fun setText(text: CharSequence) {
textToShow.text = text
}
fun setTextSize(size: Float) {
textToShow.textSize = size
animatedTextView.textSize = size
}
fun setTextHint(textHint: CharSequence) {
textToShow.setHint(textHint)
}
fun setTextColor(color: Int) {
textToShow.setTextColor(color)
animatedTextView.setTextColor(color)
}
fun stopAnimation() {
animatedTextView.stopAnimation()
}
fun noOfDots(dotsCount: Int) {
animatedTextView.noOfDots(dotsCount)
}
fun animationDelay(animationDelayTime: Long) {
animatedTextView.animationDelay(animationDelayTime)
}
}
STEP 2- Create layout.xml in res/layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="#+id/text_to_show"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="end|center"
android:text="Progress"
android:textSize="12sp" />
<com.DotAnimatedTextView //ADD YOU Package NAME
android:id="#+id/progress_dots_txt"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="2dp"
android:layout_toRightOf="#id/text_to_show"
android:gravity="start|bottom"
android:maxLines="1"
android:text="..." />
</LinearLayout>
STEP 3 Create attrs.xml in res/values
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TextAndAnimationView">
<attr name="setText" format="string"/>
<attr name="setTextSize" format="float"/>
<attr name="setTextHint" format="string"/>
<attr name="setTextColor" format="integer"/>
<attr name="numberOfDots" format="integer"/>
</declare-styleable>
</resources>
STEP 4 Create class- TextAndAnimationView
class TextAndAnimationView : LinearLayout {
lateinit var textToShow: TextView
lateinit var animatedTextView: DotAnimatedTextView
constructor(context: Context) : super(context) {
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
showTextAndAnimation(context, attrs)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
showTextAndAnimation(context, attrs)
}
private fun showTextAndAnimation(context: Context, attrs: AttributeSet) {
inflate(context, R.layout.layout, this)
textToShow = this.findViewById(R.id.text_to_show)
animatedTextView = this.findViewById(R.id.progress_dots_txt)
val ta = context.obtainStyledAttributes(attrs, R.styleable.TextAndAnimationView, 0, 0)
try {
val text = ta.getText(R.styleable.TextAndAnimationView_setText)
val textHint = ta.getText(R.styleable.TextAndAnimationView_setTextHint)
val color = ta.getInt(R.styleable.TextAndAnimationView_setTextColor, 0)
val textSize = ta.getFloat(R.styleable.TextAndAnimationView_setTextSize, 0f)
val dotsCount = ta.getInt(R.styleable.TextAndAnimationView_numberOfDots, 0)
if (text != null)
setText(text)
if (textHint != null)
setTextHint(textHint)
if (color != 0)
setTextColor(color)
if (textSize != 0f)
setTextSize(textSize)
if (dotsCount != 0)
noOfDots(dotsCount)
} finally {
ta.recycle()
}
animatedTextView.showDotsAnimation()
}
fun setText(text: CharSequence) {
textToShow.text = text
}
fun setTextSize(size: Float) {
textToShow.textSize = size
animatedTextView.textSize = size
}
fun setTextHint(textHint: CharSequence) {
textToShow.setHint(textHint)
}
fun setTextColor(color: Int) {
textToShow.setTextColor(color)
animatedTextView.setTextColor(color)
}
fun stopAnimation() {
animatedTextView.stopAnimation()
}
fun noOfDots(dotsCount: Int) {
animatedTextView.noOfDots(dotsCount)
}
fun animationDelay(animationDelayTime: Long) {
animatedTextView.animationDelay(animationDelayTime)
}
}
STEP 5 - Add this code in your XML code
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.TextAndAnimationView //ADD YOU Package name first
android:id="#+id/bmi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
STEP 6 Add in your Activity class
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.bmi.setText("HELLO")
}
override fun onStop() {
super.onStop()
binding.bmi.stopAnimation()
}
}
This is the simplest and easiest way to achieve this.
Run this function where you want this will add a new . (dot) every second to TextView. And if the you want to terminate the process just break the operation as shown in condition
fun addDotEverySecond() {
Handler(Looper.getMainLooper()).postDelayed({
tvOne.text = tvOne.text + "."
//condition to check needs to terminate or not.
if(your condition here){
// just return from here to terminate this.
}else{
addDotEverySecond()
}
}, 1000)
}

Android two-way data binding, ObservableDouble doesn't trigger anything when changed

I'd appreciate your help please. I want to build a custom Checkbox Group like in the food delivery apps(uber eats, deliveroo). When the user select/unselect one item it should adjust the price automatically. This screen is built using a custom checkbox for each item, a custom checkbox group, two-way data binding, and epoxy. When select/unselect an item the correct binding adapter is called, but it never update the viewmodel.
Here is the custom checkbox.
class CheckboxCustom #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) {
private val root: ViewGroup
private val tvTitle: AppCompatTextView
private val tvPrice: AppCompatTextView
private val checkbox: AppCompatCheckBox
init {
inflate(context, R.layout.checkbox_custom, this)
root = findViewById(R.id.root)
tvTitle = findViewById(R.id.tvTitle)
tvPrice = findViewById(R.id.tvPrice)
checkbox = findViewById(R.id.checkbox)
checkbox.isChecked
root.setOnClickListener {
checkbox.toggle()
this.callOnClick()
}
}
fun setTitle(title: String) {
tvTitle.text = title
}
fun setPrice(price: String) {
tvPrice.text = price
}
fun isChecked(): Boolean = checkbox.isChecked
}
This is the custom checkbox group:
class CheckBoxGroup #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
LinearLayout(context, attrs) {
val parent: LinearLayout
private lateinit var mOnCheckedChangeListener: OnCheckedChangeListener
var totalPrice: Double = 0.0
init {
inflate(context, R.layout.checkbox_group, this)
parent = findViewById(R.id.root)
}
fun setItems(toppings: Array<Topping>) {
toppings.forEach { topping ->
val checkBox = CheckboxCustom(context)
checkBox.layoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
checkBox.setTitle(topping.name)
checkBox.setPrice(topping.price.toString())
checkBox.setOnClickListener {
Timber.tag(TAG).d("checkbox checked: %s", checkBox.isChecked())
if(checkBox.isChecked()) {
totalPrice+= topping.price
mOnCheckedChangeListener.onCheckedChange(totalPrice)
} else {
totalPrice-= topping.price
mOnCheckedChangeListener.onCheckedChange(totalPrice)
}
}
parent.addView(checkBox)
}
}
fun setOnCheckedChangeListener(listener: OnCheckedChangeListener?) {
listener?.let { this.mOnCheckedChangeListener = it}
}
}
interface OnCheckedChangeListener {
fun onCheckedChange(totalPrice: Double)
This is the binding adapters
#BindingAdapter("setItems")
fun setItems(checkboxGroup: CheckBoxGroup, toppings: Array<Topping>) {
checkboxGroup.setItems(toppings)
}
#BindingAdapter(value = ["setOnCheckedChangeListener", "totalPriceAttrChanged"] , requireAll = false)
fun setOnCheckedChangeListener(checkboxGroup: CheckBoxGroup, listener: OnCheckedChangeListener,
totalPriceAttrChanged: InverseBindingListener) {
checkboxGroup.setOnCheckedChangeListener(listener)
if (totalPriceAttrChanged != null) {
totalPriceAttrChanged.onChange()
}
}
#BindingAdapter( "totalPrice")
fun setTotalPrice(checkboxGroup: CheckBoxGroup, totalPrice: Double) {
Timber.d("setTotalPrice binding called")
if(checkboxGroup.totalPrice != totalPrice) {
checkboxGroup.totalPrice.plus(1.0)
}
}
#InverseBindingAdapter(attribute = "totalPrice")
fun getTotalPrice(checkboxGroup: CheckBoxGroup): Double {
Timber.d("getTotalPrice binding called")
return checkboxGroup.totalPrice
}
The item checkbox where checkbox is used, it's an epoxy item.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="toppings"
type="com.jephtecolin.model.Topping[]" />
<variable
name="listener"
type="com.jephtecolin.kwii.ui.custom.OnCheckedChangeListener" />
<variable
name="viewModel"
type="com.jephtecolin.kwii.ui.consumable_detail.ConsumableDetailViewModel" />
</data>
<com.jephtecolin.kwii.ui.custom.CheckBoxGroup
android:id="#+id/gp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:totalPrice="#={viewModel.totalCost}"
setOnCheckedChangeListener="#{listener}"
setItems="#{toppings}"/>
</layout>
This is the viewmodel:
#HiltViewModel
class ConsumableDetailViewModel #Inject constructor() : ObservableViewModel() {
val topping = Topping("1", "Hot Cheese", 30.00)
val consumable = Consumable(
"1",
"Griot de boeuf",
"Food",
250.00,
"https://www.innovatorsmag.com/wp-content/uploads/2017/03/IF-Burger-1024x772.jpg",
"Griot de boeuf en sauce, servis avec des bananes pese, des frites et du salade",
"Boeuf, Banane, Choux, Radis",
)
val totalCost: ObservableDouble = ObservableDouble(0.0)
#Bindable get() {
return field
}
}
This is the custom viewmodel
open class ObservableViewModel : ViewModel(), Observable {
private val callbacks: PropertyChangeRegistry by lazy { PropertyChangeRegistry() }
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
callbacks.add(callback)
}
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
callbacks.remove(callback)
}
/**
* Notifies listeners that all properties of this instance have changed.
*/
#Suppress("unused")
fun notifyChange() {
callbacks.notifyCallbacks(this, 0, null)
}
/**
* Notifies listeners that a specific property has changed. The getter for the property
* that changes should be marked with [Bindable] to generate a field in
* `BR` to be used as `fieldId`.
*
* #param fieldId The generated BR id for the Bindable field.
*/
fun notifyPropertyChanged(fieldId: Int) {
callbacks.notifyCallbacks(this, fieldId, null)
}
}
This code is from the fragment, and it's not being triggered:
viewModel.totalCost.addOnPropertyChangedCallback(
object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
Toast.makeText(context, (sender as ObservableDouble).get().toString(), Toast.LENGTH_LONG).show()
Timber.d(" totalPrice :" + (sender as ObservableDouble).get())
}
})
This code also is from the fragment, and not being triggered either
rvConsumableDetail.withModels {
itemCheckbox {
id("topping")
toppings(toppings.toTypedArray())
listener(object : OnCheckedChangeListener {
override fun onCheckedChange(totalPrice: Double) {
// todo("Not yet implemented")
Timber.d("itemCheckBox totalPrice :" + totalPrice)
Toast.makeText(context, "itemCheckBox totalPrice :" + totalPrice, Toast.LENGTH_LONG).show()
}
})
}
}
The binding adapters seems to work fine, when I put breakpoints inside them it works, but for some reason, observing for change on totalPrice from the fragment never work.
Thanks for helping
You are having 2 statements for check change listener in your binding adapter method. This would be causing the listener objects to be replaced. So you are not getting the events.
checkboxGroup.setOnCheckedChangeListener(listener)
checkboxGroup.setOnCheckedChangeListener(object : OnCheckedChangeListener {
override fun onCheckedChange(totalPrice: Double) {
Timber.d("binding adapter works")
setTotalPrice(checkboxGroup, totalPrice)
if (totalPriceAttrChanged != null) {
totalPriceAttrChanged.onChange()
}
}
})

Invisible items becomes visible on scroll in RecyclerView

I have two buttons to play and pause a track in a RecyclerView item. When play button tapped, I want to hide it and show pause button. I've done this and it's working but I have a problem. Once I scroll to (down or up), the play button appears again and pause button disappears. I also have a progress bar to show the time of the track. As the track play, the bar fills out and its progress is zero at the beginning. When I scroll the list, this progress bar also resets to zero and doesn't move but the track continues to play. I tried three ways to fix this:
Setting setIsRecyclable to false
Adding and else condition to views
Adding default visibility to the views in the XML file
Here's my complate code:
class BackstageProcessorAdapter(private val stickyHeaderChangedCallback: (ProcessorGroupId) -> Unit) : RecyclerView.Adapter<RecyclerView.ViewHolder>(),
StickyHeaderItemDecoration.StickyHeaderInterface {
private var callback: ProcessorViewHolderCallback? = null
private var backStageProcessorItemList = emptyList<BackStageProcessorItem>()
private var stickyHeaderPosition = 0
private val processorGroupHeaderPositionMap = mutableMapOf<ProcessorGroupId, Int>()
private var parentRecyclerViewHeight = 0
private var lastItemPosition = 0
private var currentPreviewSound: String = ""
private var processorHeaderNameForEvent: String = ""
private lateinit var timer: CountDownTimer
var prevHolder: ProcessorViewHolder? = null
var mediaPlayer: MediaPlayer? = null
fun registerCallback(callback: ProcessorViewHolderCallback) {
this.callback = callback
}
fun setItems(items: List<BackStageProcessorItem>) {
if (backStageProcessorItemList.isNotEmpty()) return
backStageProcessorItemList = items
var headerPos = 0
for ((index, item) in items.withIndex()) {
if (item is BackStageProcessorItem.Header) {
headerPos = index
processorGroupHeaderPositionMap[item.processorGroupUiModel.processorGroupId] =
headerPos
}
item.headerPosition = headerPos
}
lastItemPosition = items.lastIndex
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
HEADER_ITEM -> HeaderViewHolder(parent.inflate(R.layout.item_processor_header))
else -> ProcessorViewHolder(parent.inflate(R.layout.item_backstage_processor))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val backStageProcessorItem = backStageProcessorItemList[position]) {
is BackStageProcessorItem.Header -> {
(holder as HeaderViewHolder).bindTo(backStageProcessorItem)
}
is BackStageProcessorItem.Content -> {
(holder as ProcessorViewHolder).bindTo(backStageProcessorItem.processorUiModel)
holder.setMargin(position)
}
}
}
override fun getItemViewType(position: Int): Int {
return when (backStageProcessorItemList.get(position)) {
is BackStageProcessorItem.Header -> HEADER_ITEM
else -> PROCESSOR_ITEM
}
}
override fun getItemCount() = backStageProcessorItemList.size
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
recyclerView.post {
parentRecyclerViewHeight = recyclerView.height
}
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
callback = null
}
override fun getHeaderPositionForItem(itemPosition: Int) =
backStageProcessorItemList[itemPosition].headerPosition
override fun getHeaderLayout(headerPosition: Int) = R.layout.item_processor_header
override fun bindHeaderData(header: View, headerPosition: Int) {
val headerItem = backStageProcessorItemList[headerPosition] as BackStageProcessorItem.Header
(header as TextView).setText(headerItem.processorGroupUiModel.nameResId)
if (headerPosition != stickyHeaderPosition) {
stickyHeaderPosition = headerPosition
stickyHeaderChangedCallback(headerItem.processorGroupUiModel.processorGroupId)
}
}
override fun isHeader(itemPosition: Int): Boolean {
if (itemPosition == backStageProcessorItemList.size) return true
return backStageProcessorItemList[itemPosition] is BackStageProcessorItem.Header
}
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
super.onViewDetachedFromWindow(holder)
}
fun getHeaderPositionViewGroupId(processorGroupId: ProcessorGroupId): Int {
return processorGroupHeaderPositionMap[processorGroupId]!!
}
inner class HeaderViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
fun bindTo(header: BackStageProcessorItem.Header) {
(itemView as TextView).setText(header.processorGroupUiModel.nameResId)
}
}
inner class ProcessorViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val textViewProcessorName = itemView.findViewById<TextView>(R.id.textViewProcessorName)
private val textViewProcessorDescription = itemView.findViewById<TextView>(R.id.textViewProcessorDescription)
private val imageViewProcessorImage = itemView.findViewById<ImageView>(R.id.imageViewProcessorImage)
private val buttonAddProcessor = itemView.findViewById<Button>(R.id.buttonAddProcessor)
private val buttonUnlockEverything = itemView.findViewById<TextView>(R.id.buttonUnlockEverything)
private val buttonPlayPreview = itemView.findViewById<Button>(R.id.buttonPlayPreview)
private val buttonPausePreview = itemView.findViewById<Button>(R.id.buttonPausePreview)
fun setMargin(position: Int) {
val margin =
if (position != lastItemPosition) dpToPx(20)
else {
val contentHeight = getDimen(R.dimen.backstage_processor_item_height)
val headerHeight = getDimen(R.dimen.processor_header_height)
val topMargin = dpToPx(20)
parentRecyclerViewHeight - (contentHeight + headerHeight + topMargin)
}
(itemView.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = margin
}
#SuppressLint("ClickableViewAccessibility")
fun bindTo(processor: ProcessorUiModel) {
val processorId = processor.processorId
val canProcessorBeEnabled = callback?.canProcessorBeEnabled(processorId) == true
val isProcessorAdded = callback?.isProcessorAddedBefore(processorId) == true
val processorName = itemView.context.resources.getText(processor.nameId).toString()
val processorNameForEvent = processorName.toLowerCase().replace(" ", "_")
this.setIsRecyclable(false)
if (prevHolder != null) prevHolder?.setIsRecyclable(false)
imageViewProcessorImage.setImageResource(processor.storeIconResId)
textViewProcessorName.setText(processor.nameId)
textViewProcessorDescription.setText(processor.descriptionId)
buttonUnlockEverything.isVisible = canProcessorBeEnabled.not()
buttonAddProcessor.isGone = canProcessorBeEnabled.not()
buttonAddProcessor.isEnabled = isProcessorAdded.not()
this.setIsRecyclable(false)
buttonAddProcessor.setOnTouchListener { v, event ->
return#setOnTouchListener when (event.action) {
KeyEvent.ACTION_DOWN -> {
v.alpha = 0.75f
true
}
KeyEvent.ACTION_UP -> {
v.alpha = 1f
callback?.addProcessor(processorId)
true
}
else -> v.onTouchEvent(event)
}
}
buttonPlayPreview.setOnClickListener {
if (currentPreviewSound.isNotEmpty()) {
pausePreviewSound()
}
if (currentPreviewSound.isNotEmpty() && prevHolder != this) {
currentPreviewSound = ""
prevHolder?.itemView?.buttonPausePreview?.isVisible = false
prevHolder?.itemView?.buttonPlayPreview?.isVisible = true
} else {
prevHolder?.itemView?.buttonPausePreview?.isVisible = true
prevHolder?.itemView?.buttonPlayPreview?.isVisible = false
}
processorName.playPreviewSound(processorNameForEvent)
prevHolder = this
notifyDataSetChanged()
}
buttonPausePreview.setOnClickListener() {
pausePreviewSound()
}
buttonUnlockEverything.setOnClickListener {
getHeaderNameClickProcessorForEvent()
callback!!.sendEvent("goPremiumClicked", processorHeaderNameForEvent, processorName)
callback?.openInAppBilling()
}
}
private fun String.playPreviewSound(processorNameForEvent: String) {
callback?.stopVG()
currentPreviewSound = this
buttonPlayPreview.isVisible = false
buttonPausePreview.isVisible = true
mediaPlayer = MediaPlayer.create(itemView.context, AmpSoundType.getAmpType(this))
mediaPlayer?.start()
val maxTrackDuration = mediaPlayer?.duration!!
itemView.progressBarPreview.max = maxTrackDuration
itemView.progressBarPreview.progress = 0
// The first arg of the CountDownTimer is the tick count. Which is (maxTrackDuration (lets say this is 18000) / 1000) = 18 ticks in total duration with 200ms interval
timer = object : CountDownTimer(maxTrackDuration.toLong(), 200) {
override fun onTick(millisUntilFinished: Long) {
updatePreviewSoundProgressBar()
}
override fun onFinish() {
setPlayButton()
}
}
timer.start()
callback!!.sendEvent("playClicked", processorHeaderNameForEvent, processorNameForEvent)
}
private fun pausePreviewSound() {
setPlayButton()
mediaPlayer?.stop()
timer.cancel()
}
private fun setPlayButton() {
buttonPlayPreview.isVisible = true
buttonPausePreview.isVisible = false
}
private fun updatePreviewSoundProgressBar() {
itemView.progressBarPreview.progress += 200
}
private fun getHeaderNameClickProcessorForEvent() {
val processorHeaderPosition = backStageProcessorItemList[getHeaderPositionForItem(position)]
val processorHeaderData = (processorHeaderPosition as BackStageProcessorItem.Header).processorGroupUiModel.nameResId
val processorHeaderName = itemView.context.resources.getString(processorHeaderData)
processorHeaderNameForEvent = processorHeaderName.toLowerCase().substring(0, 3)
}
private fun dpToPx(dp: Int) = (dp * itemView.resources.displayMetrics.density).toInt()
private fun getDimen(dimenRes: Int) = itemView.resources.getDimensionPixelSize(dimenRes)
}
}
And a part of my layout:
<LinearLayout
android:id="#+id/layoutHearTone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="#id/buttons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.46"
app:layout_constraintStart_toStartOf="parent">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="12dp">
<Button
android:id="#+id/buttonPausePreview"
android:layout_width="30dp"
android:layout_height="30dp"
android:visibility="invisible"
tools:visibility="invisible"
android:background="#drawable/ic_preset_view_pause" />
<Button
android:id="#+id/buttonPlayPreview"
android:layout_width="30dp"
android:layout_height="30dp"
android:visibility="visible"
tools:visibility="visible"
android:background="#drawable/ic_preset_view_play" />
</RelativeLayout>
<ProgressBar
android:id="#+id/progressBarPreview"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:minWidth="140dp"
android:progress="0" />
</LinearLayout>
RecyclerViews work by creating a pool of ViewHolder objects (got by calling onCreateViewHolder) which are used to display stuff. No matter how many items the view represents, there are only a handful of ViewHolders being used, enough to fill the visible part of the RecyclerView and a few either side so you can peek to the next item.
So it works by shuffling those ViewHolders around to put them ahead of the scrolling, and the stuff they're displaying gets updated to represent a particular item in the list. This is done in onBindViewHolder.
Basically, if you have items with state, i.e. whether the play button is visible, whether a seek bar is at a particular position, if it has some kind of controller attached that updates the seek bar - you need to restore all that in onBindViewHolder when that item comes into view and a ViewHolder is being told to display that item. That means you have to keep track of that state somewhere (usually in the adapter), so you can restore it when an item pops into view.

android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; Custom component

I have a complicated layout that caused me to create some custom components. The app requires that I need to add number of rooms, and in each, select number of adults and number of children per room. For each room, I need to allow multiple children per room, but for each child under 18 i need to know their age from a spinner. I can add and remove rooms and add/remove children all day long. In each room I can add children and an age dropdown for each child and remove them. All that works no problem. I get the error when I try to tap any of the age selector dropdowns. I suspect its something to do with context and activity, but not sure where.
The layout looks like this:
I have a fragment controlling all of this, it controls the room stepper and adds/removes rooms.
HotelSearchFragment
class SearchHotelsFragment : Fragment(), StepperView.StepperListener, CustomCalendarView.DayClickListener {
private var listener: OnSearchUpdateListener? = null
private lateinit var mViewModel: HotelRepositoryViewModel
private lateinit var mBinding: com.lixar.allegiant.databinding.FragmentSearchHotelsBinding
private lateinit var mRoomList: MutableList<HotelSearchRoomInput>
private lateinit var mRoomMgrList: MutableList<RoomGuestInputManager>
private var mRoomCount = 1;
interface OnSearchUpdateListener {
fun onSearchUpdate()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mViewModel = activity?.let { ViewModelProviders.of(it).get(HotelRepositoryViewModel::class.java) }!!
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_search_hotels, container, false)
mBinding.handler = clickHandler
mBinding.roomStepper.setCallback(this)
setupInitialRoom()
hideKeyboard()
return mBinding.root
}
public fun getSearchCriteria(): HotelSearchInput {
val mRoomData = HotelSearchInput.builder()
.locationCode("")
.from(mViewModel.fromDateLocal)
.to(mViewModel.toDateLocal)
.rooms(mRoomList)
.build()
return mRoomData
}
private fun setupInitialRoom() {
val roomMgr = activity?.applicationContext?.let { RoomGuestInputManager(it) }
roomMgr?.setActivity(activity!!)
roomMgr?.setRoomNumber(mRoomCount)
mBinding.roomExpansionZone.addView(roomMgr)
mRoomMgrList = listOf(roomMgr!!).toMutableList()
mRoomList = listOf(roomMgr.mRoom!!).toMutableList()
}
override fun onDecrement(id: Int, count: Int) {
when (id) {
R.id.room_stepper -> {
val lastRoomMgr = mRoomMgrList.get(mRoomCount-1)
mBinding.roomExpansionZone.removeView(lastRoomMgr)
mRoomMgrList.remove(lastRoomMgr)
mRoomList.remove(lastRoomMgr.mRoom)
mRoomCount--
// once we have ONLY one room grow the left side to hider room numbers
if (mRoomCount == 1) {
val room = mRoomMgrList.get(0)
room.adjustRoomOnRemoval()
}
}
}
}
override fun onIncrement(id: Int, count: Int) {
when (id) {
R.id.room_stepper -> {
val roomMgr = activity?.applicationContext?.let { RoomGuestInputManager(it) }
roomMgr?.setActivity(activity!!)
mRoomCount++
roomMgr?.setRoomNumber(mRoomCount)
mBinding.roomExpansionZone.addView(roomMgr)
mRoomList.add(roomMgr?.mRoom!!)
mRoomMgrList.add(roomMgr)
// once we have more than one room shrink the left side to allow for room numbers
if (mRoomCount > 1) {
val room = mRoomMgrList.get(0)
room.adjustRoomOnAddition()
}
}
}
}
}
Within that fragment, I've incorporated a custom component, RoomGuestInputManager. It's job is to add "rooms", with an adult stepper and child stepper or remove them
RoomGuestInputManager
class RoomGuestInputManager #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr), StepperView.StepperListener {
val FULLSIZE_LEFT = 0.0f
val FULLSIZE_RIGHT = 0.5f
val SIZE_LEFT = 0.2f
val SIZE_RIGHT = 0.6f
val mRoom = HotelSearchRoomInput.builder()
.adults(1) // there must be at least one adult per room - sorry kids
.childrenAges(listOf(0))
.build()
private lateinit var mBinding: RoomSelectLayoutBinding
private var mNumChildren = 0
private var mNumChildMgrs = 0
private var mActivity: Activity? = null
private lateinit var mChildMgrList: MutableList<RoomChildManager>
init {
mBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.room_select_layout, this, true)
mBinding.adultStepper.setCallback(this)
mBinding.childStepper.setCallback(this)
mBinding.roomNumber.visibility = View.GONE
}
public fun setRoomNumber(room: Int) {
val label = context.resources.getString(R.string.room_num)
if (room == 1) {
mBinding.roomNumber.visibility = View.GONE
mBinding.guidelineleft.setGuidelinePercent(FULLSIZE_LEFT)
mBinding.guidelineright.setGuidelinePercent(FULLSIZE_RIGHT)
} else {
mBinding.roomNumber.visibility = View.VISIBLE
mBinding.roomNumber.text = String.format(label, room)
mBinding.guidelineleft.setGuidelinePercent(SIZE_LEFT)
mBinding.guidelineright.setGuidelinePercent(SIZE_RIGHT)
}
}
public fun setActivity(activity: Activity) {
mActivity = activity
}
public fun adjustRoomOnAddition() {
mBinding.roomNumber.visibility = View.VISIBLE
mBinding.guidelineleft.setGuidelinePercent(SIZE_LEFT)
mBinding.guidelineright.setGuidelinePercent(SIZE_RIGHT)
}
public fun adjustRoomOnRemoval() {
mBinding.roomNumber.visibility = View.GONE
mBinding.guidelineleft.setGuidelinePercent(FULLSIZE_LEFT)
mBinding.guidelineright.setGuidelinePercent(FULLSIZE_RIGHT)
}
public fun getGuestInfoThisRoom(): HotelSearchRoomInput {
return mRoom
}
private fun setupNewChild() {
val childMgr = RoomChildManager(mActivity?.applicationContext!!)
mBinding.childExpansionZone.addView(childMgr)
childMgr.setInitialChild(mNumChildren)
if (mNumChildren == 1) {
mChildMgrList = mutableListOf(childMgr)
} else {
mChildMgrList.add(childMgr)
}
mNumChildMgrs++
}
override fun onDecrement(id: Int, count: Int) {
when (id) {
R.id.adult_stepper -> {
mRoom.adults().minus(1)
}
R.id.child_stepper -> {
// depending on how many kids there are now, do we remove a layout or just make gone?
if (mNumChildren == 1) {
mBinding.childExpansionZone.removeAllViews()
mChildMgrList.clear()
mNumChildren = 0
} else if (mNumChildren.rem(2) == 0) {
// remove the secondary ageSelector
val childMgr = mChildMgrList.get(mNumChildMgrs - 1)
childMgr.removeChild()
mNumChildren--
} else {
// remove the entire 2-selector layout
val childMgr = mChildMgrList.get(mNumChildMgrs - 1)
childMgr.removeChild()
mChildMgrList.removeAt(mNumChildMgrs - 1)
mBinding.childExpansionZone.removeView(childMgr)
mNumChildMgrs--
mNumChildren--
}
}
}
}
override fun onIncrement(id: Int, count: Int) {
when (id) {
R.id.adult_stepper -> {
mRoom.adults().plus(1)
}
R.id.child_stepper -> {
// depending on how many kids there are now, do we add a layout
if (mNumChildren == 0) {
mNumChildren = 1
setupNewChild()
} else if (mNumChildren.rem(2) == 0) {
mNumChildren++
setupNewChild()
} else {
// expose 2nd selector
val childMgr = mChildMgrList.get(mNumChildMgrs - 1)
mNumChildren++
childMgr.addChild(mNumChildren)
}
}
}
}
That class controls a second custom component, RoomChildManager. It's job is to manage the age selectors for each room and make sure we have an age for each child. Its adds/removes the dropdown selectors in pairs to satisfy the design that when touched, give me the error in the title:
android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
RoomChildManager
class RoomChildManager #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr), DropdownAgeSelectView.AgeSelectListener {
private lateinit var mBinding: ChildAgeselectLayoutBinding
init {
mBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.child_ageselect_layout, this, true)
mBinding.child2Age.visibility = View.GONE
mBinding.child2Num.visibility = View.GONE
mBinding.child1Age.setCallback(this)
mBinding.child2Age.setCallback(this)
}
public fun setInitialChild(num: Int) {
mBinding.child1Num.text = String.format(context.resources.getString(R.string.child_age), num)
}
public fun addChild(num: Int) {
mBinding.child2Age.visibility = View.VISIBLE
mBinding.child2Num.visibility = View.VISIBLE
mBinding.child2Num.text = String.format(context.resources.getString(R.string.child_age), num)
}
public fun removeChild() {
mBinding.child2Age.visibility = View.GONE
mBinding.child2Num.visibility = View.GONE
}
public fun getChildAges(): List<Int> {
return listOf(mBinding.child1Age.getSelection(), mBinding.child2Age.getSelection())
}
override fun onAgeSelect(id: Int, age: Int) {
when (id) {
R.id.child1_age -> {
// do something
}
R.id.child2_age -> {
// do something
}
}
}
}
Any ideas why touching any dropdown gives me the error?
I knew this was going to be a context issue, just couldn't tell from the error messages exactly where it was hitting.
It turns out, that the way to solve this issue lies in the way context was passed from the HotelSearchFragment to the RoomGuestInputManager. I was assuming the context I could get a fragment was good enough. Nope, apparently, they get confused, so I had to spell out exactly where the activity is. In HotelSearchFragment, we set that as:
val hotelActivity = context as HotelSelectActivity
then we can invoke RoomInputGuestManager like this:
val roomMgr = RoomGuestInputManager(hotelActivity)
and also inside RoomInputGuestManager when we create the Child manager as
val hotelActivity = context as HotelSelectActivity
val childMgr = RoomChildManager(hotelActivity )
After that, everything works fine.

TextInputLayout Error right align

I've an EditText enclosed within a TextInputLayout. I wish to display errors under the EditText, but aligned to the right end of the screen.
This is what I currently have:
The error is displayed like this:
What I want it to look like:
My XML is this:
<android.support.design.widget.TextInputLayout
android:id="#+id/text_input_email"
style="#style/forgot_pass_text_inputlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/tv_enter_email_message"
android:layout_marginTop="#dimen/email_padding_top"
android:hintTextAppearance="#{forgotPasswordVM.hintTextAppearance}"
android:theme="#style/forgot_pass_til_state"
app:error="#{forgotPasswordVM.email.textInputError}">
<EditText
android:id="#+id/actv_email"
style="#style/forgot_pass_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/email_address"
android:imeActionId="#+id/btn_submit"
android:imeOptions="actionSend"
android:inputType="textEmailAddress"
android:maxLines="1"
android:onEditorAction="#{forgotPasswordVM.onEditorAction}"
android:singleLine="true"
app:binding="#{forgotPasswordVM.email.text}" />
</android.support.design.widget.TextInputLayout>
I'm using data-binding.
I've tried setting android:gravity="right" and android:layout_gravity="right" on both the EditText and the TextInputLayout. Neither works the way I want it to.
I've tried setting right gravity in the theme as well as the style. Neither has any effect on the error.
I've tried right gravity on the style that is applied within app:errorTextAppearance="#style/right_error". Even this doesn't work.
I tried programmatically shifting the Error to the right by using a SpannableStringBuilder with an AlignmentSpan following this link. Even that doesn't work for me.
I'm not sure what else to try. Please suggest.
So thanks to Mike's answer I was able to figure out the solution.
I created a custom class:
public class CustomTextInputLayout extends TextInputLayout {
public CustomTextInputLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public void setErrorEnabled(boolean enabled) {
super.setErrorEnabled(enabled);
if (!enabled) {
return;
}
try {
Field errorViewField = TextInputLayout.class.getDeclaredField("mErrorView");
errorViewField.setAccessible(true);
TextView errorView = (TextView) errorViewField.get(this);
if (errorView != null) {
errorView.setGravity(Gravity.RIGHT);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.END;
errorView.setLayoutParams(params);
}
}
catch (Exception e) {
// At least log what went wrong
e.printStackTrace();
}
}
}
Now I simply replaced my TextInputLayout with the CustomTextInputLayout.
Works like a charm. Thanks a lot Mike. I wish I could credit you more for this answer.
Here's a hack in Kotlin that doesn't depend on view hierarchy:
class CustomTextInputLayout #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : TextInputLayout(context, attrs, defStyleAttr) {
override fun setError(errorText: CharSequence?) {
super.setError(errorText)
with(findViewById<TextView>(R.id.textinput_error)) {
// this will work as long as errorView's layout width is
// MATCH_PARENT -- it is, at least now in material:1.2.0-alpha06
textAlignment = TextView.TEXT_ALIGNMENT_VIEW_END
}
}
}
My custom class is in Kotlin
class CustomTextInputLayout(context: Context, attrs: AttributeSet) :
TextInputLayout(context, attrs) {
override fun setErrorEnabled(enabled: Boolean) {
super.setErrorEnabled(enabled)
if (!enabled) {
return
}
try {
val layout = this
val errorView : TextView = ((this.getChildAt(1) as ViewGroup).getChildAt(0) as ViewGroup).getChildAt(0) as TextView
(layout.getChildAt(1) as ViewGroup).layoutParams.width = LayoutParams.MATCH_PARENT
(layout.getChildAt(1) as ViewGroup).getChildAt(0).layoutParams.width = FrameLayout.LayoutParams.MATCH_PARENT
errorView.gravity = Gravity.END
} catch (e: Exception) {
e.printStackTrace()
}
}
}
With material-componets version 1.2.0-rc01, you can create a CustomTextInputLayout that inherits from TextInputLayout to change the text alignment to the end (or where you want it to be) like this:
class CustomTextInputLayout : TextInputLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttrs: Int
) : super(context, attrs, defStyleAttrs)
override fun setErrorEnabled(enabled: Boolean) {
super.setErrorEnabled(enabled)
if (!enabled) {
return
}
try {
changeTextAlignment(
com.google.android.material.R.id.textinput_error,
View.TEXT_ALIGNMENT_VIEW_END
)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun changeTextAlignment(textViewId: Int, alignment: Int) {
val textView = findViewById<TextView>(textViewId)
textView.textAlignment = alignment
}
}
The alignment is specified in the IndicatorViewController class to always be View.TEXT_ALIGNMENT_VIEW_START. This class is used by the TextInputLayout.
From the IndicatorViewController class
void setErrorEnabled(boolean enabled) {
if (errorEnabled == enabled) {
return;
}
cancelCaptionAnimator();
if (enabled) {
errorView = new AppCompatTextView(context);
errorView.setId(R.id.textinput_error);
if (VERSION.SDK_INT >= 17) {
errorView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
}
//More code...
If selected answer not works. Another solution is below:
Create custom class as below:
public class CustomTextInputLayout extends TextInputLayout
{
public CustomTextInputLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
}
#Override
public void setErrorEnabled(boolean enabled)
{
super.setErrorEnabled(enabled);
if (!enabled)
{
return;
}
try
{
TextView errorView = this.findViewById(R.id.textinput_error);
FrameLayout errorViewParent = (FrameLayout) errorView.getParent();
errorViewParent.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.WRAP_CONTENT));
errorView.setGravity(Gravity.CENTER); // replace CENTER with END or RIGHT
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
Then create your custom TextInputLayout as:
<CustomTextInputLayout
android:id="#+id/til_first_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="#dimen/activity_horizontal_margin"
app:layout_constraintBottom_toTopOf="#+id/til_last_name">
<EditText
android:id="#+id/et_first_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="#string/first_name"
android:gravity="right"
/>
</CustomTextInputLayout>
After that initialize in your code as:
private CustomTextInputLayout tilFirstName;
tilFirstName = view.findViewById(R.id.til_first_name);
Congratulations! You have done what you have required.
Based on #Emmanuel Guerra's answer. I've applied it on helperText too:
class CustomTextInputLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = com.google.android.material.R.attr.textInputStyle
) : TextInputLayout(context, attrs, defStyleAttr) {
override fun setErrorEnabled(enabled: Boolean) {
super.setErrorEnabled(enabled)
if (!enabled) return
centerMiniText(com.google.android.material.R.id.textinput_error)
}
override fun setHelperTextEnabled(enabled: Boolean) {
super.setHelperTextEnabled(enabled)
if (!enabled) return
centerMiniText(com.google.android.material.R.id.textinput_helper_text)
}
private fun centerMiniText(textViewId: Int) {
try {
changeTextAlignment(textViewId, View.TEXT_ALIGNMENT_CENTER)
} catch (e: Exception) {
e.printStackTrace()
}
}
#Suppress("SameParameterValue")
private fun changeTextAlignment(textViewId: Int, alignment: Int) {
findViewById<TextView>(textViewId)?.textAlignment = alignment
}
}

Categories

Resources