How to modify the layout expression dynamically in data binding layout? - android

Let's say I have a data binding layout that looks something like this,
<layout xmlns:android="http://schemas.android.com/apk/res/android"
...
<data>
<variable
name="foo"
type="com.example.sudokugame.Foo" />
</data>
...
<TextView
android:id="#+id/cell_1"
style="#style/cell"
android:text="#{foo.bar}" />
...
</layout>
I want to inflate the layout and add it to another parent layout. So, my question is, is there any to modify the layout expression in the textview so it would look something like this,
...
<TextView
android:id="#+id/cell_1"
style="#style/cell"
android:text="#{foo.baz}" <!-- or something else like foo.something -->
....
Is it possible? And if not is there any way to build a data binding layout completely programmatically without even defining an XML layout so that it could be added to another layout?

Use binding adapters to achieve this. Here is how you can do,
#BindingAdapter("dynamicText")
fun TextView.setDynamicText(foo: Foo) {
text = if (<your_condition>) {
foo.bar;
} else {
foo.something;
}
}
Now change your xml textview property to,
<TextView
android:id="#+id/cell_1"
style="#style/cell"
android:dynamicText="#{foo}" />

Related

Include layout in XML depends on condition

I have parent layout with databinding. I want to add child layout depends on logic.
I expected to need similar code:
<include
android:id="#+id/layout_id"
layout="#{model.isOk ? #layout/layout_container_1 : #layout/layout_container_2}"
app:model="#{model}" />
But I get error:
android.databinding.tool.processing.ScopedException: [databinding] {"msg":"included value (#{model.isOk ? #layout/layout_container_1 : #layout/layout_container_2}) must start with #layout/.","file":"x.xml","pos":[]}
Is it possible to include child layout dynamically through "if-else" condition? If yes what is the best way? Any samples are welcome!
Is it possible to include child layout dynamically through "if-else" condition?
So far, this is not possible, because the layout attribute must point to a certain layout before the data-binding works:
(Data Binding) As its name implies, it binds data to a layout, so there must be a layout first, in order to bind data to it.
In other words, the layout="#{model.isOk ? #layout/layout_container_1 : #layout/layout_container_2}" doesn't work because there should be an existing layout before examining model.isOk, and also the layout attribute expects a layout resource after the # symbol.
So, we need to do that programmatically instead of the XML layout. And this an be done by replacing the <include> with ViewStub which is a sort of deferring layout inflation to be done in the activity.
So before inflating the ViewStub layout, you can have a chance to check whether the model.isOk in the activity before deciding which layout can be inflated on the ViewStub.
And hence based on the model.isOk value, we can set the layout with binding.myLayoutStub.myViewStub?.layoutResource = R.layout.layout_container_1
Eventually we can inflate the new ViewStub layout using binding.myLayoutStub.myViewStub?.inflate() method.
Using the ViewStub, will make you have a couple of binding object, one for the activity (or fragment), and the other for the inflated layout of the ViewStub.
Here is an example like yours:
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="model"
type="com.example.android.databindingexample.ActivityViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ViewStub
android:id="#+id/layout_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And we've layout_container_1 & layout_container_2 that only differ in the the textview value:
layout_container_1.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<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:background="#color/purple_200">
<TextView
android:id="#+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Layout 1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ViewModel:
class ActivityViewModel : ViewModel() {
var isOk = true
}
Then decide which layout in the activity based on the model.isOk whenever you inflate the ViewStub:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// This is of Any type as it will be casted later to the proper DataBinding generated class when the ViewStub layout inflated
private lateinit var stubBinding: Any
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val model = ViewModelProvider(this).get(ActivityViewModel::class.java)
binding.model = model
// Listener to ViewStub inflation so that we can change its layout
binding.layoutStub.setOnInflateListener { _, inflated ->
stubBinding =
(if (model.isOk) DataBindingUtil.bind<LayoutContainer1Binding>(
inflated
)
else DataBindingUtil.bind<LayoutContainer2Binding>(
inflated
))!!
// Changing the text of the inflated layout in the ViewStub using the ViewStubBinding
if (model.isOk)
(stubBinding as (LayoutContainer1Binding)).textview.text = "This is Layout container 1"
else
(stubBinding as (LayoutContainer2Binding)).textview.text = "This is Layout container 2"
}
// Inflating the ViewStub
if (!binding.layoutStub.isInflated) {
binding.layoutStub.viewStub?.layoutResource = if (model.isOk)
R.layout.layout_container_1 else R.layout.layout_container_2
binding.layoutStub.viewStub?.inflate()
}
}
}
Preview:
This is impossible with the current implementation of <include> tag. By "implementation" I mean how it is actually used by the SDK.
<include> tag is parsed in the process of layout inflation which means layout attribute of <include> tag must have a layout resource ID defined before any of your code logic can be executed. While data-binding is what you can use after a layout was successfully inflated.

