I am facing a very strange issue. I have an Activity, with the ActionBar containing a custom view.
If I load the XML of this custom view like this:
LayoutInflater inflater = LayoutInflater.from(this);
ViewGroup vg = (ViewGroup) inflater.inflate(R.layout.action_search_form,null);
It leaks: the activity is not GC'd. The following fix works (why?):
LayoutInflater inflater = LayoutInflater.from(getApplicationContext());
However if I set a OnClickListener on a child view it leaks again:
ImageButton clear = (ImageButton) vg.findViewById(...);
clear.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// Even if empty
}
});
}
All this happen in a private method of the activity and all views are local variables.
I found a fix but do not understand why it works: The view was simple so I coded it in Java instead of inflating the XML. I suspected it is related to the fact that no context is passed to the LayoutInflater and nothing happens out of the Activity itself, but if someone could help me understand what happened I would greatly appreciate it.
All View classes require a Context parameter. When you are inflating the XML, the context provided to the created View is the same context provided to the LayoutInflater. Your view is retaining a reference to your activity.
The reason why when you use getApplicationContext() and the Activity doesn't leak is because you are passing a context that is global to the entire lifecycle of the application, not just the activity.
** EDIT **
In regards to why the OnClickListener is also retaining your Activity, it is because of a side effect of using anonymous inner classes. By default, these inline implementations retain a reference to the parent / wrapping class instance. This is so you can call methods of the parent from the implementation of the inner.
For example:
myView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
MyActivity.this.doSomethingCool();
}
}
Whether or not you have anything in the method body, the inner class still maintains a reference to the parent class.
Related
This is a followup of my previous question where #pskink adviced me to implement custom ViewGroup that will paint what I need. I succeeded with a prototype using hard coded values. I want to move it to further level where I can pass initialization parameters from an Activity. I need to pass a resource id and open an image to be used in onPaint method.
This is the activity. I can get the ViewGroup instance there but it is already instantiated so it makes no sense to pass the resource id in its constructor. I tried to use a setter but I need a Context to initialize Drawable from the resource.
protected void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_puzzle);
hiddenPicture = (TileGroupLayout) findViewById(R.id.hidddenPictureArea);
hiddenPicture.setPictureResource(R.drawable.pic_cute_girl);
ViewGroup's setter does not have the context like the constructor.
public void setPictureResource(int resourceId) {
int pictureResource = resourceId;
mCustomImage = context.getResources().getDrawable(R.drawable.pic_cute_girl);
pictureRect = mCustomImage.getBounds();
}
How to get from this issue? I need to pass the initialization parameter before ViewGroup is painted. Activity has many onXY() methods to override but there is no similar ViewGroup methods. What is its lifecycle?
inside your ViewGroup class, just call getContext() from anywhere inside of the class.
Edit
on top of getting the context from the ViewGroup class you actually don't need to call getContext().getResources() instead you should call getResources() directly.
Very simple question:
I am inflating a view:
final View dialogView = inflater.inflate(R.layout.dialog_location, null);
final EditText location_input = (EditText) dialogView.findViewById(R.id.location_input);
final View location_button_clear = dialogView.findViewById(R.id.location_button_clear);
final Button current_location_dialog = (Button) dialogView.findViewById ( R.id.current_location_dialog );
And I am later passing a lot of listeners to each child view e.g.
location_button_clear.setOnClickListener(new View.OnClickListener() { ... }
current_location_dialog.setOnClickListener(new View.OnClickListener() { ... }
location_input.addTextChangedListener(new TextWatcher() { ... }
That makes my code a spageti and I do not like it. For maintainability reasons I would like to create a new separate class having inside that file the listeners etc.
Is it possible to extend dialogView as a view? e.g.
public class DialogView extends View{
// somehow here inflate the R.layout.dialog_location
// get the children and set the listeners
}
or my only choice is to extend DialogView as a Fragment e.g.
public class DialogView extends Fragment{
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.dialog_location, container, false);
}
// other stuff here
}
Update:
My main problem is how to inflate the R.layout.dialog_location if I extend the view
You can perfectly extend View for this, as your original shows dialogView as of type View. However, as you didn't specify how are you planning to organize it, I'm not sure how will this enhace your code readability.
In order to maintainability reasons I'd preferably suggest the following:
If you're using the same Listener for several Views, define it separately and assign the same Listener to all the Views that will have the same behavior.
If you're having really lots of Listeners, I'd create a separate class as a Singleton and define the whole implementation there. Afterwards, if you need to assign them to some of your Views, you'd call something like mySingleton.getMyFooListener() on myView.setOnWhateverListener(...).
I have a normal class (not an activity). Inside that class, I have a reference to an activity.
Now I want to access a view (to add a child) contained in the layout xml of that activity.
I don't know the name of the layout file of that activity. I only know the ID of the view, which I want to access (for example: R.id.my_view).
How can I do that?
Regarding the NullPointerException (which you should add to the question), always make sure you've called setContentView() in your Activity before trying to access a View defined in XML. Example usage:
public class MainActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}
...
}
Then, somewhere,
ViewGroup group = (ViewGroup) context.findViewById(R.id.group); // In your example, R.id.my_view
The reason you need to have called setContentView() is that before it's called, your View(Group) doesn't exist. Because findViewById() is unable to find something that doesn't exist, it returns null.
As simple as that!
View view = activity.findViewById(R.id.my_view);
In case of the Layout:
LinearLayout layout = (LinearLayout) activity.findViewById(R.id.my_layoutId);
And to add the Views:
layout.addView(view);
You could make your method accept an Activity parameter and then use it to find the view by id.
Ex:
public class MyClass{
public void doSomething(Activity context){
TextView text=(TextView)context.findViewById(R.id.my_textview);
}
}
Then in your activity:
obj.doSomething(YourActivity.this);
I try my hardest to develop a clever way to clean out piles of Blah blah = (Blah) this.findViewById(R.id.blah) that otherwise pollute the field and the onCreate() method of my little Activity, and to do so, I feel I should not use setContentView() but getViewInflate().inflate() for every View defined in XMLs.
Is Activity.setContentView() is sorta a syntax sugar and it's virtually repeating getViewInflate().inflate() for every View on XML? I read something saying as if they were the same.
If I can get an answer by looking into the code, please tell so. I checked Activity.class, but only comments were found.
The setContentView on your Activity actually calls the setContentView on the Window used by the activity, which itself does a lot more than just inflating the layout.
What you could do is to map the views to the class field using reflexion. You can download a utility class on Github that does this.
It will parse all the views declared in the layout, then try to find the name corresponding to the id in your R.id class. Then it will try to find a field with the same name in the target object and set it with the corresponding view.
For example, if you have a layout like this
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="#+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</LinearLayout>
it will automatically map it to a textView1 field in your activity.
I'm posting my poor research. In summary, Activity.setContentView() delegates PhoneWindow.setContentView() (the only concrete class of Window ) within which LayoutInflater.inflate() is called, so saying "setContentView() == ViewInflate().inflate()" is not so overly off, I guess.
public class Activity extends ContextThemeWrapper{
private Window mWindow;
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initActionBar();
}
public Window getWindow() {
return mWindow;
}
}
public class PhoneWindow extends Window {
private LayoutInflater mLayoutInflater;
#Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
**mLayoutInflater.inflate(layoutResID, mContentParent);**
final Callback cb = getCallback();
if (cb != null) {
cb.onContentChanged();
}
}
}
Actually you're right, there's two ways to achieve the same thing:
1) setContentView(R.layout.layout);
2)
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflater.inflate(R.layout.layout, null);
setContentView(v);
You decide what is more appropriate for you. Hope this helps.
While researching how to create custom compound views in Android, I have come across this pattern a lot (example comes from the Orange11 blog) :
public class FirstTab extends LinearLayout {
private ImageView imageView;
private TextView textView;
private TextView anotherTextView;
public FirstTab(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.firstTab, this);
}
}
I mostly understand how this is working, except for the part where inflate() is called. The documentation says that this method returns a View object, but in this example the author does not store the result anywhere. After inflation, how is the new View created fromt eh XML associated with this class? I thought about assigning it to "this", but that seems very wrong.
thanks for any clarification.
The reference to this would be the viewgroup root. See here:
http://developer.android.com/reference/android/view/LayoutInflater.html#inflate(int, android.view.ViewGroup)
What this means is it is inflating the designated view from xml with this as the parent view. The xml ends up inside the Linear layout defined by the class.
edit: put in the full link as I can't seem to get URLs with brackets to escape properly