刚入职,接手的第一个问题,原理很简单,就是android事件分发的问题。

【背景】51

用户反馈应用列表只有第一项(默认项)功能正常,其他item无法点击。

【问题定位】

1. 观察界面发现除了第一个item,其他项都是置灰的,怀疑是不是这个界面本身就不能点击,由于产品目前属于后转维过来的,这个目前已经无法考证了,没办法,没有捷径只能手撕代码了。

2. 手撕代码发现listview 同时设置了onTouch和onItemClickListenner

listview.setOnTouchListener(this);
listview.setOnItemClickListener(this);
  @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                if (mShowPlayHide != null) {
                    mShowPlayHide.setVisibility(View.GONE);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        return false;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        _log("点击Item" + position);
        if (position == mAdapter.getCurPos())
            return;
        ctrlPlay(Lis_Stop);

        if (mOp.detach(IA_ClickItem,position)) {
            clickItem(position);
        }
    }

逻辑很简单,这里只描述原理,刚看到这段代码时,我屮艸芔茻,这问题不是找到了嘛,onTouch把itemClick拦截了啊,so easy。。。仔细想想,就这么定眼一看,事情没有这么简单,如果是重写了onTouchEvent,没有处理performClick的话应该会影响,但是这里实现的是onTouchListener的onTouch,事件分发中的逻辑是,先判断onTouch如果返回true表示消费,直接返回;如果是onTouch返回false,会调用view的onTouchEvent方法,并且在这里面会触发点击等等事件。不难发现,这里面的逻辑属于第二种(onTouch返回false)情况,这种无论如何都是会触发onTouchEvent的,所以对点击事件的响应应该没有影响,这里再多提一句,这种方法我们可以在特定的点击事件之前添加逻辑,这个方法还是值得借鉴的(应该属于AOP 吧)。应该不是这里的问题。按照事件分发的顺序下边应该找他的子布局,也就是item了。

3. 上网查阅了一下,有好多人都遇到过这个问题,大体的思路就是item中存在了某些如Button,ImageButton等控件,导致将焦点抢了过去,listview无法获取到焦点也就无法响应事件了。解决方法的话就是设置属性:详细用法自己查阅一下吧,很简单。

android:descendantFocusability="blocksDescendants"

感觉这个说的很有道理,但是看了一下代码,布局中并没有会抢占焦点的控件,试了一下,在listview中添加了如上的属性,无效。此时就有些绝望了,从原理上解决不了,那就只能一个子布局一个子布局的排查了。

4. 排查的话我是按照先看布局,理解一下布局的大体作用,然后在代码中根据viewTree的层级逐层排查是否设置了onTouch,onClick等等。排查结果发现item中并没有设置了Touch事件处理的相关监听,但是有两个怀疑点:1. 有一个layout中动态添加布局,动态添加的布局是一个可以播放音频的课点击的按钮。(注释掉发现还是不能点击,排除);2. item中存在一个自定义view,大体的思路是一个可以点击的文本(哎呀,有点内味儿了)。注释掉发现果然,item可以点击了,于是锁定了问题就出在了这个自定义view中。

5. 具体排查细节不赘述了,大体思路和上面保持一致,找到怀疑点然后逐一排查。(我通常使用的方法就是注释掉,看功能是否有变化)。定位发现在自定义view中有这么一个方法

setMovementMethod(LinkMovementMethod.getInstance());

这个方法主要就是为了实现可点击的链接文本功能。该方法源码如下:

public final void setMovementMethod(MovementMethod movement) {
    if (mMovement != movement) {
        mMovement = movement;

        if (movement != null && !(mText instanceof Spannable)) {
            setText(mText);
        }

        fixFocusableAndClickableSettings();

        // SelectionModifierCursorController depends on textCanBeSelected, which depends on
        // mMovement
        if (mEditor != null) mEditor.prepareCursorControllers();
    }
}

在fixFocusableAndClickableSettings()方法中

private void fixFocusableAndClickableSettings() {
    if (mMovement != null || (mEditor != null && mEditor.mKeyListener != null)) {
        setFocusable(true);
        setClickable(true);
        setLongClickable(true);
    } else {
        setFocusable(false);
        setClickable(false);
        setLongClickable(false);
    }
}

可以看出来,设置了焦点等等,导致了listview在点击的时候无法抢占到焦点了。无法抢占焦点了,listview中的item就无法响应onItemClick事件了,于是解决方案也很简单,在该方法之后将焦点设置成false。

setMovementMethod(LinkMovementMethod.getInstance());
setFocusable(false);

【总结】

1. listview设置了onTouchListener如果在onTouch中返回false,不会对后续事件分发造成影响,借此可以通过该方法实现特定功能。

2. listview的item中不要抢占焦点,或者focusable为true,可能会导致listview的onItemClick失效,当然,如果业务需要一定要处理好事件分发的逻辑。

3. view的 setMovementMethod() 方法会使当前view的focusable为true,具体分析如上。

 

过河不忘渡船人!

感谢 chzphoenix 大佬的简书《https://www.jianshu.com/p/148df4818b42》。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