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
Related
I have a view that inherits from ConstraintLayout. Inside this layout I place the children by use of a ConstraintSet.
This works as long as use a given AttributeSet form outside in the constructor:
public AnswerView(Context context, AttributeSet attrs) {
super(context, attrs);
It does not work, if I try to assign the layout otherwise, when I don't have attrs available.
public AnswerView(Context context) {
super(context);
setLayoutParams(new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
In the first case the children get a pretty distribution like defined by the ConstraintSet, in the second case the all line up at the left border.
In both cases the layout stretches full width, as I can prove by setting a background color.
What is missing in my code?
This is not a direct answer to the question. A direct answer is still missing. This answer is of the type, "take a better approach!".
When there are few or no answers to a question, I am usually on a track, that few people go. Then there are reasons why they don't.
My approach to programatically nest views within other views is cool, but it turns out to be difficult to do this without the use of layouts. It's too expensive to set up all the benefits of the configurations programatically, that can easily done within a layout. The API of Android is not well prepared for this.
So I turned back to the approach, to create the view classes based on layouts. This means, I create the view with the two parameter constructor for layouts.
In the enclosing view class I can't create the nested view directly any more, as there is no constructor for this. Instead I read the configured view from a layout.
I created a small class to assist extracting configured subparts of a layout:
public class Cloner {
LayoutInflater inflater;
int layoutId;
public Cloner of(Context context) {
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return this;
}
public Cloner from(#LayoutRes Integer layoutId) {
this.layoutId = layoutId;
return this;
}
public View clone(#IdRes int id) {
assert inflater != null;
View layout = inflater.inflate(layoutId, null);
View view = layout.findViewById(id);
((ViewManager) view.getParent()).removeView(view);
return view;
}
}
It is used like this:
MultipleChoiceAnswerView mcav =
(MultipleChoiceAnswerView) new Cloner().of(getContext())
.from(layoutId).clone(R.id.multipleChoiceAnswerView);
mcav.plugModel(challenge.getAnswer());
This already shows, how I connect the model in a second step, because I can't feed it into by the constructor any more.
In the constructor I first evaluate the given attributes. Then I set up the view by inflating an accompanying second layout file, that I don't show here. So there are two layouts involved, one to configure the input to the constructor, one for the internal layout.
When the call to plugModel happens, the inflated internal layout is used and extended by objects matching the given model. Again I don't create this objects programatically, but read them from a third (or the second) layout file as templates. Again done with the assistance of the Cloner given above.
private Button getButton(final Integer index, String choice) {
Button button = (Button) new Cloner().of(getContext()).from(layoutId).clone(R.id.button);
button.setId(generateViewId());
button.setText(choice);
button.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
answer.choiceByIndex(index);
}
});
return button;
}
In practice I put this object templates (like a button) as children into the second layout file. So I can style it as a whole in Android Studio Designer.
Programatically I remove the templates before I actually fill the layout with the cloned objects. By this approach I only need to maintain one layout file per view class. (The configuration of the constructor happens in the layout file of the enclosing view.)
So if I were to have a LinearLayout and had several children Views inside of it, say like a couple of Buttons a TextView and a CheckBox, using the LinearLayout's getChildAt(x) I would then get an unspecified View. To note, I'm not using an xml in this so it's all done programatically.
public class CustomViewClass extends LinearLayout {
private Context context;
public CustomViewClass (Context context) {
super(context);
this.context = context;
setOrientation(LinearLayout.VERTICAL);
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
setBackgroundColor(Color.DKGRAY);
// Code which adds Buttons and such to the LinearLayout
getChildAt(1)
}
}
The getChildAt(1), is there anyway that I can find out what kind of View it is, whether it's a Button or a TextView or whatever progamatically?
One way to do this is to call getClass. This will get you a Class object representing the type of view.
For example:
Class clazz = getChildAt(1).getClass();
After you have the class, you can do all kinds of things with it. e.g. get the name:
System.out.println(clazz.getName());
Now you know what kind of view it is.
Another way is to use the instanceof operator. Here's an example:
if (getChildAt(1) instanceof TexView) {
// getChildAt(1) is a TextView or an instance of a subclass of TextView
}
Use instanceOf method. Sample example is
if (view instanceof ImageView)
Or You can use below to find out the type of view. But if you want to do any computation on it, instance of is the best choice.
view.getClass
if (view.getClass().getName().equalsIgnoreCase("android.widget.ImageView"))
I'm currently working on an app that contains a timetable screen, which is built in a highly customised way and contains a lot of 'repeated' views.
I've got each individual view that I need (eg, a view for a box that contains the title of an event and the time that it's on) set up in XML, which I inflate in a custom view class. For example:
public class EventCell extends RelativeLayout {
private TextView eventTitle;
private TextView eventTime;
private Button favouritesButton;
public EventCell(Context context) {
super(context);
setupView(context);
}
public EventCell(Context context, AttributeSet attrs) {
super(context, attrs);
setupView(context);
}
private void setupView(Context context) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.timetable_event, this);
eventTitle = (TextView) findViewById(R.id.event_title);
eventTime = (TextView) findViewById(R.id.event_time);
favouritesButton = (Button) findViewById(R.id.favourites_button);
}
...
}
This is fine except for the fact that this view is reused quite a lot in its containing activity. Eg, it might be instantiated 50 times. My problem is that this wastes a lot of memory and causes some devices to crash.
In ListViews, there's the getView() method which gives an a convertView parameter that lets you check if the current row has already been instantiated, and then lets you update the values on that. What I'm after is a similar thing for this custom view; ideally reusing it rather than instantiating it a bunch of times.
If there isn't a way, what's the best method of getting around it? The views themselves aren't particularly complicated but still manage to bring the most devices to their knees it seems.
I have the following problem: I want to add a custom view (custom_view.xml and associated CustomView.java class) to my main activity.
So, I do the following:
1) In my main activity (linked to main.xml):
CustomView customView = new CustomView(this);
mainView.addView(customView);
2) In my CustomView.java class (that I want to link to custom_view.xml):
public class CustomView extends View {
public CustomView(Context context)
{
super(context);
/* setContentView(R.layout.custom_view); This doesn't work here as I am in a class extending from and not from Activity */
TextView aTextView = (TextView) findViewById(R.id.aTextView); // returns null
///etc....
}
}
My problem is that aTextView remains equal to null... It seems clearly due to the fact that my custom_view.xml is not linked to my CustomView.java class. How can I do this link ? Indeed, I tried setContentView(R.layout.custom_view); but it doesn't work (compilation error) as my class extends from View class and not Activity class.
Thanks for your help !!
If I get you correctly, you are trying to build customview from layoutfile(R.layout.custom_view). You want to find a textview from that layout file. Is that right?
If so, you need to inflate the layout file with context u have. Then u can find the textview from the layout file.
Try this.
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflater.inflate(R.layout.custom_view, null);
TextView aTextView = (TextView) v.findViewById(R.id.aTextView);
I would suggest you to try this:
TextView aTextView = (TextView) ((Activity)this.getContext()).findViewById(R.id.aTextView);
It worked for me!!!
Try inflating the customView.
Second, try (I presume that aTextView id is present in side CustomView)
TextView aTextView = (TextView) mainView.findViewById(R.id.aTextView);
Can anyone suggest a way to improve this API8 example? While they say the views could have been defined in XML, what they have in fact done is code them in java. I see why they wanted to. They have added some members to an extended LinearLayout, and the values are determined at runtime.
According to oh, everyone in the universe, the layout directives should move to XML. But for this app, it makes sense to keep setting the text as-is in the runtime logic. So we've got a hybrid approach. Inflate the views, then populate the dynamic text. I'm having trouble figuring out how to accomplish it. Here's the source and what I tried.
from API8 Examples, List4.java
private class SpeechView extends LinearLayout {
public SpeechView(Context context, String title, String words) {
super(context);
this.setOrientation(VERTICAL);
// Here we build the child views in code. They could also have
// been specified in an XML file.
mTitle = new TextView(context);
mTitle.setText(title);
...
I figured since the LinearLayout has an android:id="#+id/LinearLayout01", I should be able to do this in the OnCreate
SpeechView sv = (SpeechView) findViewById(R.id.LinearLayout01);
but it never hits the minimal constructor I added:
public class SpeechView extends LinearLayout {
public SpeechView(Context context) {
super(context);
System.out.println("Instantiated SpeechView(Context context)");
}
...
I just ran into this exact problem myself. What I think you(we) need is this, but I'm still working through some bugs so I can't yet say for sure:
public class SpeechView extends LinearLayout {
public SpeechView(Context context) {
super(context);
View.inflate(context, R.layout.main_row, this);
}
...
I'll be anxious to hear if you have any luck with it.
Edit: It's now working for me just like this.
It looks like you inflated your Layout, which resides in the file main_row.xml. Correct? My need is different. I want to inflate a TextView child of the Layout I have in main.xml.
Still, I used a similar solution. Since I had already inflated the LinearLayout from XML in the onCreate
setContentView(R.layout.main);
what remained is to inflate the TextView from XML in my View constructor. Here's how I did it.
LayoutInflater li = LayoutInflater.from(context);
LinearLayout ll = (LinearLayout) li.inflate(R.layout.main, this);
TextView mTitle = (TextView) ll.findViewById(R.id.roleHeading);
R.id.roleHeading is the id of the TextView I'm inflating.
<TextView android:id="#+id/roleHeading" ... />
To increase efficiency I was able to move the LayoutInflater to an Activity member, so that it just gets instantiated once.