一、淘宝首页布局结构设计及实现
仿淘宝首页的嵌套布局,TabLayout 上面放置一个 RecyclerView 模拟嵌套效果(TopRecyclerView 继承 RecyclerView 填充了几条静态数据且不能滑动),ViewPager的Fragment 中只有一个 RecyclerView 控件,整体的布局是 ScrollView + RecyclerView + TabLayout + ViewPager + Fragment + RecyclerView。
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- top rcv -->
<com.example.practicedemo.ui.nest.widget.TopRecyclerView
android:id="@+id/rcv_top"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</LinearLayout>
</ScrollView>
按照以上布局显示,发生嵌套冲突只有ViewPager下的RecyclerView能滑动,而且滑动空间仅限于ViewPager范围内,TopRecyclerView展示全部数据不能滑动。
题外话:ViewPager2(19年11月正式发布)
以上布局中用到了ViewPager2,相对原来的ViewPager API有如下改动:
- 继承自ViewGrop,是ViewPager2被声明成final。意味着我们不可能再像ViewPager一样通过继承来修改ViewPager2的代码;
- FragmentStatePagerAdapter被FragmentStateAdapter 替代;
- PagerAdapter被RecyclerView.Adapter替代;
- addPageChangeListener被registerOnPageChangeCallback替代。(ViewPager的addPageChangeListener接收的是一个OnPageChangeListener的接口,当要监听页面变化时需要重写接口中的三个方法。而ViewPager2的registerOnPageChangeCallback方法接收的是OnPageChangeCallback的抽象类,因此可选择性的重写需要的方法即可);
- 移除了setPargeMargin方法。
使用时需添加依赖包:
// androidx viewPager2
implementation "androidx.viewpager2:viewpager2:1.0.0-alpha01"
// ViewPager2 与 TabLayout联动
implementation 'com.google.android.material:material:1.2.0-alpha03'
二、解决不能一起滑动的问题------点击事件冲突源码分析
解决问题之前首先要清楚为什么会有此现象,所以先来理一下事件冲突源码。
1、事件类型及说明
MotionEvent事件 | 说明 |
---|---|
ACTION_DOWN | 手指初次接触到屏幕时触发 |
ACTION_MOVE | 手指在屏幕上滑动时触发,会多次触发 |
ACTION_UP | 手指离开屏幕时触发 |
ACTION_CANCEL | 事件被上层拦截时触发 |
2、View只能处理事件,ViewGroup才能分发事件(先分发再处理,需包含子View)
有一点要明白,在代码层面View是ViewGroup的父类,但在运行时ViewGroup是View的父类(studio中用Tools–layout Inspector查看),以下 2.1 流程中的DecorView继承自FrameLayout(继承自ViewGroup),但运行时他是所有控件的父类(如图):
2.1 事件分发处理流程前奏
- 点击事件后先走 Activity 的 dispatchTouchEvent()
- Window ==> PhoneWindow 的 superDispatchTouchEvent()
- DecorView的superDispatchTouchEvent(),其中直接调的是super.dispatchTouchEvent()
- DecorView继承自FrameLayout,但其并未重写dispatchTouchEvent()方法,故调用的是ViewGroup中的dispatchTouchEvent()--------事件分发的逻辑在此方法中
2.2 View的事件处理流程
先看一个案例,根据打印log来看当setOnTouchListener中 return false 时 onClick 方法会执行,但 return true时 onClick 方法不会执行,这是为什么呢?
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_click = findViewById(R.id.btn_click);
btn_click.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG, "onClick");
}
});
btn_click.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e(TAG, "onTouch: " + event.getAction());
// return true;
return false;
}
});
}
源码中View 的 dispatchTouchEvent()方法,主要看 # 标记的代码:
- 判断条件是一个短路与,只要前面有不成立的则直接跳出,条件不满足;
- 经过分析,只要设置了onTouch的点击监听事件前三个条件都为true,所有最后一个条件onTouch返回的值直接影响 result 的值;
- 再往下走,# 号标记的最后两行判断又是一个短路与,只要result 为 true 则不会走第二个判断条件onTouchEvent(),情况和上面代码中的onClick() 执行流程结果一样,所以大胆猜测 onClick() 是否在 onTouchEvent() 中;
- 果然 onTouchEvent() ==》(MotionEvent.ACTION_UP)performClickInternal() ==》 performClick(),走到这之后以上案例结果的原因也明了了。
- 另外,performClick() 方法中当执行了 onClick() 对应 result 为 true(处理了事件),否则为 false(未处理事件),这也能解释为什么 onTouch() 有返回值而 onClick() 中无需返回值。
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
# ListenerInfo li = mListenerInfo;
# if (li != null && li.mOnTouchListener != null
# && (mViewFlags & ENABLED_MASK) == ENABLED
# && li.mOnTouchListener.onTouch(this, event)) {
# result = true;
# }
# if (!result && onTouchEvent(event)) {
# result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
2.3 ViewGroup 的事件分发流程(事件冲突关键点)
//ViewGroup 中 事件分发流程
@Override
public boolean dispatchTouchEvent(Motio