Using <include> in custom compound view results in NullPointerException in onFinishInflate() - java

I'm trying to implement a simple custom dial pad view. It was all working fine until I tried to use <include> to include a "template" for my buttons instead of explicitly defining each button in the XML file. Since I'm gonna have 12 near-identical buttons I thought I'd just make my XML a bit neater and shorter by doing this.
The problem is that in onFinishInflate() in my custom view, when I call findViewById(R.id.dialpad_view_btn1) it returns null. I am of course assigning the id dialpad_view_btn1 to one of my <include>'s. If I simply define the button explicitly instead of including from my "template" it works fine. Is there any workaround for this, or should I just accept that findViewById() and <include> don't play well together?
DialPadView.java
public class DialPadView extends android.support.v7.widget.GridLayout {
public DialPadView(Context context) {
super(context);
initializeViews(context);
}
public DialPadView(Context context, AttributeSet attrs) {
super(context, attrs);
initializeViews(context);
}
public DialPadView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initializeViews(context);
}
private void initializeViews(Context context) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.dialpad_view, this);
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
// This call to findViewById() results in a NullPointerException
((ImageButton) findViewById(R.id.dialpad_view_btn1)).setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// I'm just a dummy
}
});
}
}
dialpad_view.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.GridLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.me.myproject.DialPadView"
xmlns:grid="http://schemas.android.com/apk/res-auto"
android:id="#+id/dialpad_grid_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
grid:alignmentMode="alignMargins"
grid:columnCount="3"
grid:rowCount="4">
<include
android:id="#+id/dialpad_view_btn1"
layout="#layout/dialpad_button" />
<include
android:id="#+id/dialpad_view_btn2"
layout="#layout/dialpad_button" />
</android.support.v7.widget.GridLayout>
dialpad_button.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:grid="http://schemas.android.com/apk/res-auto">
<Button
android:layout_width="0dp"
android:layout_height="0dp"
grid:layout_columnWeight="1"
grid:layout_rowWeight="1"
grid:layout_gravity="fill"
android:layout_margin="5dp"
android:scaleType="centerInside"
android:text="Test" />
</merge>

Problem with ids:
The problem you encountered is caused by mixing <include> and <merge> tags.
Please notice that <merge> serves different purpose and it should not be used in this case, so please use only <incluce> and make <Button> a root in your dialpad_button.xml file. Then all ids should be assigned properly.
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:grid="http://schemas.android.com/apk/res-auto">
android:layout_width="0dp"
android:layout_height="0dp"
grid:layout_columnWeight="1"
grid:layout_rowWeight="1"
grid:layout_gravity="fill"
android:layout_margin="5dp"
android:scaleType="centerInside"
android:text="Test" />
Details:
If you are curious about this problem you can check the parseInclude() method in LayoutInflater source file. You can find there a code handling <merge> inflation. You can see that the code for passing layout and id attributes to the child of <include> is not invoked for <merge> because <merge> should be used for different scenarios. there:
if (TAG_MERGE.equals(childName)) {
// Inflate all children.
rInflate(childParser, parent, childAttrs, false);
} else {
[...]
// We try to load the layout params set in the <include /> tag. If
// they don't exist, we will rely on the layout params set in the
// included XML file.
[...]
// Attempt to override the included layout's android:id with the
// one set on the <include /> tag itself.
TypedArray a = mContext.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.View, 0, 0);
int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID);
// While we're at it, let's try to override android:visibility.
[...]
}
Other problems:
It's not strictly related with your problem with missing ids, but as pskink mentioned you wrongly inflate <android.support.v7.widget.GridLayout> inside your custom DialPadView so you will end up with hierarchy like this:
<DialPadView>
<android.support.v7.widget.GridLayout>
<Button/>
<Button/>
</android.support.v7.widget.GridLayout>
</DialPadView>
but <DialPadView> is already an instance of android.support.v7.widget.GridLayout, so basically you end up having GridLayout inside of GridLayout. Apart from not being optimal it may result in layout looking in a different way than you've imagined it to look and behave.

Related

How to add a background color to a specific component in the UI without causing Overdraw?

