ListView中嵌套EditText造成ListView的子Item点击无响应问题简要分析

解析ListView嵌套EditText时,EditText导致ListView点击回调失效的原因。通过对源码的深入分析,找到问题关键在于EditText使得child.hasExplicitFocusable()返回true,进而阻止了回调的执行。

继上一篇处理ListView嵌套EditText出现的问题,有一个问题EditText使ListView的setOnItemClickListener回调失效,最新看了源码,得出了一些结论,如有错误欢迎指出,不需要看完整代码,看标记的最关键的就行。

1.我们来一步步看这个回调过程

    首先我们看到,这个回调发生在ListView的父类AdapterView的performItemClick中,下面是源码
    /**
     * Call the OnItemClickListener, if it is defined. Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @param view The view within the AdapterView that was clicked.
     * @param position The position of the view in the adapter.
     * @param id The row id of the item that was clicked.
     * @return True if there was an assigned OnItemClickListener that was
     *         called, false otherwise is returned.
     */
    public boolean performItemClick(View view, int position, long id) {
        final boolean result;
        if (mOnItemClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            mOnItemClickListener.onItemClick(this, view, position, id);
            result = true;
        } else {
            result = false;
        }

        if (view != null) {
            view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        }
        return result;
    }

2.我们再来看是哪里调用了performItemClick,可以看到是在AbsListView中

    private class PerformClick extends WindowRunnnable implements Runnable {
        int mClickMotionPosition;

        @Override
        public void run() {
            // The data has changed since we posted this action in the event queue,
            // bail out before bad things happen
            if (mDataChanged) return;

            final ListAdapter adapter = mAdapter;
            final int motionPosition = mClickMotionPosition;
            if (adapter != null && mItemCount > 0 &&
                    motionPosition != INVALID_POSITION &&
                    motionPosition < adapter.getCount() && sameWindow() &&
                    adapter.isEnabled(motionPosition)) {
                final View view = getChildAt(motionPosition - mFirstPosition);
                // If there is no view, something bad happened (the view scrolled off the
                // screen, etc.) and we should cancel the click
                if (view != null) {
                //这里这里这里!!!!!
                    performItemClick(view, motionPosition, adapter.getItemId(motionPosition));
                }
            }
        }
    }

3.当然这里是利用线程,然后再看在哪里启动的,结果还是在这个类里面

   private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
        case TOUCH_MODE_DOWN:
        case TOUCH_MODE_TAP:
        case TOUCH_MODE_DONE_WAITING:
            final int motionPosition = mMotionPosition;
            final View child = getChildAt(motionPosition - mFirstPosition);
            if (child != null) {
                if (mTouchMode != TOUCH_MODE_DOWN) {
                    child.setPressed(false);
                }

                final float x = ev.getX();
                final boolean inList = x > mListPadding.left && x < getWidth() - mListPadding.right;
                //!!!!!!!!!!
                if (inList && !child.hasExplicitFocusable()) {
                    if (mPerformClick == null) {
                        mPerformClick = new PerformClick();
                    }

                    final AbsListView.PerformClick performClick = mPerformClick;
                    performClick.mClickMotionPosition = motionPosition;
                    performClick.rememberWindowAttachCount();

                    mResurrectToPosition = motionPosition;

                    if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
                        removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
                                mPendingCheckForTap : mPendingCheckForLongPress);
                        mLayoutMode = LAYOUT_NORMAL;
                        if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                            mTouchMode = TOUCH_MODE_TAP;
                            setSelectedPositionInt(mMotionPosition);
                            layoutChildren();
                            child.setPressed(true);
                            positionSelector(mMotionPosition, child);
                            setPressed(true);
                            if (mSelector != null) {
                                Drawable d = mSelector.getCurrent();
                                if (d != null && d instanceof TransitionDrawable) {
                                    ((TransitionDrawable) d).resetTransition();
                                }
                                mSelector.setHotspot(x, ev.getY());
                            }
                            if (mTouchModeReset != null) {
                                removeCallbacks(mTouchModeReset);
                            }
                            mTouchModeReset = new Runnable() {
                                @Override
                                public void run() {
                                    mTouchModeReset = null;
                                    mTouchMode = TOUCH_MODE_REST;
                                    child.setPressed(false);
                                    setPressed(false);
                                    if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) {
                                        performClick.run();
                                    }
                                }
                            };
                            postDelayed(mTouchModeReset,
                                    ViewConfiguration.getPressedStateDuration());
                        } else {
                            mTouchMode = TOUCH_MODE_REST;
                            updateSelectorState();
                        }
                        return;
                    } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                        //线程在这里被启动!!!!!!!!!!!
                        performClick.run();
                    }
                }
            }
            mTouchMode = TOUCH_MODE_REST;
            updateSelectorState();
            break;

