Okay, bear with me this is a very odd & complex issue and I'm having a hard time explaining it. I'm using Xamarin.Android (Monodroid) although the source of the issue is likely an android thing.
I have an Activity which pages through Fragments manually by adding and removing them.
next = Fragments[nextIndex];
FragmentTransaction ftx = FragmentManager.BeginTransaction ();
ftx.SetTransition (FragmentTransit.FragmentFade);
ftx.SetCustomAnimations (Resource.Animator.slide_in_right, Resource.Animator.slide_out_left);
ftx.Remove (CurrentFragment);
ftx.Add (Resource.Id.fragmentContainer, next, next.Tag);
ftx.Commit();
The Fragments have a LinearLayout, which is populated with Row Views at run-time. (ListViews introduced too many focus issues with validating text entry).
// Manual ListView
this.Layout.RemoveAllViewsInLayout ();
for (int n = 0; n < Adapter.Count; n++)
{
View view = Adapter.GetView(n,null,Layout);
if (view != null) {
if (view.Parent != Layout) {
Layout.AddView(view);
}
}
}
Some of these Row Views have within them a GridView. (The gridview does not scroll)
TextView title = view.FindViewById<TextView>(Resource.Id.titleView);
GridView grid = view.FindViewById<GridView>(Resource.Id.gridView);
if (grid.Adapter == null) {
InlineAdapter adapter = new InlineAdapter(view.Context, list, this.Adapter);
// Set Grid's parameters
grid.Adapter = adapter;
grid.OnItemClickListener = adapter;
grid.SetNumColumns(adapter.GetColumns(((Activity)view.Context).WindowManager));
grid.StretchMode = StretchMode.StretchColumnWidth;
}
grid.ClearChoices();
// Get Current Value(s)
if (list.Mode == ListMode.SingleSelection)
{
grid.ChoiceMode = ChoiceMode.Single;
for (int b = 0; b < list.Selections.AllItems.Count; b++)
{
grid.SetItemChecked(b, false);
}
grid.SetItemChecked(list.GetSelectedIndex(DataStore), true);
}
The grid views have in them CheckedTextViews (either single or multiple choice).
// From "InlineAdapter"
public override View GetView(int position, View convertView, ViewGroup parent)
{
GridView grid = ((GridView)parent);
int layout = (list.Mode == ListMode.MultiSelection) ? Resource.Layout.RowListInclusive : Resource.Layout.RowListExclusive;
CheckedTextView view = (CheckedTextView)convertView;
if (view == null)
{
view = (CheckedTextView)inflator.Inflate(layout, parent, false);
}
view.Text = items[position];
// Calabash
view.ContentDescription = list.ContentDescription + "." + Selections.ContentDescription(items [position]);
return view;
}
When a fragment is presented the first time (before it is removed) everything performs as expected.
When a fragment is presented the second and subsequent times (after it is removed and re-added) only the last grid view accepts user input. In addition, whatever is selected on this last grid view ALL of the grid views select. (e.g. if the last one selects choice 2, then all of the grid views change to selecting choice 2)
I have:
Verified that the underlying data is correct
Verified using .GetHash() that all of the CheckedTextViews, GridViews, and Adapters are unique for their given rows.
Verified that touches propagate to the correct CheckedTextViews, and modify the correct data.
Verified that NotifyDataSetChanged() is called for the correct GridView.
I am personally stumped to gump on this one.
Related
I have a ListView that keeps track of amount paid, if user makes a payment twice using the same type of payment then those two entries on the List should be merged together as one and amount added.
Payment 1 - $50 - Check
Payment 2 - $100 - Check
The above looks like so in the ListView:
$150 - Check
$50 - Check
So the amounts are indeed being added but my logic to remove the row holding the $50 amount does not work... Not quite sure why but the point is to merge them together and remove that unnecessary row holding the $50.
Here is the relevant code from my update method and the getView method from a Adapter class that extends ArrayAdapter<ClassObject>.
getView()
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
LayoutInflater vi;
vi = LayoutInflater.from(getContext());
v = vi.inflate(R.layout.fragment_payscreen_tab2_listgrid_row, null);
}
//Get item from list
InvoiceListModel item = this.getItem(position);
if (item != null) {
TextView txtAmount = (TextView)v.findViewById(R.id.PayScreen_Tab2_Columns_txtAmount);
TextView txtPaidBy = (TextView)v.findViewById(R.id.PayScreen_Tab2_Columns_txtPaidBy);
txtPaidBy.setText(item.paidBy);
txtAmount.setText(item.amount);
}
return v;
}
Adapter custom update method
public boolean update(InvoiceListModel object){
for(int x=0; x< uiListViewCollectionData.size(); x++){
InvoiceListModel tempObj = uiListViewCollectionData.get(x);
if(tempObj != null &&
tempObj.paidBy.equals(object.paidBy)){
//Set the data on the temp object
tempObj.amount = uiListViewCollectionData.get(x).amount.plus(object.amount);
//Remove the old outdated object from datasource
uiListViewCollectionData.remove(x);
this.notifyDataSetChanged();
//Add the new model containing the new amount back to list
uiListViewCollectionData.add(tempObj);
this.notifyDataSetChanged();
return true;
}
}
return false;
}
I'm pretty sure notifyDataSetChanged() is async and this is causing the next line to quickly execute. Is it possible to directly render the ListView UI component right then and there as soon as I need it?
How do you pass the data to the list?
You should 'repack' your old_data and pass new_data to the adapter. Something like:
new_data = repack (old_data);
myAdapter = new MyAdapter(new_data);
So, if old_data have 50 items, new data will have only 35 (as example), and you present this 35 items in the list. Merging logic should be out of the getView().
I have a list in my app, and it's implemented as an AbsListView, so that when we are working on a smaller screen (phone) it's a List and when we are on a larger screen (tablet) it's a Grid.
All works well.
Now I want to add in a header item which is completely different from the regular items on my list - it has a completely different xml file. It's always the first item in the list.
I've added in code to my Adapter class like so:
public override View GetView(int position, View convertView, ViewGroup parent)
{
SavedInfo subViews = null;
var rowView = convertView;
var channel = items[position];
// don't want to reuse if our previous view was a header
if (rowView?.Tag != null)
{
subViews = rowView.Tag as SavedInfo;
}
// try to put in a different view if there is a header shown
// special id value for header is -1
if (position == 0)
{
subViews = null;
}
if (subViews == null)
{
if (position == 0)
{
rowView = context.Activity.LayoutInflater.Inflate(Resource.Layout.header_layout, null);
// Do setup stuff with this layout
rowView.Tag = null;
return rowView;
}
rowView = context.Activity.LayoutInflater.Inflate(cellLayout, null);
// Do stuff with regular layout, take savedInfo from Tag
rowView.Tag = subViews;
}
// other adjustments to the regular layout
return rowView;
}
So that's fine in a regular list - I have one header item and lots of regular items. However, when I switch to GridView (which uses the same adapter) my "header" item is now just the first cell in the grid.
What I want it to be is more or less the same as it is in the list view - a single column which fills the width of the screen, then followed by the regular grid. The point is I want an item which fills all the columns across, but scrolls up with the grid. Is there a way to do this? I understand I might need to replace with some sort of custom Adapter View. Does anyone have an example of code doing a grid with one item filling multiple grid columns?
Thanks
Turns out this is not possible - the recommended way to put in a header is to use a CoordinatorLayout and a CollapsingToolbarLayout while changing the ListView to a RecyclerView
The scenario
I'm trying the create something akin to the featured page on Google Play Store. But instead of showing three items for a category I'm allowing it show any number of items in a two column staggered grid view fashion.
So each list item has a header that has a title and a description followed by a custom view (lets call this SVG, as in Staggered View Group) that shows some number of children views in a staggered grid view fashion.
I have a class called FeaturedItems that hold the data for a row in the featured list. Here is an extract:
public class FeaturedItems {
private String mName;
private String mDescription;
private ArrayList<Object> mList;
public FeaturedItems(String name, String description, Object... items) {
mName = name;
mDescription = description;
mList = new ArrayList<Object>();
for (int i = 0; i < items.length; i++) {
mList.add(items[i]);
}
}
public int getItemCount() {
return mList.size();
}
public Object getItem(int position) {
return mList.get(position);
}
public String getFeatureName() {
return mName;
}
public String getFeatureDescription() {
return mDescription;
}
}
The FeaturedListAdapter binds the data with the views in the getView() method of the adapter. The getView() method is as follows:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
FeaturedItems items = getItem(position);
if (convertView == null) {
LayoutInflater infalInflater = (LayoutInflater) this.mContext
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = infalInflater.inflate(mResource, null);
holder = new ViewHolder();
holder.title = (TextView) convertView.findViewById(R.id.list_item_shop_featured_title);
holder.description = (TextView) convertView.findViewById(R.id.list_item_shop_featured_description);
holder.svg = (StaggeredViewGroup) convertView.findViewById(R.id.list_item_shop_featured_staggered_view);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.title.setText(items.getFeatureName());
holder.description.setText(items.getFeatureDescription());
// HELP NEEDED HERE
// THE FOLLOWING PART IS VERY INEFFICIENT
holder.svg.removeAllViews();
for (int i = 0; i < items.getItemCount(); i++) {
FeaturedItem item = new FeaturedItem(mContext, items.getItem(i));
item.setOnShopActionsListener((ShopActions) mContext);
holder.svg.addView(item);
}
return convertView;
}
The problem
In the getView() method, each time a view is returned, it removes all the child views in the SVG and instantiates new views called FeaturedItem that are then added to the SVG. Even if the SVG in a particular row, say first row, was populated, when the user scrolls back to it from the bottom, the getView() method will remove all the children views in the SVG and instantiates new views to be populated with.
The inefficiency here is very obvious, and the list view animation skips frames when scrolled quite often.
I can't just reuse the convertView here because it shows the wrong featured items in the StaggeredViewGroup. Therefore I have to remove all children from the StaggeredViewGroup and instantiate and add the views that are relevant to the current position.
The question
Is there a way around this problem? Or are there some alternative approaches to creating a page similar to the Google Play Store featured page, but with each row having different number of featured items thus having its unique height?
There should be an easy way to improve this solution. Just reuse the svg children that are already present, add new ones if they are not enough, and then remove any surplus ones.
For example (in semi-pseudocode, method names may not be exact):
for (int i = 0; i < items.getItemCount(); i++)
{
if (i < svg.getChildCount())
{
FeaturedItem item = i.getChildAt(i);
// This item might've been set to invisible the previous time
// (see below). Ensure it's visible.
item.setVisibility(View.VISIBLE);
// reuse the featuredItem view here, e.g.
item.setItem(items.getItem(i));
}
else
{
// Add one more item
FeaturedItem item = new FeaturedItem(mContext, items.getItem(i));
...
holder.svg.addView(item);
}
}
// hide surplus item views.
for (int i = items.getItemCount(); i < svg.getChildCount(); i++)
svg.getChildAt(i).setVisibility(View.GONE);
/**
as an alternative to this last part, you could delete these surplus views
instead of hiding them -- but it's probably wasteful, since they may need
to be recreated later
while (svg.getChildCount() > items.getItemCount())
svg.removeChildView(svg.getChildCount() - 1);
**/
I have a ListView that's being populated by an ArrayAdapter:
someListView.setAdapter(adapter);
Each element in the adapter is inflated using the same layout.xml. Now I want to add an element of a different type (inflated using a different layout file) to the beginning of the ListView.
What I want to achieve is, to have a special element on top of all other elements in the list view, but also scrolls with the list (exits the screen from top if the user scrolls down).
I've tried to add the new element to the array but it's a different type so that won't work.
I've tried to insert a dummy element to the array at position 0, and modify the adapter's getView() so that if (position == 0) return myUniqueView, but that screwed up the entire list view somehow: items not showing, stuff jumping all over the place, huge gaps between elements, etc.
I start to think the best practice of achieving what I want, is not through editing the array adapter. But I don't know how to do it properly.
You don't need anything special to do what you ask. Android already provides that behavior built in to every ListView. Just call:
mListView.addHeaderView(viewToAdd);
That's it.
ListView Headers API
Tutorial
Do't know exactly but it might usefull
https://github.com/chrisjenx/ParallaxScrollView
In your adapter add a check on the position
private static final int LAYOUT_CONFIG_HEADER = 0;
private static final int LAYOUT_CONFIG_ITEMS = 1;
int layoutType;
#Override
public int getItemViewType(int position) {
if (position== 0){
layoutType = LAYOUT_CONFIG_HEADER;
} else {
layoutType = LAYOUT_CONFIG_ITEMS;
}
return layoutType;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
LayoutInflater inflater = null;
int layoutType = getItemViewType(position);
if (row == null) {
if (layoutType == LAYOUT_CONFIG_HEADER) {
//inflate layout header
}
} else {
//inflate layout of others rows
}
}
After I have gotten the data for a single row of a ListView, I want to update that single row.
Currently I am using notifyDataSetChanged(); but that makes the View react very slowly. Are there any other solutions?
As Romain Guy explained a while back during the Google I/O session, the most efficient way to only update one view in a list view is something like the following (this one update the whole view data):
ListView list = getListView();
int start = list.getFirstVisiblePosition();
for(int i=start, j=list.getLastVisiblePosition();i<=j;i++)
if(target==list.getItemAtPosition(i)){
View view = list.getChildAt(i-start);
list.getAdapter().getView(i, view, list);
break;
}
Assuming target is one item of the adapter.
This code retrieve the ListView, then browse the currently shown views, compare the target item you are looking for with each displayed view items, and if your target is among those, get the enclosing view and execute the adapter getView on that view to refresh the display.
As a side note invalidate doesn't work like some people expect and will not refresh the view like getView does, notifyDataSetChanged will rebuild the whole list and end up calling getview for every displayed items and invalidateViews will also affect a bunch.
One last thing, one can also get extra performance if he only needs to change a child of a row view and not the whole row like getView does. In that case, the following code can replace list.getAdapter().getView(i, view, list); (example to change a TextView text):
((TextView)view.findViewById(R.id.myid)).setText("some new text");
In code we trust.
One option is to manipulate the ListView directly. First check if the index of the updated row is between getFirstVisiblePosition() and getLastVisiblePosition(), these two give you the first and last positions in the adapter that are visible on the screen. Then you can get the row View with getChildAt(int index) and change it.
This simpler method works well for me, and you only need to know the position index to get ahold of the view:
// mListView is an instance variable
private void updateItemAtPosition(int position) {
int visiblePosition = mListView.getFirstVisiblePosition();
View view = mListView.getChildAt(position - visiblePosition);
mListView.getAdapter().getView(position, view, mListView);
}
The following code worked for me. Note when calling GetChild() you have to offset by the first item in the list since its relative to that.
int iFirst = getFirstVisiblePosition();
int iLast = getLastVisiblePosition();
if ( indexToChange >= numberOfRowsInSection() ) {
Log.i( "MyApp", "Invalid index. Row Count = " + numberOfRowsInSection() );
}
else {
if ( ( index >= iFirst ) && ( index <= iLast ) ) {
// get the view at the position being updated - need to adjust index based on first in the list
View vw = getChildAt( sysvar_index - iFirst );
if ( null != vw ) {
// get the text view for the view
TextView tv = (TextView) vw.findViewById(com.android.myapp.R.id.scrollingListRowTextView );
if ( tv != null ) {
// update the text, invalidation seems to be automatic
tv.setText( "Item = " + myAppGetItem( index ) + ". Index = " + index + ". First = " + iFirst + ". Last = " + iLast );
}
}
}
}
There is another much more efficient thing you can do, if it fits your use-case that is.
If you are changing the state and can somehow call the proper (by knowing the position) mListView.getAdapter().getView() it will the most efficient of all.
I can demonstrate a really easy way to do it, by creating an anonymous inner class in my ListAdapter.getView() class. In this example I have a TextView showing a text "new" and that view is set to GONE when the list item is clicked:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// assign the view we are converting to a local variable
View view = convertView;
Object quotation = getItem(position);
// first check to see if the view is null. if so, we have to inflate it.
if (view == null)
view = mInflater.inflate(R.layout.list_item_quotation, parent, false);
final TextView newTextView = (TextView) view.findViewById(R.id.newTextView);
view.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (mCallbacks != null)
mCallbacks.onItemSelected(quotation.id);
if (!quotation.isRead()) {
servicesSingleton.setQuotationStatusReadRequest(quotation.id);
quotation.setStatusRead();
newTextView.setVisibility(View.GONE);
}
}
});
if(quotation.isRead())
newTextView.setVisibility(View.GONE);
else
newTextView.setVisibility(View.VISIBLE);
return view;
}
The framework automatically uses the correct position and you do have to worry about fetching it again before calling getView.
For me below line worked:
Arraylist.set(position,new ModelClass(value));
Adapter.notifyDataSetChanged();
Just for the record, did anyone consider the 'View view' on the override method ?
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//Update the selected item
((TextView)view.findViewById(R.id.cardText2)).setText("done!");
}