PopupMenu源码分析

本文详细介绍了PopupMenu的使用方法,包括初始化、加载菜单项、显示与关闭流程。通过实例代码展示了如何实现菜单项点击事件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1, 概述

PopWindow (弹出框)和Dialog最关键的区别是Dialog不能指定显示位置,只能默认显示在屏幕中间(当然可以通过设置WindowManager参数来改变位置)。而PopWindow可以指定显示位置,更加灵活。PopupMenu就是利用PopWindow来实现的。

2 实现

PopupMenu是一个子窗口,不仅依附于Activity还依附于一个View控件,开发步骤如下,

1,初始化
View vpop = findViewById(R.id.~~~);  // View
mMenu = new PopupMenu(this, vpop);
    mMenu.inflate(R.menu.menu~~~); // 添加菜单项
    mMenu.setOnMenuItemClickListener(this);
R.menu格式如下,
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_4g_conference_call"
        android:title="@string/menu_4g_conference_call" />
    <item
        android:id="@+id/menu_history"
        android:icon="@drawable/ic_menu_history_lt"
        android:title="@string/action_menu_call_history_description" />
    <item
        android:id="@+id/menu_import_export"
        android:title="@string/menu_import_export" />
    <item
        android:id="@+id/menu_clear_frequents"
        android:title="@string/menu_clear_frequents" />
    <item
        android:id="@+id/menu_add_contact"
        android:title="@string/menu_newContact"/>
    <item
        android:id="@+id/menu_call_settings"
        android:title="@string/dialer_settings_label" />
</menu>

2,在acvitity中显示PopupMenu并且实现单击方法,
   mMenu.show(); // 一般在单击vpop时显示
    实现PopupMenu.OnMenuItemClickListener方法,
@Override
    public boolean onMenuItemClick(MenuItem item) {  // 菜单单击事件
        switch (item.getItemId()) {
            case R.id.menu_4g_conference_call:
                •••
                break;
            case R.id.menu_history:
                •••
                break;
            ~~~
        }
        return false;
    }

3,源码解析

3.1 对象创建

创建流程如下,


首先看PopupMenu中的构造方法,构造方法中为变量赋值,并且构造了2个对象,

public PopupMenu(Context context, View anchor, int gravity, int popupStyleAttr,
            int popupStyleRes) {
        mContext = context;
        mMenu = new MenuBuilder(context);
        mMenu.setCallback(this);
        mAnchor = anchor; // 赋值
    mPopup = new MenuPopupHelper(context, mMenu, anchor, false, popupStyleAttr, popupStyleRes);
        mPopup.setGravity(gravity);
        mPopup.setCallback(this);
    }

3.2 加载菜单项


菜单项加载方法,

mMenu.inflate(R.menu.menu~~~);

Inflate方法如下,

public void inflate(@MenuRes int menuRes) {
        getMenuInflater().inflate(menuRes, mMenu); // mMenu是MenuBuilder对象
    }

getMeuInflater方法如下,

public MenuInflater getMenuInflater() {
        return new MenuInflater(mContext);  // 新建了一个MenuInflater对象
    }

parseMenu方法如下,很明显的是一个xml文件解析过程,和Activity中布局文件解析的过程几乎完全一样,

private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)
            throws XmlPullParserException, IOException {
        MenuState menuState = new MenuState(menu);

        int eventType = parser.getEventType();
        String tagName;
        boolean lookingForEndOfUnknownTag = false;
        String unknownTagName = null;

        // This loop will skip to the menu start tag
        do {
            if (eventType == XmlPullParser.START_TAG) {
                tagName = parser.getName();
                if (tagName.equals(XML_MENU)) {
                    // Go to next tag
                    eventType = parser.next();
                    break;
                }
                
                throw new RuntimeException("Expecting menu, got " + tagName);
            }
            eventType = parser.next();
        } while (eventType != XmlPullParser.END_DOCUMENT);
        
        boolean reachedEndOfMenu = false;
        while (!reachedEndOfMenu) {
            switch (eventType) {
                case XmlPullParser.START_TAG:
                    if (lookingForEndOfUnknownTag) {
                        break;
                    }
                    
                    tagName = parser.getName();
                    if (tagName.equals(XML_GROUP)) {
                        menuState.readGroup(attrs);
                    } else if (tagName.equals(XML_ITEM)) {
                        menuState.readItem(attrs);
                    } else if (tagName.equals(XML_MENU)) {
                        // A menu start tag denotes a submenu for an item
                        SubMenu subMenu = menuState.addSubMenuItem();
                        registerMenu(subMenu, attrs);

                        // Parse the submenu into returned SubMenu
                        parseMenu(parser, attrs, subMenu);
                    } else {
                        lookingForEndOfUnknownTag = true;
                        unknownTagName = tagName;
                    }
                    break;
                    
                case XmlPullParser.END_TAG:
                    tagName = parser.getName();
                    if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {
                        lookingForEndOfUnknownTag = false;
                        unknownTagName = null;
                    } else if (tagName.equals(XML_GROUP)) {
                        menuState.resetGroup();
                    } else if (tagName.equals(XML_ITEM)) {
                        // Add the item if it hasn't been added (if the item was
                        // a submenu, it would have been added already)
                        if (!menuState.hasAddedItem()) {
                            if (menuState.itemActionProvider != null &&
                                    menuState.itemActionProvider.hasSubMenu()) {
                                registerMenu(menuState.addSubMenuItem(), attrs);
                            } else {
                                registerMenu(menuState.addItem(), attrs);
                            }
                        }
                    } else if (tagName.equals(XML_MENU)) {
                        reachedEndOfMenu = true;
                    }
                    break;
                    
                case XmlPullParser.END_DOCUMENT:
                    throw new RuntimeException("Unexpected end of document");
            }
            
            eventType = parser.next();
        }
    }