上面的代码没有截取完整的,但是这部分就够了,实验发现,问题出在了上面第一次的连续!标记处,即
inList && !child.hasExplicitFocusable(),根据debug发现inList为true,所以问题在右边 !child.hasExplicitFocusable()为false造成下面的代码没执行,performclick没有启动,也就是child.hasExplicitFocusable()为true,这个方法代码如下

只要自己或自己里面的的View有明确的可获得焦点的都会返回true
/**
     * Returns true if this view is focusable or if it contains a reachable View
     * for which {@link #hasExplicitFocusable()} returns {@code true}.
     * A "reachable hasExplicitFocusable()" is a view whose parents do not block descendants focus.
     * Only {@link #VISIBLE} views for which {@link #getFocusable()} would return
     * {@link #FOCUSABLE} are considered focusable.
     *
     * <p>This method preserves the pre-{@link Build.VERSION_CODES#O} behavior of
     * {@link #hasFocusable()} in that only views explicitly set focusable will cause
     * this method to return true. A view set to {@link #FOCUSABLE_AUTO} that resolves
     * to focusable will not.</p>
     *
     * @return {@code true} if the view is focusable or if the view contains a focusable
     *         view, {@code false} otherwise
     *
     * @see #hasFocusable()
     */
    public boolean hasExplicitFocusable() {
        return hasFocusable(false, true);
    }

    boolean hasFocusable(boolean allowAutoFocus, boolean dispatchExplicit) {
        if (!isFocusableInTouchMode()) {
            for (ViewParent p = mParent; p instanceof ViewGroup; p = p.getParent()) {
                final ViewGroup g = (ViewGroup) p;
                if (g.shouldBlockFocusForTouchscreen()) {
                    return false;
                }
            }
        }

        // Invisible and gone views are never focusable.
        if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) {
            return false;
        }

        //debug发现最终代码执行到了这个if语句里面,说明这个if是成立的
        // Only use effective focusable value when allowed.
        if ((allowAutoFocus || getFocusable() != FOCUSABLE_AUTO) && isFocusable()) {
            return true;
        }

        return false;
    }

4.根据文档说明
if the view is focusable or if the view contains a focusable
view, {@code false} otherwise
意思是 如果这个View是可获得焦点或者它包含一个可获得焦点的View就返回True,经过验证EditText是默认可获得焦点的,这就使方法返回true,所以child.hasExplicitFocusable()为true,所以造成回调无法被执行。

这个回调可以看到是在onTouchUp中被调用,而onTouchUp是在OnTouchEvent中,接收到事件ACTION_UP时候被调用

 @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (!isEnabled()) {
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return isClickable() || isLongClickable();
        }

        if (mPositionScroller != null) {
            mPositionScroller.stop();
        }

        if (mIsDetaching || !isAttachedToWindow()) {
            // Something isn't right.
            // Since we rely on being attached to get data set change notifications,
            // don't risk doing anything where we might try to resync and find things
            // in a bogus state.
            return false;
        }

        startNestedScroll(SCROLL_AXIS_VERTICAL);

        if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) {
            return true;
        }

        initVelocityTrackerIfNotExists();
        final MotionEvent vtev = MotionEvent.obtain(ev);

        final int actionMasked = ev.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0;
        }
        vtev.offsetLocation(0, mNestedYOffset);
        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
                onTouchDown(ev);
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                onTouchMove(ev, vtev);
                break;
            }
