Is there a way to set drawableTint of TextView using viewmodel? - java

I want to set the color of a compound drawable programmatically. I think the best solution would be to use a ViewModel.
This is what I have so far:
...
<data>
<import type="androidx.core.content.ContextCompat"/>
<variable
name="viewmodel"
type="com.pezcraft.myapplication.MainViewModel" />
</data>
...
<TextView
android:id="#+id/textViewState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text"
app:drawableLeftCompat="#drawable/ic_circle"
app:drawableTint="#{ContextCompat.getColor(context, viewmodel.colorState)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textViewIP" />
public class MainViewModel extends ViewModel {
private MutableLiveData<Integer> colorState;
public MainViewModel() {
colorState = new MutableLiveData<>();
}
public MutableLiveData<Integer> getColorState() {
return colorState;
}
}
But it doesn't work when building...
[databinding] {"msg":"Cannot find a setter for \u003candroid.widget.TextView app:drawableTint\u003e that accepts parameter type \u0027int\u0027\n\nIf a binding adapter provides the setter, check that the adapter is annotated correctly and that the parameter type matches."

Related

Pass parameters to method in xml

I have several buttons, each which calls its own function onClick. These buttons indicate to a ScrollView to scroll to a particular element (like links in a Table of Contents). I have three methods, each with almost identical code; Obviously, it would be better to pass the View to scroll to in the method.
info_page.xml:
<TextView
...
android:onClick="scrollToSetup"
... />
<TextView
...
android:onClick="scrollToObjective"
.../>
<TextView
...
android:onClick="scrollToGameplay"
.../>
InfoPageActivity.java:
...
public void scrollToSetup(View v) {
View setupHeader = findViewById(R.id.header_setup);
scrollView.smoothScrollTo(0, setupHeader.getTop());
}
public void scrollToObjective(View v) {
View setupHeader = findViewById(R.id.header_objective);
scrollView.smoothScrollTo(0, setupHeader.getTop());
}
public void scrollToGameplay(View v) {
View setupHeader = findViewById(R.id.header_gameplay);
scrollView.smoothScrollTo(0, setupHeader.getTop());
}
...
I'm not quite sure how this would work exactly, but hopefully something like:
info_page.xml:
<TextView
...
android:onClick"scrollTo","header_setup"
... />
<TextView
...
android:onClick"scrollTo","header_objective"
... />
<TextView
...
android:onClick"scrollTo","header_gameplay"
... />
and InfoPageActivity.java:
...
public void scrollToGameplay(View v, View target) {
scrollView.smoothScrollTo(0, target.getTop());
}
...
Does anyone have any way to do this, or pointers to docs I can read for this? Thanks very much!
You could achieve this by using DataBinding. Then passing a custom Listener to the Binding and if for example, you want to scroll to view with id (in the same XML) R.id.my_target_view1 your xml onClick method should look something like this:
android:onClick='#{(v) -> listener.scrollToGameplay(v, myTargetView1)}'
Because when generating the Binding, the generator converts snake_case to camelCase.
Note: My proposal could be overkill but I don't think there is another way achieving this.

Databinding, MaterialCardView should act like Radiogroup

I am trying to implement two MaterialCardViews that should act like a Radiogroup. So if I click one, the other should be unchecked. I am using viewModel, liveData and custom two-way data binding to save these values for later purpose (sending per email).
I had success writing the .xml and implementing the check logic, but I struggle implementing uncheck logic.
XML, short version for better visibility
<layout
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="com.example.app.data.viewmodel.EmailViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardViewOne"
android:checkable="true"
android:clickable="true"
android:focusable="true"
<!-- Custom Two way databinding -->
app:state_checked="#={vm.cardOptionOneChecked}"
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardViewTwo"
android:checkable="true"
android:clickable="true"
android:focusable="true"
<!-- Custom Two way databinding -->
app:state_checked="#={vm.cardOptionTwoChecked}">
</com.google.android.material.card.MaterialCardView>
</layout>
ViewModel
class EmailViewModel #ViewModelInject constructor(
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// Variable for Id = cardViewOne
val cardOptionOneChecked = MutableLiveData<Boolean>()
// Variable for Id = cardViewTwo
val cardOptionTwoChecked = MutableLiveData<Boolean>()
}
CardViewAdapter.kt
#BindingAdapter("state_checked")
fun setStateChecked(view: MaterialCardView, liveData: MutableLiveData<Boolean>) {
if (view.isChecked != liveData.value) {
liveData.value = view.isChecked
}
}
#InverseBindingAdapter(attribute = "state_checked")
fun getStateChecked(view: MaterialCardView,): Boolean {
return view.isChecked
}
// I don't know what logic belongs here to make it work!
// Current approach just checks the current view and does nothing more. How can I save the last
// checked value?
#BindingAdapter("state_checkedAttrChanged")
fun setCheckedAttrListener(
view: MaterialCardView,
attrChange: InverseBindingListener,
) {
view.apply {
setOnClickListener { view.isChecked = true }
setOnCheckedChangeListener { card, isChecked ->
if (card.isChecked && card != view) {
card.isChecked = false
}
}
attrChange.onChange()
}
}
I appreciate every help, thank you very much!
P.S: If there is a better and easier way to achieve this e.g. telling the viewModel from the view to save isChecked, please inform me. MaterialCardView has implemented "isChecked" by default but no logic.
Okay, I've solved the Problem:
First, Change Binding Adapter
I actually don't saw any way to use two-way data binding to achieve the above written case. Here is the new Binding Adapter
// View = Clicked MaterialCard, liveData = value in viewModel
#BindingAdapter("state_checked")
fun setStateChecked(view: MaterialCardView, liveData: MutableLiveData<Boolean>) {
if (view.isChecked != liveData.value) {
if (liveData.value != null) {
view.isChecked = liveData.value!!
}
}
}
Second, Change XML Layout, because we don't use two-way data binding anymore
<layout
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="com.example.app.data.viewmodel.EmailViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardViewOne"
android:checkable="true"
android:clickable="true"
android:focusable="true"
<!-- Deleted "=" -->
app:state_checked="#{vm.cardOptionOneChecked}"
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="#+id/cardViewTwo"
android:checkable="true"
android:clickable="true"
android:focusable="true"
<!-- Deleted "=" -->
app:state_checked="#{vm.cardOptionTwoChecked}">
</com.google.android.material.card.MaterialCardView>
</layout>
Third, Change viewmodel
class EmailViewModel #ViewModelInject constructor(
#ApplicationContext context: Context,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val cardOptionOneChecked = MutableLiveData<Boolean>()
val cardOptionTwoChecked = MutableLiveData<Boolean>()
// Added
fun firstCardClicked() {
cardOneChecked.value = true
cardTwoChecked.value = false
}
fun secondCardClicked() {
cardOneChecked.value = false
cardTwoChecked.value = true
}
}
Fourth, add clickListener to XML or Fragment (here Fragment)
cardViewOne.setOnClickListener {
viewModel.firstCardClicked()
}
cardViewTwo.setOnClickListener {
viewModel.secondCardClicked()
}
If someone has any questions, just write it in the comments, I will help.

