In init method, findViewById returns null on R.id.disable_view_content.
public class DisableView extends RelativeLayout {
private View content;
private View disableView;
private int disableBackgroundColor;
private boolean isEnabled;
public DisableView(Context context) {
super(context);
init();
}
public DisableView(Context context, AttributeSet attrs) {
super(context, attrs);
obtainAttributes(context, attrs);
init();
}
public DisableView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
obtainAttributes(context, attrs);
init();
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public DisableView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
obtainAttributes(context, attrs);
init();
}
private void obtainAttributes(Context context, AttributeSet attributeSet) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attributeSet, R.styleable.DisableView, 0, 0);
disableBackgroundColor = typedArray.getColor(R.styleable.DisableView_disableBackground, 0x00000000);
isEnabled = typedArray.getBoolean(R.styleable.DisableView_enabled, false);
typedArray.recycle();
}
private void init() {
content = findViewById(R.id.disable_view_content);
disableView = new View(getContext());
RelativeLayout.LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
lp.addRule(RelativeLayout.ALIGN_TOP, R.id.disable_view_content);
lp.addRule(RelativeLayout.ALIGN_BOTTOM, R.id.disable_view_content);
lp.addRule(RelativeLayout.ALIGN_LEFT, R.id.disable_view_content);
lp.addRule(RelativeLayout.ALIGN_TOP, R.id.disable_view_content);
disableView.setBackgroundResource(disableBackgroundColor);
disableView.setLayoutParams(lp);
setEnabled(isEnabled);
}
public void setEnabled(boolean enabled) {
content.setEnabled(enabled);
if(enabled) disableView.setVisibility(View.GONE);
else disableView.setVisibility(View.VISIBLE);
}
}
Here's xml
<com....view.common.DisableView
android:id="#+id/..._disableView_...Disable"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="#id/disable_view_content"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/..._textView_..."
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/shape_rect_green"
android:gravity="center"
android:paddingTop="18dp"
android:paddingBottom="18dp"
android:layout_marginBottom="12dp"
android:textSize="#dimen/size_text"
android:text="#string/..."
android:textColor="#android:color/white"
android:duplicateParentState="true"/>
</LinearLayout>
</com....view.common.DisableView>
id disable_view_content is in attr.xml
<resources>
<item type="id" name="disable_view_content"/>
</resources>
Everything seems fine, view with the id disable_view_content is a direct child of DisableView. But either in edit mode or on app, i get a null pointer because the reference content is null.
Edit: Here's the stack trace:
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.View.setEnabled(boolean)' on a null object reference
at com....view.common.DisableView.setEnabled(DisableView.java:71)
at com....view.common.DisableView.init(DisableView.java:67)
at com....view.common.DisableView.<init>(DisableView.java:35)
You are calling findViewById() far too early in the View's lifecycle.
Your init() is called directly from your constructor, and before you have even created a layout for your own View. At this point in time, your View has no layout and has no children either.
The earliest you can access child Views is after one of the addView() methods has been called.
Related
I want to implement a custom ImageView with some predefined attributes based on the xml file. To do that I prepared a layout wrapped within merge tag:
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView
android:id="#+id/my_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="center"
android:src="#drawable/icon" />
</merge>
And extended ImageView class:
public class CustomImageView extends LinearLayout{
public ImageFormField(Context context) {
super(context);
init();
}
public ImageFormField(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public ImageFormField(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
#RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public ImageFormField(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
View.inflate(getContext(), R.layout.custom_image_view, this);
}
}
It works so far, but I actually don't need that LinearLayout as I could extend directly from the ImageView. By extending ImageView I'd like to have the possibility to override src parameter from the default layout.
So I removed merge tag to have only ImageView in the layout and tried this:
public class CustomImageView extends AppCompatImageView{
public ImageFormField(Context context) {
super(context);
init();
}
public ImageFormField(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public ImageFormField(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
View.inflate(getContext(), R.layout.custom_image_view, null); //can't pass root here
}
}
... but the image is simply not displayed. I want to be able to use my view this way:
<com.my.package.CustomImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
with possibility to override src attribute. Is there a way to do that by inflating layout or do I have to go deep with attributes (including custom ones)?
UPDATE
By "overriding src attribute I mean that by default image will have source from its xml file, but user can use it that way to pass another value within this custom view:
<com.my.package.CustomImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/another_icon" />
To provide a "default" image but also allow the user to override that default by specifying the android:src attribute, you can do this:
package com.example.stackoverflow;
import android.content.Context;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;
public class CustomImageView extends AppCompatImageView {
public CustomImageView(Context context, AttributeSet attrs) {
super(context, attrs);
if (getDrawable() == null) {
setImageResource(R.drawable.default_image);
}
}
}
Then you can use it like this:
<!-- will display `default_image` -->
<com.example.stackoverflow.CustomImageView
android:layout_width="200dp"
android:layout_height="200dp"/>
Or:
<!-- will display `other_image` -->
<com.example.stackoverflow.CustomImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:src="#drawable/other_image"/>
There's no need to inflate anything inside the custom image view, and no need to create custom attributes (though you could certainly create custom attributes and use them if you wanted to).
You could update the Java code to apply other "default" styles too.
I'm trying to implement a custom view with textview children added dynamically. But it isn't showing any result. there are no error, just nothing shows up:
My Class:
public class CustomPasscodeEntryView extends LinearLayout {
private Context mContext;
private CustomPasscodeEntryView thisView;
public CustomPasscodeEntryView(Context context) {
super(context);
/* same as below */
}
public CustomPasscodeEntryView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
/*same as below*/
}
public CustomPasscodeEntryView(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
thisView = (CustomPasscodeEntryView) inflater.inflate(R.layout.view_passcode_entry,this);
}
public void addDigit(int digit){
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final pinView pin = (pinView) inflater.inflate(R.layout.password_bullet_view,null);
pin.setDigit(digit);
thisView.addView(pin, thisView.getChildCount());
pin.setVisibility(VISIBLE);
}
public void deleteDigit(){
if(getChildCount() > 0)
thisView.removeView(thisView.getChildAt(getChildCount()-1));
}
}
class pinView extends TextView {
int digit;
public pinView(Context context) {
super(context);
}
public pinView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public pinView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setText('•');
}
public void setDigit(int digit) {
this.digit = digit;
this.setText(Integer.toString(digit));
}
}
and my XMLs:
view_passcode_entry:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:animateLayoutChanges="true">
</LinearLayout>
password_bullet_view:
<com.github.lockpin.lollipin.lib.views.pinView
xmlns:android="http://schemas.android.com/apk/res/android"
android:textColor="#android:color/white"
android:text="•"
android:textSize="20sp"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="2.5dp"
/>
EDIT: the view in my activity:
<com.github.lockpin.lollipin.lib.views.CustomPasscodeEntryView
android:id="#+id/custom_passcode_entry_view"
android:layout_width="300dp"
android:layout_below="#id/pin_code_text_view"
android:layout_centerHorizontal="true"
android:layout_height="40dp"/>
I cant see what's wrong, and I've followed tutorials online and made a few changes. Am I missing something here? Or is this plainly wrong?
You use wrong view id view_passcode_entry when inflating view, which is instance of LinearLayout, and typecast it to CustomPasscodeEntryView.
You only need to use this when call addView() in addDigit() because CustomPasscodeEntryView is also instance of LinearLayout. So layout view_passcode_entry can be removed because it is not needed anymore.
this.addView(pin, this.getChildCount());
But if you want to add view view_passcode_entry as child of CustomPasscodeEntryView instance then you need to add thisView also
private ViewGroup thisView;
...
thisView = (ViewGroup) inflater.inflate(R.layout.view_passcode_entry, this);
this.addView(thisView, 0);
and then if you want to use view_passcode_entry as parent of pinView then you can use
thisView.addView(pin, thisView.getChildCount());
Is it because of you have already mentioned the height of CustomPasscodeEntryView in the xml (android:layout_height="40dp")?
Can you try wrap_content instead?
I am really worried about the .findViewById() method and its use in a custom compound view creation. I am not sure about the exact place that it is guaranteed that it will never return null.
Let's say I have this custom compound view and that it is added to some .xml like this:
<com.app.path.TwoButtonsView
android:id="#+id/ok_cancel_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
two_buttons_view.xml:
<?xml version="1.0" encoding="utf-8"?>
<merge>
<LinearLayout 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"
android:orientation="horizontal">
<TextView
android:id="#+id/first_button"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:gravity="center"
android:textAllCaps="true"/>
<TextView
android:id="#+id/second_button"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:gravity="center"
android:textAllCaps="true"/>
</LinearLayout>
</merge>
TwoButtonsView.java
public class TwoButtonsView extends LinearLayout {
// views
private TextView mFirstButtonView;
private TextView mSecondButtonView;
public TwoButtonsView(Context context) {
super(context);
init(context, null);
}
public TwoButtonsView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public TwoButtonsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public TwoButtonsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.two_buttons_view, this);
// retrieve views
mFirstButtonView = (TextView) findViewById(R.id.first_button); // is there a chance it will return null?
mSecondButtonView = (TextView) findViewById(R.id.second_button); // is there a chance it will return null?
}
}
My question is: is there any chance that .findViewById(RESOURCE_ID) will return null right after calling inflater.inflate(R.layout.two_buttons_view, this);, or should I call my init() method on onFinishInflate() callback?
My question is: is there any chance that .findViewById(RESOURCE_ID)
will return null right after calling
inflater.inflate(R.layout.two_buttons_view, this)
No, there is not, as long as the views you are looking for are in the layout and you explicitly added to the view's hierarchy. In the end, findViewById loops on View[], filled up by addView.
A small note about
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.two_buttons_view, this);
ViewGroup has the static method inflate, so you don't need to retrieve the inflater calling getSystemService
I am extending a frame layout
and when in the constructor (after it loaded the view from xml) i call getChildCount()
I get 0. How to fix this ?
public class DisabledFrameLayout extends FrameLayout {
public DisabledFrameLayout(Context context) {
super(context);
init(context, null,0);
}
public DisabledFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context,attrs,defStyle);
}
public DisabledFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context,attrs,0);
}
private void init(Context context, AttributeSet attrs, int defStyle) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DisabledFrameLayout, 0, 0);
if(a.hasValue(R.styleable.DisabledFrameLayout_disable_descendants)) {
boolean disable = a.getBoolean(R.styleable.DisabledFrameLayout_disable_descendants, false);
if(disable) {
disableDescendants(this);
}
}
a.recycle();
}
private void disableDescendants(ViewGroup v) {
for (int i=0; i<v.getChildCount();i++) {
if(v.getChildAt(i) instanceof ViewGroup) {
disableDescendants((ViewGroup)v.getChildAt(i));
}
v.setEnabled(false);
v.setFocusable(false);
v.setFocusableInTouchMode(false);
}
}
}
xml
<com.lenabru.DisabledFrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:disable_descendants="true"
>
<include
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
layout="#layout/fragment_p" />
</com.lenabru.DisabledFrameLayout>
In state of constructing your FrameLayout, children are still not fully attached to the view. So, calling getChildCount() will return you 0.
If you want to iterate over child views and update them, do it inside onLayout() or onMeasure().
Refs:
http://blog.denevell.org/android-custom-views-onlayout-onmeasure.html
ViewGroup - check this example code.
I'm trying something like this
public class CustomViewSubclass extends HorizontalScrollView{
private LinearLayout layout;
public CustomViewSubclass(Context context) {
this(context,null,0);
}
public CustomViewSubclass(Context context, AttributeSet attrs) {
this(context,attr,0);
}
public CustomViewSubclass(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
layout = new LinearLayout(context);
}
// This is called from the `Activity`
public void startAsyncTask() { // code }
// This method is called in the `onPostExecute()` of an `AsyncTask` subclass
public void doSomething(Context context) {
ImageView image = ImageView(context);
layout.addView(image); // NullPointerException here, layout seems to be null
}
but it seems that layout on doSomething() is null. How is that even possible? I'm initializing it on the constructor... and I never re-initialize it again;
I'm adding my custom view via XML
<com.mypackage.CustomViewSubclass
android:layout_width="wrap_content"
android:layout_width="match_parent" />
Ok I fixed it, it was an stupid mistake made by me:
I used super() on the 3 methods, instead of using this().
public CustomViewSubclass(Context context) {
super(context,null,0);
}
public CustomViewSubclass(Context context, AttributeSet attrs) {
super(context,attr,0);
}
public CustomViewSubclass(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
layout = new LinearLayout(context);
}
Solution:
public CustomViewSubclass(Context context) {
this(context,null,0);
}
public CustomViewSubclass(Context context, AttributeSet attrs) {
this(context,attr,0);
}
public CustomViewSubclass(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
layout = new LinearLayout(context);
}