androidandroid-custom-viewtypedarray

TypedArray.getText() on android.R.attr.text returns null


I am trying to implement a custom textview which uses StaticLayout. To get 'android:text' attribute of the view, I implemented like this in initializing field:

@SupressLint("ResourceType")
class ExampleTextView(context: Context, attrs: AttributeSet?, defStyleAttr: Int): View(...) {
    private var mText: String

    init {
        val styledAttributes = context.obtainStyledAttributes(attrs, intArrayOf(
            ...
            android.R.attr.text,
            ...
        ))

        mText = styledAttributes.getText(1)
        ...

        styledAttributes.recycle()
    }
    ...
}

Then in layout xml:

<com.example.package.ExampleTextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="This is an example text" />

I obviously set 'android:text' in the custom textview. But when I try to get attribute from TypedArray, it returns null. Another try with styledAttributes.getString(1) also does return null.

Stack Trace:

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.package, PID: 19154
    android.view.InflateException: Binary XML file line #10: Binary XML file line #10: Error inflating class com.example.package.ExampleTextView
    Caused by: android.view.InflateException: Binary XML file line #10: Error inflating class com.example.package.ExampleTextView
    Caused by: java.lang.reflect.InvocationTargetException
        at java.lang.reflect.Constructor.newInstance0(Native Method)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
        at android.view.LayoutInflater.createView(LayoutInflater.java:647)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:790)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:730)
        at android.view.LayoutInflater.rInflate(LayoutInflater.java:863)
        at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:824)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:515)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:423)
        at com.example.package.adapter.elementsAdapter.onCreateViewHolder(PatchNoteElementsAdapter.kt:54)
        at androidx.recyclerview.widget.RecyclerView$Adapter.createViewHolder(RecyclerView.java:7078)
        at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6235)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6118)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6114)
        at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2303)
        at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1627)
        at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1587)
        at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:665)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4134)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3851)
        at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4404)
        at android.view.View.layout(View.java:20691)
        at android.view.ViewGroup.layout(ViewGroup.java:6194)
        at com.google.android.material.appbar.HeaderScrollingViewBehavior.layoutChild(HeaderScrollingViewBehavior.java:148)
        at com.google.android.material.appbar.ViewOffsetBehavior.onLayoutChild(ViewOffsetBehavior.java:43)
        at com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior.onLayoutChild(AppBarLayout.java:1996)
        at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayout(CoordinatorLayout.java:918)
        at android.view.View.layout(View.java:20691)
        at android.view.ViewGroup.layout(ViewGroup.java:6194)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
        at android.view.View.layout(View.java:20691)
        at android.view.ViewGroup.layout(ViewGroup.java:6194)
        at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1812)
        at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1656)
        at android.widget.LinearLayout.onLayout(LinearLayout.java:1565)
        at android.view.View.layout(View.java:20691)
        at android.view.ViewGroup.layout(ViewGroup.java:6194)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
        at android.view.View.layout(View.java:20691)
        at android.view.ViewGroup.layout(ViewGroup.java:6194)
        at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1812)
        at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1656)
        at android.widget.LinearLayout.onLayout(LinearLayout.java:1565)
        at android.view.View.layout(View.java:20691)
        at android.view.ViewGroup.layout(ViewGroup.java:6194)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
E/AndroidRuntime:     at com.android.internal.policy.DecorView.onLayout(DecorView.java:765)
        at android.view.View.layout(View.java:20691)
        at android.view.ViewGroup.layout(ViewGroup.java:6194)
        at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2806)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2333)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1473)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7215)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1004)
        at android.view.Choreographer.doCallbacks(Choreographer.java:816)
        at android.view.Choreographer.doFrame(Choreographer.java:751)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:990)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:280)
        at android.app.ActivityThread.main(ActivityThread.java:6706)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
     Caused by: kotlin.KotlinNullPointerException
        at com.example.package.ExampleTextView.<init>(ExampleTextView.kt:66)
        at com.example.package.ExampleTextView.<init>(ExampleTextView.kt:22)
            ... 67 more

Is there anything that I missed?


Solution

  • SHORT ANSWER: Sort your attribute IDs in ascending order.

    I found an answer. It is because the order of IntArray which is being passed as paramter on context.obtainStyledAttributes().

    When I defined styledAttributes, I wrote like this:

    val styledAttributes = context.obtainStyledAttributes(attrs, intArrayOf(
        android.R.attr.fontFamily,
        android.R.attr.text,
        android.R.attr.textSize,
        android.R.attr.textColor,
        android.R.attr.textAlignment,
        android.R.attr.lineSpacingExtra
    ))
    

    According to above, the styledAttributes didn't get ordered. We can know it when we check a value of constant through android studio documentation (Ctrl+Q). Which is equal to this:

    val styledAttributes = context.obtainStyledAttributes(attrs, intArrayOf(
        16843692,
        16843087,
        16842901,
        16842904,
        16843697,
        16843287
    ))
    

    Although I didn't metioned in the question, I also got null when I tried to get value of textSize and lineSpacingExtra.

    Why? Because when obtainStyledAttributes called with unordered list, the attribute value will be null whatever you do; getText(), getString(), getDimension(), getFont(), getResourceId(), etc... if the next value of attribute constant is smaller than before one.

    That's the reason that I got null when I tried to get value of attribute constant 16843087, 16842901, 16843287 which is smaller than before ones; 16843692, 16843087, 16843697.

    In addition, according to Android Developer Reference, they are obviously saying that array of attribute constants must be sorted in ascending order. https://developer.android.com/reference/android/content/res/Resources.Theme#obtainStyledAttributes(android.util.AttributeSet,%20int[],%20int,%20int)

    attrs | int: The desired attributes in the style. These attribute IDs must be sorted in ascending order. This value cannot be null.

    So, to get attribute values correctly, I should did like this:

    val styledAttributes = context.obtainStyledAttributes(attrs, intArrayOf(
        android.R.attr.textSize,
        android.R.attr.textColor,
        android.R.attr.text,
        android.R.attr.lineSpacingExtra,
        android.R.attr.fontFamily,
        android.R.attr.textAlignment
    ))
    

    which is sorted in ascending order very well. Hope this helped for future readers.