BottomNavigationView的Item长按为什么会弹出toast

  • 概述

    应用BottomNavigationView的时候偶然发现长按菜单项的时候会弹出一个toast,toast的内容和菜单项的title是一致的,通常我们不会喜欢有这么个画蛇添足的效果,想着能不能把这个效果关掉,但是看过源码后没发现可配置这个效果的属性,也没发现设置长按事件的代码,最后发现一个View内嵌的长按设置,比较新颖,记录一下。

  • 最初思路的源码分析

    最开始,找到NavigationBarMenuView中添加item的地方:

    public void buildMenuView() {
      removeAllViews();
      ...
      buttons = new NavigationBarItemView[menu.size()];
      ...
      for (int i = 0; i < menu.size(); i++) {
        ...
        NavigationBarItemView child = getNewItem();
        buttons[i] = child;
        ...
        int itemId = item.getItemId();
        child.setOnTouchListener(onTouchListeners.get(itemId));
        child.setOnClickListener(onClickListener);
        ...
        addView(child);
      }
      ...
    }
    

    可以看到,这里并没有发现child调用setOnLongClickListener方法,buttons也搜了,没有额外的设置长按事件,BottomNavigationView、NavigationBarView、NavigationBarItemView都没搜到设置长按。

    事情好像开始无解了。

  • 新鲜的东西

    常规思路无法解决的时候,我试着去网上搜一搜,结果有很多说设置:

    //遍历子View,重写长按点击事件
    for (position in 0 until ids.size){
        bottomNavigationMenuView.getChildAt(position).findViewById<View>(ids[position]).setOnLongClickListener { true }
    }
    

    这还真的奏效了,不过你不能设置setOnLongClickListener为null。

    那这是为什么呢?

    然后我搜到了一位同学写的针对这个问题Why的源码分析。

    在NavigationBarItemView中的setTitle方法中:

    @Override
    public void setTitle(@Nullable CharSequence title) {
      smallLabel.setText(title);
      largeLabel.setText(title);
      ...
      if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP || VERSION.SDK_INT > VERSION_CODES.M) {
        TooltipCompat.setTooltipText(this, tooltipText);
      }
    }
    

    重点在于TooltipCompat.setTooltipText:

    public static void setTooltipText(@NonNull View view, @Nullable CharSequence tooltipText) {
        if (Build.VERSION.SDK_INT >= 26) {
            view.setTooltipText(tooltipText);
        } else {
            TooltipCompatHandler.setTooltipText(view, tooltipText);
        }
    }
    

    小于26版本时TooltipCompatHandler.setTooltipText中会设置长按事件为null,而在26版本以上的才会设置长按事件,接着往下看:

    public void setTooltipText(@Nullable CharSequence tooltipText) {
        if (TextUtils.isEmpty(tooltipText)) {
            setFlags(0, TOOLTIP);
            hideTooltip();
            mTooltipInfo = null;
        } else {
            setFlags(TOOLTIP, TOOLTIP);
            if (mTooltipInfo == null) {
                mTooltipInfo = new TooltipInfo();
                mTooltipInfo.mShowTooltipRunnable = this::showHoverTooltip;
                mTooltipInfo.mHideTooltipRunnable = this::hideTooltip;
                mTooltipInfo.mHoverSlop = ViewConfiguration.get(mContext).getScaledHoverSlop();
                mTooltipInfo.clearAnchorPos();
            }
            mTooltipInfo.mTooltipText = tooltipText;
        }
    }
    

    这里会根据tooltipText是否为空来决定是否设置TOOLTIP这个flag,如果不为空则设置TOOLTIP,然后再往下就没逻辑了,我们还是没有找到设置长按事件的地方。

    但是我们可以找到show的方法:

    private boolean showTooltip(int x, int y, boolean fromLongClick) {
        if (mAttachInfo == null || mTooltipInfo == null) {
            return false;
        }
        if (fromLongClick && (mViewFlags & ENABLED_MASK) != ENABLED) {
            return false;
        }
        if (TextUtils.isEmpty(mTooltipInfo.mTooltipText)) {
            return false;
        }
        hideTooltip();
        mTooltipInfo.mTooltipFromLongClick = fromLongClick;
        mTooltipInfo.mTooltipPopup = new TooltipPopup(getContext());
        final boolean fromTouch = (mPrivateFlags3 & PFLAG3_FINGER_DOWN) == PFLAG3_FINGER_DOWN;
        mTooltipInfo.mTooltipPopup.show(this, x, y, fromTouch, mTooltipInfo.mTooltipText);
        mAttachInfo.mTooltipHost = this;
        // The available accessibility actions have changed
        notifyViewAccessibilityStateChangedIfNeeded(CONTENT_CHANGE_TYPE_UNDEFINED);
        return true;
    }
    

    可以看到,这里通过TooltipPopup(可以认为是一个PopupWindow,只是通过WindowManager.addView来添加的)来展示的。

    我们找一下showTooltip方法调用的地方,发现有一个showLongClickTooltip方法,查看他的调用链,发现源头在onTouchEvent的ACTION_DOWN分支下:

    public boolean onTouchEvent(MotionEvent event) {
        ...
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                      //这里控制在手指抬起后经过一定时间才隐藏toast
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    ...
                    break;
                case MotionEvent.ACTION_DOWN:
                    ...
                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                       ...
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                          //在这里触发到showTooltip中去的
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    break;
    
                case MotionEvent.ACTION_CANCEL:
                    ...
                    break;
    
                case MotionEvent.ACTION_MOVE:
                    ...
                    break;
            }
            return true;
        }
        return false;
    }
    
    private void checkForLongClick(long delay, float x, float y, int classification) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
            mHasPerformedLongPress = false;
    
            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.setAnchor(x, y);
            mPendingCheckForLongPress.rememberWindowAttachCount();
            mPendingCheckForLongPress.rememberPressedState();
            mPendingCheckForLongPress.setClassification(classification);
            postDelayed(mPendingCheckForLongPress, delay);
        }
    }
    

    checkForLongClick方法中会调用postDelayed方法,delay是ViewConfiguration.getLongPressTimeout(),就是延迟长按的时间,运行的Runnable就是mPendingCheckForLongPress,它是CheckForLongPress类型的:

    private final class CheckForLongPress implements Runnable {
          ...
        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                recordGestureClassification(mClassification);
                if (performLongClick(mX, mY)) {
                    mHasPerformedLongPress = true;
                }
            }
        }
        ...
    }
    

    这里会调用performLongClick,最终会调用到performLongClickInternal方法:

    private boolean performLongClickInternal(float x, float y) {
        ...
        boolean handled = false;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        ...
        if ((mViewFlags & TOOLTIP) == TOOLTIP) {
            if (!handled) {
                handled = showLongClickTooltip((int) x, (int) y);
            }
        }
        ...
        return handled;
    }
    

    因为我们之前在setTooltipText方法中设置了TOOLTIP,因此这里只要handled为false就能执行showLongClickTooltip方法,如果没有设置OnLongClickListener并且在其onLongClick方法中返回true的话,showLongClickTooltip肯定是会被执行的,这也就是为什么BottomNavigationView的菜单项默认会有长按弹出toast的效果,也是为什么给NavigationBarItemView设置OnLongClickListener为null后仍然无法禁止长按弹出toast的效果的原因。

    看到这,你可能知道应该怎么做来禁用这个效果了。

    //禁用item的长按事件
    val menuView: NavigationBarMenuView = getChildAt(0) as NavigationBarMenuView
    for (index in 0 until menuView.childCount) {
        menuView.getChildAt(index).setOnLongClickListener { true }
    }
    
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容