5

I'm trying to inflate a simple PopupMenu for rename/delete options when a RecylerView item is longClicked. For some reason i'm getting an XML inflate error when I call mPopup.show() after loading my xml file into the inflater.

I use similar logic elsewhere in my app to make a PopupMenu and it works fine. I've even tried loading the working PopupMenu from an unrelated part of the app into this inflater and I see the same android.view.InflateException: Binary XML file line #17: Failed to resolve attribute at index 1 error in logcat, so maybe the XML file isn't the problem?

How can I get this PopupMenu to inflate and show itself?

Fatal Exception Logcat

05-31 23:02:27.421 19597-20019/? E/AndroidRuntime: FATAL EXCEPTION: main
                                               Process: com.example.foo, PID: 19597
                                               android.view.InflateException: Binary XML file line #17: Failed to resolve attribute at index 1: TypedValue{t=0x2/d=0x7f01005d a=-1}
                                               Caused by: java.lang.UnsupportedOperationException: Failed to resolve attribute at index 1: TypedValue{t=0x2/d=0x7f01005d a=-1}
                                                   at android.content.res.TypedArray.getLayoutDimension(TypedArray.java:761)
                                                   at android.view.ViewGroup$LayoutParams.setBaseAttributes(ViewGroup.java:7060)
                                                   at android.view.ViewGroup$MarginLayoutParams.<init>(ViewGroup.java:7241)
                                                   at android.widget.FrameLayout$LayoutParams.<init>(FrameLayout.java:438)
                                                   at android.widget.FrameLayout.generateLayoutParams(FrameLayout.java:370)
                                                   at android.widget.FrameLayout.generateLayoutParams(FrameLayout.java:369)
                                                   at android.view.LayoutInflater.inflate(LayoutInflater.java:505)
                                                   at android.view.LayoutInflater.inflate(LayoutInflater.java:426)
                                                   at android.support.v7.view.menu.MenuAdapter.getView(MenuAdapter.java:93)
                                                   at android.support.v7.view.menu.MenuPopup.measureIndividualMenuWidth(MenuPopup.java:160)
                                                   at android.support.v7.view.menu.StandardMenuPopup.tryShow(StandardMenuPopup.java:153)
                                                   at android.support.v7.view.menu.StandardMenuPopup.show(StandardMenuPopup.java:187)
                                                   at android.support.v7.view.menu.MenuPopupHelper.showPopup(MenuPopupHelper.java:290)
                                                   at android.support.v7.view.menu.MenuPopupHelper.tryShow(MenuPopupHelper.java:175)
                                                   at android.support.v7.view.menu.MenuPopupHelper.show(MenuPopupHelper.java:141)
                                                   at android.support.v7.widget.PopupMenu.show(PopupMenu.java:233)
                                                   at com.example.foo.FragmentChordMenu.showChordOptionsMenu(FragmentChordMenu.java:132)
                                                   at com.example.foo.CustomChordAdapter$ChordViewHolder$2.onLongClick(CustomChordAdapter.java:138)
                                                   at android.view.View.performLongClickInternal(View.java:5687)
                                                   at android.view.View.performLongClick(View.java:5645)
                                                   at android.view.View.performLongClick(View.java:5663)
                                                   at android.view.View$CheckForLongPress.run(View.java:22234)
                                                   at android.os.Handler.handleCallback(Handler.java:751)
                                                   at android.os.Handler.dispatchMessage(Handler.java:95)
                                                   at android.os.Looper.loop(Looper.java:154)
                                                   at android.app.ActivityThread.main(ActivityThread.java:6077)
                                                   at java.lang.reflect.Method.invoke(Native Method)
                                                   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865)
                                                   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)

FragmentActivity

public class FragmentChordMenu extends Fragment implements CustomChordAdapter.onItemClickListener {    
    private static RecyclerView mCustomChordList;
    private static CustomChordAdapter mRecyclerViewAdapter;
    private static Context mContext;

    private FloatingActionButton mFAB;
    private View mPopupView;
    private PopupWindow mCustomChordMenu;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mRecyclerViewAdapter = new CustomChordAdapter(this);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        mContext = getActivity().getApplicationContext();   //stores application context for later use in fragment without risk
                                                            //of detachment

