Custom TextView - setText() called before construc

2019-04-22 05:11发布

问题:

I've got a problem with my CustomTextView. I'm trying to get a custom value from my layout-xml file and use this in my setText() method. Unfortunately the setText() method gets called before the constructor and because of this I can't use the custom value in this method.

Here's my code (broken down to the relevant parts):

CustomTextView.class

public class CustomTextView extends TextView {

    private float mHeight;
    private final String TAG = "CustomTextView";
    private static final Spannable.Factory spannableFactory = Spannable.Factory.getInstance();

    public CustomTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        Log.d(TAG, "in CustomTextView constructor");
        TypedArray values = context.obtainStyledAttributes(attrs, R.styleable.CustomTextView);
        this.mHeight = values.getDimension(R.styleable.CustomTextView_cHeight, 20);
    }

    @Override
    public void setText(CharSequence text, BufferType type) {
        Log.d(TAG, "in setText function");
        Spannable s = getCustomSpannableString(getContext(), text);
        super.setText(s, BufferType.SPANNABLE);
    }

    private static Spannable getCustomSpannableString(Context context, CharSequence text) {
        Spannable spannable = spannableFactory.newSpannable(text);
        doSomeFancyStuff(context, spannable);
        return spannable;
    }

    private static void doSomeFancyStuff(Context context, Spannable spannable) {
        /*Here I'm trying to access the mHeight attribute.
        Unfortunately it's 0 though I set it to 24 in my layout 
        and it's correctly set in the constructor*/
    }
}

styles.xml

<declare-styleable name="CustomTextView">
    <attr name="cHeight" format="dimension"/>
</declare-styleable>

layout.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ctvi="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.mypackage.views.CustomTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/my_fancy_string"
        android:textSize="16sp"
        ctvi:cHeight="24dp" />

</LinearLayout>

And just as a proof - here's the LogCat output:

30912-30912/com.mypackage.views D/CustomTextView﹕ in setText function
30912-30912/com.mypackage.views D/CustomTextView﹕ in CustomTextView constructor

So as you can see the setText() method is called before the constructor. That's kinda weird and I don't know what I need to change in order to use my custom attribute (cHeight) in the setText-method.

Thanks in advance for any help!

回答1:

It's the TextView super() constructor that calls your setText() based on the attribute values.

If you really need to access your custom attribute when setting a text value, use a custom attribute for the text as well.



回答2:

I don't think any of this solutions are good, IMHO. What if you just use a custom method, like setCustomText() instead of overriding the custom TextView.setText(). I think it could be much better in terms of scalability, and hacking / overriding the implementation of the TextView could lead you into future problems.

Cheers!



回答3:

First of all, remember to always recycle the TypedArray after using it.

TextView calls #setText(CharSequence text, BufferType type) during its construction therefore define a delayed call to setText as so:

private Runnable mDelayedSetter;
private boolean mConstructorCallDone;

public CustomTextView(Context context, AttributeSet attrs) {
    super(context, attrs);
    Log.d(TAG, "in CustomTextView constructor");
    TypedArray values = context.obtainStyledAttributes(attrs, R.styleable.CustomTextView);
    this.mHeight = values.getDimension(R.styleable.CustomTextView_cHeight, 20);
    mConstructorCallDone = true;
}

Then inside your setText-override:

public void setText(final CharSequence text, final TextView.BufferType type) {
    if (!mConstructorCallDone) {
        // The original call needs to be made at this point otherwise an exception will be thrown in BoringLayout if text contains \n or some other characters.
        super.setText(text, type);
        // Postponing setting text via XML until the constructor has finished calling
        mDelayedSetter = new Runnable() {
            @Override
            public void run() {
                CustomTextView.this.setText(text, type);
            }
        };
        post(mDelayedSetter);
    } else {
        removeCallbacks(mDelayedSetter);
        Spannable s = getCustomSpannableString(getContext(), text);
        super.setText(s, BufferType.SPANNABLE);
    }
}


回答4:

Unfortunately it is a limitation on Java, that requires to call super(..) in constructor before anything else. So, your only workaround is to call setText(..) again after you initialize the custom attributes.

Just remember, as setText called also before you initialize your custom attributes, they may have null value and you can get NullPointerException

Check my example of customTextView which capitalize first letter and adds double dots at the and (I use it in all my activities)

package com.example.myapp_android_box_detector;

import android.content.Context;
import android.content.res.TypedArray;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;

public class CapsTextView extends AppCompatTextView {
    public Boolean doubleDot;
    private Boolean inCustomText = false;

    public CapsTextView(Context context){
        super(context);
        doubleDot = false;
        setText(getText());
    }

    public CapsTextView(Context context, AttributeSet attrs){
        super(context, attrs);
        initAttrs(context, attrs);
        setText(getText());
    }

    public CapsTextView(Context context, AttributeSet attrs, int defStyle){
        super(context, attrs, defStyle);
        initAttrs(context, attrs);
        setText(getText());
    }

    public void initAttrs(Context context, AttributeSet attrs){
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CapsTextView, 0, 0);
        doubleDot = a.getBoolean(R.styleable.CapsTextView_doubleDot, false);
        a.recycle();
    }

    @Override
    public void setText(CharSequence text, BufferType type) {
        if (text.length() > 0){
            text = String.valueOf(text.charAt(0)).toUpperCase() + text.subSequence(1, text.length());
            // Adds double dot (:) to the end of the string
            if (doubleDot != null && doubleDot){
                text = text + ":";
            }
        }
        super.setText(text, type);
    }
}