I was trying to extend CardView on Android by kotlin using the following code:
HotCommentView.kt
class HotCommentView #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : CardView(context, attrs) {
private var binding: LayoutHotCommentBinding
init {
binding = LayoutHotCommentBinding.inflate(LayoutInflater.from(context), this, true)
}
layout_hot_comment.xml
<androidx.cardview.widget.CardView 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="wrap_content"
app:cardBackgroundColor="#f6f6f6"
app:cardCornerRadius="5dp"
app:cardElevation="0dp">
<!-- Some widgets -->
</androix.cardview.widget.CardView>
After that I tried to use my HotCommentView in other layout file, and I found out that cardElevation, cardCornerRadius did not function at all. The custom card view still have elevation and did not have 5dp of corner radius.
<com.xxx.view.HotCommentView
android:id="#+id/hot_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="13dp" />
What am I doing wrong with extend cardview?
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.
I am working on a custom expandable view in android.
The goal is that I can add child elements in the xml files and they will be expanded and collapsed when the user clicks the expand/collapse button as on the picture below.
The expananding/collapsing works fine, but I cannot find out how to handle the child views.
In the constructor of my custom view, I inflate an xml layout, and I have a linear layout inside, in which i would like to put the child elements.
I tried using the solution suggested in the answer to the question here.
But I get StackOverflowError, and about a hundres of these:
"at android.view.ViewGroup.resetResolvedLayoutDirection(ViewGroup.java:7207)", even if I try to use the solution in the second aswer, using a while loop instead of the for.
Here is the kotlin class of my view:
class CollapsableCategoryView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
/** Declare some variables */
private var titleString : String = ""
private var subtitleString : String = ""
private var isExpaneded : Boolean = false
/** The required views */
private lateinit var ivIcon : ImageView
private lateinit var llExpandableContent : LinearLayout
init {
/** Receive the attributes */
context.theme.obtainStyledAttributes(
attrs,
R.styleable.CollapsableCategoryView,
0, 0
).apply {
try {
titleString = getString(R.styleable.CollapsableCategoryView_categoryTitle) ?: ""
subtitleString = getString(R.styleable.CollapsableCategoryView_categorySubtitle) ?: ""
} finally {
recycle()
}
}
/** Inflate the layout */
val root : View = View.inflate(context, R.layout.collapsable_task_category, this)
/** Find the views we need*/
ivIcon = root.findViewById(R.id.ivCollapsableCategoryIcon) as ImageView
llExpandableContent = root.findViewById(R.id.llExpandableContent) as LinearLayout
/** onClickListener for the icon */
ivIcon.setOnClickListener {
toggleExpanded()
}
}
override fun onFinishInflate() {
for(i in 0..childCount){
var view : View = getChildAt(i)
removeViewAt(i)
llExpandableContent.addView(view)
}
super.onFinishInflate()
}
/** This method is called when user clicks the expand/collapse button */
fun toggleExpanded(){
isExpaneded = !isExpaneded
if(isExpaneded)
{
ivIcon.setImageResource(R.drawable.ic_collapse)
llExpandableContent.visibility = VISIBLE
}else{
ivIcon.setImageResource(R.drawable.ic_expand)
llExpandableContent.visibility = GONE
}
}
}
I read somewhere else about a different solution, which also doesn't work. That solution suggests to ovverride the addView() method something like this:
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
llExpandableContent.addView(child, params)
}
But if I do so, I get an exception that the lateinint var llExpandableContent is never initialized.
I have also seen solutions that override onMeasure() method but that doesn't seem to be the right approach for me to this problem, since I don't wan't to lay my views out in a special way, just want to add them in a linear layout.
Here is the xml resource file for the layout of the custom view:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="#dimen/collapsable_category_corner_radius"
android:background="#drawable/bg_collapsable_category_top"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/clCollapsableCategoryMain"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#drawable/bg_collapsable_category_middle">
<ImageView
android:id="#+id/ivCollapsableCategoryIcon"
android:layout_width="38dp"
android:layout_height="38dp"
android:layout_marginStart="8dp"
android:src="#drawable/ic_expand"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/clCollapsableCategoryTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Title"
app:layout_constraintStart_toEndOf="#+id/ivCollapsableCategoryIcon"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/clCollapsableCategorySubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Subtitle"
app:layout_constraintStart_toEndOf="#+id/ivCollapsableCategoryIcon"
app:layout_constraintTop_toBottomOf="#+id/clCollapsableCategoryTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="#+id/llExpandableContent"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#drawable/bg_collapsable_category_middle"
android:visibility="gone">
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="#dimen/collapsable_category_corner_radius"
android:background="#drawable/bg_collapsable_category_bottom"/>
</LinearLayout>
And here is how I am trying to use my custom view in a layout xml file:
<com.test.test.util.CollapsableCategoryView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Child view 1"/>
</com.test.test.util.CollapsableCategoryView>
Does anyone know how to solve this problem?
Thank you very much in advance for any help. Best regards,
Agoston
So I found the solution at another question, which I cannot find again...
But this solution works like a charm :)
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
if(llExpandableContent == null){
super.addView(child, index, params)
}else{
llExpandableContent?.addView(child, index, params)
}
}
Hope it will help someone else at some point :)
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.
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