DataBinding - boolean condition not evaluated as it should

In my xml I set visiblity condition for control as follows:
android:visibility="#{event.isMessage?(event.dateEventText!=null? View.VISIBLE:View.GONE):View.VISIBLE}"
So, if event.isMessage is true, this: (event.dateEventText!=null? View.VISIBLE:View.GONE) should be evalueated, otherwise, View.VISIBE should be returned.
But data binding throws error message:
****/ data binding error ****msg:Cannot find the setter for attribute 'android:visibility' with parameter type boolean on
android.widget.TextView
Does anybody know what's wrong?
Try this
.
.
.
android:visibility="#{event.isMessage && event.dateEventText!=null ? View.VISIBLE : View.GONE}"
.
.
.
I checked this approach because it looks okay. and it works. However you can check getter setter of Model, View class import in XML.
Following Code Works Well.
Event.class
public class Event {
boolean isMessage;
String dateEventText;
public boolean isMessage() {
return isMessage;
}
public void setMessage(boolean message) {
isMessage = message;
}
public String getDateEventText() {
return dateEventText;
}
public void setDateEventText(String dateEventText) {
this.dateEventText = dateEventText;
}
}
layout_text.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<variable
name="event"
type="com.innovanathinklabs.sample.data.Event" />
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{event.isMessage?(event.dateEventText!=null? View.VISIBLE:View.GONE):View.VISIBLE}" />
</layout>
Suggestion:
Move your logical part in Handler.
1. Create Handler
EventHandler.class
public class EventHandler {
private Event event;
public EventHandler(Event event) {
this.event = event;
}
public int getTextVisibility() {
if (event.isMessage && event.dateEventText != null) return View.VISIBLE;
else return View.GONE;
}
}
2. Import Handler in Layout
<variable
name="handler"
type="com.innovanathinklabs.sample.data.EventHandler" />
3. Set handler value from activity
activity.setHandler(new EventHandler(yourEventModel))
4. Use handler method to set visibility
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{handler.textVisibility}" />
That's all!
Another Approach
If you don't want add new class of Handler. You can also place visibility method in model class.
1. Put getTextVisibility method in Model
public class Event{
// other variables
public int getTextVisibility() {
if (event.isMessage && event.dateEventText != null) return View.VISIBLE;
else return View.GONE;
}
}
2. Use in layout
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{event.textVisibility}" />
You can have boolean to int conversion adapter. If it's static (the same way as BindingAdapter), it will convert boolean fields which expect integer (e.g. View.VISIBLE).
#BindingConversion
int convertBooleanToVisibility(boolean isVisible) {
return isVisible ? View.VISIBLE : View.GONE;
}
In XML you'd use your method which return boolean for visibiliy:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{event.isMessageVisible()}" />

