I have a gallery where I'm displaying a number of ID cards as images.
When the user clicks on an image, I want to change the image to another one which shows the 'back' of the card.
I have bound an arraylist of objects to the gallery, each object contains the front and back images.
I have two image views, one for the front and one for the back image. The gallery will initially show all front images ( the 'back' image view visibility is set to GONE). My idea was to change the visibility of the relevant image view when the user clicks.
That's about as far as I've got, buy I'm not sure how to implement the click functionality to change the views.
Any assistance would be greatly appreciated.
A Gallery is a specialization of AdapterView, which also contains ListView, Spinner, GridView... All of these have in common to display n times the same 'kind' of view with different data.
These particular views use an Adapter to handle the view creation and population process. The getView method is called on the adapter, providing this class with the data for creating the view for a particular row (cell, gallery item ...).
The getView method is called when the AdapterView deems it necessary, and the developer has no control on which view gets created or when it does.
Therefore, it is practically impossible to modify a child of the AdapterView directly. It would result it the modification being arbitrarily overwritten, or even transferred to another row of the view.
Instead, to act on the child views, it is necessary to act on the data that are used to populate the view. (Note that this enforce the MVC model, as the view remains a reflection of the model, rather than being manipulated by the controller.)
In this particular example, the view is to display a different image based on the checked status of the data. To achieve that, add a checked member to the model :
public class MyModel {
// ... Other members
public boolean checked = false;
}
Use that value in getView :
#Override
public View getView(int position, View convertView, View parent) {
MyModel current = getItem(position);
// ... Inflate and populate
if (current.checked) {
// Specific checked behavior
} else {
// Not checked behavior
}
}
The checked value is to be modified in the controller, probably a OnItemClickListener:
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// Here, there can be several ways to access the clicked item.
// I assume myAdapter is the adapter used for the gallery
MyModel current = myAdapter.get(position);
current.checked = !current.checked;
myAdapter.notifyDataSetChanged();
}
Related
I have a custom list view which is being popluated via an array adaptor.
Each item/row contains three buttons and some related textViews.
All elements in a row describe the details for a device on the cloud. So data is fetched from the cloud and then the list is populated. No. of rows is equal of the number of devices.
Everything was fine till I added the feature for a periodic update for the items.
The problem is that after each periodic update it over writes the data for a device in the wrong row.
I tried two ways to refresh each row.
I kept a map for (DeviceID and view) and then based on the deviceId
i would get the view and update it. Now,this didn't work as the views are reused and so as i scroll
down, basically the same view is reused as shows the new data. And
so the map entry of the previous device is over written with the new
one.
I tried to directly call getView() and pass the position but that
also didn't work.
I understand that the views are reused so there is no way to know exactly which view is associated with a deviceID.
But could some please help me figure out how to update the correct view with the correct data?
Thanks.
If you are using Holder pattern, then there is a way to do this.
Step 1: Add one attribute i.e. position to Holder.
private class ViewHolder {
....
....
int position;
}
Step 2: Initialise the holder position into getView()
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
...
holder.position = position;
}
....
}
Step 3: Check holder position and view's position values. If both are same then do your task.
if (mHolder.position == mPosition) {
// This is you required row. Do your task.
}
Read Async loading for more details.
I know the methods getFirstVisiblePosition() and getLastVisiblePosition() but these are only for the visible grid, if you check for the visibility for the element at position in the getView(int position, View convertView, ViewGroup parent) you will see that it lies outside the range of visible elements(which makes perfect sense). What can I do to check if an element at a position is visible or has been requested by the gridView/listView. Something which returns true in the getView(...) method itself.
Why I am trying to do it:
I have a bunch of images that I am showing in a gridView. The images have to be fetched via the network(I am caching them once they have been downloaded), so I have created an asyncTask to download the image and then show it in the corresponding grid. Since there might be a lot of images and the user might quickly scroll down, I want to insert some check in the onPreExecute() method which can tell me if it visible, otherwise I cancel the task for now.
if (!requested()) {
this.cancel(true);
}
I am looking for appropriate code for requested().
Really sorry for such a long question, finding it really difficult to explain. Any solutions or modifications to the question are most welcome.
I'm not sure if I understand your question. Do you want to know if, given a certain position, that view has ever been shown?
If that's what you want, you should have a boolean array in your list/grid adapter. In the getView function you set that position to true. Then you only have to implement a function to check if a certain position is true.
public class MyAdapter extends BaseAdapter {
private boolean[] hasBeenSeen;
public View getView (int position, View convertView, ViewGroup parent) {
hasBeenSeen[position] = true;
...
}
public boolean checkPosition(int position){
return hasBeenSeen[position];
}
}
The boolean array has to be initialized when you set the list/grid content.
You can try using View.getLocationOnScreen(); If the x and y positions are within your screen bounds then it should be on your screen. I'm not sure what this will return if it isn't on your screen, though.
I'm creating kind of music player which has a listview with songs (having progress bar near currently playing song)
What is important is that I have an Item with views which can be changed from outside (in some handler)
public View getView(int i, View convertView, ViewGroup viewGroup) {
RelativeLayout result;
if (convertView == null) {
result = (RelativeLayout) inflater.inflate(R.layout.list_item, viewGroup, false);
} else {
result = (RelativeLayout) convertView;
}
...
ProgressBar progressBar = result.findViewById(R.id.progressBar)
...
if (i == currentSong) {
// saving to instance variable
this.currentlyPlayingProgressBar = progressBar;
} else {
progressBar.setVisibility(View.GONE);
}
...
return result;
}
(Code was changed to focus on my problem)
Btw currentSong can be changed from outside, adapter.notifyDataSetChanged() is being called in this case.
It seems like I'm using listView incorrectly, but I don't know the better way.
The main problem is that I need to have links to views to change them.
And the only way where I can get them is in getView method which reuses those view in a way only google developers can explain=(
First problem
This is all happening in Fragment which is just a part of a viewPager. when user scrolls of this fragment and then scrolls back then getView method is being called with some strange objects inside.. And I override currentlyPlayingProgressBar with this invalid value. Which causes the freeze of my statusbar. (it starts updating wrong view)
And I have no idea which view is it..
Second problem
I am reusing list items and this means that when user scrolls list view down - then sometimes he gets actually the same list item with the same progressBar.
This progressBar must be invisible but it's not (I think it's all because of my usage of currentlyPlayingProgressBar from outside)
Thanks in advance.
You can do this in two ways:
1) notifyDataSetChanged, which just resets entire ListView and assigns all visible list items again to views. notifyDataSetChanged is very expensive, since it makes entire list and view hierarchy to be recreated, layouts are measured, etc, which is slow. For frequent update of single list item, such as progress bar change, this is overkill.
2) Find view of particular listview item, and update only that one. We'll focus on this approach.
First you need to somehow identify which list view contains which list item. Common approach is to use View.setTag in your Adapter.getView, where setTag parameter is Object of your choice, may be same item as you return for Adapter.getItem.
So later you know which ListView child view has which item assigned.
Assuming you want to update particular item displayed in ListView, you have to iterate through ListView child views to find which view displays your item:
Object myItem = ...;
for(int i = list.getChildCount(); --i>=0; ){
View v = list.getChildAt(i);
Object tag = v.getTag();
if(tag==myItem) {
// found, rebind this item
bindItemToView(myItem, v);
break;
}
}
You must expect that ListView currently may not display your list item (is scrolled away).
From code above you see that it calls bindItemToView, which is your function to bind item to list view. You'd probably call same function to setup the item view in Adapter.getView.
You may also optimize it further, assuming you want to update only ProgressBar, then don't call bindItemToView, but update only ProgressBar in your listitem view (findViewById, setup values).
Hint: you can make it even more optimal by using ViewHolder approach. Then setTag would not contain your item object, but your ViewHolder object.
#Pointer null has also given very usefull aproach, but in your case I think u are updating the list which is not visible, in this case you have to set the tag from the adapter just like the list index and curresponding check if the list item exist between the last visible item or first visible item then update it else donot update..
I am new to Android development and reading through some example code. I have copied one method from the sample code in an Adapter class (derived from ArrayAdapter), the derived class has a checkbox in addition to the text view:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View listItem = super.getView(position, convertView, parent);
CheckedTextView checkMark = null;
ViewHolder holder = (ViewHolder) listItem.getTag();
if (holder != null) {
checkMark = holder.checkMark;
} else {
checkMark = (CheckedTextView) listItem.findViewById(android.R.id.text1);
holder = new ViewHolder(checkMark);
listItem.setTag(holder);
}
checkMark.setChecked(isInCollection(position));
return listItem;
}
private class ViewHolder {
protected final CheckedTextView checkMark;
public ViewHolder(CheckedTextView checkMark) {
this.checkMark = checkMark;
}
}
The sample code is to optimize the getView by caching the View within a ViewHolder object.
Where I am confused is I thought the convertView, if not null, would be re-purposed and then the View data is populated into it and returned.
If this is the case, then how could the setTag / getTag methods called in the code be relied upon? It would seem that the same object would have to be retrieved in order for it to work?
perhaps view returned from getTag on a subsequent call is for a different list item, and returns the wrong view
Adapters use a RecycleBin. This class allows the ListView to only create as many row layouts as will fit on the screen, plus one or two for scrolling and pre-loading. So if you have a ListView with 1000 rows and a screen that only displays 7 rows, odds are the ListViiew will only have 8 unique Views.
Now to your question using my example above: only eight row layouts and 8 subsequent ViewHolders are ever created. When the users scrolls no new row layouts are ever created; only the content of the row layout changes. So getTag() will always have a valid ViewHolder that references the appropriate View(s).
(Does that help?)
You're on the right track, here's some information that may help make more sense of how ListViews work:
A simple implementation of the getView() method has two goals. The first is inflating the View to be shown on the list. The second is populating the View with the data that needs to be shown.
As you stated, ListViews re-purpose the Views that compose the list. This is sometimes referred to as view recycling. The reason for this is scalability. Consider a ListView that contains the data of 1000 items. Views can take up a lot of space, and it would not be feasible to inflate 1000 Views and keep them all in memory as this could lead to performance hits or the dreaded OutOfMemoryException. In order to keep ListViews lightweight, Android uses the getView() method to marry Views with the underlying data. When the user scrolls up and down the list, any Views that move off the screen are placed in a pool of views to be reused. The convertView parameter of getView() comes from this list. Initially, this pool is empty, so null Views are passed to getView(). Thus, the first part of getView should be checking to see if convertView has been previously inflated. Additionally, you'll want to configure the attributes of convertView that will be common to all list items. That code will look something like this:
if(convertView == null)
{
convertView = new TextView(context);
convertView.setTextSize(28);
convertView.setTextColor(R.color.black);
}
The second part of an implementation of getView() looks at your underlying data source for the list and configures this specific instance of the View. For example, in our test list, we may have an Array of Strings to set the text of the view, and want to set the tag as the current position in the Data of this View. We know which item in the list we're working with based on the position parmeter. This configuration comes next.
String listText = myListStringsArray[position];
((TextView)convertView).setText(listText);
convertView.setTag(position);
This allows us to minimize the amount of time we spend inflating/creating new views, a costly operation, while still being able to quickly configuring each view for display. Putting it all together, your method will look like this:
#Override
public View getView(int position, View convertView, ViewGroup)
{
if(convertView == null)
{
convertView = new TextView(context);
//For more complex views, you may want to inflate this view from a layout file using a LayoutInflator, but I'm going to keep this example simple.
//And now, configure your View, for example...
convertView.setTextSize(28);
convertView.setTextColor(R.color.black);
}
//Configure the View for the item at 'position'
String listText = myListStringsArray[position];
((TextView)convertView).setText(listText);
convertView.setTag(position);
//Finally, we'll return the view to be added to the list.
return convertView;
}
As you can see, a ViewHolder isn't needed because the OS handles it for you! The Views themselves should be considered temporary objects and any information they need to hold onto should be managed with your underlying data.
One further caveat, the OS does nothing to the Views that get placed in the pool, they're as-is, including any data they've been populated with or changes made to them. A well-implemented getView() method will ensure that the underlying data keeps track of any changes in the state of views. For example, if you change text color of your TextView to red onClick, when that view is recycled the text color will remain red. Text color, in this case, should be linked to some underlying data and set outside of the if(convertView == null) conditional each time getView() is called. (Basically, static setup common for all convertViews happens inside the conditional, dynamic setup based on the current list item and user input happens after) Hope this helps!
Edited - Made the example simpler and cleaned up the code, thanks Sam!
I'm now developing an application that uses a ListView with a
CheckedTextView on every item that managed by an ArrayAdapter to
support multiple chooses. The contents in my ListView are dynamic, that
means, can be changed during runtime. Now I try to use
ListView.getCheckedItemPositions() to get all checked items, Because I want
to save all the checked positions and auto-check them when user go back to
this page again. So I need to save checked results for every page.
For the first page everything works fine as expected. But when user goes to
another page and make some chooses, the result array that ListView returned
contains some positions that are never checked. I don't why ListView has
this strange behavior. Even for a page that in fact no checked happens but
ListView gives me a result that indicates there's one item has been checked.
could anyone who can teach me how to get the position of CheckedTextView
in its OnClickListener callback?
example code is appreciate.
Thanks in advance...
The listview recycles its views so when you go to a different page and then return to the previous page, the listview recalls the getView() function for its views. To make sure that the order of the checked views are not mixed up, create an arraylist that contains the check state of all the views before initializing the adapter. Then pass the arraylist as an argument for the adapter's constructor. There, in the getView() function, set the checked state of each checkable textview based on the arraylist. Then, return to the activity class and override the onItemClick() event. Using the view that is given to you when the function is called, do the following to get the checkable textview and set its checked state:
listView1.setOnItemClickListener(new OnItemClickListener()
{
#Override
public void onItemClick(AdapterView<?> arg0, View selectedView, int position , long id)
{
CheckedTextView tv = (CheckedTextView)selectedView.findViewById(R.id.textview);
if (tv.isChecked())
{
tv.setChecked(false);
checkStatesOfViews.get(position) = false;
}
else
{
tv.setChecked(true);
checkStatesOfViews.get(position) = true;
}
}
});