        View v = inflater.inflate(R.layout.menu_fragment_chord, container, false);
        LayoutInflater layoutInflater = (LayoutInflater)getActivity().getBaseContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        ...

        mFAB = (FloatingActionButton) v.findViewById(R.id.addChord);
        mFAB.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                mCustomChordMenu.showAtLocation(mPopupView, Gravity.CENTER, 10, 10);
                mCustomChordList = (RecyclerView) mPopupView.findViewById(R.id.rv_userChords);
                LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
                mCustomChordList.setLayoutManager(layoutManager);
                mCustomChordList.setAdapter(mRecyclerViewAdapter);
            }
        });

        return v;
    }

    public static void showChordOptionsMenu(final int position){
        View anchorView = mCustomChordList.findViewHolderForAdapterPosition(position).itemView;
        PopupMenu mPopup = new PopupMenu(mContext, anchorView);
        mPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem item) {
                switch (item.getItemId()){
                    case R.id.delete:
                        mRecyclerViewAdapter.deleteChord(position);
                        return true;
                    case R.id.rename:
                        Log.d("FragmentChordMenu: ", "Rename clicked");
                }
                return true;
            }
        });

        MenuInflater popupInflater = mPopup.getMenuInflater();
        popupInflater.inflate(R.menu.popup_delete_chord, mPopup.getMenu());
        mPopup.show();              //ERROR HERE
    }

    ...
}

PopupMenu XML

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content">
    <item
        android:id="@+id/rename"
        android:title="@string/rename"/>

    <item
        android:id="@+id/delete"
        android:title="@string/delete"/>
</menu>

FragmentActivity XML

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:app="http://schemas.android.com/apk/res-auto"
                xmlns:android.support.design="http://schemas.android.com/tools"
                android:id="@+id/chordMenu"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

    <ScrollView
        android:id="@+id/scrollview"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            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:id="@+id/chordButtons"
                android:orientation="vertical"  >
            </LinearLayout>

            <android.support.design.widget.FloatingActionButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:layout_margin="16dp"
                android:clickable="true"
                app:fabSize="mini"
                android:id="@+id/addChord"
                app:borderWidth="0dp"
                app:useCompatPadding="false"
                android:src="@drawable/ic_add_black_24dp"/>
        </LinearLayout>

    </ScrollView>