3.3 显示(show)



PopupWindow的showAsDropDown方法如下,

public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
        if (isShowing() || mContentView == null) {
            return;
        }

        TransitionManager.endTransitions(mDecorView);

        registerForScrollChanged(anchor, xoff, yoff, gravity);

        mIsShowing = true;
        mIsDropdown = true;
         // 构造LayoutParams对象
       final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
        preparePopup(p);  // createDecorView方法最后会构造一个PopupDecorView对象

        final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, gravity);
        updateAboveAnchor(aboveAnchor);

        invokePopup(p);
    }
private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }

        final PopupDecorView decorView = mDecorView;
        decorView.setFitsSystemWindows(mLayoutInsetDecor);
        setLayoutDirectionFromAnchor();
        mWindowManager.addView(decorView, p); // 完成View的测量,定位,绘制等流程
        if (mEnterTransition != null) {
            decorView.requestEnterTransition(mEnterTransition);
        }
    }

3.4 单击事件


onItemClick方法如下,

public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        MenuAdapter adapter = mAdapter;
        adapter.mAdapterMenu.performItemAction(adapter.getItem(position), 0);
    }

performItemAction方法如下,

public boolean performItemAction(MenuItem item, MenuPresenter preferredPresenter, int flags) {
        MenuItemImpl itemImpl = (MenuItemImpl) item; // 类型转换
        
        if (itemImpl == null || !itemImpl.isEnabled()) {
            return false;
        }

        boolean invoked = itemImpl.invoke(); // 分发菜单单击事件

        final ActionProvider provider = item.getActionProvider();
        final boolean providerHasSubMenu = provider != null && provider.hasSubMenu();
        if (itemImpl.hasCollapsibleActionView()) {
            invoked |= itemImpl.expandActionView();
            if (invoked) close(true);  // 处理完成之后,关闭弹出菜单
        } else if (itemImpl.hasSubMenu() || providerHasSubMenu) {
            close(false);

            if (!itemImpl.hasSubMenu()) {
                itemImpl.setSubMenu(new SubMenuBuilder(getContext(), this, itemImpl));
            }

            final SubMenuBuilder subMenu = (SubMenuBuilder) itemImpl.getSubMenu();
            if (providerHasSubMenu) {
                provider.onPrepareSubMenu(subMenu);
            }
            invoked |= dispatchSubMenuSelected(subMenu, preferredPresenter);
            if (!invoked) close(true);
        } else {
            if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) {
                close(true);
            }
        }
        
        return invoked;
    }

performItemAction逻辑比较简单,首先分发单击事件,处理完成之后,关闭菜单。

3.5 关闭

关闭菜单流程图如下,


接着上面的小节, close方法如下,

public final void close(boolean allMenusAreClosing) {
        if (mIsClosing) return;

        mIsClosing = true;
        for (WeakReference<MenuPresenter> ref : mPresenters) {
            final MenuPresenter presenter = ref.get();
            if (presenter == null) {
                mPresenters.remove(ref);
            } else {
                presenter.onCloseMenu(this, allMenusAreClosing);
            }
        }
        mIsClosing = false;
    }

MenuPresenter只是一个接口,继承该接口的也有多个类,到底调用谁的呢?

MenuBuilder有一个addMenuPresenter方法,添加MenuPresenter实例,

public void addMenuPresenter(MenuPresenter presenter, Context menuContext) {
        mPresenters.add(new WeakReference<MenuPresenter>(presenter));
        presenter.initForMenu(menuContext, this);
        mIsActionItemsStale = true;
    }

谁调用该方法呢? 在MenuPopupHelper的构造方法中,

menu.addMenuPresenter(this, context);

所以调用的是MenuPopupHelper的onCloseMenu方法,

最后的dismissImmediate方法如下,

private void dismissImmediate(View decorView, ViewGroup contentHolder, View contentView) {
        // If this method gets called and the decor view doesn't have a parent,
        // then it was either never added or was already removed. That should
        // never happen, but it's worth checking to avoid potential crashes.
        if (decorView.getParent() != null) {
            mWindowManager.removeViewImmediate(decorView); // 关闭
        }
        if (contentHolder != null) {
            contentHolder.removeView(contentView);
        }
        // This needs to stay until after all transitions have ended since we
        // need the reference to cancel transitions in preparePopup().
        mDecorView = null;
        mBackgroundView = null;
        mIsTransitioningToDismiss = false;
    }

4 PopupMenu

PopupMenu中的方法如下,

setGravity

对其方法

inflate

加载资源

show

显示

dismiss

关闭

setOnMenuItemClickListener

菜单项单击监听

内部接口,OnMenuItemClickListener

public interface OnMenuItemClickListener {
        /**
         * This method will be invoked when a menu item is clicked if the item itself did
         * not already handle the event.
         *
         * @param item {@link MenuItem} that was clicked
         * @return <code>true</code> if the event was handled, <code>false</code> otherwise.
         */
        public boolean onMenuItemClick(MenuItem item);
    }

一般需要实现OnMenuItemClickListener接口的onMenuItemClick方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值