I have a custom view. In onFinishInflate() I bind the view.
class CreatePlaylistButton(context: Context, attributeSet: AttributeSet) :
ViewGroup(context, attributeSet) {
private lateinit var binding: ButtonCreatePlaylistBinding
.....
......
override fun onFinishInflate() {
super.onFinishInflate()
binding = ButtonCreatePlaylistBinding.bind(this)
}
The layout is simple. See snippet below
<com.tendai.musicx.ui.discover.customviews.CreatePlaylistButton xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true">
<TextView
android:id="#+id/textview_create_playlist"
style="#style/TextAppearance.AppCompat.Caption"
.....
...
android:clickable="false"
/>
<ImageView
android:id="#+id/imageViewCreatePlaylist"
......
.....
android:src="#drawable/ic_add" />
</com.tendai.musicx.ui.discover.customviews.CreatePlaylistButton>
I use the custom view in a fragment's layout like so:
<androidx.constraintlayout.widget.ConstraintLayout />
.....
....
<com.tendai.musicx.ui.discover.customviews.CreatePlaylistButton
android:id="#+id/button_create_playlist"
........
/>
When I try to run the app I get the error Missing required view with ID: packageName/imageViewCreatePlaylist but the ImageView is there? What I am missing?
Your layout in first file has ImageView with the id nested in CreatePlaylistButton.
In fragment layout you're just using plain CreatePlaylistButton which is essentially a ViewGroup without any children, so the view is not there - hence the error.
To get what you're trying to do, you need to replace CreatePlaylistButton in first layout with e.g. LinearLayout and then inflate it inside the CreatePlaylistButton class.
Steps to reproduce my problem
Create a custom view
class VideoTrimmerView(context: Context, attrs: AttributeSet) : View(context) {
private val backgroundPaint: Paint = Paint().apply {
color = Color.BLACK
}
override fun draw(canvas: Canvas?) {
super.draw(canvas)
canvas?.drawRect(Rect(0, 0, width, height), backgroundPaint)
}
}
Add <declare-styleable> resources to the project
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="VideoTrimmerView" />
</resources>
Add layout xml file activity_video_trimmer.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".VideoTrimmerActivity">
<com.udara.developer.myapp.videotrimmer.VideoTrimmerView
android:id="#+id/video_trimmer_view"
android:layout_width="match_parent"
android:layout_height="64dp"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="#+id/open_video_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Open Video"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Enable view binding in module level build.gradle
android {
buildFeatures {
viewBinding true
}
}
Use view bindings to access views
class VideoTrimmerActivity : AppCompatActivity() {
private lateinit var binding: ActivityVideoTrimmerBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityVideoTrimmerBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
}
}
I get the following error when running the app
Caused by: java.lang.NullPointerException: Missing required view with ID: com.udara.developer.my_app:id/video_trimmer_view
The problem only happens when custom views are used. How to solve this issue?
I have invoked super constructor without attrs parameter.
Previous constructor version
class VideoTrimmerView(context: Context, attrs: AttributeSet) : View(context) {
}
After adding attrs parameter
class VideoTrimmerView(context: Context, attrs: AttributeSet) : View(context, attrs) {
}
In the android documentation it just tells
To allow Android Studio to interact with your view, at a minimum you must provide a constructor that takes a Context and an AttributeSet object as parameters.
Now the problem is solved!
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
class MyCustomView #JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null) : FrameLayout(
context,
attributeSet
) {
val binding = MylayoutBinding.inflate(
LayoutInflater.from(context),
this,
true
)
}
// mylayout.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<merge>
<FrameLayout
android:id="#+id/myChildContainer"
android:layout_width="100dp"
android:layout_height="100dp"/>
</merge>
</layout>
I am using my custom view in the form below.
// myactivity.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent">
<MyCustomView
android:id="#+id/myView"
android:layout_width="300dp"
android:layout_height="300dp">
<Button
android:id="#+id/myButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</MyCustomView>
</FrameLayout>
</layout>
In the example above, I want myButton to go into MyCustomView as a child of myChildContainer.
I followed the example from android: how to add children from an xml layout into a custom view and modified MyCustomView as below.
class MyCustomView #JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null) : FrameLayout(
context,
attributeSet
) {
val binding = MylayoutBinding.inflate(
LayoutInflater.from(context),
this,
true
)
override fun onFinishInflate() {
super.onFinishInflate()
while (childCount > 1) {
val child = getChildAt(1)
val param = child.layoutParams
removeView(child)
binding.myChildContainer.addView(child, param)
}
}
}
This code is fine if I don't apply data binding.
The problem is app has crashed when I apply data binding.
internal class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<MyactivityBinding>(this, R.layout.myactivity)
binding.myButton.text = "Test Text" <-- crash
}
}
Caused by: java.lang.NullPointerException: binding.myButton must not be null
When I change the execution point of remove view and addview of my code from onFinishInflate to onAttachedToWindow, the error generally does not occur, but I have confirmed that it recurs intermittently, so I think this is not a solution.
Is there any way to add Child View to CustomView while keeping DataBinding?
I actually use multiple such views, so I don't want to call individual functions like myView.sortView() after setContentView.
I'm not able to get a onClick method to work with a custom FrameLayout view.
When I refactor a default Button view into a custom ProgressButton view containing a Button, the onClick method doesn't get called.
Custom ProgressButton view class:
class ProgressButton #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyle, defStyleRes){
private var button: Button
init {
val view = LayoutInflater.from(context).inflate(R.layout.view_progress_button, this, true)
button = view.findViewById(R.id.button)
context.theme.obtainStyledAttributes(
attrs,
R.styleable.ProgressButton,
0, 0
).apply {
try {
if (hasValue(R.styleable.ProgressButton_buttonText)) {
button.text = getString(R.styleable.ProgressButton_buttonText)
}
if (hasValue(R.styleable.ProgressButton_buttonBackground)) {
button.background = getDrawable(R.styleable.ProgressButton_buttonBackground)
}
} finally {
recycle()
}
}
}
}
ProgressButton XML Layout file: res/layout/view_progress_button.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="#+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</FrameLayout>
Usage of ProgressButton view with android:onClick:
<com.example.ui.common.ProgressButton
android:id="#+id/progress_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="#{() -> viewModel.onProgressButtonClick()}"
app:buttonText="#string/test"
app:buttonBackground="#drawable/selectable_button_blue"
/>
What's the best way to pass a click listener to a custom view using Android databinding?
Assuming that you have dataBinding.enabled true defined in your build.gradle. If not, check this out.
It doesn't look like your layout is currently a databinding layout. First you'd want to convert view_progress_button.xml to a databinding layout like the following:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.your.package.YourViewModelClass" />
</data>
<FrameLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.ui.common.ProgressButton
android:id="#+id/progress_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="#{() -> viewModel.onProgressButtonClick()}"
app:buttonText="#string/test"
app:buttonBackground="#drawable/selectable_button_blue"
/>
</FrameLayout>
There should be an option in Android Studio that lets you auto-convert your layout. Do this by hitting alt + enter (Windows) or option + enter (OSX) on the root ViewGroup in your layout. In this case you'd want to hit that combination on your FrameLayout.
In your ProgressButton class, you'd want to do something like this:
val binding: ViewProgressButtonBinding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.view_progress_button, null, false);
instead of:
val view = LayoutInflater.from(context).inflate(R.layout.view_progress_button, this, true)
Using the binding object, you can do:
binding.setViewModel(yourViewModelInstance)
You can instantiate your ViewModel however, but inside your ViewModel class you should have onProgressButtonClick() defined.
If I remember correctly, you might need to do something like this in your layout for the onClick:
android:onClick="#{(v) -> viewModel.onProgressButtonClick()}"
because the lambda expects the view as a parameter.
Another note is that you no longer need to do findViewById since you have access to that view like so:
binding.button
The Android data binding guide discusses binding values within an activity or fragment, but is there a way to perform data binding with a custom view?
I would like to do something like:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.mypath.MyCustomView
android:id="#+id/my_view"
android:layout_width="match_parent"
android:layout_height="40dp"/>
</LinearLayout>
with my_custom_view.xml:
<layout>
<data>
<variable
name="myViewModel"
type="com.mypath.MyViewModelObject" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{myViewModel.myText}" />
</LinearLayout>
</layout>
While it appears possible to do this by setting custom attributes on the custom view, this would quickly become cumbersome if there's a lot of values to bind.
Is there a good way to accomplish what I'm trying to do?
In your Custom View, inflate layout however you normally would and provide a setter for the attribute you want to set:
private MyCustomViewBinding mBinding;
public MyCustomView(...) {
...
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mBinding = MyCustomViewBinding.inflate(inflater);
}
public void setMyViewModel(MyViewModelObject obj) {
mBinding.setMyViewModel(obj);
}
Then in the layout you use it in:
<layout xmlns...>
<data>
<variable
name="myViewModel"
type="com.mypath.MyViewModelObject" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.mypath.MyCustomView
android:id="#+id/my_view"
app:myViewModel="#{myViewModel}"
android:layout_width="match_parent"
android:layout_height="40dp"/>
</LinearLayout>
</layout>
In the above, an automatic binding attribute is created for app:myViewModel because there is a setter with the name setMyViewModel.
Data binding works even with merge only parent had to be "this" and attach to parent true.
binding = DataBindingUtil.inflate(inflater, R.layout.view_toolbar, this, true)
First, don't do this if this custom view is already being <include> in another layout, such as activity etc. You'll just get an exception about the tag being unexpected value. The data binding already ran the binding on it, so you're set.
Did you try using onFinishInflate to run the bind? (Kotlin example)
override fun onFinishInflate() {
super.onFinishInflate()
this.dataBinding = MyCustomBinding.bind(this)
}
Keep in mind that if you use the binding in your view, it won't be able to be created programmatically, at least it would be very convoluted to support both even if you can.
Following the solution presented by george the graphical editor in android studio was no longer able to render the custom view. The reason is, that no view is actually inflated in the following code:
public MyCustomView(...) {
...
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mBinding = MyCustomViewBinding.inflate(inflater);
}
I suppose that the binding handles the inflation, however the graphical editor did not like it.
In my specific use case I wanted to bind a single field and not an entire view model. I came up with (kotlin incoming):
class LikeButton #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
val layout: ConstraintLayout = LayoutInflater.from(context).inflate(R.layout.like_button, this, true) as ConstraintLayout
var numberOfLikes: Int = 0
set(value) {
field = value
layout.number_of_likes_tv.text = numberOfLikes.toString()
}
}
The like button consists of an image and a text view. The text view holds the number of likes, which I want to set via data binding.
By using the setter for numberOfLikes as an attribute in the following xml, data binding automatically makes the association:
<views.LikeButton
android:id="#+id/like_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:numberOfLikes="#{story.numberOfLikes}" />
Further reading: https://medium.com/google-developers/android-data-binding-custom-setters-55a25a7aea47
Today, I want to use the dataBinding on my Custom View class. But I don't know how to create data binding to my class. so I search the answer on StackOverflow.
Firstly I try the answer:
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
BottomBarItemCustomViewBinding binding = BottomBarItemCustomViewBinding.inflate(inflater);
but, I found this is not working for my code
so I change another method:
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
BottomBarItemCustomViewBinding binding = DataBindingUtil.inflate(inflater, R.layout.bottom_bar_item_custom_view, this, true);
It's working for me.
the complete code is:
bottom_bar_item_custom_view.xml
<data>
<variable
name="contentText"
type="String" />
<variable
name="iconResource"
type="int" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<ImageView
android:id="#+id/bottomBarItemIconIv"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="2dp"
android:src="#{iconResource}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/bottomBarItemContentTv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="#{contentText}"
android:textColor="#color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/bottomBarItemIconIv" />
</androidx.constraintlayout.widget.ConstraintLayout>
BottomBarItemCustomView.java
public class BottomBarItemCustomView extends ConstraintLayout {
public BottomBarItemCustomView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
//use dataBinding on custom view.
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
BottomBarItemCustomViewBinding binding = DataBindingUtil.inflate(inflater, R.layout.bottom_bar_item_custom_view, this, true);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BottomBarItemCustomView);
int iconResourceId = typedArray.getResourceId(R.styleable.BottomBarItemCustomView_bottomBarIconResource, R.drawable.my_account_icon);
binding.setIconResource(iconResourceId);
String contentString = typedArray.getString(R.styleable.BottomBarItemCustomView_bottomBarContentText);
if (contentString != null) {
binding.setContentText(contentString);
}
typedArray.recycle();
}
hope is useful for you!
In Kotlin we can directly use ViewBinding:
class BenefitView(context: Context, attrs: AttributeSet) : ConstraintLayout(context, attrs) {
init {
val binding = BenefitViewBinding.inflate(LayoutInflater.from(context), this, true)
val attributes = context.obtainStyledAttributes(attrs, R.styleable.BenefitView)
binding.image.setImageDrawable(attributes.getDrawable(R.styleable.BenefitView_image))
binding.caption.text = attributes.getString(R.styleable.BenefitView_text)
attributes.recycle()
}
}
There are some good answers on here already, but I wanted to offer what I believe to be the simplest.
Create your custom control with the layout tags surrounding it, just like any other layout. See the following toolbar for example. this gets used in each of the activity classes
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="YACustomPrefs" type="com.appstudio35.yourappstudio.models.YACustomPreference" />
</data>
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/YATheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#color/colorPrimary"
app:popupTheme="#style/YATheme.PopupOverlay"/>
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
Now this custom layout is a child of every Activity. You simply treat it as such in the onCreate binding setup.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.yaCustomPrefs = YACustomPreference.getInstance(this)
binding.toolbarMain?.yaCustomPrefs = YACustomPreference.getInstance(this)
binding.navHeader?.yaCustomPrefs = YACustomPreference.getInstance(this)
binding.activity = this
binding.iBindingRecyclerView = this
binding.navHeader?.activity = this
//local pointer for notify txt badge
txtNotificationCountBadge = txtNotificationCount
//setup notify if returned from background so we can refresh the drawer items
AppLifeCycleTracker.getInstance().addAppToForegroundListener(this)
setupFilterableCategories()
setupNavigationDrawer()
}
Notice I set the children's content at the same time I do the parent and it is all done through dot notation access. As long as the files are surrounded with layout tags and you named them, it is simple to do.
Now if the custom class has it's own associated code inflation, then it can easily just do it's own binding in it's onCreate or constructor, but you get the picture. If you have your own class just throw the following in the constructor to match it's named binding class. It follows the name convention of the layout file pascal cased, so it's easy to find and auto fill.
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mBinding = NameOfCustomControlBinding.inflate(inflater);
Hope that helps.
I faced the same issue when I am trying to add a child views to a LinearLayout inside my host fragment(Nested fragment UI/UX)
here is my solution
var binding: LayoutAddGatewayBinding? = null
binding = DataBindingUtil.inflate(layoutInflater, R.layout.layout_add_gateway,
mBinding?.root as ViewGroup?, false)
binding?.lifecycleOwner=this
val nameLiveData = MutableLiveData<String>()
nameLiveData.value="INTIAL VALUE"
binding?.text=nameLiveData
Here mBinding is child fragment ViewDataBinding object and I have used nameLiveData for two-way databinding