How can I change the cardview border color dynamically?

I would like to change the border color of my cardview based on the content.Is it possible to reach somehow the xml file from the recyclerview adapter and change the color?
xml file from the recyclerview adapter and change the color?
No. XML is read only. And you do not need to touch it, but use i.f. findViewById(), find your card view, and use its methods to alter the color.
This is possible by using the Databinding library.
Let's say that your content is a User and you need to change the color of your CardView if he/she is an adult or a child. So you can pass the object in your Activity or Fragment like this:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
User user = new User("Test", "User");
binding.setUser(user);
}
And then, add the data tag and the variable in your XML file:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<android.support.v7.widget.CardView
android:background="#{user.isAdult ? #color/yellow : #color/gray }"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{user.firstName}"/>
</android.support.v7.widget.CardView>
</layout>
If you want to know more, check the documentation : https://developer.android.com/topic/libraries/data-binding/index.html?hl=pt-br

Android Data Binding Programmatically Instantiated View

The Android documentation does a great job of describing how one can create a binding class using a layout xml file. But I have a couple of questions.
Is there a way to create a data binding class for a custom view that is instantiated programmatically? For example, lets say I have two custom view classes and I want to bind the same view model object to them programmatically without using any xml. The classes are as follows:
class MyViewModel {
}
class MyCustomView extends View {
}
class MyAnotherCustomView extends MyCustomView {
}
Now lets say I instantiate MyCustomView/MyAnotherCustomView using:
MyCustomView customView = new MyCustomView(context);
How do I go about using data binding in this case? Is this possible using the official Android data binding framework? If not, what other frameworks/libraries are available or recommended to achieve this?
My second question is a follow up of the first question. Lets say it is not possible to achieve what I want in my first question. Then, I will have to define a my_custom_view.xml file. This will look something like this:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<com.example.name.MyCustomView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#{user.firstName}"/>
</layout>
Now, if I want to use MyAnotherCustomView which is a subclass of MyCustomView keeping the binding logic the same, will I have to create a new xml file my_another_custom_view.xml just to replace MyCustomView with MyAnotherCustomView to define the same binding?
The answer to the first question is "No." Android data binding requires the XML to generate the binding classes.
In your second question, you offer a solution that will work. If you go that route, one way to do it is to use the ViewDataBinding base class setters to set your variables. I can imagine a method like this:
public void addCustomView(LayoutInflater inflater, ViewGroup container, User user) {
ViewDataBinding binding = DataBindingUtil.inflate(inflater,
this.layoutId, container, true);
binding.setVariable(BR.user, user);
}
Here, I've assumed the selection of which custom View is determined by a field layoutId. Each possible layout will have to define a user variable of type User.
I don't know the particulars of your use, but if you want to dynamically choose which custom view to load, you could use a ViewStub. You could also do the same thing with just visibility if you don't have any tremendous overhead in loading your custom Views.
<?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="android.view.View"/>
<variable name="user" type="com.example.User"/>
<variable name="viewChoice" type="int"/>
</data>
<FrameLayout ...>
<!-- All of your outer layout, which may include binding
to the user variable -->
<ViewStub android:layout="#layout/myCustomView1"
app:user="#{user}"
android:visiblity="#{viewChoice == 1} ? View.VISIBLE : View.GONE"/>
<ViewStub android:layout="#layout/myCustomView2"
app:user="#{user}"
android:visiblity="#{viewChoice == 2} ? View.VISIBLE : View.GONE"/>
</FrameLayout>
</layout>