Background
I have a TabLayout with some elements in it.
In portrait mode, the elements don't have enough space to appear. So I used:
app:tabMode="scrollable"
On the other hand, in landscape mode, the elements have excess space and I want it to be centered. So I used this approach:
<android.support.design.widget.TabLayout
android:id="#+id/tabs"
style="#style/Tab"
android:layout_width="wrap_content" <!-- Used wrap_content -->
android:layout_height="wrap_content"
android:layout_centerHorizontal="true" <!-- And CenterHorizontal -->
app:tabMode="scrollable" />
But since the background color is null in my MainActivity:
window.setBackgroundDrawable(null)
The TabLayout appears with (Black Wings) in Landscape Mode.
I want it to have #color/colorPrimary wings instead.
So I had two options:
1- Make the background of my MainActivity, not null (aka #color/colorPrimary)
I don't want to do that since all my fragments in the companion ViewPager will experience Overdraw because they all have different backgrounds set programmatically.
OR
2- Add a Container to incubate my TabLayout and set its background with my #color/colorPrimary, like so:
<RelativeLayout
android:id="#+id/tabs_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/colorPrimary">
<android.support.design.widget.TabLayout
android:id="#+id/tabs"
style="#style/Tab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
app:tabMode="scrollable" />
</RelativeLayout>
I will discuss the problems of this approach below.
Problem
Using Option #2 above:
There is still a tiny bit of Overdraw where the two views, parent RelativeLayout and child TabLayout, overlap.
So how can I remove this extra bit of Overdraw?
Thoughts
I am thinking of overriding the OnDraw method in the View class to suit my needs, but the challenge is that how to know the actual positions I would need to ClipRect()
Another thought is to come up with a, you know, (simpler) approach different from Option #2 above, to solve the Background issue.
Thanks in advance.
I looked all over for an answer to this exact problem. In my case I was dynamically adding and removing tabs, so I wanted it to fill the screen when there were only a few tabs, but start scrolling when there were too many rather than shrinking them or putting the titles on two lines. Using the following custom tab layout finally got it working for me. It was key to set the minimum width before calling super.onMeasure().
public class CustomTabLayout extends TabLayout {
public CustomTabLayout(Context context) {
super(context);
}
public CustomTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
ViewGroup tabLayout = (ViewGroup)getChildAt(0);
int childCount = tabLayout.getChildCount();
DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
int tabMinWidth = displayMetrics.widthPixels/childCount;
for(int i = 0; i < childCount; ++i){
tabLayout.getChildAt(i).setMinimumWidth(tabMinWidth);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
Set the tab mode to scrollable in the xml.
<com.package.name.CustomTabLayout
android:id="#+id/my_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="scrollable">
I think you don't need to wrap your TabLayout with the additional RelativeLayout
just add the background to your tabLayout like so,
<android.support.design.widget.TabLayout
android:id="#+id/tabs"
style="#style/Tab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#color/colorPrimary"
android:layout_centerHorizontal="true"
app:tabMode="scrollable" />

Set tag abbreviations for custom views on XML

In my app I extend LinearLayout and RelativeLayout, so every time I need to declare a layout in XML (which you may know it's quite often) I need to write a long tag, in order to point to the view's package.
So the XML files look like this:
<com.company.material.widget.LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.company.material.widget.RelativeLayout
android:layout_width="match_parent"
android:layout_height="#dimen/titlebar_height"
android:background="#color/primary"
android:orientation="horizontal">
<TextView
style="#style/Text.Field.Small"
android:id="#+id/form_title"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
<Button
style="#style/Button.Main"
android:id="#+id/form_submit"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
</com.company.material.widget.RelativeLayout>
<com.company.essentials.view.FormView
android:id="#+id/formview"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
This is chaotic!
Is there any way to abbreviate this? Like AppLinearLayout instead of com.company.material.widget.LinearLayout?
Thanks in advance!
You could move your View class to the android.view package. This would allow you to just write <AppRelativeLayout /> instead of <com.company.material.widget.AppRelativeLayout />, since View tag names without package prefixes are 'auto-completed' to the android.view package.
If you don't want to move your whole class to this package, you may just create a dummy sub-class à la:
package android.view;
public class AppRelativeLayout extends com.company.material.widget.RelativeLayout {
// same constructors as super class
public AppRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
You just have to make sure you don't use the same class names as the Android framework already does, such as RelativeLayout, since that would clash with the existing views and layout names. That's why I named the example above AppRelativeLayout.
If it's worth it to you, you can customize your activity's layout inflater.
public class MyActivity extends Activity implements LayoutInflater.Factory {
#Override public void onCreate(Bundle _icicle) {
super.onCreate(_icicle);
setContentView(R.layout.my_activity);
}
#Override public Object getSystemService(String _name) {
Object service = super.getSystemService(_name);
if(LAYOUT_INFLATER_SERVICE.equals(_name)) {
LayoutInflater myInflater = ((LayoutInflater)service).cloneInContext(this);
myInflater.setFactory(this);
return myInflater;
}
return service;
}
#Override public View onCreateView (String _tag, Context _ctx, AttributeSet _as) {
if("mytag".equals(_tag))
return new MyLinearLayout(_ctx, _as);
else
return null;
}
}
setContentView will call getSystemService to obtain a layout inflater, and for each tag that layout inflater will query each of its factories (including our custom one) to see if the factory knows how to create the object that corresponds to that tag.
There is alternative option as well if you like you can do it like this
<view
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.company.material.widget.LinearLayout"
android:orientation="vertical">

Custom linear layout not showing

I am making a custom layout, but it's not showing, and i do not know why.
Here is the XML file where the class is defined
<com.example.name.gw2applicaton.SpecializationView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="65dp"
android:layout_height="match_parent">
<Button
android:layout_width="50dp"
android:layout_height="50dp"
android:text="yop2" />
<Button
android:layout_width="50dp"
android:layout_height="50dp"
android:text="yop2" />
</LinearLayout>
</com.example.name.gw2applicaton.SpecializationView>
Here is the class, just a constructor
public class SpecializationView extends LinearLayout {
public SpecializationView(Context context) {
super(context);
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.layout_specialization, this, true);
}
}
And finally where the class is used
<?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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<com.example.name.gw2applicaton.SpecializationView
android:id="#+id/view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical">
</com.example.name.gw2applicaton.SpecializationView>
</LinearLayout>
</LinearLayout>
The SpecializationView is not visible, I do not know why.
What am I doing wrong here?
That's not how it works for a custom view, as you are trying to do. Use this convention instead:
<?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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<!-- just include the layout you defined else where -->
<include layout="#layout/layout_specialization"/>
</LinearLayout>
Where layout_specialization.xml is:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="65dp"
android:layout_height="match_parent">
<Button
android:layout_width="50dp"
android:layout_height="50dp"
android:text="yop2" />
<Button
android:layout_width="50dp"
android:layout_height="50dp"
android:text="yop2" />
</LinearLayout>
Note: You should use custom view definitions when you need to modify an existing view or viewgroup to have special programatic functionality, such as positioning, dynamic content, niche widget, etc... When you want to use a view like you are where it is just using existing widget functionality, do as I described. The include xml tag is great for defining an xml layout and re-using it through your project so there is a minimized duplication of code.
EDIT:
The reason you layout is not showing by the way is you have only defined the constructor for programmatically creating a view (via java code, not xml). To allow for an xml definition of your custom view extend the class as follows with the additional constructors needd:
public class SpecializationView extends LinearLayout {
/* Programmatic Constructor */
public SpecializationView(Context context) {
super(context);
init(context, null, 0);
}
/* An XML Constructor */
public SpecializationView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
/* An XML Constructor */
public SpecializationView(Context context, AttributeSet attrs, int resId) {
super(context, attrs, resId);
init(context, attrs, resId);
}
/**
* All initialization happens here!
*/
private void init(Context context, AttributeSet attrs, int resId){
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.layout_specialization, this, true);
}
}
This definition now includes the xml ability to create the custom view (which should now probably work for you). The reason it will work is now you send the attribute set, or the attributes definied via xml to the constructor. Since you didn't include it, it doesn't know what to do for your custom view when defined in xml and you cannot access the layout's attributes that you may define as custom.

