在使用ViewGroup派生类(LinearLayout、RelativeLayout等)嵌套RecyclerView,给ViewGroup设置点击事件后,你会发现点击RecyclerView的部分无响应点击事件。
比如下面的示例布局文件:
MainActivity.java文件代码如下:
package com.axen.module.activity;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Toast;
import com.axen.module.R;
public class MainActivity extends AppCompatActivity {
private LinearLayout ll;
private RecyclerView rv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ll = findViewById(R.id.ll);
rv = findViewById(R.id.rv);
ll.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 点击在RecyclerView的区域将无法响应点击事件
Toast.makeText(MainActivity.this, "我点击了LinearLayout", Toast.LENGTH_SHORT).show();
}
});
}
}
从网上寻求解答,发现大部分的博客提供的解决办法都是在父布局添加属性android:descendantFocusability="blocksDescendants"
或者给控件设置android:focusable="false"之类的,但是经过实践证明这样做没有效果。
另外一个办法是重写RecyclerView的onTouchListener事件:
package com.axen.module.activity;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Toast;
import com.axen.module.R;
public class MainActivity extends AppCompatActivity {
private LinearLayout ll;
private RecyclerView rv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ll = findViewById(R.id.ll);
rv = findViewById(R.id.rv);
ll.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 点击在RecyclerView的区域将无法响应点击事件
Toast.makeText(MainActivity.this, "我点击了LinearLayout", Toast.LENGTH_SHORT).show();
}
});
rv.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
Toast.makeText(MainActivity.this, "我点击了LinearLayout", Toast.LENGTH_SHORT).show();
}
return false;
}
});
}
}
通过此方法可以实现响应点击效果,但是onTouch事件包含了许多操作,直接重写,可能会导致一系列问题,因此也不推荐使用该方法。
后来在阅读稀土掘金的《LinearLayout包裹RecycleView点击事件不响应》一文中找到了解决办法,原来,在RecyclerView的onTouchEvent和onInterceptTouchEvent事件中,存在一个名为mLayoutFrozen的布尔值变量,当这个变量的值为true时,RecyclerView就不会拦截点击事件了,下面是RecyclerView的关键源码:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
....
@Override
public boolean onTouchEvent(MotionEvent e) {
if (mLayoutFrozen || mIgnoreMotionEventTillDown) {
return false;
}
...
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (mLayoutFrozen) {
// When layout is frozen, RV does not intercept the motion event.
// A child view e.g. a button may still get the click.
return false;
}
...
}
}
要设置mLayoutFrozen的值,可通过setLayoutFrozen方法实现:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
....
/**
* Enable or disable layout and scroll. After setLayoutFrozen(true)
is called,
* Layout requests will be postponed until setLayoutFrozen(false)
is called;
* child views are not updated when RecyclerView is frozen, {@link #smoothScrollBy(int, int)},
* {@link #scrollBy(int, int)}, {@link #scrollToPosition(int)} and
* {@link #smoothScrollToPosition(int)} are dropped; TouchEvents and GenericMotionEvents are
* dropped; {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} will not be
* called.
*
*
* setLayoutFrozen(true)
does not prevent app from directly calling {@link
* LayoutManager#scrollToPosition(int)}, {@link LayoutManager#smoothScrollToPosition(
* RecyclerView, State, int)}.
*
* {@link #setAdapter(Adapter)} and {@link #swapAdapter(Adapter, boolean)} will automatically
* stop frozen.
*
* Note: Running ItemAnimator is not stopped automatically, it's caller's
* responsibility to call ItemAnimator.end().
*
* @param frozen true to freeze layout and scroll, false to re-enable.
*/
public void setLayoutFrozen(boolean frozen) {
if (frozen != mLayoutFrozen) {
assertNotInLayoutOrScroll("Do not setLayoutFrozen in layout or scroll");
if (!frozen) {
mLayoutFrozen = false;
if (mLayoutWasDefered && mLayout != null && mAdapter != null) {
requestLayout();
}
mLayoutWasDefered = false;
} else {
final long now = SystemClock.uptimeMillis();
MotionEvent cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
onTouchEvent(cancelEvent);
mLayoutFrozen = true;
mIgnoreMotionEventTillDown = true;
stopScroll();
}
}
}
}
阅读注释会发现,在使用setAdapter方法的时候,mLayoutFrozen这个变量的值会默认设置为false:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
....
/**
* Set a new adapter to provide child views on demand.
*
* When adapter is changed, all existing views are recycled back to the pool. If the pool has
* only one adapter, it will be cleared.
*
* @param adapter The new adapter to set, or null to set no adapter.
* @see #swapAdapter(Adapter, boolean)
*/
public void setAdapter(Adapter adapter) {
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, false, true);
processDataSetCompletelyChanged(false);
requestLayout();
}
}
因此在setAdapter之后,调用setLayoutFrozen将mLayoutFrozen的值设置为true,就能够解决父布局事件不响应的问题。