Is it possible to use one adapter class (and it's instance) (e.g. own sub-class of ArrayAdapter<ownDataType> for two (or more) different views?
In my Activity, I have two Spinner objects that are used to show (and select) related data: A list of parent categories and a list of sub-categories of the selected parent category. Of course, the list in the sub-category-Spinner is updated (setting a new ArrayAdapter), when the parent category is changed. However, I also want to be able to set a sub-category directly (and choose the parent category automatically) when the Activity is created. This makes things complicated.
My idea for a simple and clean solution now is to create ONE own Adapter-Class (e.g. derived from ArrayAdapter<CategoryPair>, where CategoryPair is a simple class that holds a pair of parent- and sub-category) and this own adapter class handles all the logic to keep data consistent (much simpler there).
In the getView(int position, View convertView, ViewGroup parent) method of the Adapter-class I need to recognize reliably for which of the two Spinner objects in my activity I need to create a view.
Can I rely on the parent parameter?
Do you think my idea is a good one?
Edit: Code example:
My adapter class:
public class CategoryPair
{
Category parent;
Category sub;
}
public class CategoriesAdapter extends ArrayAdapter<CategoryPair> {
private Spinner parentSpinner;
private Spinner subSpinner;
public CategoriesAdapter(Context context, int textViewResourceId, Spinner par, Spinner sub) {
super(context, textViewResourceId);
parentSpinner = par;
subSpinner = sub;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (parent == parentSpinner)
{
//
}
else if (parent == subSpinner)
{
//
}
else
{
Log.e(TAG, "Invalid parent in CategoriesAdapter");
}
return super.getView(position, convertView, parent);
}
Yup, that's fine. Each view should use a different instance of your Adapter (make sure of this), so, unless your Adapter code is buggy and uses static variables, everything should work just fine.
Related
I have a list that I want to display differently in two fragments.
That means I need two ListViews (one in each fragment) but do I also need two adapters ? Or can I simply use one adapter for the two list given that when the data changes, I want the two lists to update.
EDIT : Display differently means I want to display the tasks sorted by alphabetical order in the first fragment and on a calendar in the second fragment.
(Or maybe there is a simpler way to go about the problem ?)
The adapter :
public class TasksAdapter extends ArrayAdapter<Task> {
public TasksAdapter(Context context, ArrayList<Task> tasks) {
super(context, 0, tasks);
}
#Override
public View getView(int position, View convertView, #NonNull ViewGroup parent) {
Task task = getItem(position);
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_task, parent, false);
}
return convertView;
}
}
Yes, you can certainly use a single adapter if the adapter is built in such a way that it accommodates the type of data you want to use in both Fragment's.
This is dependent on the specifics of what you want to accomplish, but here are some things to consider:
Simply instantiate two separate instances of the adapter and pass the instance into the .setAdapter method of your ListView or RecyclerView.
You can programmatically use different view holders in your adapter, look into the getItemViewType() method.
If the data in your adapter is more than simple primitives, consider writing a model class to model list objects that works with both types of data you are using.
Please note, you should probably be using a RecyclerView.
I'm trying to create a UI similar to Google Keep. I know how to create a staggered View using a Recycler View. If i click a specific Card. Then it has to open a activity.
I can achieve this using onclick method.
This same scenario happens in atleast 5 different Activities in my App.
My question is that
Can I use this single Adapter in all those 5 places ?
If yes, where should i place the onclick actions ?
If no, How can I Create a staggered layout like Keep?
Thanks in Advance!
(See application for RecyclerView below in edits)
Like I mentioned in my comment, it's certainly fine to have separate adapters for all your Activities which use different data and views. As your app data and layouts get more complex, so does your code...that's just the way it is.
But if some of your Activities used similar data in their ListViews -- maybe, for example, two TextViews and an ImageButton -- you could save some effort by defining a single adapter that can be used for multiple Activities. You would then instantiate separate objects for each Activity, similar to the way you would create several ArrayAdapter<String> objects to populate multiple ListViews.
The BaseAdapter is a great class to extend when writing a custom adapter. It's flexible and allows you complete control over the data that's getting shown in your ListView. Here's a minimal example:
public class CustomBaseAdapter extends BaseAdapter {
private Context context;
private ArrayList<String> listData;
public CustomBaseAdapter(Context context, ArrayList<String> listData) {
this.context = context;
this.listData = listData;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.your_list_item_layout, parent, false);
//populate the view with your data -- some examples...
TextView textData = (TextView) convertView.findViewById(R.id.yourTextView);
textData.setText(listData.get(position));
ImageButton button = (ImageButton) convertView.findViewById(R.id.yourImageButton);
button.setOnClickListener(new View.OnClickListener() {
//...
//...
});
}
return convertView;
}
#Override
public Object getItem(int position) {
return 0;
}
#Override
public long getItemId(int position) {
return 0;
}
#Override
public int getCount() {
return listData.size();
}
}
So the key part of this code is obviously the getView() method, which is called every time the ListView needs some data to display. For efficiency, views are stored in something called a convertView so they may be re-used and not have to be inflated every time a view appears on the screen.
So what we do in getView() is first find out if the convertView exists. If it does, we just pass that back to the calling ListView because everything should already be instantiated and ready to go. If the convertView is null, it means the data hasn't been instantiated (or needs to be re-instantiated for whatever reason), and so we inflate a brand new view and populate it with our data.
So in the case of this example adapter above, if several of your Activities all displayed a single list of Strings, you could reuse this adapter for each one, passing in a different ArrayList<String> through the constructor each time you created a new object. But obviously you could pass in more than just Strings if you had more data to show. The level of complexity is up to you. And if the difference among your Activities was too great, I would just create custom versions of this class for all of them and just instantiate them and populate them however you'd like. It will keep all your data very organized and encapsulated.
Hope this helps! Feel free to ask questions for more clarification if you need it.
EDIT IN RESPONSE TO COMMENTS
Since you are using a RecyclerView instead of just plain ListViews (which I, for some reason, totally forgot) you could still do something very similar using a RecyclerView.Adapter<YourViewHolder> instead. The difference would be that instead of inflating the views in a getView() method, they are inflated inside your custom ViewHolder, which I assume you already have. The code might look something like this:
public class CustomRecyclerViewAdapter extends RecyclerView.Adapter<StringViewHolder> {
private final List<String> items;
public CustomRecyclerViewAdapter(ArrayList<String> items) {
this.items = items;
}
#Override
public StringViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//So instead of inflating the views here or in a getView() like in
//in the BaseAdapter, you would instead inflate them in your custom
//ViewHolder.
return new StringViewHolder(parent);
}
#Override
public void onBindViewHolder(StringViewHolder holder, int position) {
holder.setModel(items.get(position));
}
#Override
public long getItemId(int position) {
return items.get(position).hashCode();
}
#Override
public int getItemCount() {
return items.size();
}
}
I need to call the BaseExpandableListAdapter method
#Override
public View getChildView(int groupPosition, final int childPosition, boolean isLastChild, View convertView, ViewGroup parent)
or something like that to get the view that the method returns. The problem is, if I call that method, I have to pass null to convertView, which will reinflate the view, which is the view I need.
I need the view because it contains a custom view with code I need to retrieve from it without resetting it to a new instance of itself. Take this method for example:
public ArrayList<CustomViewData> getCustomViewData ()
{
ArrayList<CustomViewData> customViewDatas = new ArrayList<>();
for(int i = 0; i < getGroupCount(); i++)
if(listView.isGroupExpanded(i))
{
View v = //getConvertView
customViewDatas.add((CustomViewData) v.findViewById(R.id.customView);
}
return customViewDatas;
}
How can I get the actual view from the ExpandableListView (or its base adapter) without creating a new view?
Never store the Views generated by getView(). This is a very dangerous practice. You risk leaking Views. Additionally the adapter recycles Views as you scroll, so there's a chance what you are storing will not be representative of what you need.
You are thinking in the wrong direction. The adapter binds data to Views. Meaning, how a View is rendered is based upon the data itself. If you need to update one of the Views as a result of some event...then the data representative of that View must be updated. The adapter can then re-render it's View accordingly.
When solving such problems, just keep telling yourself "If I need to modify the View, then I need to modify it's data."
The solution to this problem is not an easily apparent one.
There is no way to replicate behaviors like ArrayAdapter's capabilities for RecyclerView Adapters.
Instead, if you need to get data from a custom view that is not automatically stored, override
#Override
public void onViewDetachedFromWindow (ViewHolder holder)
{
//STORE VIEW DATA
super.onViewDetachedFromWindow(holder);
}
#Override
public void onViewRecycled(ViewHolder holder)
{
//STORE VIEW DATA
super.onViewRecycled(holder);
}
Use those methods to get the data from the view stored in your ViewHolder.
You must get the data from the visible views after you are done with the recycler. Those visible views are not recycled automatically so you have to retreive the view data from the visible views still on the adapter.
To do this, see this post.
I have a listview that is populated via an adapter. I need one of the items (which are just bits of text) in the listview to have a different background compared to the others (to mark it as the currently selected one).
I have all of the background logic, I just need a way to say listview.setBackgroundById(int position)
or something like that.
How do I do this?
This needs to be as simple as possible, 'cause all of the other things done in the Activity are currently working perfectly. :)
As asked, this is my Adapter that I'm using:
public class SampleAdapter extends ArrayAdapter<SampleItem> {
private String title;
public SampleAdapter(Context context) {
super(context, 0);
}
public View getView(final int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.row_station, null);
}
TextView title = (TextView)convertView.findViewById(R.id.station_name);
font.setFont(title, 2, getActivity());
title.setText(getItem(position).title);
RelativeLayout info = (RelativeLayout)convertView.findViewById(R.id.info_relative_button);
info.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
MainActivity.setCurrentTab(41);
MainActivity.setBackDisabled(true);
Log.e("current tab:",String.valueOf(MainActivity.getCurrentTab()));
Fragment fragment = new StationInfo();
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction().replace(R.id.content_frame, fragment).commit();
UserManager.getInstance().setStationId(getItem(position).id);
}
});
return convertView;
}
}
The SampleItem has 2 String fields, title and id, it's very simple.
You need to use a custom list adapter and have it return views with your desired background. Create a class extending ListAdapter or any of the existing SimpleAdapter etc and override getView to inflate a suitable view for your element, and add any logic you need to set the background of that view.
There is no way to tell the listview itself to decorate some of its elements by id or position.
Update: I just noticed you added the list adapter code.
Since you are already implementing getView, to change the background of your element simply call convertView.setBackgroundColor, or have two different views inflated depending on the situation.
(BTW it's really bad practice to call static methods on your activity like in your onClickListener.)
In ListView adapter:
#Override
public View getView(int position, View view, ViewGroup viewGroup) {
if(view==null)
....
//for example every even list item to be grey, every odd to be white
if(((position+1)%2)==0)
view.setBackgroundColor(mContext.getResources().getColor(R.color.grey));
else view.setBackgroundColor(mContext.getResources().getColor(android.R.color.white));
Hope you get an idea...
I am trying to modify the sample code below. It currently populates a View that contains an imageview and a textview. I have added an additional textview to my XML layout and am trying to figure out how to replace the simple array with a hash map or even a multidimensional array to populate not just the imageview and the first textview but also the second one.
I would appreciate sample code that shows the entire process. Thanks!
public class DynamicDemo extends ListActivity {
TextView selection;
private static final String[] items={"lorem", "ipsum", "dolor",
"sit", "amet"}
#Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
setListAdapter(new IconicAdapter());
selection=(TextView)findViewById(R.id.selection);
}
public void onListItemClick(ListView parent, View v,
int position, long id) {
selection.setText(items[position]);
}
class IconicAdapter extends ArrayAdapter<String> {
IconicAdapter() {
super(DynamicDemo.this, R.layout.row, R.id.label, items);
}
public View getView(int position, View convertView,
ViewGroup parent) {
View row=super.getView(position, convertView, parent);
ImageView icon=(ImageView)row.findViewById(R.id.icon);
if (items[position].length()>4) {
icon.setImageResource(R.drawable.delete);
}
else {
icon.setImageResource(R.drawable.ok);
}
return(row);
}
}
}
The easiest thing to do is use an ArrayAdapter<MyDataObject> where
public class MyDataObject {
public String string1;
public String string2;
// any other useful attributes
}
And then you would change items to a MyDataObject[] items stored in your class, and instead of doing super.getView(index) you'd do items[index] (which would yield a MyDataObject) and use that data instead.
Also, importantly: you should use the convertView. And possibly the ViewHolder pattern.
Edit: At OP's request, a little more elaboration. Note that this uses the convertView pattern but not the ViewHolder pattern (you should be able to adopt that fairly easily).
In your Adapter, you'd change getView() as follows:
public View getView(int position, View convertView,
ViewGroup parent) {
ViewGroup row;
if(convertView == null){
// create your view here.
row = getLayoutInflater().inflate(R.layout.row);
} else {
row = convertView;
}
// note: when you implement ViewHolder, the ViewHolder will
// hold this reference so that you don't need to look it up every time.
ImageView icon=(ImageView)row.findViewById(R.id.icon);
// here you're employing the "items" array that you were using
// before, except now it contains MyDataObjects. pick out the
// string (or other data you want to check) from the resulting MyDataObject,
// and see if it's longer than 4 characters.
MyDataObject objectAtThisPosition = items[position];
if (objectAtThisPosition.string1.length()>4) {
icon.setImageResource(R.drawable.delete);
}
else {
icon.setImageResource(R.drawable.ok);
}
// Do whatever else you want to with objectAtThisPosition.
return(row);
}
That's it for the easy way, and quite similar to what you have.
Some more detail; if you don't care, skip it. :)
I know that Adapters can seem magical, so in the interest of showing how ListView adapters work, here's an example using a List instead of an Array, so we can remove any magic that ArrayAdapter does with the array behind the scenes. I use a List because they can be more versatile for whatever you're trying to accomplish (ArrayList or LinkedList or what-have-you).
To use a List you'd have the following in your Activity:
private List<MyDataObject> myList = new ArrayList<MyDataObject>();
And instead of items[position] you'd use
MyDataObject objectAtThisPosition = myList.get(position);
If you want to change your data set dynamically, you should probably use this approach (keeping myList at the Activity level) instead of using an Array and an ArrayAdapter. That would mean you'd need to change from extending ArrayAdapter<String> to just extending BaseAdapter<MyDataObject> (most of the methods in BaseAdapter are trivial to implement) since our data size, for example, would be determined by our list, and not the ArrayAdapter's array.
I know that's kind of a fire hose, but let me know if you have any questions!
Use a separator in string, like \t.
Or use an array of straing arrays.
Or use an array of Pair<String, String>.
Or use an array of custom objects.