extend Android View while using XML?

I want to use Android's typical XML to define my layout but I need to override onScrollChanged() in ScrollView. Here is my current attempt which generates a class cast exception:
class MyHorizontalScrollView extends HorizontalScrollView {
private final AbstractChartActivity abstractChartActivity;
public MyHorizontalScrollView(AbstractChartActivity abstractChartActivity,
Context context) {
super(context);
this.abstractChartActivity = abstractChartActivity;
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
String tag = (String) this.getTag();
// if the instance is listening to the column header scroll, then
// move the body
if (tag.equalsIgnoreCase(AbstractChartActivity.COL_HEADER_SCROLL)) {
abstractChartActivity.goodiBodyHorizontalScrollView.scrollTo(l, 0);
} else {
// if the instance is listening to the body scroll, then move
// the header
abstractChartActivity.columnHeaderHorizontalScrollView.scrollTo(l, 0);
}
}
}
and the layout...
<TableLayout
android:id="#+id/blank_cell_above_labels_table"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
</TableLayout>
<HorizontalScrollView
android:id="#+id/column_header_horizontal_scroll"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="#id/blank_cell_above_labels_table" >
<TableLayout
android:id="#+id/column_header_table"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
</TableLayout>
</HorizontalScrollView>
This line throws a class cast exception because MyHorizontalScrollView extends HorizontalScrollView but is not exactly the same type.
columnHeaderHorizontalScrollView = (HorizontalScrollView) findViewById(R.id.column_header_horizontal_scroll);
I have this all working by programmatically implementing the layout. But I worry it will be a maintenance nightmare so I want to shift functionality to XML where possible.
How can I extend an Android class and use it in XML?
Thanks
Use
<com.example.app.MyHorizontalScrollView
android:id="#+id/column_header_horizontal_scroll"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="#id/blank_cell_above_labels_table" >
Where com.example.app is the package name where MyHorizontalScrollView is.
Then
columnHeaderHorizontalScrollView = (MyHorizontalScrollView) findViewById(R.id.column_header_horizontal_scroll);
Also add 2 more constructors
public MyHorizontalScrollView(Context context) {
super(context);
}
public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
You may want to read a book my Retro Meir Professional Android Application Development. Chapter 4

