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.
Related
Let's say I have a custom view:
<com.xx.xx.xx.CustomView
android:id="#+id/customView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="doAction"/>
Then in my Activity I have:
public void doAction(View v) {
if(customView == null) customView = (CustomView) v;
... do stuff
}
So as you can see, first time I click the view, I retrieve it and store it in an Activity field. Then I can use it anywhere. With this, I don't need to use findViewById.
I have to questions:
Is this more performant or is the same?
Is this a correct way to go?
Many thanks!
I have my own style for buttons defined as themes but I also use my own class to handle buttons (because of own fonts). Is it possible to call my button with a pretty name such as
<MyButton>
instead of
<com.wehavelongdomainname.android.ui.MyButton>
So the answer, surprisingly, is "yes". I learned about this recently, and it's actually something you can do to make your custom view inflation more efficient. IntelliJ still warns you that its invalid (although it will compile and run successfully) -- I'm not sure whether Eclipse warns you or not.
Anyway, so what you'll need to do is define your own subclass of LayoutInflater.Factory:
public class CustomViewFactory implements LayoutInflater.Factory {
private static CustomViewFactory mInstance;
public static CustomViewFactory getInstance () {
if (mInstance == null) {
mInstance = new CustomViewFactory();
}
return mInstance;
}
private CustomViewFactory () {}
#Override
public View onCreateView (String name, Context context, AttributeSet attrs) {
//Check if it's one of our custom classes, if so, return one using
//the Context/AttributeSet constructor
if (MyCustomView.class.getSimpleName().equals(name)) {
return new MyCustomView(context, attrs);
}
//Not one of ours; let the system handle it
return null;
}
}
Then, in whatever activity or context in which you're inflating a layout that contains these custom views, you'll need to assign your factory to the LayoutInflater for that context:
public class CustomViewActivity extends Activity {
public void onCreate (Bundle savedInstanceState) {
//Get the LayoutInflater for this Activity context
//and set the Factory to be our custom view factory
LayoutInflater.from(this).setFactory(CustomViewFactory.getInstance());
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_with_custom_view);
}
}
You can then use the simple class name in your XML:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<MyCustomView
android:id="#+id/my_view"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_gravity="center_vertical" />
</FrameLayout>
Defining your own subclass of LayoutInflater.Factory seems a lot of work me.
Simply override the Activity's onCreateView() with some generic code:
#Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
View view;
// No need wasting microseconds getting the inflater every time.
// This method gets called a great many times.
// Better still define these instance variables in onCreate()
if (mInflator == null){
mInflator = LayoutInflater.from(context);
mPrefix = ((Activity) context).getComponentName().getClassName();
// Take off the package name including the last period
// and look for custom views in the same directory.
mPrefix = mPrefix.substring(0, mPrefix.lastIndexOf(".")+1);
}
// Don't bother if 'a path' is already specified.
if (name.indexOf('.') > -1) return null;
try{
view = mInflator.createView(name, mPrefix, attrs);
} catch (ClassNotFoundException e) {
view = null;
} catch (InflateException e) {
view = null;
}
// Returning null is no big deal. The super class will continue the inflation.
return view;
}
Note the custom views must reside in the same package (i.e. in the same directory) as this activity, but then it's just a generic piece of code you can slap in any activity (or even better, inherit from a custom parent activity class).
You're not worried about looking out for a particular class as specified in the solution offered by kcoppock:
if (MyCustomView.class.getSimpleName().equals(name)) {....
You're certainly not creating a whole new class.
The real magic is in the core library class, LayoutInflator.java. See the call, mPrivateFactory.onCreateView(), below?:
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, mContext, attrs);
}
if (view == null) {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
}
You see, if the so called, mPrivateFactory, returns null (mPrivateFactory happens to be your activity class by the way), the LayoutInflator just carries on with it's other alternative approach and continues the inflation:
if (view == null) {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
}
It's a good idea to 'walk through' the library classes with your IDE debugger and really see how Android works. :)
Notice the code,if (-1 == name.indexOf('.')) {, is for you guys who still insist on putting in the full path with your custom views, <com.wehavelongdomainname.android.ui.MyButton> If there is a 'dot' in the name, then the creatview() is called with the prefix (the second parameter) as null: view = createView(name, null, attrs);
Why I use this approach is because I have found there were times when the package name is moved (i.e. changed) during initial development. However, unlike package name changes performed within the java code itself, the compiler does not catch such changes and discrepancies now present in any XML files. Using this approach, now it doesn't have to.
Cheers.
You can also do this:
<view
class="com.wehavelongdomainname.android.ui.MyButton"
... />
cf. http://developer.android.com/guide/topics/ui/custom-components.html#modifying
No. You need to to give "full path" to your class, otherwise framework will not be able to inflate your layout.
I'm new to the this excellent site and in the Java programming Android. I started to make a small test application for the listing of my favorite places in my town. I tried to follow some tutorials on different pages, but when I do I put in my project in Eclipse always gives me more than a million mistakes although imports classes and other methods.
I want to build on the example image discotheque next to the image name discos and under that name Diskotek smaller text additional info.
It would be really grateful for all the help
A Custom ListView requires total 4 basic stuff 2 in design (layout .xml) & 2 class (.java)
2 Layout
a) Basically a container which has a listview with either a heading or button depends on you
b) How each row should look like should they have buttons, images, textview how ever you want.
2 Java Class File
a) One is the Activity which you will have definitely.
b) Custom Adapter which says which value from your Activity will go to which View (Button , image ) depending on your requirement.
The best example is to follow this Tutorial
The first thing I think when someone says 'I tried to follow some tutorials...' but they don't work, It gets kind of hard to believe.
Where's the code you tried?
What are the import errors on the editor?
That'd be an easier problem to address.
To give you a simple ListView example:
First, create a resource file at your preference: (example.xml)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/disco_image"
android:layout_alignParentTop="true"
android:src="#drawable/ic_home"
android:layout_width="96dp"
android:layout_height="96dp"/>
<TextView
android:id="#+id/disco_title"
android:padding="12dp"
android:layout_toRightOf="#+id/disco_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="#+id/disco_info"
android:padding="12dp"
android:layout_toRightOf="#+id/disco_image"
android:layout_below="#+id/disco_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>
Then, create a custom adapter, they are very simple. Just extend a BaseAdapter for now.
public class ExampleAdapter extends BaseAdapter {
//Let's create some constants first, to fill out the rows
private static final String [] DISCO_NAMES = {"Disco One", "Disco Two", "Disco Three", "Disco Four"};
private static final String [] DISCO_INFO = {"Some Info One", "Some Info Two", "Some Info Three", "Some Info Four"};
private LayoutInflater mInflater;
//Our custom adapter needs a constructor, so you can create from your activity.
public ExampleAdapter (final Context context) {
//for now, let's just get the context, we'll need it to inflate views
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
#Override
public int getCount() {
//this needs to return to the amount of rows you want to display.
//right now we return a fixed value, this could vary based on your needs
return DISCO_NAMES.length;
}
#Override
public Object getItem(int pos) {
//this is useful for knowing what item is at what position
//for now, let's just return the disco name shall we?
return DISCO_NAMES[pos];
}
#Override
public long getItemId(int pos) {
//This returns an id to the item
//personally I don't use this, so you can just return the position
return pos;
}
#Override
public View getView(int position, View view, ViewGroup viewGroup) {
//Ha, here's the important part
//ListViews reuse rows, so let's check if the view (also known as convertview) is new or being reused
if (view == null) {
//this means it's a new view, so we need to inflate it
view = mInflater.inflate(R.layout.example, null);
}
((TextView) view.findViewById(R.id.disco_title)).setText(DISCO_NAMES[position]);
((TextView) view.findViewById(R.id.disco_info)).setText(DISCO_INFO[position]);
//You can also set some images to the imageview on the layout we created earlier
return view;
}
}
Then, let's create a ListActivity for example purposes. NOTE ListActivit does not require a Layout Resource to be set via setContentView, so we don't call it here.
public class ExampleListActivity extends ListActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//create the adapter
ExampleAdapter mExampleAdapter = new ExampleAdapter(this);
//fill the listView
setListAdapter(mExampleAdapter);
}
}
That should compile as is, but you may want to look into ViewHolder patterns and blah blah blah for performance reasons. Obviously you need to read more, but I hope this helps as a starting point.
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.
I'm trying to write a custom compound view composed by a TextView and an EditText, _compound_view.xml_:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/compoundText"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="#+id/textLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Label" />
<EditText
android:id="#+id/textEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="enter text here" >
</EditText>
and this is the class extending LinearLayout:
public class CompoundView extends LinearLayout {
public CompoundView(Context context, AttributeSet attrs) {
super(context, attrs);
readAttributes(context, attrs);
init(context);
}
public CompoundView(Context context) {
super(context);
init(context);
}
private void init(Context c) {
final LayoutInflater inflater = (LayoutInflater) c
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.compound_view, this);
}
}
Now, if I use 2 of these View in my _activity_main.xml_:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<it.moondroid.compoundview.example.CompoundView
android:id="#+id/compoundview1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true" />
<it.moondroid.compoundview.example.CompoundView
android:id="#+id/compoundview2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/compoundview1" />
</RelativeLayout>
and in the Activity code I only inflate the RelativeLayout, without managing onSaveInstanceState:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
then when I write something in the 2nd EditText and I rotate my device, the same text appears in the EditText of the first custom View.
Why is happening this behaviour?
EDIT:
I solved the issue by removing android:id and using android:tag for the EditText in compound_view.xml, then managing the saving of the EditText state in CompoundView class:
#Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("instanceState", super.onSaveInstanceState());
bundle.putString("currentEdit", mEditText.getText().toString());
bundle.putBoolean("isFocused", mEditText.hasFocus());
return bundle;
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
mEditText.setText(bundle.getString("currentEdit"));
if (bundle.getBoolean("isFocused")) {
mEditText.requestFocus();
}
super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
return;
}
super.onRestoreInstanceState(state);
}
You need to disable SaveEnabled property of EditText using android:saveEnabled="false"
In your custom view, you are inflating layout from XML which has ID defined. Android OS has default functionality to save and restore the state of the view if the view has ID defined.
It means that it will save the text of the EditText when Activity gets paused and restore automatically when Activity gets restored. In your case, you are using this custom view multiple times and that is inflating the same layout so your all EditText have the same ID. Now when Activity will get pause Android will retrieve the value of the EditText and will save against their ID but as you have the same ID for each EditText, values will get override and so it will restore same value in all your EditText.
I'll start off by saying that I haven't confirmed this... but I experienced the same issues when using a compound view, similar to what you were doing.
I think the root of the problem is how Android automatically saves the state of EditText controls, and I think it saves it by "id". So if you have multiple controls in your xml with same "id", then when it saves state, and then restores state, all controls with the same id get the same value. You can try this by adding 2 EditText contols to you normal xml and give them the same android:id value and see if they end up getting the same value.
In your case, you can try to NOT use ids in the compound view and rather find the elements another way, either by tag (View.findViewWithTag), or by name, and see if that makes a difference.
In my case, I solved the problem by doing the latter.
I had the same issue, This is how I made it to work. First need to set false for saveEnabled for editText. We can keep android:id in our layout.
<EditText
android:id="#+id/editText"
android:saveEnabled="false"
Then override below methods in your compound view and manage state by your own. Feel free to ask working example if needed.
#Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("state", super.onSaveInstanceState());
String text = editText.getText().toString();
bundle.putString("text", text);
return bundle;
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
String text = bundle.getString("text");
state = bundle.getParcelable("state");
editText.setText(text);
}
super.onRestoreInstanceState(state);
}
Take a look at my comment in your question and also make sure that you're getting correctly the references to your views.
I'm using your code like this:
CompoundView cv1 = (CompoundView) findViewById(R.id.compoundview1);
TextView tv1 = (TextView) cv1.findViewById(R.id.textEdit);
CompoundView cv2 = (CompoundView) findViewById(R.id.compoundview2);
TextView tv2 = (TextView) cv2.findViewById(R.id.textEdit);
In case you have more complex compound view with many child view, you can consider overriding the dispatchSaveInstanceState of your most outer ViewGroup class and don't call the super implementation.
Like this:
#Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
//If you don't call super.dispatchRestoreInstanceState(container) here,
//no child view gets its state saved
}
I use this, because in my case I have hierarchy of many different compound views that have common super class in which I did this override. (I kept forgetting to set the SaveEnabled attribute to false for new layouts.) However there are many caveats, for example you need to manually save focused view and then request its focus, so your app doesn't behave oddly when screen is rotated.
EDIT:
If you actually need to save state of your compound view, overriding dispatchSaveInstanceState with an empty body will cause onSave/RestoreInstanceState not being called. If you want to use them and still not save state of any of the child views, you need to call super.dispatchFreezeSelfOnly(container) in your override.
More on this here: http://charlesharley.com/2012/programming/views-saving-instance-state-in-android
The issue is happening because of the id field on compound_view.xml
From your code, I just noticed that you inflated compound_view layout file in CompoundView class.
As soon as you create compound_view.xml and put android:id="#+id/textLabel" and android:id="#+id/textEdit" id in your layout xml file, android studio automatically create those ids into int values in R class for single time.
So, when you put CompoundView twice time in your activity_main.xml layout file, you just creating two instance of CompoundView but, both instances textEdit and textLabel have only 1 address location for each one. So, they are pointing to same address locations which are declared in R class.
That's why, whenever you change textEdit or textLabel text programatically, they also change other textEdit or textLabel which are presented in both of your CompoundView
I would like to emphasize a great article, which opened my eyes. It is based on reimplementing onSaveInstanceState() and onRestoreInstanceState(state: Parcelable?).
The advantage of this is that you can use the same compound view multiple times in the same layout (no duplicate ids problem).
In case someone has troubles with incorrect focus being restored after screen rotation, which occurs due to the shared ids of inner views, you can control which id is saved as focused view by overriding findFocus method like this:
override fun findFocus(): View {
if (focusedChild != null) {
return this
}
return super.findFocus()
}
Then the focus gets restored to your compound view, however you should handle the requestFocus call, so the proper child view gets focus upon restoration.
I got the same annoying problem and solved it with another solution than the ones suggested. When the custom view is inflated, I don't find them with IDs or tags, I get them using the getChildAt method. I did not have to save the instance state.
Here is an example:
Custom XML to inflate (custom_view.xml):
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:textAppearance="?android:attr/textAppearanceMedium"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"/>
</merge>
You then just get the views this way during the view initialization:
private void initView(Context context) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.custom_view, this, true);
setOrientation(LinearLayout.HORIZONTAL);
setGravity(Gravity.CENTER);
TextView mTitleTV = (TextView) getChildAt(0);
EditText mContentET = (EditText) getChildAt(1);
}
IMPORTANT: you must remove all IDs from the XML