先看实现的UI效果
其实就是仿BOSS的页面效果,第二层tab下的viewpager滑到最右边再右滑,就操作第一层viewpager滑动。页面上滑时把第一层tab和vp里的banner都推出界面,让第二层tab吸顶。
滑上去第二个tab块卡在顶部,如图
我混乱的思路:
之前没有外层tab的需求,直接用CoordinatorLayout的一套放在布局文件里做吸顶就行了。
现在是把CoordinatorLayout挪到第一层用(用scrollView嵌套也行,但是也要处理滑动冲突,以及第一层viewpager卡在界面上才行,感觉更麻烦)
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinator"
style="forn"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleEnabled="false">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_44"
app:tabIndicator="@drawable/indicator_tab" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.shirley.widget.ChildViewPager
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
这样外层页面上滑时只会推出第一层tab,第一层viewpager都会卡在屏幕里,至于banner,在第二层页面里处理。
第二层页面用NestedScrollView嵌套了viewpager,放了一个第二层的假tabLayout在第二层vp顶部,计算banner滑出页面时,也就是真tab滑到顶部时,显示假tab,做吸顶效果。
我这里的NestedScrollView外面还套了一层SmartRefreshLayout,因为这个页面还有整体刷新功能。
第二层fragment中代码如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/windowBackground"
android:orientation="vertical">
<com.scwang.smartrefresh.layout.SmartRefreshLayout
android:id="@+id/smart_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/rl_bottom">
<com.scwang.smartrefresh.layout.header.ClassicsHeader
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.shirley.widget.CustomNestedScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- banner -->
<com.youth.banner.Banner
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_114"
android:contentDescription="@null"/>
<!-- 第二层tab -->
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="50dp" />
<com.shirley.widget.ChildViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</com.shirley.widget.CustomNestedScrollView>
<com.scwang.smartrefresh.layout.footer.ClassicsFooter
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.scwang.smartrefresh.layout.SmartRefreshLayout>
<!-- 第二层假tab吸顶占位 -->
<com.google.android.material.tabs.TabLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:visibility="gone" />
</RelativeLayout>
不行我要下班了 今天先写到这
继续
布局中用到的viewpager是自定义的ChildViewPager,可以判断在最后一个item滑动时,把事件让给父viewpager。
public class ChildViewPager extends ViewPager {
int startX;
int startY;
public ChildViewPager(@NonNull Context context) {
super(context);
}
public ChildViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//页面当前位置
int currentPosition;
//总页数
int count = Objects.requireNonNull(getAdapter()).getCount();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) ev.getRawX();
//startY = (int) ev.getY();
//申请让父View 不要拦截触摸事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int endX = (int) ev.getRawX();
int disX = endX - startX;
currentPosition = this.getCurrentItem();
//最后一页且往左滑,由父View拦截触摸事件
if (currentPosition == count - 1 && disX < 0) {
//申请让父View拦截触摸事件
getParent().requestDisallowInterceptTouchEvent(false);
}
//第一页且右滑,由父View拦截触摸事件
else if (currentPosition == 0 && disX > 0) {
//申请让父View拦截触摸事件
getParent().requestDisallowInterceptTouchEvent(false);
}
//其他情况,由自己拦截
else {
//申请让父View拦截触摸事件
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
}
自定义的CustomNestedScrollView,判断NestedScrollView是否拦截滑动事件
public class CustomNestedScrollView extends NestedScrollView {
private boolean isNeedScroll = true;
public CustomNestedScrollView(@NonNull Context context) {
super(context);
}
public CustomNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
return isNeedScroll;
}
return super.onInterceptTouchEvent(ev);
}
/*
* 处理NestedScrollView是否拦截滑动事件
*/
public void setNeedScroll(boolean isNeedScroll) {
this.isNeedScroll = isNeedScroll;
}
}
第二层vp的子fragment中处理scrollView的事件分发和假tab吸顶,判断已经划过banner,显示假tab。
有个问题是scrollVIew一直拿到触摸事件,会抢走页面上的点击事件,加了个判断
val measureW = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
val measureH = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
binding.rlBanner.measure(measureW, measureH)
binding.scrollView.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
if (Math.abs(scrollY - oldScrollY) < 2 || scrollY == 0) {
//滑动不超过2 认为是点击事件(否则isNeedScroll=true会拦截界面点击事件)
binding.scrollView.setNeedScroll(false)
} else {
if (scrollY > binding.rlBanner.measuredHeight) {
binding.llTabFake.visibility = View.VISIBLE
binding.scrollView.setNeedScroll(false)
} else {
binding.llTabFake.visibility = View.GONE
binding.scrollView.setNeedScroll(true)
}
}
}
binding.scrollView.setNeedScroll(false)
还要处理scrollVIew嵌套viewpager时,vp的高度为0,这里动态给它一个高度,它的最大高度也就是吸顶时tab下方的屏幕高度
binding.llTabFake.measure(measureW, measureH)
val params: ViewGroup.LayoutParams = binding.viewPager.layoutParams
params.height = ScreenUtil.getScreenH(requireContext()) - binding.llTabFake.measuredHeight + 1
binding.viewPager.layoutParams = params
注意这里的高度有一个+1,因为不+1就刚好是TabLayout要出现的临界点,也就是ViewPager恰好的高度,但是这个时候又刚好是我们NestedScrollView拦截没有取消的临界点,所以在上滑时,TabLayout刚好悬浮顶部的时候,RecyclerView没有获取事件,就会无法进行滑动。
这里是参考的这个https://www.cnblogs.com/shen-hua/p/8052459.html
大概就是这样了
再次更新
发现嵌套的组件向上滑动时,经常会卡在banner上,只滑动最里层viewpager里的list,滑到最后才会把整个banner顶上去,露出隐藏的假tab
(如果你的页面没用中间那块banner就不用管,之前那样是刚好的)
手指在列表处向上滑动时,如下图,第一层tab已经推上去,banner和第二层tab卡在顶部不动,list可以往上滑,但是不会把上面那块顶出去
这里也找到了一个解决办法,参照https://www.jianshu.com/p/b9ddac13b135
在第二层自定义的viewPager中重写onMeasure()方法,把它测量中的MeasureSpec.EXACTLY换成MeasureSpec.UNSPECIFIED
public class ChildViewPager extends ViewPager {
int startX;
int startY;
public ChildViewPager(@NonNull Context context) {
super(context);
}
public ChildViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//页面当前位置
int currentPosition;
//pager无数据
if (getAdapter() == null)
return super.dispatchTouchEvent(ev);
//总页数
int count = Objects.requireNonNull(getAdapter()).getCount();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) ev.getRawX();
startY = (int) ev.getY();
//申请让父View 不要拦截触摸事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int endX = (int) ev.getRawX();
int disX = endX - startX;
int moveY = (int) ev.getRawY();
currentPosition = this.getCurrentItem();
//最后一页且往左滑,由父View拦截触摸事件
if (currentPosition == count - 1 && disX < 0) {
//申请让父View拦截触摸事件
getParent().requestDisallowInterceptTouchEvent(false);
}
//第一页且右滑,由父View拦截触摸事件
else if (currentPosition == 0 && disX > 0) {
//申请让父View拦截触摸事件
getParent().requestDisallowInterceptTouchEvent(false);
}
//其他情况,由自己拦截
else {
//申请让父View拦截触摸事件
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int height = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if(child.getVisibility() != GONE){
child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
int h = child.getMeasuredHeight();
if(h > height){
height = h;
}
}
}
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}