Finding the ID of a widget in a view class

What I have is a canvas that takes up nearly all of the screen, then under that I want a row of buttons and some other widgets. So I did this.
XML Code
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:id="#+id/myLayout"
android:orientation="vertical"
android:layout_height="match_parent" >
<com.zone.manager.Tab3
android:id="#+id/tab3_display"
android:layout_width="fill_parent"
android:layout_height="620dp" />
<LinearLayout
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="match_parent" >
<Button
android:id="#+id/addZone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add Zone" />
<Button
android:id="#+id/helpZone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Help" />
<SeekBar
android:id="#+id/seekBar1"
android:paddingTop="9dp"
android:layout_width="179dp"
android:layout_height="wrap_content" />
</LinearLayout>
Java Code
public class Tab3 extends View implements OnTouchListener, OnClickListener {
public Tab3(Context context, AttributeSet attrs) {
View parent = (View) getParent();
addZone = (Button) parent.findViewById(R.id.addZone);
addZone.setOnClickListener(this);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
. . . draws a bunch of stuff
}
#Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.addZone:
String w = "W~20~0~0~0~0~0";
Log.d("ZoneSize", "Zone set");
MyApplication.preferences.edit().putString( "ZoneSize", w ).commit();
MyApplication.preferences.edit().putBoolean("ZoneSizeReady", true).commit();
break;
}
}
However my problem with this is that I believe the code is not reconising where the addZone is. Because when I have the addZone.setOnClickListener active my program will crash, but when I don't have it active the layout looks like how I want it. What must I do to fix this?
addZone = (Button) findViewById(R.id.addZone);
addZone will be null because com.zone.manager.Tab3 does not have any children
So it is obvious that your code will crash
So either you will give com.zone.manager.Tab children which require also to change the base class from View to ViewGroup,
or you start with com.zone.manager.Tab parent. Something like
View parent = (View) getParent ();
addZone = (Button) parent.findViewById(R.id.addZone);
i have some tips which will make you avoid such weird bugs and others:
the code of the custom view relies on an xml layout that uses the custom view .
this is bad coding.
instead, you should use LayoutInflater , use a layout xml file for the custom view for it , and then do the "findViewById" and add the clickListeners to any view inside that you wish.
i think it's also wrong to set the custom view to hold the click listener of the other views without checking which of them was clicked . either add a check , or add a different listener for each of them (which i personally prefer) .

Categories