</RelativeLayout>
Cody
  • 1,801
  • 3
  • 28
  • 53
  • `mContext = getActivity().getApplicationContext();` - I would bet that that's your problem. You're using the v7 appcompat `PopupMenu`, and the application `Context` won't have the right theme resources set on it. Just use `getActivity()`. You really don't need to keep a `Context` field, btw. You can call `getActivity()` wherever you need one. – Mike M. Jun 01 '17 at 04:50
  • @MikeM. how can I use getActivity() in a non-static context? showChordOptionsMenu() is a static method, hence the issue. – Cody Jun 02 '17 at 01:30
  • Ah, I didn't notice it was. Yeah, you really don't want to keep all those `static` members in your `Fragment` class. Normally, you'd wanna add a `Context` parameter to the method to pass one in, but that method is using other static members with `Context`s, so it's a moot point here. You'd have to redesign how you're communicating with that `Fragment`. Anyhoo, the current issue is likely the wrong `Context`, so just removing the `getApplicationContext()` call in the code you have now will let you know if that is indeed the issue. – Mike M. Jun 02 '17 at 01:55
  • @MikeM. Good suggestion, however, i'm getting a new runtime error now. `android.view.WindowManager$BadTokenException: Unable to add window -- token android.view.ViewRootImpl$W@f847ac3 is not valid; is your activity running?` I think i might be trying to secure the wrong context, I think I should instead be trying to get the context of the PopupWindow this transaction occurs in. Do you know how I would do this? Or rather, what the source of the issue is? – Cody Jun 02 '17 at 02:24
  • Oh, you're trying to show a `PopupMenu` from a `PopupWindow`? I don't think that's gonna work. I don't believe you can anchor a popup to another popup. – Mike M. Jun 02 '17 at 02:46
  • Uh oh. I'll ask a new question about this. Hopefully its doable, I want to avoid major code rework. – Cody Jun 02 '17 at 02:57
  • Just use a `Dialog` instead of the `PopupWindow`. – Mike M. Jun 02 '17 at 02:58
  • That's undesirable for a few reasons, the biggest being that I don't need the action buttons or the interaction-with-parent-view methods that the Dialog class provides. – Cody Jun 02 '17 at 03:01
  • `Dialog`s don't have any buttons - or any kind of `View` - by default. I think you're thinking of `AlertDialog`. You can make it be just a floating `RecyclerView`, if you want. I don't know what you mean by "interaction-with-parent-view methods". – Mike M. Jun 02 '17 at 03:05
  • I suppose I was thinking about this answer "If you want to add more control and feedback between your View then use a Dialog. If you, like me, want master control over everything, I would suggest a PopupWindow since it has fewer user-evident default methods to override." from here https://stackoverflow.com/questions/4710361/when-to-use-android-popupwindow-vs-dialog. Are you sure that a PopupMenu works when anchored to a dialog? If there really isn't much trade off I'll use that instead. – Cody Jun 02 '17 at 03:09
  • 1
    Sure, I've done it before. Of course, there's a way you can find out for certain yourself. :-) Looks like you'd have to change all of about three lines of code. – Mike M. Jun 02 '17 at 03:22
  • Thanks. If you post the suggested changes for the ~3 relevant lines I will award you the accepted answer. – Cody Jun 02 '17 at 03:29
  • 1
    You can actually do it rather simply with `AlertDialog`; just don't setup any buttons. Remove/comment out the `mCustomChordMenu.showAtLocation()` line, and insert - just as an example - `new AlertDialog.Builder(getActivity()).setView(mPopupView).show();`. – Mike M. Jun 02 '17 at 03:47
  • Amazing, works. Thanks. If you want rep post this so I can accept it. – Cody Jun 03 '17 at 01:11
  • Cool, glad it worked. Will do. It'll be a little bit before I get a chance to put an answer together, though. Cheers! – Mike M. Jun 03 '17 at 01:23

1 Answers1

7

The cause of the immediate issue here - the InflateException - was using the application Context for an appcompat-v7 PopupMenu.

mContext = getActivity().getApplicationContext();
...
PopupMenu mPopup = new PopupMenu(mContext, anchorView);

That Context won't have the correct theme resources set for a v7 widget, leading to the InflateException. The Activity does have the appropriate theme, though, and using that instead solves that particular issue.

mContext = getActivity();

After fixing that, however, a WindowManager$BadTokenException came up due to the PopupMenu being passed an anchor View from a PopupWindow. Popups must be anchored to a View in a top-level Window, and the Popup* classes are basically simple Views, thus the Exception.

A simple solution for this is to replace the PopupWindow with a Dialog, which does have a Window. For example:

AlertDialog dlg = new AlertDialog.Builder(getActivity()).setView(mPopupView).show();

Lastly, I would suggest that you modify your setup to remove the need for the static members in your Fragment class. Those static fields are likely to cause memory leaks, and your IDE may very well be warning you of that now. A listener interface similar to the one you have in CustomChordAdapter would suffice.

Mike M.
  • 38,532
  • 8
  • 99
  • 95
  • I'd like to learn more about the last part of your answer; do you have a link to any resources about preventing memory leaks in static members? – Cody Jun 03 '17 at 16:48
  • 1
    Hmm, not really. I mean, there's pretty much only two ways to prevent leaks with them: 1.) Don't use them. 2.) Make sure you set the references to null as soon as you're done with them. If you're just looking for general info on static leaks and such, there's been volumes written on it. Here are some on-site posts, but you might find more thorough explanations on some blogs or tutorials or whatnot: https://stackoverflow.com/a/11908685, https://stackoverflow.com/a/641473, https://stackoverflow.com/a/28091135. That last one has a link to an old Android Developers Blog page on it. – Mike M. Jun 03 '17 at 17:12
  • 1
    Saved my day, thank you so much. I had a similar issue when trying to inflate a layout in my adapter, and after having read your comment I realized I created this adapter with application's Context instead of Activity's. – Vladimir Kondenko Aug 19 '18 at 08:05