Do I have to rely on ListView/Recycler view each time I need to loop over data to repeat a layout ?
I totally understand it for long lists of data where scroll/performance is involved, but let's say I am sure i'll only have 0...3max items and need to display in very simple single-line-layout for each (1 image, 1 textview + button).. isn't there a simplier pattern than using adapters ?
Seems like overkill (and a pain to deal with for every little part of my screen where I need to loop overs small lists).
What are the other options while using Components architecture (databinding) ?
Manually inflating my layout ? In viewmodel ? fragment? Do I need to create another viewModel specially for this child layout ?
Thanks
I recently have a similar issue recently, but my problem was that of nested lists i.e. I needed to inflate another list inside a recycler view. Here is a minimal example of how I went about it.
Add a LinearLayout to your layout XML file:
<LinearLayout
android:id="#+id/smallList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:list="#{viewModel.yourList}" />
Create a binding adapter where you inflate the items. Like so:
#BindingAdapter("app:list")
fun setList(layout: LinearLayout, yourList: List<ListItemModel>) {
layout.removeAllViews() // Remove previous items if your list does change
for (listItem in yourList) {
ListItemBinding.inflate( // inflate your list item
LayoutInflater.from(layout.context),
layout, // pass your LinearLayout as root
true // attachToRoot is true so that the inflated view is added to the LinearLayout
).apply {
// set your binding variables
this.listItem = listItem
}
}
}
Note: This is a minimal example to solve the issue since actual data and functionality is unknown. You may want to:
Add a click listener variable to your list item XML file and set that similarly.
Create a custom view for the view if it is to be reused and write the binding adapter there.
Related
I am aware of a post that was made before this pertaining to the same topic, however it was in java.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
// create a new view
val adapterLayout = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false)
return ItemViewHolder(adapterLayout)
}
This was the definition the docs gave me, "Instantiates a layout XML file into its corresponding android.view.View objects." What does this mean?
Then there is the inflate method which the docs state "Inflate a new view hierarchy from the specified XML node."
Could someone explain to me in simpler terms what Layoutinflater and it's method inflater do?
TL;DR The "inflater" reads the XML layout file, which defined what View objects to create and where to put them, and actually does the View creation.
Some helpful terms:
View - Anything that inherits from the View class, e.g. TextView or Button
Layout - an arrangement of different View objects on the screen (this is what you see looking at the app)
XML Layout File - An XML text file that describes a layout - specifically what Views go in that layout and how to position them (e.g. activity_main.xml). The XML file is not the layout, it is a description of how to build the layout.
Inflater - A routine/object that takes an XML layout file, reads it, and actually creates the Layout (arrangement of View objects). Calling inflate on the inflater returns a View object that contains everything you defined in the XML file.
More Details
A screen you see in an Android app is a collection of "Views". These may be things like TextView, ConstraintLayout, EditText, Button, etc... - all different types of views (they all inherit from the View class).
When you build up a layout of those views you typically use an XML file to define what views to create and where to position those views. The XML file is not the view itself, it is just a description of how the views should be constructed and positioned.
The layout inflater takes that XML file and actually goes about building the views as you see them on the screen in the app. When you call inflate it reads all the data about the views from the XML file, creates instances of those views, and positions them in the parent view container based on what you told it to do in the XML file.
In the example code you showed (from a RecyclerView adapter) the XML file it is referring to is the one that describes how to arrange the views in a given row in the RecyclerView. Once the adapter "inflates" the view for that row, then the actual view objects (e.g. TextViews) have been created and positioned within that row.
The RecyclerView adapter will call this method multiple times, to "inflate" a new unique view instance for each displayed row. The "recycler" part of the RecyclerView means that it will try not to call this more than necessary, and will re-use the views on new rows where it can as you scroll.
Additional Reading
Official docs,
What does layout inflater in Android do?
What is layout inflater and how do I use it?
I'm having some problems when trying to remove the header from a listView. At first I use addHeaderView() to add it, but when I change to another layout I want it to disappear but removeHeaderView() doesn't work...
I also tried setting visibility to GONE and it doesn't disappear...
What can I do?
Thanks in advance
Try the approach mentioned below..
Android ListView#addHeaderView and ListView#addFooterView methods are strange: you have to add the header and footer Views before you set the ListView's adapter so the ListView can take the headers and footers into consideration -- you get an exception otherwise. Here we add a ProgressBar (spinner) as the headerView:
// spinner is a ProgressBar
listView.addHeaderView(spinner);
We'd like to be able to show and hide that spinner at will, but removing it outright is dangerous because we'd never be able to add it again without destroying the ListView -- remember, we can't addHeaderView after we've it's adapter:
listView.removeHeaderView(spinner); //dangerous!
So let's hide it! Turns out that's hard, too. Just hiding the spinner view itself results in an empty, but still visible, header area.
Now try to hide the spinner:
spinner.setVisibility(View.GONE);
Result: header area still visible with an ugly space:
The solution is to put the progress bar in a LinearLayout that wraps it's content, and hiding the content. That way the wrapping LinearLayout will collapse when its content is hidden, resulting in a headerView that is technically still present, but 0dip high:
<LinearLayout
xmlns:a="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<!-- simplified -->
<ProgressBar
android:id="#+id/spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
Then, set the layout as the header:
spinnerLayout = getLayoutInflater().inflate(R.layout.header_view_spinner, null);
listView.addHeaderView(spinnerLayout);
And when we need to hide it, hide the layout's content, not the layout:
spinnerLayout.findViewById(R.id.spinner).setVisibility(View.GONE);
Now the header disappears from view. No more ugly space at the top!
Most people don't like to use AddHeaderView, however I sometimes found it very convenient, to avoid modifying complex adapters, or if the headers are not very related to them.
With this easy trick you will be able to seamlessly remove/add headers:
I add an empty LinearLayout with orientation vertical, and height wrap_content, as the only Header View (let mListView be the target listView):
LinearLayout mCustomHeaders=new LinearLayout(context);
mCustomHeaders.setOrientation(LinearLayout.VERTICAL);
mListView.addHeaderView(mCustomHeaders);
mListView.setAdapter (.......)
Thenafter, I can add random stuff, anywhere, to the list as header, even when the list is full:
mCustomHeaders.add(myHeaderView1);
mCustomHeaders.add(myHeaderView2);
mCustomHeaders.add(myHeaderView3);
You can also delete all headers, anytime:
mCustomHeaders.removeAllViews(); // will erase all headers
You get the idea .... Hope it helps !
The problem is that you are always creating a new object when you do:
View headerView = someView
So the new view is not the same as the view already added as listView header, try this:
View headerView = inflater.inflate(R.layout.adapter_datatable_saleitem_header, null, false);
headerView.setTag(this.getClass().getSimpleName() + "header");
if (listView.getHeaderViewsCount() > 0) {
View oldView = listView.findViewWithTag(this.getClass().getSimpleName() + "header");
if (oldView != null) {
listView.removeHeaderView(oldView);
}
}
You can check if header count > 0 then remove the header and add it again.
its works fine for me.
View _headerView;
private void function HandleHeaderView(){
if(listView.getHeaderViewsCount() > 0)
{
listView.removeHeaderView(_headerView);
}
/* Add list view header */
_headerView = GetHeaderView();
listView.addHeaderView(_headerView);
}
private View GetHeaderView()
{
View header = getLayoutInflater().inflate(R.layout.header_layout, null);
// TODO: ...
return header
}
Where drawerLogoView is my headerview, here's what I do:
drawerLogoView = mInflater.inflate(R.layout.navigation_drawer_custom_layout, null);
mDrawerList.addHeaderView(drawerLogoView,null,false);
LinearLayout layOut = ((LinearLayout)drawerLogoView.findViewById(R.id.NavProfilePreviewLayout));
layOut.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, 0));
That way, it becomes invisible :D
To show it back, you can use this:
LinearLayout layOut = ((LinearLayout)drawerLogoView.findViewById(R.id.NavProfilePreviewLayout));
layOut.setLayoutParams(newRelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT));
If you are using addHeaderView(), you can't delete your header after that.
So, don't use addHeaderView(). Rather, create your own adapter that
blends your main roster of Views with your header. While my
MergeAdapter will not handle your specific case, you can use it to see
the concept of blending multiple row sources:
https://github.com/commonsguy/cwac-merge
I encountered this problem in a slightly disguised scenario: the ListView I was dealing with came from PreferenceFragment, and the header represents a PreferenceCategory. So, my freedom of setting up the ListView was severely limited. But there were two approaches (partly inspired by other answers on this page). One was to add a custom layout to my PreferenceCategory (using a class that extends android.preference.PreferenceCategory, see Custom PreferenceCategory Headings). But I found an easier workaround: for the first Preference in this PreferenceCategory, I override onCreateView():
#Override public View onCreateView(ViewGroup parent) {
parent.setTop(-parent.getChildAt(0).getTop());
return super.onCreateView(parent);
}
Short Story:
I have a layout "layout.xml", which gets replaced by another layout "success.xml" after a successful web request. Both layouts have an ImageView that provides the backgrounds to the layouts. These 2 backgrounds both need to be the same, and both are dependent on a user preference.
Longer Story: This all happens in a Fragmnet with an AsyncTask replacing the contentView with "success.xml" in onPostExecute after the web request. This happens as follows:
View view = getView();
view = null;
view = View.inflate(context, R.layout.success, null);
What I tried to do is give both ImageViews the following android:id="#+id/background_image" and then call
ImageView background = (ImageView)view.findViewById(R.id.background_image);
background.setImageResource(R.drawable.bg1);
This background-setting works for the initial view (layout.xml), but on trying to change to "success.xml", I get a NullPointException because background is null.
I've checked and the View's id is set to -1 while the original view's background_image id is set to something sensible and valid.
I've also tried setting the second view's background id like this: android:id="#id/background_image", i.e. without the '+', but still no luck.
The added complication is that it's not just 2 layouts, but about 5 that I need to do this for, so it would be really handy to recycle view id's.
Any help would be greatly appreciated.
Your code for replacing the fragment's view will not do what you want, the original view will remain the same as you change only a reference to that view and not the actual object.
To replace the view of the fragment with the new layout you could have another ViewGroup(for example a FrameLayout) in the basic layout (layout.xml) wrapping your current content(don't forget to give it an id) of layouts.xml(as I understand this is the basic layout). Then, when it's time to replace the layout you could simply do:
// remove the current content
((ViewGroup) getView().findViewById(R.id.yourWrapperLayout)).removeAllViews();
// add the new content
View.inflate(context, R.layout.success, ((ViewGroup) getView().findViewById(R.id.yourWrapperLayout)));
You could avoid adding an extra layout if, by any chance, all your five layouts have the same type for the root view(like a LinearLayout etc). In this case you would use the same code as above but you'll modify the other layouts file to use a merge tag. Also, you'll be looking for the id of the root in the layout.xml layout into which you'll add the content of the other files.
Then you could have the same ids, but you'll have to reinitialize any reference to the views(meaning that you'll have to search for the view again if you store a reference to the view(like a Button field in the fragment class)).
I have an android app which asks a question followed by x number of options.
Each option contains a textview, ImageView and a radio button.
The value of x (i.e. the number of options) is not constant. I want to dynamically add UI content to satisfy this requirement.
At the moment I have written the code in the layout's xml to display a maximum of 4 options. If number of options is 2 I hide the options 3 and 4 using something like
tvoption1.setVisibility(View.GONE);
tvoption2.setVisibility(View.GONE);
However this is not very scalable. Can anyone tell me how to add options for java dynamically. Or is there a better approach?
A View can be added at runtime by using the inflater like this:
LinearLayout linearLayout = (LinearLayout)inflater.inflate(R.layout.news_categories_item, null);
TextView categoryValueTextView = (TextView)linearLayout.findViewById(R.id.news_category_item_value);
mMainLinearLayout.addView(categoryValueTextView);
In this example, a LinearLayout containing a TextView is inflated. A reference to the constituent TextView is then obtained, and the TextView is dynamically added (at runtime) to the main linear layout (mMainLinearLayout).
The inflater object may be obtained in an Activity by using getLayoutInflater().
create your row layout separately, from the main xml
Get LayoutInflater service from context:
LayoutInflater inflater=(LayoutInflater)getSystemService(LAYOUT_INFLATE_SERVICE);
use following method to addview to main xml, I assume you have parent layout llParent in xml and you want to add items in this llPaent, from list list.
for(int i=0;i<list.size();i++)
{
LinearLayout llView=(LinearLayout)inflater.inflate(R.layout.row);
//get view id and set values
TextView txt=(TextView)llView.findViewById(R.id.text);
}
A ListView is a good view for displaying several similar items. Here is a tutorial (Other views with adapters are good too, such as GridView or Gallery).
You will probably want to create your own adapter for the list, so you can display all three views (checkbox, image and text) as one item, but there are lots of examples on that available on the net as well as here on SO.
I have a complex empty view in a layout, with an icon, text, button, etc.
It is easy to select a view within my layout.xml to use when the listview is empty, similar to
getListView().setEmptyView(findViewById(R.id.empty));
This code sets the empty view works just fine when it resides in the layout.xml file.
Now I want to refactor this view into its own empty.xml layout file, and have coded it similar to the following:
// Setup the empty layout.xml
LayoutInflater vi = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View vlEmpty = vi.inflate(R.layout.empty, null);
// Find the empty layout view
vEmpty = vlEmpty.findViewById(R.id.llEmpty);
vEmpty.setOnClickListener(ocl);
// Find the ListView
vListView = (ListView) findViewById(R.id.lvWords);
vListView.setEmptyView(vEmpty);
The problem is that the details within llEmpty never show up; The exact same layout and view works withing the main layout, just not refactored into its own xml file.
Has anyone got something like this to work?
You might need to pass the proper context to the inflater:
vListView = (ListView) findViewById(R.id.lvWords);
View vlEmpty = vi.inflate(R.layout.empty, (ViewGroup)vListView.getParent());
which (should) make them both live in the same root view. It may be sufficient to just pass the root view of the parent activity.
Let me know if that works.
I doubt that setEmptyView() automatically makes the supplied View a child of any container in your activity.
Personally, I'd just use the <include> element rather than inflating it separately. But, if you really want to inflate it separately, the answer that Femi posted while I was writing this may work, depending on what the ListView's parent is.