I want to create a custom Compound Control in Android that holds some logic. For the purpose of this example, let's say I want it to switch between two views when clicked.
According to the API guide, it looks like the way to do that is to create a new class that extends Layout, and do everything in there.
So I did just that:
I created a XML layout to inflate for my custom component:
.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/view1"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="Hello"/>
<TextView
android:id="#+id/view2"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="World"
android:visibility="gone"/>
</RelativeLayout>
Then I created my custom Layout class, and added the logic in there:
public class MyWidget extends RelativeLayout {
public final View mView1;
public final View mView2;
public MyWidget(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
RelativeLayout view = (RelativeLayout) inflater.inflate(R.layout.my_widget, this, true);
mView1 = view.findViewById(R.id.view1);
mView2 = view.findViewById(R.id.view2);
view.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
switchViews();
}
});
}
public void switchViews() {
if (mView1.getVisibility() == View.VISIBLE) {
mView1.setVisibility(View.GONE);
} else {
mView1.setVisibility(View.VISIBLE);
}
if (mView2.getVisibility() == View.VISIBLE) {
mView2.setVisibility(View.GONE);
} else {
mView2.setVisibility(View.VISIBLE);
}
}
}
And finally, I included my custom view in some layout:
.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.example.MyWidget
android:layout_height="match_parent"
android:layout_width="match_parent"/>
</RelativeLayout
And that works.
I am not completely happy with that solution though, for 2 reasons:
In the constructor of MyWidget, I instantiate 2 nested RelativeLayout by calling the super() constructor, and the one that is at the root of my XML layout. For that, I know I can instead use <merge> as my XML root and that gets me rid of the extra RelativeLayout. Except that defining XML attributes, such as android:background on my <merge> tag doesn't have any effect, so I have to define it programmatically, which is not as nice.
The custom View is a subclass of RelativeLayout, and therefore expose methods it inherits from it, such as addView(), even if it doesn't make sense to add child views to it. I know I can override those methods to prevent users from doing that, but I would still find it cleaner to inherit from View.
Related
I want to implement custom ViewGroup in my case derived from FrameLayout but I want all child views added from xml to be added not directly into this view but in FrameLayout contained in this custom ViewGroup.
Let me show example to make it clear.
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="#+id/frame_layout_child_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<FrameLayout
android:id="#+id/frame_layout_top"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</merge>
And I want to redirect adding all child view to FrameLayout with id frame_layout_child_container.
So of course I overrode methods addView() like this
#Override
public void addView(View child) {
this.mFrameLayoutChildViewsContainer.addView(child);
}
But for sure this doesn't work because for this time mFrameLayoutChildViewsContainer is not added to the root custom view.
My idea is always keep some view on on the top in this container frame_layout_top and all child views added into custom component should go to frame_layout_child_container
Example of using custom view
<CustomFrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"/>
</CustomFrameLayout>
So in this case TextView should be added to the frame_layout_child_container
Is it possible to delegate adding all views into child ViewGroup like I described.
I have other ideas like using bringToFront() method every time view is added to keep them in correct z-axis order or for example when view is added, save it to array and than after inflating custom view add all views to this child FrameLayout
Suggest what to do in this case in order not to hit performance with reinflating all layout every time new view is added, if it is possible to implement in other way.
Views inflated from a layout - like your example TextView - are not added to their parent ViewGroup with addView(View child), which is why overriding just that method didn't work for you. You want to override addView(View child, int index, ViewGroup.LayoutParams params), which all of the other addView() overloads end up calling.
In that method, check if the child being added is one of your two special FrameLayouts. If it is, let the super class handle the add. Otherwise, add the child to your container FrameLayout.
public class CustomFrameLayout extends FrameLayout {
private final FrameLayout topLayout;
private final FrameLayout containerLayout;
...
public CustomFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.custom, this, true);
topLayout = (FrameLayout) findViewById(R.id.frame_layout_top);
containerLayout = (FrameLayout) findViewById(R.id.frame_layout_child_container);
}
#Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
final int id = child.getId();
if (id == R.id.frame_layout_top || id == R.id.frame_layout_child_container) {
super.addView(child, index, params);
}
else {
containerLayout.addView(child, index, params);
}
}
}
I have a custom Table Row that I am in the process of making. I want to use an XML file to define what a single row looks like. I would like to have a class extend TableRow and define itself to be the file as defined in the XML. The XML file might look like:
<TableRow xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<TextView
android:id="#+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingRight="10dp"
android:text="#string/loading"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="#+id/data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:text="#string/loading"
android:textAppearance="?android:attr/textAppearanceLarge" />
</TableRow>
And the code might look like:
public class SpecialTableRow extends TableRow {
public SpecialTableRow (Context context) {
}
}
Is there something that I can put into the constructor to have the class assume it is the tableRow in it's entirety? Alternatively, is there another structure which would work better? The best that I've figured out is this:
TableRow tr=(TableRow) LayoutInflater.from(context).inflate(R.layout.text_pair,null);
TextView mFieldName=(TextView) tr.findViewById(R.id.label);
TextView mValue=(TextView) tr.findViewById(R.id.data);
tr.removeAllViewsInLayout();
addView(mFieldName);
addView(mValue);
But this removes the layout parameters from the XML. Anything better out there?
Take a look at the tutorial on creating custom views. You will want to subclass TableRow and add the additional views you want to display. Then, you can use your new view directly in your XML layouts and additionally create any custom attributes you might want. I've included an example which creates a custom TableRow named TextPairRow, inflates a layout with two TextViews to show within the TableRow and adds showLabel and showData custom attributes which show/hide the two TextViews. Finally, I've included how you would use your new view directly in your XML layouts.
class TextPairRow extends TableRow {
private TextView label, data;
public TextPairRow (Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.TextPairRow, 0, 0);
try {
showLabel = a.getBoolean(R.styleable.TextPairRow_showLabel, false);
showData = a.getBoolean(R.styleable.TextPairRow_showData, false);
} finally {
a.recycle();
}
initViews();
}
private void initViews(){
// Here you can inflate whatever you want to be in your
// view or add views programatically.
// In this example, we'll just assume you have a basic XML
// layout which defines a LinearLayout with two TextViews.
LinearLayout mLayout = (LinearLayout)
LayoutInflater.from(getContext()).inflate(R.layout.textview_layout, this);
label = (TextView) mLayout.findViewById(R.id.label);
data = (TextView) mLayout.findViewById(R.id.data);
if(showLabel)
label.setVisibility(View.VISIBLE);
else
label.setVisibility(View.GONE); // can also use View.INVISIBLE
// depending on your needs
if(showData){
data.setVisibility(View.VISIBLE);
else
data.setVisibility(View.GONE); // can also use View.INVISIBLE
// depending on your needs
}
}
This is where you define your custom XML attributes (locate or create this file: res/values/attrs.xml)
<resources>
<declare-styleable name="TextPairRow">
<attr name="showText" format="boolean" />
<attr name="showLabel" format="boolean" />
</declare-styleable>
</resources>
Finally, to use your new view directly in your XML layouts:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto">
<com.thefull.packageforyourview.TextPairRow
android:orientation="horizontal"
custom:showData="true"
custom:showLabel="true" />
</LinearLayout>
Note that you might need to use xmlns:custom="http://schemas.android.com/apk/res/com.thefull.packageforyourview" depending on if your custom view will be in a library project. Regardless, either this or what's in the example will work.
The real trick to doing this is actually quite simple. Use the second parameter of the inflate method. In fact, the best thing to do is this:
LayoutInflater.from(context).inflate(R.layout.text_pair,this);
This will inflate the R.layout.text_pair into this, effectively using the entire row. No need to add the view manually, Android takes care of it for you.
The only thing I can think of is to use a static method instead of constructor. For example:
public static void newInstance (Context context) {
this = context.getLayoutInflater().inflate(R.layout.text_pair, null, null);
}
Then don't use constructor for initializing an object, call this method.
So I'm experimenting with implementing an MVC pattern in Android where my views are subclassed from RelativeLayout, LinearLayout, ScrollView, etc... It's working until I try to get a hold of a view within my view. I get an NPE. I've tried accessing the view in order to set the onClickListener in the constructor and also in onAttachedToWindow(), but I get the NPE in both places.
For example, here's a view class:
public class ViewAchievements extends LinearLayout
{
private RelativeLayout mRelativeLayoutAchievement1;
public ViewAchievements(Context context, AttributeSet attrs)
{
super(context, attrs);
mRelativeLayoutAchievement1 = (RelativeLayout) findViewById(R.id.relativeLayout_achievement1);
mRelativeLayoutAchievement1.setOnClickListener((OnClickListener) context); //NPE on this line
}
#Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
mRelativeLayoutAchievement1.setOnClickListener(mOnClickListener); //Also get NPE on this line
}
}
Can someone please tell me the proper way to get a hold of my subviews, in this case mRelativeLayoutAchievement1?
Here's an XML snippet:
<com.beachbody.p90x.achievements.ViewAchievements xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#color/gray_very_dark"
android:orientation="vertical" >
<!-- kv Row 1 -->
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="0dp"
android:orientation="horizontal"
android:layout_weight="1"
android:baselineAligned="false">
<RelativeLayout
android:id="#+id/relativeLayout_achievement1"
style="#style/linearLayout_achievement"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_margin="#dimen/margin_sm"
android:layout_weight="1" >
<TextView
android:id="#+id/textView_achievement1"
style="#style/text_small_bold_gray"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="#dimen/margin_large"
android:text="1/20" />
</RelativeLayout>
...
And here's how I'm creating the view from my Activity:
public class ActivityAchievements extends ActivitySlidingMenu
{
private ViewAchievements mViewAchievements;
#Override
public void onCreate(Bundle savedInstanceState)
{
mViewAchievements = (ViewAchievements) View.inflate(this, R.layout.view_achievements, null);
setContentView(mViewAchievements);
...
You're trying to get the child views during the view's constructor. Since they are child views, they haven't been inflated yet. Can you move this code out of the constructor, possibly into View.onAttachedToWindow()?
http://developer.android.com/reference/android/view/View.html#onAttachedToWindow()
Sorry if this redundant with the ton of questions/answers on inflate, but I could not get a solution to my problem.
I have a compound view (LinearLayout) that has a fixed part defined in XML and additional functionalities in code. I want to dynamically add views to it.
Here is the XML part (compound.xml):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/compoundView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView android:id="#+id/myTextView"
android:layout_width="110dp"
android:layout_height="wrap_content"
android:text="000" />
</LinearLayout>
I have defined in code a LinearLayout to refer to the XML:
public class CompoundControlClass extends LinearLayout {
public CompoundControlClass (Context context) {
super(context);
LayoutInflater li;
li = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
li.inflate(R.layout.compound_xml,*ROOT*, *ATTACH*);
}
public void addAView(){
Button dynBut = new Button();
// buttoin def+layout info stripped for brevity
addView(dynBut);
}
}
I tried to programmatically add a view with addAView.
If ROOT is null and ATTACH is false, I have the following hierarchy (per HierarchyViewer):
CompoundControlClass>dynBut
The original TextView in the XML is gone.
If ROOT is this and ATTACH is true, I have the following hierarchy:
CompoundControlClass>compoundView>myTextView
CompoundControlClass>dynBut
I would like to have
CompoundControlClass>myTextView
CompoundControlClass>dynBut
where basically the code and XML are only one unique View.
What have I grossly missed?
ANSWER BASED on feedback from D Yao ----------------------
The trick is to INCLUDE the compound component in the main layout instead of referencing it directly.
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">
<include layout="#layout/comound"
android:id="#+id/compoundView"
android:layout_alignParentBottom="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>
mainActivity.java
public class MainActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CompoundControlClass c = (CompoundControlClass) this.findViewById(R.id.compoundView);
c.addAView(this);
}
}
CompoundControlClass.java
public class CompoundControlClass extends LinearLayout {
public CompoundControlClass(Context context) {
super(context);
}
public CompoundControlClass(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CompoundControlClass(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void addAView(Context context){
ImageView iv = new ImageView(context);
iv.setImageResource(R.drawable.airhorn);
addView(iv);
}
}
compound.xml
<com.sounddisplaymodule.CompoundControlClass xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/compoundView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:layout_width="110dp"
android:layout_height="wrap_content"
android:gravity="right"
android:textSize="40sp"
android:textStyle="bold"
android:text="0:00" />
</com.sounddisplaymodule.CompoundControlClass>
Why not just call addView on the linearlayout? I don't see the need for CompoundControlClass based on the needs you have listed.
LinearLayout v = (LinearLayout)findViewById(R.id.compoundView);
v.addView(dynBut);
In this case, v will contain myTextView, then dynBut.
if you wish to have other functions added and thus really feel a need for creating the compound control class, just leave the constructor as super(etc) and remove the rest
Then your xml would look like this:
<com.yourpackage.CompoundControlClass xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/compoundView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView android:id="#+id/myTextView"
android:layout_width="110dp"
android:layout_height="wrap_content"
android:text="000" />
</com.yourpackage.CompoundControlClass>
you will also have to ensure your CompoundControlClass.java contains the appropriate Constructor which takes both a Context and an attribute set.
Then, in your java, after you've called setContentView, you can do the following:
CompoundControlClass c = (CompoundControlClass)findViewById(R.id.compoundView);
Button b = new Button(context);
//setup b here or inflate your button with inflater
c.addView(b);
this would give you your desired heirarchy.
I come to you on bended knee, question in hand. I am relatively new to Android, so pardon any sacrilegious things I might say.
Intro: I have several layouts in the app, that all have to include a common footer. This footer has some essential buttons for returning to the home page, logging out, etc.
I managed to get this footer to appear in all the requisite pages with the help of the Include and Merge tags. The issue lies in defining on click listeners for all the buttons. Although I can define the listeners in every activity associated with screens that include the footer layout, I find that this becomes terribly tedious when the number of screens increases.
My question is this: Can I define a button click listener that will work across the application, which can be accessed from any screen with the use of the android:onClick attribute of the Button?
That is to say, I would like to define the button click listener once, in a separate class, say FooterClickListeners, and simply name that class as the listener class for any button clicks on the footer. The idea is to make a single point of access for the listener code, so that any and all changes to said listeners will reflect throughout the application.
I had the same problem with a menu which I used in several layouts. I solved the problem by inflating the layout xml file in a class extending RelativeLayout where I then defined the onClickListener. Afterwards I included the class in each layout requiring the menu. The code looked like this:
menu.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ImageButton android:id="#+id/map_view"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:src="#drawable/button_menu_map_view"
android:background="#null"
android:scaleType="fitCenter"
android:layout_height="#dimen/icon_size"
android:layout_width="#dimen/icon_size">
</ImageButton>
<ImageButton android:id="#+id/live_view"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:src="#drawable/button_menu_live_view"
android:background="#null"
android:scaleType="fitCenter"
android:layout_height="#dimen/icon_size"
android:layout_width="#dimen/icon_size">
</ImageButton>
<ImageButton android:id="#+id/screenshot"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:src="#drawable/button_menu_screenshot"
android:background="#null"
android:scaleType="fitCenter"
android:layout_height="#dimen/icon_size"
android:layout_width="#dimen/icon_size">
</ImageButton>
</merge>
MenuView.java
public class MenuView extends RelativeLayout {
private LayoutInflater inflater;
public MenuView(Context context, AttributeSet attrs) {
super(context, attrs);
inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.menu, this, true);
((ImageButton)this.findViewById(R.id.screenshot)).setOnClickListener(screenshotOnClickListener);
((ImageButton)this.findViewById(R.id.live_view)).setOnClickListener(liveViewOnClickListener);
((ImageButton)this.findViewById(R.id.map_view)).setOnClickListener(mapViewOnClickListener);
}
private OnClickListener screenshotOnClickListener = new OnClickListener() {
public void onClick(View v) {
getContext().startActivity(new Intent(getContext(), ScreenshotActivity.class));
}
};
private OnClickListener liveViewOnClickListener = new OnClickListener() {
public void onClick(View v) {
getContext().startActivity(new Intent(getContext(), LiveViewActivity.class));
}
};
private OnClickListener mapViewOnClickListener = new OnClickListener() {
public void onClick(View v) {
getContext().startActivity(new Intent(getContext(), MapViewActivity.class));
}
};
}
layout.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/main"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<SurfaceView android:id="#+id/surface"
android:layout_width="fill_parent"
android:layout_weight="1"
android:layout_height="fill_parent">
</SurfaceView>
<!-- some more tags... -->
<com.example.inflating.MenuView
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</RelativeLayout>
with the <com.example.inflating.MenuView /> tag, you are now able to reuse your selfwritten Layout (incl onClickListener) in other layouts.
This is something that is getting added to roboguice in the near the future. It will allow you to build controller classes for things like titlebar's and footers and have the events autowired for you.
Checkout http://code.google.com/r/adamtybor-roboguice/ for the initial spike.
Basically if you are using roboguice you can define a component for footer and just inject that footer component into each activity.
Unfortunately you still have to add the controller to every activity, just like you did with the include layout, but the good news is everything gets wired up for you and all your logic stays in a single class.
Below is some pseudo code of some example usage.
public class FooterController {
#InjectView(R.id.footer_button) Button button;
#Inject Activity context;
#ContextObserver
public void onViewsInjected() {
button.setOnClickListener(new OnClickListener() {
void onClick() {
Toast.makeToast(context, "My button was clicked", Toast.DURATION_SHORT).show();
}
});
}
}
public class MyActivity1 extends RoboActivity {
#Inject FooterController footer;
}
public class MyActivity2 extends RoboActivity {
#Inject FooterController footer;
}
The solution as you describe is impossible, sorry. But you can have common parent activity for all your activities that use the footer. In the activity just provide handler methods for your footer buttons, then just inherit from it every time you need to handle the footer actions.