Is it possible to conditionally show an XML include based on a classes method response?

I'm wondering if it's possible to use DataBinding to do conditionally show a layout based on a methods boolean response. Here's what I'm trying to do
XML Layout:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="View"
type="android.view.View"/>
<variable
name="App"
type="com.app.JTApp"/>
</data>
<include layout="#layout/view_checkout_total_cardview"
android:visibility="#{App.isLandscape() ? View.GONE : View.VISIBLE}" />
</layout>
JTApp class:
public class JTApp {
public boolean isLandscape() {
Timber.d("putty-- isLandscape: --------------------------");
return getResources().getBoolean(R.bool.is_landscape);
}
…
}
Currently this doesn't work. Am I missing something or is this not possible? I'm coming from the web where this is possible with frameworks like Angular.
Yes, using a conditional statement within XML is possible. I am not too familiar with data binding library, but a similar functionality is used in the documentation:
Zero or more import elements may be used inside the data element.
These allow easy reference to classes inside your layout file, just
like in Java.
<data>
<import type="android.view.View"/>
</data>
Now, View may be used within your binding expression:
<TextView
android:text="#{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{user.isAdult ? View.VISIBLE : View.GONE}"/>
I believe the only issue with your code is that you are using the View as a variable instead of as an import in your <data> element.
You can do this way easier with resource-modifiers:
have one layout in layout-land that does not contain the cardview
have another layout in layout that does contain the cardview
so you will get this effect and in landscape it is not even inflated and then set invisible

Include layout with custom attributes