/**
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!这里
*/
            case MotionEvent.ACTION_UP: {
                onTouchUp(ev);
                break;
            }

通过给Item的外层ViewGroup添加OnClick,根据事件分发规则,内部的item会优先处理事件,不能处理的再抛给父控件,
所以onclick可以响应回调,然后再把事件抛给ListView,只是ListView会消耗事件(onTouchEvent返回true) 但是不会处理点击回调

分析到这里就结束了,涉及的类不是很多,大家只需要看最关键的几个方法就行,如果有不对欢迎指出

<think> 首先,用户的问题是:"我想获取使用Android Studio创建记事本UI界面的方法"。用户提供了几个引用,但是用户真正的需求;系统指令说参考用户上一次的问题和我的回答。但在这个对话中,这是第一个用户消息,所以没有上一次的问题或回答。我需要根据当前消息处理。 系统指令关键点: - 所有行内数学表达式必须使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 - 回答结构清晰,帮助用户逐步解决问题,尽量保证回答真实可靠。 用户的问题是关于使用Android Studio创建记事本UI界面的方法。引用[1],[2],[3]提供了相关信息: - 引用[1]谈到了使用Android Studio设计UI界面,包括视图、布局等。 - 引用[2]讨论了安卓用户界面设计的基础知识。 - 引用[3]涉及设计美观的用户界面。 虽然引用是用户真正的需求,但内容相关,我需要在回答中自然地引用。 我的回答结构: 1. **回答用户问题**:逐步解释如何创建记事本UI界面。 2. **使用引用**:在相关部分添加引用标识。 3. **生成相关问题**:在最后添加相关问题部分。 步骤: - **理解记事本应用UI**:典型的记事本UI包括编辑文本区域、保存按钮、标题栏、列表视图等。 - **使用Android Studio**:涉及XML布局文件、Java/Kotlin代码。 - **逐步指南**: 1. 创建项目。 2. 设计布局:使用XML布局文件。 3. 添加组件:如EditText、Button等。 4. 处理逻辑:如保存文本。 5. 引用相关知识点。 确保回答真实可靠:基于Android开发知识。 **生成相关问题**:在最后添加"相关问题"部分,类似示例中的"§§相关问题§§"。 **语言和格式**:使用中文;行内数学表达式用$...$,但在这个主题中可能涉及数学,所以可能需要。如果有的话,比如在代码中变量,可以应用,但通常UI设计涉及数学公式。所以可能跳过数学部分。 现在,构建回答: 1. **介绍**:简要说明创建记事本UI界面的步骤。 2. **详细步骤**: - 步骤1: 创建新项目。 - 步骤2: 设计布局文件(XML)。 - 步骤3: 添加必要的组件(EditText for notes, Button for save)。 - 步骤4: 在活动文件中添加逻辑(Kotlin/Java)。 - 步骤5: 测试和优化。 3. **引用**:在适当地方添加引用标识。例如,当提到布局设计时,引用[2]。 4. **代码示例**:提供简单代码段,使用代码块。 5. **结尾**:总结。 6. **相关问题**:生成2-3个相关问题。 相关问题示例: 1. 如何优化记事本应用的UI性能? 2. 记事本应用中如何处理用户输入事件? 3. 有哪些高级UI组件可以增强记事本应用? 确保自然添加引用:例如,在讨论布局时,说"如引用[2]所述"。 最后,添加"相关问题"部分。 </think> ### 使用Android Studio创建记事本UI界面的方法 创建记事本应用的UI界面是Android开发中的常见任务。下面我将分步解释如何实现,包括布局设计、组件添加和基本逻辑实现。整个过程基于Android Studio工具,使用XML布局文件和Kotlin/Java代码。我会确保步骤清晰,帮助您轻松上手。设计时需参考用户界面设计的基础原则,如视图层次结构、事件处理等[^2]。 #### 步骤1: 创建新项目并设置环境 1. **启动Android Studio**:打开Android Studio,选择"Start a new Android Studio project"。 2. **选择模板**:在模板列表中,选择"Empty Views Activity"(用于最基础UI),或"Basic Activity"(包含工具栏)。项目名称设为"NotepadApp",语言选择Kotlin或Java。 3. **配置项目**:设置最低SDK版本(建议Android 8.0+),点击"Finish"。项目创建后,Android Studio会自动生成MainActivity和相关资源文件。 #### 步骤2: 设计XML布局文件 记事本UI的核心是布局文件,通常使用LinearLayout或ConstraintLayout来组织组件。以下是基础记事本界面的XML设计: - **打开布局文件**:在`res/layout`文件夹中,双击`activity_main.xml`(或创建新文件如`note_editor.xml`)。 - **添加组件**: - 一个`EditText`用于输入和编辑笔记文本。 - 一个`Button`用于保存笔记。 - 可选组件:`Toolbar`(用于标题栏)、`ListView`或`RecyclerView`(用于显示笔记列表)。 - **示例XML代码**: ```xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp"> <!-- 垂直布局,简单易用 --> <!-- 标题栏 --> <TextView android:id="@+id/titleTextView" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="我的记事本" android:textSize="24sp" android:textAlignment="center" android:padding="8dp" /> <!-- 笔记编辑区域 --> <EditText android:id="@+id/noteEditText" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" <!-- 占据剩余空间 --> android:hint="输入笔记内容..." android:gravity="top" android:padding="8dp" android:background="@android:drawable/editbox_background" /> <!-- 保存按钮 --> <Button android:id="@+id/saveButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="保存" android:layout_gravity="end" <!-- 右对齐 --> android:padding="12dp" /> </LinearLayout> ``` 此布局使用LinearLayout实现垂直堆叠,EditText用于文本输入,Button用于触发保存操作。设计时应优化布局性能,避免嵌套过多视图组[^2]。 #### 步骤3: 在活动中添加逻辑(Kotlin示例) 接下来,在活动文件中处理用户交互: - **打开MainActivity.kt**:在`app/src/main/java/your-package-name/`文件夹中。 - **实现逻辑**: - 绑定布局组件。 - 添加按钮点击事件,保存笔记内容(这里使用SharedPreferences存储简单数据)。 - 可选:添加列表视图逻辑来显示历史笔记。 示例Kotlin代码: ```kotlin import android.os.Bundle import android.widget.Button import android.widget.EditText import android.widget.Toast import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 设置布局文件 // 绑定组件 val noteEditText: EditText = findViewById(R.id.noteEditText) val saveButton: Button = findViewById(R.id.saveButton) // 保存按钮点击事件 saveButton.setOnClickListener { val noteText = noteEditText.text.toString() if (noteText.isNotEmpty()) { // 保存笔记到SharedPreferences(简单存储) val sharedPref = getSharedPreferences("NotePrefs", MODE_PRIVATE) with(sharedPref.edit()) { putString("lastNote", noteText) apply() } Toast.makeText(this, "笔记已保存", Toast.LENGTH_SHORT).show() } else { Toast.makeText(this, "请输入内容", Toast.LENGTH_SHORT).show() } } } } ``` 此代码处理用户输入和保存逻辑,确保UI交互流畅。事件监听是提升用户体验的关键,如引用[3]所述[^3]。 #### 步骤4: 测试和优化 - **运行应用**:连接设备或使用模拟器,点击Android Studio的"Run"按钮测试UI。 - **优化建议**: - **性能优化**:使用ConstraintLayout代替LinearLayout以减少嵌套,提升渲染速度。 - **UI美化**:添加Material Design元素(如FloatingActionButton),或使用Styles和Themes统一外观。 - **功能扩展**:集成数据库(如Room)存储多篇笔记,并添加列表界面。 - **测试要点**:确保EditText能滚动输入,按钮响应快速。设计时需考虑同屏幕尺寸,使用`dimens.xml`定义尺寸[^1]。 #### 总结 通过以上步骤,您可以使用Android Studio快速创建记事本UI界面:从项目设置、XML布局设计到Kotlin逻辑实现。整个过程强调视图组件和事件处理的结合,帮助您构建美观实用的应用。开发者可以基于此扩展功能,如添加删除按钮或搜索功能[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值