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;
}
Related
Originally I got this error:
The specified child already has a parent. You must call removeView()
on the child's parent first
at
customSection.addView(customLayout);
So I added
((LinearLayout)customLayout.getParent()).removeView(customLayout);
and now get
java.lang.NullPointerException
So if the child has a parent, and I must first remove the child from the parent, why does getParent() return null?
I have an abstract fragment that allows derived classes to supply a custom layout for the list adapter. Relevant code:
Binding:
public void bind(DataObject row) {
View customLayout = getChildItemView(row);
if (customLayout != null) {
((LinearLayout) customLayout.getParent()).removeView(customLayout);
customSection.removeAllViews();
customSection.addView(customLayout);
customSection.setVisibility(View.VISIBLE);
} else {
customLayout.setVisibility(View.INVISIBLE);
}
}
protected View getChildItemView(CommonRow row) {
if (parentView == null) {
parentView = (LinearLayout) LayoutInflater.from(getActivity())
.inflate(R.layout.list_item_custom_section,
new LinearLayout(getActivity()), true);
label = (TextView) parentView.findViewById(R.id.txtData1Label);
value = (TextView) parentView.findViewById(R.id.txtData1Value);
}
label.setText("Minimum");
value.setText(manager.formatMoney(((SpecificDataRow) row).minimum));
return parentView;
}
I've also tried inflater.inflate(R.layout.list_item_custom_section, null) ... false, null / false, what gives?
EDIT:
#allprog, I knew some cleanup was needed. I wrote this at the end of the day somewhat in a hurry. I have since cleaned up the code, and separated out the binding and inflating of the view. Cleaned up code:
private class ViewHolder {
....
public ViewHolder(View v) {
Butterknife.inject(this, v);
View custom = createCustomView(customSection);
if (custom != null) {
customSection.setVisibility(View.VISIBLE);
customSection.addView(custom);
}
}
public void bind(CommonRow row) {
......
bindCustomView(row, customSection);
}
}
Child class:
#Override
protected View createCustomView(ViewGroup parent) {
return LayoutInflater.from(getActivity()).inflate(R.layout.list_item_custom_section, parent, false);
}
#Override
protected void bindCustomView(CommonRow row, ViewGroup section) {
TextView label = Views.findById(section, R.id.txtData1Label);
TextView value = Views.findById(section, R.id.txtData1Value);
label.setText("Minimum");
value.setText(manager.formatMoney(((SpecificRow) row).minimum));
}
suitianshi got it first, with my original [unkempt] code that was the solution.
try this:
public void bind(DataObject row) {
View customLayout = getChildItemView(row);
if (customLayout != null) {
if(customLayout.getParent() != null) {
((LinearLayout)customLayout.getParent()).removeView(customLayout);
}
customSection.removeAllViews();
customSection.addView(customLayout);
customSection.setVisibility(View.VISIBLE);
} else {
customLayout.setVisibility(View.INVISIBLE);
}
}
I have read related source code, getParent should return non-null value when view has a parent. You should make sure it actually has a parent before casting and calling removeView
Wish this helps.
source code :
in View :
public final ViewParent getParent() {
return mParent;
}
in ViewGroup.addViewInner
if (child.getParent() != null) {
throw new IllegalStateException("The specified child already has a parent. " +
"You must call removeView() on the child's parent first.");
}
As I can see your parentView is a field variable, this is the key. So what I suspect is really going on:
First call of bind(): you creating parentView and it is not have a parent yet, customSection.addView(customLayout); works fine, but after you added a check for parent it fails here.
Second call of bind(): parentView is now have a parent and your added check should work now, but you failed at the previous step. Without a check you are failing here with exception in title.
Solution: check for the presence of parent and remove it only if necessery.
First of all LayoutInflater inflate method always returns view without parent.
if attachToRoot == true the parentView will be that new LinearLayout(getActivity())
if attachToRoot == false the parentView will be inflated R.layout.list_item_custom_section whatever it is.
in both cases the ((LinearLayout) customLayout.getParent()) will be null. Thats why you are getting NullPointerException. You can see it in return statement in LayoutInflater documentation.
As is written above declaring parentView as field is bad aproach, it should be method parameter that you will inflate if == null (approach from AdapterView).
BTW: line 9 in your code if would be called it would throw NullPointerException because it is called only in case that customLayout == null!!!
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.
So, I am making this application. The application parses a website, or more specifically a vbulletin-board. When I'm parsing a thread in the forum, I have divided it up so that when I parse each post in that thread, I get the actual content of the post in sections such as this, and I store the sections in the correct order in an array:
[Plain text]
[Quote from somebody]
[Plain text]
[Another quote]
[Another quote again]
[Some more plain text]
However, a post can be arranged in any order as you might know, and can consist of more or fewer sections than in the example, and it doesn't have to have quotes in it either, or it might just be one or several quotes. Anything is possible.
When I list the posts in my application, I am using a ListView. Each row of this listview will then always consist of a header, and any combination of the previously mentioned sections.
The way I was thinking of doing it after googling a bit about it is to have one "Base-layout" with just a layout-tag in one XML-file, and a separate layout for each section, stored in separate XML-files, and at each call to getView() in my adapter, look at the post at that position in my "Post-list", and then loop through the sections in that particular post, and inflate a new "Quote-layout" for each quote-section stored in the post, and inflate a "Plain-text-layout" for each plain-text-section in the post. And for each of those I fill in all the content belonging to that post.
I think this would work, but there might be a performance problem? As I understand it layout inflation is quite expensive, and I won't be able to recycle the View passed in to getView() either, since it might have a bunch of sections added to it that I might not need in another call to getView().. That is, if I understand getView() and the recycling somewhat.
This is a basic example of what I mean with the getView() method of the adapter:
#Override
public View getView(int i, View view, ViewGroup viewGroup) {
// Inflate the base-layout, which the others are added to.
view = mInflater.inflate(R.layout.forum_post,null);
View header = mInflater.inflate(R.layout.post_header_layout, null);
View message = mInflater.inflate(R.layout.post_text_layout, null);
View quote = mInflater.inflate(R.layout.post_quote_layout, null);
((ViewGroup)view).addView(header);
((ViewGroup)view).addView(message);
((ViewGroup)view).addView(quote);
return view;
}
And then inflate more quote-views/message-views as needed when I extract the data from my list of saved posts.
The base-layout is just a LinearLayout-tag
The layouts I inflate are just RelativeLayouts with some TextViews and an ImageView added.
This code produces this result, where I have a Header with
username, picture, etc.., One section of Plain text, and one Quote-section.
This doesn't seem to work properly all the time though, because when I tried it out just now a copy of the list seemed to get stuck on the background and another one scrolled on top of it..
http://s14.postimg.org/rizid8q69/view.png
Is there a better way to do this? Because I imagine this isn't very efficient
You need to override getViewItemType and getViewTypeCount.
getItemViewType(int position) - returns information which layout type you should use based on position
Then you inflate layout only if it's null and determine type using getItemViewType.
Example :
private static final int TYPE_ITEM1 = 0;
private static final int TYPE_ITEM2 = 1;
private static final int TYPE_ITEM3 = 2;
#Override;
public int getItemViewType(int position)
{
int type;
if (position== 0){ // your condition
type = TYPE_ITEM1; //type 0 for header
} else if(position == 1){
type = TYPE_ITEM2; //type 1 for message
}else {
type = TYPE_ITEM3; //type 2 for Quote
}
return type;
}
#Override
public int getViewTypeCount() {
return 3; //three different layouts to be inflated
}
In getView
int type= getItemViewType(i); // determine type using position.
switch (type) {
case TYPE_ITEM1:
view= mInflater.inflate(R.layout.post_header_layout, null); // inflate layout for header
break;
case TYPE_ITEM2:
view = mInflater.inflate(R.layout.post_text_layout, null); // inflate layout for quote
break;
case TYPE_ITEM3:
quote = mInflater.inflate(R.layout.post_quote_layout, null); // inflate layout for message
break;
....
You need to use a View Holder for smooth scrolling and performance.
http://developer.android.com/training/improving-layouts/smooth-scrolling.html
You can check the tutorial below
http://android.amberfog.com/?p=296
First of all you want to reuse convertView that has been passed as one of the argument. This way you can avoid inflating the item View.
Secondly, you could use something as ViewHolder to store references to your inner Views. Using ViewHolder will increase performance whether you are inflating view or finding them by id as both methods are very expensive.
Set the ViewHolder as a Tag on item View.
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
ViewHolder viewHolder;
// if possible reuse view
if (convertView == null) {
final LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(resource, parent, false);
viewHolder = new ViewHolder(mInflater.inflate(R.layout.post_header_layout, null));
view.setTag(viewHolder);
} else {
// reuse view
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
//set text, listeners, icon, etc.
return view;
}
The ViewHolder is just private inner class storing referenced to view.
private static class ViewHolder {
private final View view;
private ViewHolder(View view) {
this.view = view;
}
}
Talk about ListView usage was given at Google IO 2010.
The inflater needs to know the real type of the futur parent ViewGroup, therefore the following code is erroneous:
view = mInflater.inflate(R.layout.forum_post,null);
and instead, you should use this one:
view = mInflater.inflate(R.layout.forum_post,viewGroup,false);
Same thing for the other inflate: use the real parent (view in this case) or another viewGroup which is of the same type as the (futur) parent; otherwise the LayoutParameters will not be set to the right type and the values that you have specified in your XML code will be lost (never used).
I have the same View inflated (from XML) multiple times. When I call findViewById(R.id.my_layout).setVisibility(View.GONE) I want to apply it on all such views.
How do I do that?
There isn't a version of findViewById() that returns all matches; it just returns the first one. You have a few options:
Give them different ids so that you can find them all.
When you inflate them, store the reference in an ArrayList, like this:
ArrayList<View> mViews = new ArrayList<View>();
Then when you inflate:
LayoutInflater inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mViews.add(inflater.inflate(R.layout.your_layout, root));
Then when you want to hide them:
for (View v : mViews) { v.setVisibility(View.GONE); }
Depending on what you're doing with these Views, the parent layout may have a way of accessing them. E.g., if you're putting them in a ListView or some such. If you know the parent element you can iterate through the children:
ViewGroup parent = getParentSomehow();
for (int i = 0; i < parent.getChildCount(); ++i) {
View v = parent.getChildAt(i);
if (v.getId() == R.id.my_layout) {
v.setVisibility(View.GONE);
}
}
If the above options don't work for you, please elaborate on why you're doing this.
Modify on the View that holds the inflated layout.
E.g:
If you have
View v = inflater.inflate(.... );
you change the visibility onto this view. v.setVisibility(View.GONE);