I'm building a complex layout and I want to use include tag for my custom component, like this:
<include layout="#layout/topbar"/>
The topbar layout has custom root component and in layout xml file it's defined like this:
<my.package.TopBarLayout
... a lot of code
Now, I wanna pass my custom defined attributes to "topbar" like this:
<include layout="#layout/topbar" txt:trName="#string/contacts"/>
And then get the value of those custom attributes in custom component code or ideally in xml.
Sadly, I cannot get value of txt:trName attribute to make it to the topbar layout, I just don't receive anything in code. If I understand correctly from that documentation page, I can set no attributes for layouts used via include, but id, height and width.
So my question is how can I pass my custom defined attributes to layout which is added via include?
I know this is an old question but I came across it and found that it is now possible thanks to Data Binding.
First you need to enable Data Binding in your project. Use DataBindingUtil.inflate (instead of setContentView, if it's Activity) to make it work.
Then add data binding to the layout you want to include:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="title" type="java.lang.String"/>
</data>
<RelativeLayout xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/screen_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:gravity="center">
...
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="20sp"
android:textStyle="bold"
android:text="#{title}"/>
...
</RelativeLayout>
</layout>
Finally, pass the variable from the main layout to the included layout like this:
<?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>
...
</data>
...
<include layout="#layout/included_layout"
android:id="#+id/title"
app:title="#{#string/title}"/>
...
</layout>
It's not possible to attributes other than layout params, visibility or ID on an include tag. This includes custom attributes.
You can verify this by looking at the source of the LayoutInflater.parseInclude method, around line 705:
http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.2_r1.1/android/view/LayoutInflater.java#640
The inflater only applies the ID and visibility attributes to the included layout.
I ran into this issue today. For whatever it is worth, I think there is a straight-forward work around. Instead of adding attributes to the include tag, create a custom wrapper view for the include and add attributes to that. Then, do the include from the wrapper. Have the wrapper class implementation extract the attributes and pass along to its single child, which is the root view of the include layout.
So, say we declare some custom attributes for a wrapper called SingleSettingWrapper like this -
<declare-styleable name="SingleSettingWrapper">
<attr name="labelText" format="string"/>
</declare-styleable>
Then, we create two custom view classes - one for the wrapper (SingleSettingWrapper) and one for the child (SingleSettingChild) that will be included -
<!-- You will never end up including this wrapper - it will be pasted where ever you wanted to include. But since the bulk of the XML is in the child, that's ok -->
<com.something.SingleSettingWrapper
android:id="#+id/wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
custom:labelText="#string/my_label_string">
<!-- Include the child layout -->
<include layout="#layout/setting_single_item"/>
</com.something.SingleSettingWrapper>
For the child, we can put whatever complex layout in there that we want. I'll just put something basic, but really you can include whatever -
<com.something.SingleSettingItem
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout >
<!-- add whatever custom stuff here -->
<!-- in this example there would be a text view for the label and maybe a bunch of other stuff -->
<!-- blah blah blah -->
</RelativeLayout>
</com.something.SingleSettingItem>
For the wrapper (this is the key), we read all of our custom attributes in the constructor. Then, we override onViewAdded() and pass those custom attributes to our child.
public class SingleSettingWrapper extends FrameLayout
{
private String mLabel;
public SingleSettingWrapper(Context context, AttributeSet attrs)
{
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.SingleSettingWrapper,
0, 0);
mLabel = a.getString(R.styleable.SingleSettingWrapper_labelText);
a.recycle();
}
public void onViewAdded(View child)
{
super.onViewAdded(child);
if (!(child instanceof SingleSettingItem))
return;
((TextView)child.findViewById(R.id.setting_single_label)).setText(mLabel);
/*
Or, alternatively, call a custom method on the child implementation -
((SingleSettingItem)child)setLabel(mLabel);
*/
}
}
Optionally, you can implement the child too and have it receive messages from the wrapper and modify itself (instead of having the wrapper modify the child as I did above).
public class SingleSettingItem extends LinearLayout
{
public SingleSettingItem(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public void setLabel(String l)
{
// set the string into the resource here if desired, for example
}
}
At the end of the day, each of the XML files where you wanted to <include> your layout will contain about 7 lines of XML for the wrapper+include instead of the single include that you wanted, but if the included view contains hundreds of lines you're still way better off. For example -
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<!-- this is the beginning of your custom attribute include -->
<com.something.SingleSettingWrapper
android:id="#+id/my_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
custom:labelText="#string/auto_lock_heading">
<include layout="#layout/setting_single_item"/>
</com.something.SingleSettingWrapper>
<!-- this is the end of your custom attribute include -->
</LinearLayout>
In practice, this seems to work pretty well and is relatively simple to set up. I hope it helps someone.
Unfortunately, the only thing I can contribute is that I was also unable to set custom attributes on an include tag, and have them pass through to the included layout.
It may well not be possible at this point.
It's not possible to use with custom attributes, or any attributes other than the ones stated on the API page (up through at least 5.0.0):
https://code.google.com/p/android/issues/detail?id=38023
http://grepcode.com/file/repo1.maven.org/maven2/org.robolectric/android-all/5.0.0_r2-robolectric-1/android/view/LayoutInflater.java
You have to include in your root xml element your custom namespace.
If your package name is com.example.test your xml shold be something like this:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:txt="http://schemas.android.com/apk/res/com.example.test" />
A nice tutorial is: http://blog.infidian.com/2008/05/02/android-tutorial-42-passing-custom-variables-via-xml-resource-files/
I had the same question. After visiting this thread, I ended up using View's setTag() methods to attach identifying information to each View during onCreate(), and then getTag() methods to retrieve it later on.

Categories

Resources