It is possible to get a list of all the currently displayed/on-screen android UI elements?
For example, if I have a app that look like this:
I would get a list that would contain:
TextView (Hello World!)
RelativeLayout (Or whatever the parent container is)
Etc if there were more elements
This would be great in the case I don't know the ID's, or even what UI elements will appear on screen, but I still want to hide/show them.
You can achieve this by using the following manager, nice and clean!
Use it anywhere you want and you'll get a list of the view and all its children.
(Needs to be done recursively)
LayoutManager.getViews(getWindow());
public class LayoutManager
{
private static List<View> views;
public static List<View> getViews(Window window) {
return getViews(window.getDecorView().getRootView(), true);
}
public static List<View> getViews(final View view, boolean starting)
{
if (starting) {
views = new ArrayList<>();
}
views.add(view);
/** Search in each ViewGroup children as well */
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
getViews(viewGroup.getChildAt(i), false);
}
}
return views;
}
}
From Android developer documentation, a statement reads as under:
An ID need not be unique throughout the entire tree, but it should be
unique within the part of the tree you are searching (which may often
be the entire tree, so it's best to be completely unique when
possible).
Please help me understand, with an example, what is meant by 'part of the tree you are searching'?
Example, given following:
<AnOutterLayout>
<Button android:id="#+id/my_button".../>
<ASuitableInnerLayout>
<Button android:id="#+id/my_button".../>
</ASuitableInnerLayout>
</AnOutterLayout>
If I have:
Button myButton = (Button) findViewById(R.id.my_button);
What will be search tree here?
Thanks!
The "part of the tree you are searching" is typically the children of the ViewGroup you're calling findViewById on.
In an Activity, the findViewById method is implemented like this (source):
public View findViewById(int id) {
return getWindow().findViewById(id);
}
Ok, so how does a Window implement findViewById (source)?
public View findViewById(int id) {
return getDecorView().findViewById(id);
}
getDecorView returns a View - and all that the implementation of View does is return itself (if the views ID matches the one passed in), or null (source):
public final View findViewById(int id) {
if (id < 0) {
return null;
}
return findViewTraversal(id);
}
protected View findViewTraversal(int id) {
if (id == mID) {
return this;
}
return null;
}
It's much more interesting if we look at the implementation for a ViewGroup (source):
protected View findViewTraversal(int id) {
if (id == mID) {
return this;
}
final View[] where = mChildren;
final int len = mChildrenCount;
for (int i = 0; i < len; i++) {
View v = where[i];
if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
v = v.findViewById(id);
if (v != null) {
return v;
}
}
}
return null;
}
So you see a ViewGroup traverses its children searching for the ID you pass in. I'm not certain of the order of mChildren, but I suspect it'll be in the order you add the views to the hierarchy (just checked - addView(View child) does add views to the end of the mChildren list, where as addView(View child, int index) adds the view at index position in the list).
So, for your example, which button was returned would depend on which ViewGroup you were calling findViewById on.
If you called anOutterLayout.findViewById(R.id.my_button), you'd get the first button - as this is the first child element it comes across that contains that id.
If you called anInnerLayout.findViewById(R.id.my_button), you'd get the second button.
However, if your layout file looked like this:
<AnOutterLayout>
<ASuitableInnerLayout>
<Button android:id="#+id/my_button".../>
</ASuitableInnerLayout>
<Button android:id="#+id/my_button".../>
</AnOutterLayout>
Then anOutterLayout.findViewById(R.id.my_button) would actually return the button inside the inner layout - as this view was added to the hierarchy earlier, and is therefore earlier in the list of children for that view.
This assumes that views are added in the order they're present in the XML view hierarchy.
The button in the outter layer will be called first.
This post has a well-explained answer for your question
Are Android View id supposed to be unique?
I just wrote an answer for someone confused by findViewById and I realised that I have a gap in my understanding. This question is for knowledge and curiosity only.
Consider this:
button = (Button)findViewById(R.id.button);
findViewById returns an instance of View, which is then cast to the target class. All good so far.
To setup the view, findViewById constructs an AttributeSet from the parameters in the associated XML declaration which it passes to the constructor of View.
We then cast the View instance to Button.
How does the AttributeSet get passed in turn to the Button constructor?
[EDIT]
So I was the confused one :). The whole point is that when the layout is inflated, the view hierarchy already contains an instance of the view descendant class. findViewById simply returns a reference to it. Obvious when you think about it - doh..
findViewById does nothing. It just looks through view hierarchy and returns reference to a view with requested viewId. View is already created and exists. If you do not call findViewById for some view nothing changes.
Views are inflated by LayoutInflator. When you call setContentView xml layout is parsed and view hierarchy is created.
attributes passed to Button's constructor by LayoutInflater. check LayoutInflator source code.
I don't think findViewById() constructs or instantiates a View. It will search in View hierarchy of already inflated layout, for a View with matching id.This method works differently for a View and for a ViewGroup.
from Android Source code:
View.findViewById() returns the same View object if this view has the given id or null, it calls:
protected View findViewTraversal(int id) {
if (id == mID) {
return this;
}
return null;
}
ViewGroup.findViewById() iterates through child views and calls same method on these Views, it calls:
protected View findViewTraversal(int id) {
if (id == mID) {
return this;
}
final View[] where = mChildren;
final int len = mChildrenCount;
for (int i = 0; i < len; i++) {
View v = where[i];
if ((v.mPrivateFlags & IS_ROOT_NAMESPACE) == 0) {
v = v.findViewById(id);
if (v != null) {
return v;
}
}
}
return null;
}
I set tag for UI widgets and I want to retrieve a list of View that has a specific tag. Using View.findViewWithTag("test_tag") just return one View not all view that support tag.
Any help appreciated.
You shouldnt expect an array of views from this method, since the method signature itself tells that it will return a single view.
public final View findViewWithTag (Object tag)
However, what you may do is to get your layout as ViewGroup and then iterate through all the child views to find out your desired view by doing a look-up on their tag. For example:
/**
* Get all the views which matches the given Tag recursively
* #param root parent view. for e.g. Layouts
* #param tag tag to look for
* #return List of views
*/
public static List<View> findViewWithTagRecursively(ViewGroup root, Object tag){
List<View> allViews = new ArrayList<View>();
final int childCount = root.getChildCount();
for(int i=0; i<childCount; i++){
final View childView = root.getChildAt(i);
if(childView instanceof ViewGroup){
allViews.addAll(findViewWithTagRecursively((ViewGroup)childView, tag));
}
else{
final Object tagView = childView.getTag();
if(tagView != null && tagView.equals(tag))
allViews.add(childView);
}
}
return allViews;
}
int tags = 6;
for (int index = 0; index < tags; index++) {
try{
TextView txtView = (TextView)getView().getRootView().findViewWithTag("txtTag-"+index);
txtView.setText(" TWitter/ #MOIALRESHOUDI ");
} catch (Exception e){}
}
hope this helps someone!
Its very simple in my use case. For example there are twelve imageviews with a same tag in a layoyut.
When findViewWithTag("image") calls on. It will simply give first index single imageview in from a layout. The solution is simple when you got a imageview with that tag, simply set that imageview tag to empty string. Next time in iteration when findViewWithTag("image") calls on. It will simply give you the next imageview.
for(int inf = 0; inf < albumPageImagesInfoList.get(cardPosition); inf++){
ImageView imageView = inflateView.findViewWithTag("image");
if(imageView!=null){
imageView.setId(uivid);
imageView.setTag("");
imgIndexId++;
}
}
I have a ListView which displays news items. They contain an image, a title and some text. The image is loaded in a separate thread (with a queue and all) and when the image is downloaded, I now call notifyDataSetChanged() on the list adapter to update the image. This works, but getView() is getting called too frequently, since notifyDataSetChanged() calls getView() for all visible items. I want to update just the single item in the list. How would I do this?
Problems I have with my current approach are:
Scrolling is slow
I have a fade-in animation on the image which happens every time a single new image in the list is loaded.
I found the answer, thanks to your information Michelle.
You can indeed get the right view using View#getChildAt(int index). The catch is that it starts counting from the first visible item. In fact, you can only get the visible items. You solve this with ListView#getFirstVisiblePosition().
Example:
private void updateView(int index){
View v = yourListView.getChildAt(index -
yourListView.getFirstVisiblePosition());
if(v == null)
return;
TextView someText = (TextView) v.findViewById(R.id.sometextview);
someText.setText("Hi! I updated you manually!");
}
This question has been asked at the Google I/O 2010, you can watch it here:
The world of ListView, time 52:30
Basically what Romain Guy explains is to call getChildAt(int) on the ListView to get the view and (I think) call getFirstVisiblePosition() to find out the correlation between position and index.
Romain also points to the project called Shelves as an example, I think he might mean the method ShelvesActivity.updateBookCovers(), but I can't find the call of getFirstVisiblePosition().
AWESOME UPDATES COMING:
The RecyclerView will fix this in the near future. As pointed out on http://www.grokkingandroid.com/first-glance-androids-recyclerview/, you will be able to call methods to exactly specify the change, such as:
void notifyItemInserted(int position)
void notifyItemRemoved(int position)
void notifyItemChanged(int position)
Also, everyone will want to use the new views based on RecyclerView because they will be rewarded with nicely-looking animations! The future looks awesome! :-)
This is how I did it:
Your items (rows) must have unique ids so you can update them later. Set the tag of every view when the list is getting the view from adapter. (You can also use key tag if the default tag is used somewhere else)
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
View view = super.getView(position, convertView, parent);
view.setTag(getItemId(position));
return view;
}
For the update check every element of list, if a view with given id is there it's visible so we perform the update.
private void update(long id)
{
int c = list.getChildCount();
for (int i = 0; i < c; i++)
{
View view = list.getChildAt(i);
if ((Long)view.getTag() == id)
{
// update view
}
}
}
It's actually easier than other methods and better when you dealing with ids not positions! Also you must call update for items which get visible.
get the model class first as global like this model class object
SampleModel golbalmodel=new SchedulerModel();
and initialise it to global
get the current row of the view by the model by initialising the it to global model
SampleModel data = (SchedulerModel) sampleList.get(position);
golbalmodel=data;
set the changed value to global model object method to be set and add the notifyDataSetChanged its works for me
golbalmodel.setStartandenddate(changedate);
notifyDataSetChanged();
Here is a related question on this with good answers.
The answers are clear and correct, I'll add an idea for CursorAdapter case here.
If youre subclassing CursorAdapter (or ResourceCursorAdapter, or SimpleCursorAdapter), then you get to either implement ViewBinder or override bindView() and newView() methods, these don't receive current list item index in arguments. Therefore, when some data arrives and you want to update relevant visible list items, how do you know their indices?
My workaround was to:
keep a list of all created list item views, add items to this list from newView()
when data arrives, iterate them and see which one needs updating--better than doing notifyDatasetChanged() and refreshing all of them
Due to view recycling the number of view references I'll need to store and iterate will be roughly equal the number of list items visible on screen.
int wantedPosition = 25; // Whatever position you're looking for
int firstPosition = linearLayoutManager.findFirstVisibleItemPosition(); // This is the same as child #0
int wantedChild = wantedPosition - firstPosition;
if (wantedChild < 0 || wantedChild >= linearLayoutManager.getChildCount()) {
Log.w(TAG, "Unable to get view for desired position, because it's not being displayed on screen.");
return;
}
View wantedView = linearLayoutManager.getChildAt(wantedChild);
mlayoutOver =(LinearLayout)wantedView.findViewById(R.id.layout_over);
mlayoutPopup = (LinearLayout)wantedView.findViewById(R.id.layout_popup);
mlayoutOver.setVisibility(View.INVISIBLE);
mlayoutPopup.setVisibility(View.VISIBLE);
For RecycleView please use this code
I used the code that provided Erik, works great, but i have a complex custom adapter for my listview and i was confronted with twice implementation of the code that updates the UI. I've tried to get the new view from my adapters getView method(the arraylist that holds the listview data has allready been updated/changed):
View cell = lvOptim.getChildAt(index - lvOptim.getFirstVisiblePosition());
if(cell!=null){
cell = adapter.getView(index, cell, lvOptim); //public View getView(final int position, View convertView, ViewGroup parent)
cell.startAnimation(animationLeftIn());
}
It's working well, but i dont know if this is a good practice.
So i don't need to implement the code that updates the list item two times.
exactly I used this
private void updateSetTopState(int index) {
View v = listview.getChildAt(index -
listview.getFirstVisiblePosition()+listview.getHeaderViewsCount());
if(v == null)
return;
TextView aa = (TextView) v.findViewById(R.id.aa);
aa.setVisibility(View.VISIBLE);
}
I made up another solution, like RecyclyerView method void notifyItemChanged(int position), create CustomBaseAdapter class just like this:
public abstract class CustomBaseAdapter implements ListAdapter, SpinnerAdapter {
private final CustomDataSetObservable mDataSetObservable = new CustomDataSetObservable();
public boolean hasStableIds() {
return false;
}
public void registerDataSetObserver(DataSetObserver observer) {
mDataSetObservable.registerObserver(observer);
}
public void unregisterDataSetObserver(DataSetObserver observer) {
mDataSetObservable.unregisterObserver(observer);
}
public void notifyDataSetChanged() {
mDataSetObservable.notifyChanged();
}
public void notifyItemChanged(int position) {
mDataSetObservable.notifyItemChanged(position);
}
public void notifyDataSetInvalidated() {
mDataSetObservable.notifyInvalidated();
}
public boolean areAllItemsEnabled() {
return true;
}
public boolean isEnabled(int position) {
return true;
}
public View getDropDownView(int position, View convertView, ViewGroup parent) {
return getView(position, convertView, parent);
}
public int getItemViewType(int position) {
return 0;
}
public int getViewTypeCount() {
return 1;
}
public boolean isEmpty() {
return getCount() == 0;
} {
}
}
Don't forget to create a CustomDataSetObservable class too for mDataSetObservable variable in CustomAdapterClass, like this:
public class CustomDataSetObservable extends Observable<DataSetObserver> {
public void notifyChanged() {
synchronized(mObservers) {
// since onChanged() is implemented by the app, it could do anything, including
// removing itself from {#link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {#link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
}
public void notifyInvalidated() {
synchronized (mObservers) {
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onInvalidated();
}
}
}
public void notifyItemChanged(int position) {
synchronized(mObservers) {
// since onChanged() is implemented by the app, it could do anything, including
// removing itself from {#link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {#link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
mObservers.get(position).onChanged();
}
}
}
on class CustomBaseAdapter there is a method notifyItemChanged(int position), and you can call that method when you want update a row wherever you want (from button click or anywhere you want call that method). And voila!, your single row will update instantly..
My solution:
If it is correct*, update the data and viewable items without re-drawing the whole list. Else notifyDataSetChanged.
Correct - oldData size == new data size, and old data IDs and their order == new data IDs and order
How:
/**
* A View can only be used (visible) once. This class creates a map from int (position) to view, where the mapping
* is one-to-one and on.
*
*/
private static class UniqueValueSparseArray extends SparseArray<View> {
private final HashMap<View,Integer> m_valueToKey = new HashMap<View,Integer>();
#Override
public void put(int key, View value) {
final Integer previousKey = m_valueToKey.put(value,key);
if(null != previousKey) {
remove(previousKey);//re-mapping
}
super.put(key, value);
}
}
#Override
public void setData(final List<? extends DBObject> data) {
// TODO Implement 'smarter' logic, for replacing just part of the data?
if (data == m_data) return;
List<? extends DBObject> oldData = m_data;
m_data = null == data ? Collections.EMPTY_LIST : data;
if (!updateExistingViews(oldData, data)) notifyDataSetChanged();
else if (DEBUG) Log.d(TAG, "Updated without notifyDataSetChanged");
}
/**
* See if we can update the data within existing layout, without re-drawing the list.
* #param oldData
* #param newData
* #return
*/
private boolean updateExistingViews(List<? extends DBObject> oldData, List<? extends DBObject> newData) {
/**
* Iterate over new data, compare to old. If IDs out of sync, stop and return false. Else - update visible
* items.
*/
final int oldDataSize = oldData.size();
if (oldDataSize != newData.size()) return false;
DBObject newObj;
int nVisibleViews = m_visibleViews.size();
if(nVisibleViews == 0) return false;
for (int position = 0; nVisibleViews > 0 && position < oldDataSize; position++) {
newObj = newData.get(position);
if (oldData.get(position).getId() != newObj.getId()) return false;
// iterate over visible objects and see if this ID is there.
final View view = m_visibleViews.get(position);
if (null != view) {
// this position has a visible view, let's update it!
bindView(position, view, false);
nVisibleViews--;
}
}
return true;
}
and of course:
#Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
final View result = createViewFromResource(position, convertView, parent);
m_visibleViews.put(position, result);
return result;
}
Ignore the last param to bindView (I use it to determine whether or not I need to recycle bitmaps for ImageDrawable).
As mentioned above, the total number of 'visible' views is roughly the amount that fits on the screen (ignoring orientation changes etc), so no biggie memory-wise.
In addition to this solution (https://stackoverflow.com/a/3727813/5218712) just want to add that it should work only if listView.getChildCount() == yourDataList.size();
There could be additional view inside ListView.
Example of how the child elements are populated: