I want to add a footer view for GridView.
I find in the documentation GridView has 2 inherited addView(View child) method.
From class android.widgetAdapterView
void addView(View child)
This method is not supported and throws an UnsupportedOperationException when called.
and
From class android.view.ViewGroup
void addView(View child)
Adds a child view.
It seems I should use the latter. But how can I call this particular inherited method?
You don't. It overwrites the original with a UnsupportedOperationException because it's.. well.. not supported.
You should be editing the adapter instead. Depending on your implementation, this will look different. But, you just have to add more data to the adapter, and call .notifyDataSetChanged() on the adapter, and your GridView will add the view by itself.
A footer view should either be a separate View after your GridView, or you will have to maintain its position in the adapter's list to always be last whenever you add new items.
Providing an example of Erics solution, the adapter could maintain two extra members for tracking the
position of the "footer", and its event handler:
class ImageViewGridAdapter : ArrayAdapter<int>
{
private readonly List<int> images;
public int EventHandlerPosition { get; set; }
public EventHandler AddNewImageEventHandler { get; set; }
public ImageViewGridAdapter(Context context, int textViewResourceId, List<int> images)
: base(context, textViewResourceId, images)
{
this.images = images;
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
ImageView v = (ImageView)convertView;
if (v == null)
{
LayoutInflater li = (LayoutInflater)this.Context.GetSystemService(Context.LayoutInflaterService);
v = (ImageView)li.Inflate(Resource.Layout.GridItem_Image, null);
// ** Need to assign event handler in here, since GetView
// is called an arbitrary # of times, and the += incrementor
// will result in multiple event fires
// Technique 1 - More flexisble, more maintenance ////////////////////
if (position == EventHandlerPosition)
v.Click += AddNewImageEventHandler;
// Technique 2 - less flexible, less maintenance /////////////////////
if (position == images.Count)
v.Click += AddNewImageEventHandler;
}
if (images[position] != null)
{
v.SetBackgroundResource(images[position]);
}
return v;
}
}
Then, before assigning the adapter to the grid view, just assign those values (position doesn't have to be at end, but for a footer it should be):
List<int> images = new List<int> {
Resource.Drawable.image1, Resource.Drawable.image2, Resource.Drawable.image_footer
};
ImageViewGridAdapter recordAttachmentsAdapter = new ImageViewGridAdapter(Activity, 0, images);
recordAttachmentsAdapter.EventHandlerPosition = images.Count;
recordAttachmentsAdapter.AddNewImageEventHandler += NewAttachmentClickHandler;
_recordAttachmentsGrid.Adapter = recordAttachmentsAdapter;
Related
I'm using listview custom adapter which with row click i'm changing row color. But when i'm scrolling bot and up again it doesnt have the right position.
It changes color in other rows...
public override View GetView(int position, View convertView, ViewGroup parent)
{
DataViewHolder holder = null;
if (convertView == null)
{
convertView = LayoutInflater.From(mContext).Inflate(Resource.Layout.TableItems, null, false);
holder = new DataViewHolder();
holder.txtDescription = convertView.FindViewById<TextView>(Resource.Id.txtDescription);
holder.txtDescription.Click += delegate
{
holder.txtDescription.SetBackgroundColor(Color.Red);
};
convertView.Tag = holder;
}
else
{
holder = convertView.Tag as DataViewHolder;
}
holder.txtDescription.Text = mitems[position].Description;
return convertView;
}
public class DataViewHolder : Java.Lang.Object
{
public TextView txtDescription { get; set; }
}
It looks like it doesnt keep in memory specific row situation.
Don't change the color in the click handler directly, instead change the data from which the adapter draws from and use that to change the color when GetView is called again.
ListView recycles the views it uses to optimize scrolling, instead it just expects the view to represent the data. If you change a color of one view directly, the view then gets recycled and you'll see "another view" (another part of the data) with a different background color.
So in summary: give each data point a color attribute and use that to set the color of each view in GetView, change the data and notify the adapter about the changes to the data.
Edit
I've never used Xamarin but maybe something like this would work
public override View GetView(int position, View convertView, ViewGroup parent)
{
DataViewHolder holder = null;
if (convertView == null)
{
convertView = LayoutInflater.From(mContext).Inflate(Resource.Layout.TableItems, null, false);
holder = new DataViewHolder();
holder.txtDescription = convertView.FindViewById<TextView>(Resource.Id.txtDescription);
holder.txtDescription.Click += delegate
{
// instead of setting the color directly here, just modify the data
(holder.txtDescription.Tag as ItemType).ItemColor = Color.Red
notifyDataSetChanged();
};
convertView.Tag = holder;
}
else
{
holder = convertView.Tag as DataViewHolder;
}
holder.txtDescription.Text = mitems[position].Description;
holder.txtDescription.Tag = mitems[position]; // this so that the click handler knows which item to modify
holder.txtDescription.SetBackgroundColor(mitems[position].ItemColor);
return convertView;
}
public class DataViewHolder : Java.Lang.Object
{
public TextView txtDescription { get; set; }
}
ListView will reuse the item layout, you can use List and View.Tag to avoid the problem caused by reusing.
I have posted my demo on github.
I am working on a project where I have a let's say 5x5 grid of TextViews and I want to check if an entire row or column has equal elements. I am using an Adapter class to inflate my gridview with simply one textview element. Here is the code that I have tried but I cannot seem to make it work:
final int size = gridView.getCount(); //25
int temp = 0;
for (int i = 0; i < size; i++) {
ViewGroup gridChild = (ViewGroup) gridView.getChildAt(i);
childSize = gridChild.getChildCount();
for (int j = 0; j < childSize; j++) {
if (gridChild.getChildAt(j) instanceof TextView &&
((TextView) gridChild.getChildAt(j)).getText().toString().equals("x")) {
temp++;
}
The thing is when i tried to debug, debugger showed null values for childSize variable and could not properly get the value from getChildAt. Basically, what I am trying to do is get inside the if statement. Also this is the first time I am working with ViewGroup calss, and the methods that I call. Any help would be appreciated.
Edit:I am looking for a way to do this outside the getView method in the adapter class and not in a onClick method as well. (Code sample answers would be highly appreciated). Also, the getChildAt method call returns null so the code I have shown would not work because I am assigning a null value to the gridChild.
This is the onClick that I use for the TextViews:
`
public void numberFill(View view) {
if (((TextView) view).getText().toString().isEmpty()) {
((TextView) view).setText(String.valueOf(numbCounter + 1));
numbCounter++;
}
else if (!((TextView) view).getText().toString().isEmpty() && numbCounter >= 16) {
((TextView) view).setText("x");
}
}
This is my adapter class:
public class GridAdapter extends BaseAdapter {
private final Context mContext;
private String[] numbers;
public GridAdapter(Context context, String[] numbers) {
this.mContext = context;
this.numbers = numbers;
}
#Override
public int getCount() {
return numbers.length;
}
#Override
public long getItemId(int position) {
return 0;
}
#Override
public Object getItem(int position) {
return numbers[position];
//return null;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater)
mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View gridView;
if (convertView == null) {
gridView = new View(mContext);
gridView = inflater.inflate(R.layout.textview_layout, null);
TextView textView = (TextView) gridView.findViewById(R.id.cell);
textView.setText(numbers[position]);
} else {
gridView = (View) convertView;
}
return gridView;
}
}
numberFill reworked:
public void numberFill(View view) {
int index = (Integer) view.getTag();
if (numbers[index].toString().isEmpty()) {
numbers[index] = String.valueOf(numbCounter + 1);
numbCounter++;
}
else if (!numbers[index].toString().isEmpty() && numbCounter >= 25) {
numbers[index] = "x";
}
gridAdapter.notifyDataSetChanged();
}
`
When using an AdapterView – such as your GridView – you generally don't want to directly access and manipulate its child Views outside of its Adapter. Instead, the dataset backing the Adapter should be updated, and the GridView then refreshed.
In your case, you presumably have a setup similar to this in your Activity:
private GridAdapter gridAdapter;
private String[] numbers;
#Override
protected void onCreate(Bundle savedInstanceState) {
...
numbers = new String[25];
gridAdapter = new GridAdapter(this, numbers);
}
Here, the numbers array is what you want to directly modify, rather than the text on the GridView's child TextViews. That array is then easily iterated over to do your row and column value checks.
Since the array will be modified in the Activity, we need a way to pass the clicked TextView's position in the Adapter to the Activity's click method, as we'll need it to access the correct array element. For this, we can utilize the tag property available on all View's, via the setTag() and getTag() methods. For example, in GridAdapter's getView() method:
...
TextView textView = (TextView) gridView.findViewById(R.id.cell);
textView.setText(numbers[position]);
textView.setTag(position);
...
In the click method, the position can be easily retrieved with getTag(), and used as the index to get the clicked TextView's text from the numbers array. You can then do the necessary processing or calculation with that text, set the modified value back to the array element, and trigger a refresh on the Adapter.
public void numberFill(View view) {
int index = (Integer) view.getTag();
// Do your processing with numbers[index]
numbers[index] = "new value";
gridAdapter.notifyDataSetChanged();
}
The notifyDataSetChanged() call will cause the GridView to update its children, and your new value will be set in the appropriate TextView. The numbers array now also has the current values, and is readily available in the Activity to perform the necessary checks there.
In the app I've been working on, I have a custom class DeviceListAdapter extending BaseAdapter which gets passed to my ListView. In my DeviceListAdapter class, I keep my own ArrayList<Device> which I use to generate the list's views with View getView(... ). Whenever the app causes a change in the data, I use custom methods in DeviceListAdapter to update the ArrayList<Device> to reflect the changes. I've used the debugger and many print statements to check that the data does get changed how I expect it to, adding and removing Device objects as specified. However, after each change to the data I also call notifyDataSetChanged(), but on the UI none of the elements get updated. In the debugger, I found that after calling notifyDataSetChanged(), the getView(... ) method was not being called, which explains why the ListView wasn't being redrawn. To figure out why, I used the debugger's "step into" function to trace where the program execution went into the android framework since I have the SDK sources downloaded. What I found was very interesting. The path of execution went like this:
DeviceListAdapter.notifyDataSetChanged()
BaseAdapter.notifyDataSetChanged()
DataSetObservable.notifyChanged()
AbsListView.onInvalidated()
Rather calling the onChanged() method, it jumped tracks and executed the onInvalidated() method once it reached AbsListView. Initially I thought this was an error with the debugger perhaps reading the wrong line number, but I restarted my Android Studio as well as totally uninstalled and reinstalled the app, but the result is the same. Can anybody tell me if this is legitimately a problem with Android's framework or if the debugger is unreliable for tracing execution outside of my own project files?
More on my implementation of notifyDataSetChanged()... I created the local method to override BaseAdapter's notifyDataSetChanged() so that I could set a boolean flag mForceRedraw inside of my DeviceListAdapter as to whether I should force redraw my list entries. In the getView(... ) method, I typically check if the second parameter, View convertView is null, if it is then I redraw the view and if not then I pass convertView through and return it. However, when 'mForceRedraw' is true, I never return convertView, I explicitly redraw the view. The problem that arises is caused by my earlier concern, which is that getView() is not called after I execute notifyDataSetChanged().
EDIT: Here's a code snippet of my DeviceListAdapter:
/**
* Serves data about current Device data to the mDeviceListView. Manages the dynamic and
* persistent storage of the configured Devices and constructs views of each individual
* list item for placement in the list.
*/
private class DeviceListAdapter extends BaseAdapter {
private boolean mForceRedraw = false;
/**
* Dynamic array that keeps track of all devices currently being managed.
* This is held in memory and is readily accessible so that system calls
* requesting View updates can be satisfied quickly.
*/
private List<Device> mDeviceEntries;
private Context mContext;
public DeviceListAdapter(Context context) {
this.mContext = context;
this.mDeviceEntries = new ArrayList<>();
populateFromStorage();
}
/**
* Inserts the given device into storage and notifies the mDeviceListView of a data update.
* #param newDevice The device to add to memory.
*/
public void put(Device newDevice) {
Preconditions.checkNotNull(newDevice);
boolean flagUpdatedExisting = false;
for (Device device : mDeviceEntries) {
if (newDevice.isVersionOf(device)) {
int index = mDeviceEntries.indexOf(device);
if(index != -1) {
mDeviceEntries.set(index, newDevice);
flagUpdatedExisting = true;
break;
} else {
throw new IllegalStateException();
}
}
//If an existing device was not updated, then this is a new device, add it to the list
if (!flagUpdatedExisting) {
mDeviceEntries.add(newDevice);
}
TECDataAdapter.setDevices(mDeviceEntries);
notifyDataSetChanged();
}
/**
* If the given device exists in storage, delete it and remove it from the mDeviceListView.
* #param device
*/
public void delete(Device device) {
Preconditions.checkNotNull(device);
//Remove device from mDeviceEntries
Iterator iterator = mDeviceEntries.iterator();
while(iterator.hasNext()) {
Device d = (Device) iterator.next();
if(device.isVersionOf(d)) {
iterator.remove();
}
}
TECDataAdapter.setDevices(mDeviceEntries);
notifyDataSetChanged();
}
/**
* Retrieves Device entries from persistent storage and loads them into the dynamic
* array responsible for displaying the entries in the listView.
*/
public void populateFromStorage() {
List<Device> temp = Preconditions.checkNotNull(TECDataAdapter.getDevices());
mDeviceEntries = temp;
notifyDataSetChanged();
}
public int getCount() {
if (mDeviceEntries != null) {
return mDeviceEntries.size();
}
return 0;
}
public Object getItem(int position) {
return mDeviceEntries.get(position);
}
public long getItemId(int position) {
return position;
}
public View getView(final int position, View convertView, ViewGroup parent) {
LinearLayout view;
if (convertView == null || mForceRedraw) //Regenerate the view
{
/* Draws my views */
} else //Reuse the view
{
view = (LinearLayout) convertView;
}
return view;
}
#Override
public void notifyDataSetChanged() {
mForceRedraw = true;
super.notifyDataSetChanged();
mForceRedraw = false;
}
}
You are in the adapter and calling notify dataset changed.This would ideally not even be needed.Because you are modifying the dataset which is used internally by your adapter.The getView method of your adapter will always be called whenever a view needs to be rendered.
The convertView approach is to solely recycle a view(not the data).It merely provides you an alternative to the expensive process of view inflation.
So what your code should be :
public View getView(final int position, View convertView, ViewGroup parent) {
LinearLayout view;
if (convertView == null) //Regenerate the view
{
/* inflate Draws my views */
} else
{
view = (LinearLayout) convertView;
}
//repopulate this view with the data that needs to appear at this position using getItem(position)
return view;
}
There are many bugs with notifyDataSetChanged() and usually they appear if you try doing some complex work with your list data.
Mostly, it is because the method is lazy and can't distinguish changes, so to avoid this problem, test your code with this scenario:
delete changing rows
call notifyDataSetChanged()
add changed rows at their indexes
again call notifyDataSetChanged()
and, tell me if it did'nt solve your problem.
Edit: After adapter code is put, I saw the flaw in your code.
Sorry for late the response:
convertView is the view which you had populated before after initializing it.
When in method getView() you get an instance of convertView, you must populate it before returning it.
so to be clear, do something like this:
public View getView(final int position, View convertView, ViewGroup parent) {
View view;
if (convertView == null) //Regenerate the view
{
/* Instantiate your view */
} else {
view = convertView;
}
// populate the elements in view like EditText, TextView, ImageView and etc
return view;
}
I have an arraylist and I want to update specific item.I am adding data to list with this line:
randomsList.add(new Random(result.get(i).getAsJsonObject(),0));
This is adding datas to 0,1,2,3,4... locations so when I try to update an item I don't know which object is where.
I am updating data with this line:
randomsList.set(position,new Random(user,1));
I think if I use the custom numbers for location I can update specific item.My prototype:
randomsList.add({USER_ID},new Random(result.get(i).getAsJsonObject(),0));
And if I want to update it then I use this line:
randomsList.set({USER_ID},new Random(user,1));
Is this a good approach ? If your answer is no,how should be ?
P.S. : I am using this arraylist with an adapter
As #itachiuchiha mentions, you should use a Map. The "custom numbers" you mention are your key (integers), and the value is the Random object.
As an aside, in response to your comment, below is an example of an Android Adapter that uses a Map as the underlying datasource.
public class RandomsAdapter extends BaseAdapter {
private Map<Integer, Random> randoms;
public RandomsAdapter(Map<Integer, Random> randoms) {
this.randoms = randoms;
}
public void updateRandoms(Map<Integer, Random> randoms) {
this.randoms = randoms;
notifyDataSetChanged();
}
#Override
public int getCount() {
return randoms.size();
}
#Override
public Random getItem(int position) {
return randoms.get(position);
}
#Override
public long getItemId(int position) {
// we won't support stable IDs for this example
return 0;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
view = createNewView();
}
update(view, songs.get(position));
return view;
}
private View createNewView() {
// TODO: create a new instance of your view and return it
return null;
}
private void update(View view, Random random) {
// TODO: update the rest of the view
}
}
Note the updateRandoms(Map<Integer, Random> randoms) method.
While you could expose a method in the adapter to update a single entry in the Map, it shouldn't be the responsibility of the Adapter to handle modifications to the map. I prefer passing the entire map again - it could still be a reference to the same object, but the Adapter doesn't know or care; it just knows: "my underlying datasource has been changed/modified, I need to tell my observers that they should refresh their views by calling notifyDataSetChanged()".
Alternatively, you could call notifyDataSetChanged() on the adapter externally when you modify the underlying Map (this tells the ListView that its data is out of date and to request its views from the adapter again).
My vision is to create a color coded listView, where I want to have a slim colored bar in each item's layout. This bar should be a certain color based on an int I pass to the activity. How do I create such a bar(essentially a filled rectangle) and set its color. Currently, I am using a custom layout for my list and using a SimpleAdapter with an ArrayList.
I know I will have to use
if (integerForColor == someNumber)
//set the color of the shape, bar
I am sure this is a simple thing that I simply cannot think of at the moment, or must be missing a fundamental. Thank you in advance for your effort.
EDIT 1:
Trying to decode some of the answers that were here, I realized that you guys need my code on how I am building my list:
public class AddScreen extends Activity implements OnClickListener,
OnItemClickListener, OnItemLongClickListener {
SimpleAdapter adapter;
List<HashMap<String, String>> painItems = new ArrayList<HashMap<String, String>>();
ListView listthings;
public void onCreate(Bundle savedInstanceState) {
from = new String[] { "row_1", "row_2" };
to = new int[] { R.id.row1, R.id.row2 };
adapter = new SimpleAdapter(this, painItems, R.layout.mylistlayout,
from, to);
listthings.setAdapter(adapter);
}
I have added the getView() Method:
public View getView (int position, View convertView, ViewGroup parent){
convertView.setBackgroundColor(R.drawable.red);
return convertView;
//I have no inflater as of now...
}
EDIT 2: As per #huntsfromshadow 's suggestion, I have created a new activity and overridden the getView() method there.
//Imports
public class Adapter extends SimpleAdapter{
public Adapter(Context context, List<? extends Map<String, ?>> data,
int resource, String[] from, int[] to) {
super(context, data, resource, from, to);
// TODO Auto-generated constructor stub
}
#Override
public View getView(int position, View convertView, ViewGroup parent){
View row = convertView;
row.setBackgroundColor(0xFF0000FF);
return row;
}
}
Unfortunately, it still does not work.
Assuming you have everything else setup,
it's probably easiest to define your colored backgrounds in xml like so (red.xml):
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="#DD0000"
android:endColor="#EE0000"
android:angle="270" />
</shape>
And then use it like this:
context.getResources().getColor(R.drawable.red).
You'll have to pass in and save a context object from the parent Activity.
Look at the answer I gave on how to create a ListView (yes it's parsing data from JSON - ignore that) here
Create a custom adapter and override getView. That is the point where you can do any complicated layout decisions based on values and logic.
A google search for custom adapter will give you a lot of resources.
As #CaspNZ said, use .xml shape files.
In your code extend a 'ListAdapter' (probably either BaseAdapter or ArrayAdapter). Override the getView function like this:
public View getView (int position, View convertView, ViewGroup parent){
if (convertView == null){
//assuming you already have inflater initilized somewhere
convertView = inflater.inflate(you_layout_file.xml, parent, false);
}
//some logic to determine what color the background should be
convertView.setBackgroundDrawable(R.drawable.the_file_you_want_to_use.xml)
}
I hacked together something very similar to color code the text of each item differently. You could adapt it pretty easily to change the color of a bar instead.
Since each element in the list has a different color, you have to remember which color is associated with which location in the array. I just used a simple integer array as a member of my ColorItemsAdapter.
private class ColorItemsAdapter extends ArrayAdapter<String>
{
/*
* For each line to be added to the log, the text is stored in "items"
* and the text color is stored in "colors"
*/
ArrayList<String> items = new ArrayList<String>();
ArrayList<Integer> colors = new ArrayList<Integer>();
int length = 0;
public ColorItemsAdapter(Context context, int textViewResourceId, List<String> stringObj)
{
super(context, textViewResourceId, stringObj);
if(stringObj != null)
{
length = stringObj.size();
}
}
/**
* Adds an item to the adapter, and specifies what color the item should be
* #param item
* The text to be added
* #param color
* The color of the text to be added
*/
public void add(String item, Integer color)
{
items.add(item);
colors.add(color);
length++;
super.add(item);
}
/**
* Adds an item to the adapter at position <i>index</i>
* #param item
* The text to be inserted
* #param index
* The index at which to insert the text
* #param color
* The color of the text to be inserted
*/
public void insert(String item, int index, Integer color)
{
items.add(index, item);
colors.add(index, color);
length++;
super.insert(item, index);
}
/*
* Changes the color of the TextView appropriately before calling the super
*/
public View getView(int position, View convertView, ViewGroup parent)
{
TextView v = (TextView) convertView;
if (v == null)
{
LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = (TextView) vi.inflate(R.layout.log_list_item, null);
}
/*
* Here I am setting the text color
* but you could set some other attribute instead.
*/
v.setTextColor(colors.get(position));
return super.getView(position, v, parent);
}
}
Then, to add an item to your list (in this case a red one), you could just use
myColorItemsAdapter.insert(message, 0, Color.RED);