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;
}
}
Related
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
}
}
I'm using a ViewPager with a FragmentPagerStateAdapter. Every page has a list, manually managed => I create views for this rows by inflating a layout from xml. Resulting in, that views in different rows have the same id. Because the rows are inflated from XML...
The behaviour now is following:
Page 1 looks like following:
// Page 1:
// Row 1: EditText = "Test1"
// Row 2: EditText = "Test2"
I swipe to Page 2 and then to Page 3, afterwards I swipe back to Page 1 and Page 1 looks like following:
// Page 1:
// Row 1: EditText = "Test2"
// Row 2: EditText = "Test2"
Problem: All EditTexts display the value of the lastEditText in the list... (Note, the EditTexts have the same ID).
If I manually set the ids of my EditTexts to the row index, everything works fine.
Is this normal? Is there an easy (good) solution for that problem?
Tt seems like a bug in Android 3.X
The solution is setting ID's manually so they never match between fragments or resetting id's when Fragment becomes invisible.
I like the second one since it can be easily disabled or enabled and requires no special id handling.
/**
* Used for resetting and restoring View id's. This is used as a workaround for
* Android 3.x when ViewPager Fragment instances with the same layout have
* problems referencing next focus id's.
*/
public final class ViewIdResetter {
private final Map<View, Integer> viewMap;
public ViewIdResetter(final View rootView) {
this.viewMap = new HashMap<View, Integer>();
this.fillmap(rootView);
}
private void fillmap(final View view) {
if (view instanceof ViewGroup) {
final ViewGroup vg = (ViewGroup) view;
for (int i = 0; i < vg.getChildCount(); i++) {
this.fillmap(vg.getChildAt(i));
}
}
this.viewMap.put(view, view.getId());
}
public void clearIds() {
for (final View key : this.viewMap.keySet()) {
key.setId(-1);
}
}
public void restoreIds() {
for (final View key : this.viewMap.keySet()) {
key.setId(this.viewMap.get(key));
}
}
}
The Fragment
private ViewIdResetter resetter;
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
this.resetter = new ViewIdResetter(view);
}
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
if (this.resetter != null) {
this.resetter.restoreIds();
}
} else {
if (this.resetter != null) {
this.resetter.clearIds();
}
}
}
setUserVisibleHint is triggered by ViewPager when the Fragment becomes visible.
The ViewIdResetter makes sure only one Fragment at a time has the same id's set.
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?
Do Android views have something equivalent to CSS class selectors? Something like R.id but usable for multiple views? I would like to hide some group of views independent of their position in the layout tree.
I think that you will need to iterate through all of the views in your layout, looking for the android:id you want. You can then use View setVisibility() to change the visibility. You could also use the View setTag() / getTag() instead of android:id to mark the views that you want to handle. E.g., the following code uses a general purpose method to traverse the layout:
// Get the top view in the layout.
final View root = getWindow().getDecorView().findViewById(android.R.id.content);
// Create a "view handler" that will hide a given view.
final ViewHandler setViewGone = new ViewHandler() {
public void process(View v) {
// Log.d("ViewHandler.process", v.getClass().toString());
v.setVisibility(View.GONE);
}
};
// Hide any view in the layout whose Id equals R.id.textView1.
findViewsById(root, R.id.textView1, setViewGone);
/**
* Simple "view handler" interface that we can pass into a Java method.
*/
public interface ViewHandler {
public void process(View v);
}
/**
* Recursively descends the layout hierarchy starting at the specified view. The viewHandler's
* process() method is invoked on any view that matches the specified Id.
*/
public static void findViewsById(View v, int id, ViewHandler viewHandler) {
if (v.getId() == id) {
viewHandler.process(v);
}
if (v instanceof ViewGroup) {
final ViewGroup vg = (ViewGroup) v;
for (int i = 0; i < vg.getChildCount(); i++) {
findViewsById(vg.getChildAt(i), id, viewHandler);
}
}
}
You can set same tag for all such views and then you can get all the views having that tag with a simple function like this:
private static ArrayList<View> getViewsByTag(ViewGroup root, String tag){
ArrayList<View> views = new ArrayList<View>();
final int childCount = root.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = root.getChildAt(i);
if (child instanceof ViewGroup) {
views.addAll(getViewsByTag((ViewGroup) child, tag));
}
final Object tagObj = child.getTag();
if (tagObj != null && tagObj.equals(tag)) {
views.add(child);
}
}
return views;
}
As explained in Shlomi Schwartz answer. Obviously this is not as useful as css classes are. But this might be a little useful as compared to writing code to iterate your views again and again.
I am trying to get have the lower part of list view slide down, by hiding an unhiding linear layout in list_item. The problem is the view seems to get reused in LayoutAdapter so that the change does not just effect the view I intended to apply it to. Instead it shows up wherever the view is reused. How can I restrict the drop down to just the view on which I requested the dropdown? By drop down I mean unhide the linear layout.
There will always be as many Views in the List as can be seen by the user. When the user scrolls, the views that go out of view get reused to show the new list data the user scrolled to. You need to reset the list item's state when they are redrawn.
Add a boolean variable 'expanded' to the object that stores the list data. (The objects you add to the ArrayAdapter). Set expanded = true when the user expands the LinearLayout in the listItem.
public class MyListItem
{
public boolean expanded = false;
// data you are trying to display to the user goes here
// ...
}
Then do this in the list adapter's getView method
public class MyListAdapter extends ArrayAdapter<MyListItem>
{
public MyListAdapter (Context context, ArrayList<AudioPlaylist> objects)
{
super(context, R.layout.list_item, objects);
}
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
LinearLayout rowLayout;
MyListItem item = this.getItem(position);
if (convertView == null)
{
rowLayout = (LinearLayout) LayoutInflater.from(this.getContext()).inflate(R.layout.list_item, parent, false);
}
else
{
rowLayout = (LinearLayout) convertView;
}
//set the textviews, etc that you need to display the data with
//...
LinearLayout expanded = rowLayout.findViewById(R.id.expanded_area_id);
if (item.expanded)
{
//show the expanded area
expanded.setVisibility(View.VISIBLE);
}
else
{
//hide the area
expanded.setVisibility(View.GONE);
}
return rowLayout;
}
}
Make sure your list_item.xml has a LinearLayout wrapping the whole thing otherwise you will get an cast exception.
Hope that helps...