I did see a bunch of other posts before asking this question. None seemed to have a definite solution.
I am running into a issue with ViewStub where I want to check if a ViewStub is inflated or not (visible or not). I have a bunch of other things to do based on that.
I am using two ViewStubs and a button in each ViewStub layout namely in layout/abc and layout/def will inflate the other ViewStub.
<ViewStub
android:id="#+id/abc_stub"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout="#layout/abc" />
<ViewStub
android:id="#+id/def_stub"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout="#layout/def" />
The issue I am running into is that the ViewStub that is not inflated is not null while I expect it to be null.
Is there anything I am missing ?
I would like to present an alternative solution that does not rely on a listener.
View stub = findViewById(R.id.my_view_stub);
if(stub instanceof ViewStub) {
// it have not been inflated
} else {
// if have been inflated
}
the logic behind that is that after inflation, the ViewStub gets removed from the view hierarchy and replaced by what was on the inflated layout.
That means stub will be an instanceOf FrameLayout/LinearLayout/Button or whatever other view was on the inflated layout and the actual ViewStub instance is gone and if you're not holding a reference to it, it might even have been garbage collected.
So what I ended up doing was to setOnInflateListener (ViewStub.OnInflateListener inflateListener)
ViewStub stub1 = ((ViewStub)findViewById(KR.get(R.abc_stub)));
stub1.setOnInflateListener(new ViewStub.OnInflateListener{
#Override
public void onInflate(ViewStub paramViewStub, View paramView) {
// Set a class boolean if it was inflated
}
});
If the boolean is true stub1 is the screen that is up. Else its not.
Thanks to #pskink for the help.
More elegant solution:
if (mViewStub.getParent() != null) {
//have not been inflated
mViewStub.inflate();
} else {
//already inflated
}
Incase somebody still confuse about this...
1. What does ViewStub.inflate() actual do?
// checkout android.view.ViewStub#inflate
// ...
// Actual inflate the xml
final View view = inflateViewNoAdd(parent);
// removte the same id view in parent, then re-add in view just inflated.
replaceSelfWithView(view, parent);
// ...
2. How do we check if it inflated?
Like Budius' anwser, below is the kotlin version:
/**
* Find ViewStub
* 1. if not yet inflate, then inflate
* 2. If it has been inflated, return it directly
* 3. ViewStub itself does not force the value of inflateId, but if you use this method and you don't set the inflateId, it will set the id of the view to the id of ViewStub when it is inflated.
* 4. If the layout you're inflated to has an id, then don't set the inflateId, as it will override your original id when you inflate
*/
inline fun <reified T : View> View?.findViewStubById(
viewStubId: Int,
inflateId: Int = viewStubId
): T? {
if (this == null) {
return null
}
val find = this.findViewById<T?>(viewStubId) ?: return findViewById(inflateId)
return if (find is ViewStub) {
val view = find.inflate() as T
if (view.id == View.NO_ID) {
view.id = inflateId
}
view
} else {
find
}
}
Related
This is my use case:
I want to change my inflated layout at run time, say first I inflate layout a, then after some time I want to show layout B, then layout C etc.
I read somewhere that rather than including layouts in main layout and then hiding/unhiding I should use viewstub and inflate.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ViewStub
android:id="#+id/layout_stub"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
My issue now is when I inflate the first layout it works fine but the next time when I try to inflate the second layout I get the stub null.
ViewStub stub = (ViewStub) findViewById(R.id.layout_stub);
stub.setLayoutResource(layoutId);
View inflated = stub.inflate();
My understanding is the Viewstub is a container in which the layouts are being loaded, if so
why am I not getting the ViewStub when trying to load the second layout?
(So this means when I inflated the first layout (A) the layout in which the ViewStub was placed was removed completely?)
I'm looking for any pointers to implementing my usecase with Viewstub or alternatives.
A ViewStub is a placeholder, which is replaced by an inflated layout as soon as ViewStub.inflate() is called. It doesn't make sense to call inflate a second time, as the ViewStub will no longer be in the hierarchy. Instead, you should obtain a reference to your LinearLayout, remove its views, and add your second layout as a child.
ViewStub stub = (ViewStub) findViewById(R.id.layout_stub);
LinearLayout ll = (LinearLayout) findViewById(R.id.ll);
stub.setLayoutResource(layoutId);
stub.inflate(); // inflate 1st layout
ll.removeAllViews(); // remove previous view, add 2nd layout
ll.addView(LayoutInflater.from(context).inflate(secondLayoutId, ll, false));
Yes, I think you can easily replace it with another ViewStub and lazily inflate your new layout in this way:
for Java
public static ViewStub deflate(View view) {
ViewParent viewParent = view.getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
int index = ((ViewGroup) viewParent).indexOfChild(view);
int inflatedId = view.getId();
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
((ViewGroup) viewParent).removeView(view);
Context context = ((ViewGroup) viewParent).getContext();
ViewStub viewStub = new ViewStub(context);
viewStub.setInflatedId(inflatedId);
viewStub.setLayoutParams(layoutParams);
((ViewGroup) viewParent).addView(viewStub, index);
return viewStub;
} else {
throw new IllegalStateException("Inflated View has not a parent");
}
}
or Kotlin with an extension
fun ViewStub.deflate(view: View): ViewStub {
val viewParent = view.parent
if (viewParent != null && viewParent is ViewGroup) {
val index = viewParent.indexOfChild(view)
viewParent.removeView(view)
val viewStub = ViewStub(context).apply {
inflatedId = this#deflate.inflatedId
layoutParams = this#deflate.layoutParams
}
viewParent.addView(viewStub, index)
return viewStub
} else {
throw IllegalStateException("Inflated View has not a parent")
}
}
Check out the gist
I am trying to understand how the Tinder like/dislike card system works by looking through this example on GitHub: https://github.com/kikoso/Swipeable-Cards/blob/master/AndTinder/src/main/java/com/andtinder/view/CardStackAdapter.java. I understand the importance of BaseAdapters and populating the view/card with the necessary info. This part of the code that is confusing the hell out of me is this:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
FrameLayout wrapper = (FrameLayout) convertView;
FrameLayout innerWrapper;
View cardView;
View convertedCardView;
if (wrapper == null) {
wrapper = new FrameLayout(mContext);
wrapper.setBackgroundResource(R.drawable.card_bg);
if (shouldFillCardBackground()) {
innerWrapper = new FrameLayout(mContext);
innerWrapper.setBackgroundColor(mContext.getResources().getColor(R.color.card_bg));
wrapper.addView(innerWrapper);
} else {
innerWrapper = wrapper;
}
cardView = getCardView(position, getCardModel(position), null, parent);
innerWrapper.addView(cardView);
} else {
if (shouldFillCardBackground()) {
innerWrapper = (FrameLayout) wrapper.getChildAt(0);
} else {
innerWrapper = wrapper;
}
cardView = innerWrapper.getChildAt(0);
convertedCardView = getCardView(position, getCardModel(position), cardView, parent);
if (convertedCardView != cardView) {
wrapper.removeView(cardView);
wrapper.addView(convertedCardView);
}
}
return wrapper;
}
Why are FrameLayouts being created dynamically? It seems like multiple FrameLayouts are being made with wrapper and inner wrapper? What does wrapper even mean? Why are classes like shouldFillCardBackground() used but not even defined anywhere in the repository?
First of all, it is important to notice that the CardStackAdapter you link to is abstract, so it will never be instantiated directly. Rather, the class SimpleCardStackAdapter will be used for instance.
That said, lets get the purpose of the method clear; The getView() method's purpose is to create and populate a view for the given position.
Creating layouts can be done either by inflating them from xml, or otherwise, by creating them in code. The latter is happening here. The main reason that they create the layout dynamically, is to keep things dynamic. The exact views in the layout depend on some configurations which are checked at runtime (in the method shouldFillCardBackground()). Note that this could also be achieved by creating the layout in xml, then inflate it, and dynamically hide/show (or remove/add) views and layouts. The auther of the code simply chose to do it in code.
A wrapper is an object (in this case a layout) which does not much more that holding other objects (other layouts). In the context of layouts, they are usually used to add some kind of background or padding. That is also what is happening here.
shouldFillCardBackground() is a method, not a class, and it is definitely defined: in line 71.
It is important to realize that it seems that the main puropose of this code is to be an example, a demo. It is possible it is not fully functional and that not everything is implemented the best way (for instance, shouldFillCardBackground() returns true by default).
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!!!
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;
}
Okay so I do this (not actual code)
try {
final View view = LayoutInflater.from(context).inflate(R.layout.ownComponent, null);
} catch (InflateExpection e) {
}
if (view != null) {
// This child is null about 5/10 times!
View child = view.findViewById(R.id.ownComponentChild);
}
I read that after inflate it is not guaranteed that child views are inflated, so what would be neat way to get callback when all childs are ready?
Maybe I misunderstood what you're trying to do, but it seems like you're inflating a View and not a layout...
Try
View view = LayoutInflater.from(context).inflate(R.layout.LAYOUT_THAT_HOLDS_ownComponent, this, true);
and then view will hold the entire layout, from which you can find the child by Id with
view.findViewById(...);
Edit:
Hard to know if it's related as you didn't post enough code but try this:
Get the View view out of the try/catch and put it as a class member. loose the final and cast the child.
example (assuming ownComponentChild is a FrameLayout):
FrameLayout child = (FrameLayout)view.findViewById(R.id.ownComponentChild);
This seems to happen randomly so my only guess is that memory is getting low in this case, because I have to recreate so many UI components fast to get this reproduced.