I want to make certain part of my text to bold whose value is set using DataBinding with ViewModel.
For e.g
If you are selected, you will pay $160 for your pair.
I am using strings resources
<string name="product_price">If you are selected, you will have to pay $%d for your pair.</string>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/spacing_xlarge"
android:layout_marginStart="#dimen/spacing_xlarge"
android:layout_marginBottom="#dimen/spacing_small"
android:text="#{#string/product_price(productPrice)}"
android:textColor="#color/button_tertiary"
android:visibility="#{productPrice > 0}"
style="#style/Body.Small"
/>
Currently passing product price using ViewModel with Binding by setting binding.setProductPrice(Object.getPrice())
I know the following solutions : But want to try using DataBinding
Using Html Text - But don't want to use it in code.
Using Different TextView in Horizontal Style. Setting styles as bold for that Product Price. - Really Bad Practice
Using SpannableString - But don't want to use it in code.
But all of the above solutions are workaround.
Question ::
Want to try DataBinding feature which can be used to style certain part of string. Just like SpannableString
Manipulate String in the Layout file using DataBinding
You have to create a BindingAdapter and SpannableStringBuilder .
Binding Adapter
object Util {
#BindingAdapter("main","secondText")
#JvmStatic
fun setBoldString(view: AppCompatTextView, maintext: String,sequence: String) {
view.text = Util.getBoldText(maintext, sequence)
}
#JvmStatic
fun getBoldText(text: String, name: String): SpannableStringBuilder {
val str = SpannableStringBuilder(text)
val textPosition = text.indexOf(name)
str.setSpan(android.text.style.StyleSpan(Typeface.BOLD),
textPosition, textPosition + name.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return str
}
}
XML
<android.support.v7.widget.AppCompatTextView
android:id="#+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:main="#{`you will pay $160 for your pair`}"
app:secondText="#{`$160`}"
android:textColor="#color/black"
android:textSize="22sp" />
May be it helps you.
As per #CommonsWare,
Tried by adding basic Html tag <string name="product_price">If you are selected, you will have to pay <![CDATA[<b>$%d</b>]]> for your pair.</string>
Layout File : Imported Html
<?xml version="1.0" encoding="utf-8"?>
<layout
<data>
<import type="android.text.Html"/>
<data>
<LinearLayout>
<android.support.design.widget.CoordinatorLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/spacing_xlarge"
android:layout_marginStart="#dimen/spacing_xlarge"
android:layout_marginBottom="#dimen/spacing_small"
android:text="#{Html.fromHtml(#string/product_price(productPrice))}"
android:textColor="#color/button_tertiary"
android:visibility="#{productPrice > 0}"
style="#style/Body.Small"
/>
</android.support.design.widget.CoordinatorLayout>
</LinearLayout>
</layout>
You can use a binding adapter coupled with SpannableString. Once you define the binding adapter, you can reuse it in all your layout files.
#BindingAdapter({"mainText", "priceToFormat"})
public static void format(TextView textView, String mainText, float
productPrice){
//Use spannable string to format your text accordingly
textView.setText(formattedText);
}
You can pass these params in your layout file like this:
<TextView
.
.
app:mainText = "#{ priceText }"
app:priceToFormat = "#{ price }"/>
Good luck.
use BindingAdapter as mentioned below
#BindingAdapter("setBold")
#JvmStatic
public static void setBold(TextView view, boolean isBold) {
if (isBold) {
view.setTypeface(null, Typeface.BOLD);
} else {
view.setTypeface(null, Typeface.NORMAL);
}
}
And use it in xml as below :
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{model.value}"
app:setBold="#{model.isBold}"
/>
As Html.fromHtml signagure changed at API level 24 (Android N), the Android team introduced HtmlCompat for using the same signature within any api level.
So, you should use the HtmlCompat class:
HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY);
To use it, you may include AndroidX core in your project's build.gradle:
implementation 'androidx.core:core:1.3.1'
Layout XML file:
<?xml version="1.0" encoding="utf-8"?>
<layout
<data>
<import type="androidx.core.text.HtmlCompat"/>
<data>
<LinearLayout>
<android.support.design.widget.CoordinatorLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/spacing_xlarge"
android:layout_marginStart="#dimen/spacing_xlarge"
android:layout_marginBottom="#dimen/spacing_small"
android:text="#{HtmlCompat.fromHtml(#string/product_price(productPrice),HtmlCompat.FROM_HTML_MODE_LEGACY)}"
android:textColor="#color/button_tertiary"
android:visibility="#{productPrice > 0}"
style="#style/Body.Small"
/>
</android.support.design.widget.CoordinatorLayout>
</LinearLayout>
</layout>
// put this method in your model class where name is string variable which is set as per given api response
public Spanned getHtmlText(){
return Html.fromHtml("<b>" + name + "</b>");
}
// in xml use this where userList is variable name of model class.
android:text="#{userList.htmlText}"
Related
I have made a binding adapter available statically inside my Fragment which basically change my button appearance from "Stop" to "Play" and vice-versa.
companion object {
#BindingAdapter("playState")
fun Button.setPlayState(item: UIState) {
item.let {
if (it.isPlaying) {
setText("Stop")
setBackgroundColor(ContextCompat.getColor(context, R.color.colorStop))
} else {
setText("Play")
setBackgroundColor(ContextCompat.getColor(context, R.color.colorPlay))
}
}
}
}
Here is my layout file. I have provided a data class for it.
<?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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<!-- stuff here -->
<variable
name="viewmodel"
type="com.mypackage.ui.ViewModel"/>
<variable
name="uistate"
type="com.mypackage.ui.UIState" />
</data>
<!-- layout, buttons, and more stuff here. Just pay attention to this following button -->
<Button
android:id="#+id/play_button"
android:layout_width="150sp"
android:layout_height="75sp"
android:layout_marginTop="20sp"
android:onClick="#{() -> viewmodel.onPlayClicked()}"
android:text="#string/play_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/minus_layout"
app:layout_constraintVertical_bias="0.026"
app:playState="#{uistate}"/>
</layout>
UIState itself is pretty self-explanatory.
data class UIState(var isPlaying: Boolean)
and the () -> viewmodel.onPlayClicked() flips the Boolean at UIState.
After compiling, Data Binding Compiler throws this error:
Cannot find a setter for <android.widget.Button app:playState>
that accepts parameter type 'com.mypackage.ui.UIState'
I have tried:
Rebuilding the project by removing .gradle folder
Looking for answer here and here.
Removed #JvmStatic annotation at the extension function
Moved the extension function to top level instead of Fragment's companion object.
I think you missed to add kotlin plugin in your gradle
apply plugin: 'kotlin-kapt'
You don't have to use #JvmStatic because you are using Kotlin extension feature.
You need to add the view reference as a paramater to your BindingAdapter method.
#BindingAdapter("playState")
fun setPlayState(button:Button,item: UIState) {
//do your work here
}
Your namespace
xmlns:app="http://schemas.android.com/apk/res-auto"
is wrong for custom binding adapters. Please use the namespace
xmlns:app="http://schemas.android.com/tools"
since app:playState is not in the namespace you have given its not working properly
I am working on new project and just started using Databinding. I heard people talking abaout code reduction.I have a RecyclerView in fragment. I have a simple ConstraintLayout in which I have 3 TextViews. This is in my Adapter for RecyclerView.
That aditional +" nazv" is just a sample and I could use String Extension.
fun bind(item: Mkdo) {
binding.txtMkdoNaziv.text = item.nazv+" nazv"
binding.txtMkdoPost.text = item.post
binding.txtmkdoPostNaziv.text = item.postNaziv
}
ConstraintLayout for single RecyclerView row (deleted positioning properties)
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constraintRowMkdo"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/txtMkdoNaziv"/>
<TextView
android:id="#+id/txtMkdoPost"/>
<TextView
android:id="#+id/txtmkdoPostNaziv"/>
</androidx.constraintlayout.widget.ConstraintLayout>
This works just fine.
If I want to convert it to Databinding I have to add:
fun bind(item: Mkdo) {
binding.mkdo=item
binding.executePendingBindings()
}
And BindingAdapter
#BindingAdapter("setNazv")
fun TextView.setNazv(item:Mkdo?){
item?.let {
text="${item.nazv} nazv"
}
}
ConstraintLayout for single RecyclerView row (deleted positioning properties) with Databinding
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constraintRowMkdo"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/txtMkdoNaziv"
app:setNazv="#{mkdo}""/>
<TextView
android:id="#+id/txtMkdoPost"
android:text="#{mkdo.post}"/>
<TextView
android:id="#+id/txtmkdoPostNaziv"
android:text="#{mkdo.postNaziv}"/>
</androidx.constraintlayout.widget.ConstraintLayout>
This makes constraintRowMkdo nonreusable and in my opinion adds more work than without it.
Also if I have some string formatting I need to have it on two places string Extension and BindingAdapter. I can reuse Extension but it is still on who places.
What are your toughts on this?
Am I missing something?
Are there any aditional benefits of using it?
Thanks
my extension function for TextView:
fun TextView.setColorifiedText(text : String) {
// Some logic to change color of text
setText(text)
}
my xml file with databinding:
<layout>
<data>
<!--some data-->
</data>
<LinearLayout>
<TextView
android:setColorifiedText="some text to be colorified"
/>
</LinearLayout>
</layout>
Yes, it is possible to access an extension function written for a view in XML.
We have prefix the extension function name with 'set' and annotate it with 'BindingAdapter' annotation., for ex:-
if your extension function name is 'colorText' change it to 'setColorText' and access the same in XML with the attribute name 'colorText' from 'app:' namespace.
Also, annotate the extension function with #BindingAdapter("colorText")
#BindingAdapter("colorText")
fun TextView.setColorText(text : String) {
// logic to something with the text
}
in XML :
<TextView
app:colorText="your String" />
you can also modify the code using the kotlin's setter getter to advantage
In case of java declare the function as static and add an argument to the method which gets reference to the textview you gave the attribute to.
I am trying to use two way data binding with the radio button. It is working fine with one way like below,
android:checked="#{registration.gender.equals(Gender.FEMALE.getValue())}".
But My problem is that, I need to set the value of selected radio button in my model.
<?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" xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.callhealth.customer.comman.enums.Gender" />
<import type="android.text.TextUtils" />
<import type="java.lang.Integer" />
<variable name="callback" type="com.callhealth.customer.usermanagement.callback.RegistrationCallback" />
<variable name="registration" type="com.callhealth.customer.usermanagement.model.request.LoginAndRegistrationRequestModel" />
</data>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:orientation="vertical">
<android.support.v7.widget.AppCompatTextView android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="#string/label_gender" android:textSize="15sp" />
<RadioGroup android:id="#+id/gender_group" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:orientation="horizontal">
<android.support.v7.widget.AppCompatRadioButton android:layout_width="wrap_content" android:checked="#={registration.gender.equals(Gender.MALE.getValue())}" android:layout_height="wrap_content" android:text="#string/label_male" />
<android.support.v7.widget.AppCompatRadioButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginStart="20dp" android:checked="#={registration.gender.equals(Gender.FEMALE.getValue())}" android:text="#string/label_female" />
</RadioGroup>
</LinearLayout>
</layout>
My Model Class
public class LoginAndRegistrationRequestModel extends BaseObservable {
private Integer gender;
#Bindable
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
notifyPropertyChanged(BR.gender);
}
}
When i am trying to use
android:checked="#={registration.gender.equals(Gender.FEMALE.getValue())}"
Gradel is throwing an error
Error:Execution failed for task ':app:compileDebugJavaWithJavac'.
> android.databinding.tool.util.LoggedErrorException: Found data binding errors.
****/ data binding error ****msg:The expression registrationGender.equals(com.callhealth.customer.comman.enums.Gender.MALE.getValue()) cannot be inverted: There is no inverse for method equals, you must add an #InverseMethod annotation to the method to indicate which method should be used when using it in two-way binding expressions
file:S:\Umesh\android\android_studio_workspace\CallHealth\app\src\main\res\layout\content_user_registration.xml
loc:148:48 - 148:97
****\ data binding error ****
Try it
After some hours I found an easy way: two-way databinding in android. Its a base skeleton with livedata and Kotlin. Also, you can use ObservableField()
Set your viewmodel to data
Create your radiogroup with buttons as you like. Important: set all
radio buttons id !!!
Set in your radio group two-way binding to checked variable (use
viewmodel variable)
Enjoy)
layout.xml
<data>
<variable
name="VM"
type="...YourViewModel" />
</data>
<LinearLayout
android:id="#+id/settings_block_env"
...
>
<RadioGroup
android:id="#+id/env_radioGroup"
android:checkedButton="#={VM.radio_checked}">
<RadioButton
android:id="#+id/your_id1"/>
<RadioButton
android:id="#+id/your_id2" />
<RadioButton
android:id="#+id/your_id3"/>
<RadioButton
android:id="#+id/your_id4"/>
</RadioGroup>
</LinearLayout>
class YourViewModel(): ViewModel {
var radio_checked = MutableLiveData<Int>()
init{
radio_checked.postValue(R.id.your_id1)//def value
}
//other code
}
You are binding it to a boolean EXPRESSION. It has no idea what method to call when setting it. I would try making your property a boolean (e.g. isFemale). It sounds like there is also a way to indicate the setter with the #InverseMethod annotation, but I haven't used that and it seems to me that the boolean approach would be more straightforward. You could always implement the boolean properties in terms of the Integer gender field if you wanted to refer to the Integer elsewhere in java code.
You can either create a #InverseMethod for the custom converter (the error says it can't convert equals to gender basically) or just use a boolean observable for the changes and the setter will work out of the box.
for example if you have
val isFemale = ObservableBoolean()
in your ViewModel this will work out of the box
android:checked="#={viewModel.isFemale}"
of course you need to provide ViewModel variable into the layout binding.
To "fix" what you have there you need to create your own inverse method and setter for checked.
#InverseMethod("toGender")
public static int isFemaleGender(Integer gender) {
return gender.equals(Gender.FEMALE.getValue());
}
public static Integer toGender(boolean checked) {
if (checked)
return Gender.FEMALE;
else
return Gender.MALE;
}
Both of these approaches will only work if you have 2 options as you do true false basically. It is either you create an Observable for each option or go with the solution provided by #Serega Maleev
What is the type of the Observable class property which getter is annotated as #Bindable in the Android Data Binding framework?
For example, let the Observable class be defined as follows:
class Localization() : BaseObservable() {
var translation: (key: String) -> String by Delegates.observable(defaultTranslation) { _, _, _ ->
notifyPropertyChanged(BR.translation)
}
#Bindable get
}
The layout XML will be then something like this:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="translation"
type="WHAT IS THE TYPE OF TRANSLATION?" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{translation.invoke(stringKey)}" />
</FrameLayout>
</layout>
The question is, what to put in the type attribute of variable "translation".
I've tried:
type="kotlin.jvm.functions.Function1<String, String>"
It compiles, but the TextView is not updated when translation property changes.
I can achieve the desired behavior by introducing localization variable in the layout XML and then calling localization.translation.invoke() in the binding expression. I am just not comfortable with this and want to know if I can reference translation directly.
The Localization extends BaseObservable while Function1 is not observable at all. So using the Localization gives you an interface for observing the changes to the properties.
If you bind the translation, it's a simple field that gets set. If you want to update it, you'd have to call setTranslation() again.