Android custom drop down view with dynamic list

I have a problem with filling android:entries with String[] from my ViewModel. Code looks like that:
attrs.xml
<declare-styleable name="AutoCompleteDropDown">
<attr name="android:entries" />
</declare-styleable>
My custom dropdown, AutoCompleteDropDown
public class AutoCompleteDropDown extends AppCompatAutoCompleteTextView {
public AutoCompleteDropDown(Context context, AttributeSet attributes) {
super(context, attributes);
TypedArray a = context.getTheme().obtainStyledAttributes(attributes, R.styleable.AutoCompleteDropDown, 0, 0);
CharSequence[] entries = a.getTextArray(R.styleable.AutoCompleteDropDown_android_entries);
ArrayAdapter<CharSequence> adapter = new ArrayAdapter<>(context, android.R.layout.simple_dropdown_item_1line, entries);
setAdapter(adapter);
}
...
ViewModel
...
private String[] genders;
public String[] getGenders() {
return genders;
}
public void setGenders(String[] genders) {
this.genders = genders;
}
...
genders are filled in ViewModel constructor:
genders = dataRepository.getGenders();
xml file
<AutoCompleteDropDown
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#={vm.title}"
android:entries="#{vm.genders}"
bind:addTextChangedListener="#{vm.titleValidationChangeListener}"/>
ViewModel is binded correctly, i'm using it many times in that xml file. When i try to run the app i'm getting:
Cannot find the setter for attribute 'android:entries' with parameter
type java.lang.String[] on AutoCompleteDropDown
It works when i use android:entries="#array/genders" but i need this list to be dynamic. Project is in MVVM pattern. Appreciate any help :)
You can use BindingAdapter for this. Like :
#BindingAdapter("entries")
public static void entries(AutoCompleteDropDown view, String[] array) {
view.updateData(array);
}
updateData it's method which you must create in your AutoCompleteDropDown.
And in xml it's using same
app:entries="#{vm.genders}"
<AutoCompleteDropDown
xmlns:customNS="http://schemas.android.com/apk/res/com.example.yourpackage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#={vm.title}"
customNS:entries="#{vm.genders}"
/>
You can check this example Android - custom UI with custom attributes

Custom setter is not called, when using databinding

I just run into a problem, using android databinding library.
Here is the xml:
<?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>
<import type="com.test.app.ObservableFieldWrapper"/>
<variable
name="org"
type="ObservableFieldWrapper"/>
</data>
<LinearLayout
android:id="#+id/headerListView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.test.app.NSpinner
android:id="#+id/orgSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:org="#{org.getSilent ? org.content : "silent"}"/>
</LinearLayout>
Here is my NSpinner:
public class ObservableFieldWrapper{
private final ObservableBoolean silent;
private final ObservableField<String> content;
#BindingAdapter("org")
public static void setOrg(Spinner view, String org) {
assert org != null;
if (org.equals("silent")) {
Log.i("ObsWrapper", "SET ORG called via binding adapter but got denied, because of SILENCE");
} else {
Log.i("ObsWrapper", "SET ORG called via binding adapter NORMALLY");
view.setSelection(Cache.GetOrgIndexForSpinner(), true);
}
}
public ObservableFieldWrapper(String startValue) {
content = new ObservableField<>(startValue);
silent = new ObservableBoolean();
silent.set(false);
}
public void setContent(String newValue) {
silent.set(false);
content.set(newValue);
content.notifyChange();
}
public void setContentSilent(String newValue) {
silent.set(true);
content.set(newValue);
}
//Bunch of getters
}
And this call should invoke the static getter provided, by ObservableFieldWrapper class (assume, that all bindings were already set):
ObservableFieldWrapper someField = new ObservableFieldWrapper("someString");
someField.setContent("some other string");
Well, problem is... It invokes nothing. But if I change my xml part from
app:org="#{org.getSilent ? org.content : "silent"}"
to common
app:org="#{org.content}"
It starts working! I realy need this extra functionality with boolean, and I am really lost trying to find the issue.
Found a work around, where didn't use any logics in xml expressions, I just passed 2 parameters to my function and did all job there.
#Bindable ("{org, silent}")
Yet, the question remains unanswered.
As George Mount mentioned - it's important to remove any getters on observable fields, otherwise it won't work, I spent pretty much time with this issue and then mentioned that I have a getter, after removing it - everything started working.

Categories