###0.前言
###1.实现思路
- 布局的嵌套层级为:
- FrameLayout
- SmartRefreshLayout
- ScrollView
- LinearLayout
- HeaderView
- TabLayout
- ViewPager
- LinearLayout
- ScrollView
- SmartRefreshLayout
- FrameLayout
- 显示的默认状态和吸顶状态图
实现思路:
第一步:吸顶效果
- 吸顶标题栏高度+内容页高度+状态栏高度=屏幕高度时,可以达到吸顶效果。
通俗的解释就是ScrollView滑动到底部且再也不能滑动了,此时,吸顶标题栏刚好处于屏幕最上方。
怎么实现呢?
- 就是通过动态计算内容页的高度:内容页高度 = 屏幕高度 - 状态栏高度 - 吸顶标题栏高度
第二步:吸顶后的内容页滑动
- 明确什么时候滑动外部的ScrollView,什么时候滑动内容页中的滑动控件(如RecycleView、NestedScrollView)
本篇例子的滑动分配思路如下:
- 当内容页未达到最顶部(系统状态栏高度+吸顶标题栏高度)时,滑动事件由顶层的ScrollView处理
- 当内容页到达最顶部时,滑动事件分配给内容页内部处理
- 内容页内部处理滑动事件分两种状况
- 正常上下滑动(内部自消化)
- 滑动到顶部后继续向上滑动(无法消化,重新交给顶层ScrollView)
以上就是实现吸顶效果的全部思路。
###2.Smartrefresh+ScrollView+ViewPager+Fragment+RecylerView实现示例
第一步思路的核心实现为内容页,即此示例的ViewPager
ViewPager计算高度的核心方法:
//MatchViewPager.java
private int calcHeight(){
ViewGroup parent = (ViewGroup) getParent();
int height = DeviceUtil.getDeviceHeight(getContext()) - mStatusBarHeight;
if(parent != null && mTargetId > 0){
View targetView = parent.findViewById(mTargetId);
if(targetView != null){
mTargetViewHeight = targetView.getHeight();
if(mTargetViewHeight <= 0){
mTargetViewHeight = ViewUtil.getViewHeight(targetView);
}
height = height - mTargetViewHeight;
}
}
height = height - mRemoveHeight;//扣除偏移量
return height;
}
- mStatusBarHeight:状态栏高度
- mTargetId:标记控件的ID,此处为吸顶标题栏
- mRemoveHeight:需要额外移除的高度(例如:吸顶标题栏上方可能存在Toolbar,此处可以设置Toolbar的高度进行移除)
1.ViewPager计算高度
方案一:重写onMeasure进行测量
//MatchViewPager.java protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int height = calcHeight() + 1;//滑动判断需要,事件处理时讲解 heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, heightMeasureSpec); }
方案二:设置viewpager的LayoutParams
//MatchViewPager.java private void initHeight(){ //在自定义控件的构造器中调用 post(new Runnable() { @Override public void run() { ViewGroup.LayoutParams params = getLayoutParams(); params.height = calcHeight()+1;//滑动判断需要,事件处理时讲解 setLayoutParams(params); } }); }
其中,方案一在onMeasure进行测量时,需要获取targetView(吸顶标题栏)的高度,所以会通过view.measure(childWidthSpec, childHeightSpec);
计算一次控件宽高。但因为获取的是view.getMeasuredHeight()
所以与实际显示时存在一定偏差。此处使用TabLayout表现出来的效果是:TabLayout的子控件全部居左且不等分布局宽度,滑动布局后恢复正常。
而方案二因为是通过post()异步设置ViewPager高度,所以不存在此问题。
2.事件的分发处理
步骤一:自定义顶层ScrollView,处理什么时候分配给内容页处理
核心是重写onInterceptTouchEvent(MotionEvent ev)
,在此方法中进行拦截:
//DetailScrollView.java
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if(action == MotionEvent.ACTION_DOWN){//按下
downY = ev.getRawY();
}else if(action == MotionEvent.ACTION_MOVE){//移动
float dy = ev.getRawY() - downY;
//1.判断上下滑动
//2.通过mIsInterceptTouchEvent将是否中断交给外部实现
if(Math.abs(dy) > mTouchSlop && mIsInterceptTouchEvent){//是否中断事件向下分发
return true;
}else{
if(mScrollStateListener != null){
//3.页面滑向顶部时,子页面不能处理则交给父控件
if(dy > 0 && !mScrollStateListener.isChildTouchEvent()){//手指向下滑(即页面滑向顶部)
return true;
}
}
}
}
return super.onInterceptTouchEvent(ev);
}
public void setInterceptTouchEvent(boolean isInterceptTouchEvent) {
this.mIsInterceptTouchEvent = isInterceptTouchEvent;
}
- mTouchSlop:configuration.getScaledTouchSlop();//移动距离要大于这个距离才开始移动控件
- mIsInterceptTouchEvent:变量,提供外部动态设置是否拦截事件
- mScrollStateListener:监听子控件触摸事件消化情况
外部实现拦截:
//MainActivity.java
private void initScrollView(){
mSvFirst.setInterceptTouchEvent(true);//设置默认为ScrollView滑动
mSvFirst.setDetailScrollStateListener(this);//设置滑动状态监听
mSvFirst.setScrollViewListener(this);//ScrollView滑动是否分发给子控件
}
@Override
public void updateTouchEvent(boolean isInterceptTouchEvent) {
if(mSvFirst != null)
mSvFirst.setInterceptTouchEvent(isInterceptTouchEvent);
}
@Override
public void onScrollChanged(ObservableScrollView scrollView, int x, int y, int oldx, int oldy) {
int[] location = new int[2];
mVpMain.getLocationOnScreen(location);
int yPosition = location[1];
if (yPosition < mVpMain.getHeightOffset()) {//移动到顶部,小于顶部偏移量(ViewPager完全显示)
updateTouchEvent(false);
} else {
updateTouchEvent(true);
}
}
通过ScrollView的滑动监听onScrollChanged(...)
,监听子控件ViewPager当前的Y轴坐标是否达到最顶部去(即ViewPager完全显示),
当ViewPager完全显示则通知ScrollView不要拦截事件。
calcHeight() + 1
的问题:
由于滑动是否拦截的判断条件为yPosition < mVpMain.getHeightOffset()
,yPosition能达到的临界值恒大于等于mVpMain.getHeightOffset()
,所以条件恒为false。如果不写calcHeight() + 1
那么可以将条件改为:yPosition <= mVpMain.getHeightOffset()
(加上等于判断)
步骤二:子控件是否处理滑动事件
核心在于将子控件的事件处理状况通知ScrollView,即实现mScrollStateListener.isChildTouchEvent()
方法。代码如下:
//MainActivity.java
@Override
public boolean isChildTouchEvent() {
Fragment fragment = mFragments.get(mVpMain.getCurrentItem());
if(fragment instanceof BaseDetailFrag){
return ((BaseDetailFrag)fragment).canChildTouch();
}
return false;
}
BaseDetailFrag类集成子控件的滑动监听实现:
//BaseDetailFrag.java
protected void updateCanTouch(boolean canTouch){
this.canTouch = canTouch;
}
public boolean canChildTouch() {
return canTouch;
}
protected RecyclerView.OnScrollListener getOnScrollListener(){
return new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if(!recyclerView.canScrollVertically(-1)){//在顶部,且当前View已经允许可滑动
if(dy < 0){//向上滑,应该直接滑动父容器
updateCanTouch(false);
}else{//向下滑,直接滑动
updateCanTouch(true);
}
}else{
updateCanTouch(true);
}
}
};
}
Fragment实现类中的使用:
ListFragment.java
private void initRecyclerView(){
//...
//设置RecyclerView的滑动监听,在基类中已有默认方法
recyclerView.addOnScrollListener(getOnScrollListener());
}
NestedScrollView.java
private void initView(View rootView){
//设置NestedScrollView的滑动监听,在基类中已有默认方法
mScrollView.setOnScrollChangeListener(getNestedScrollListener());
}
###3.总结
- Android手机的兼容问题,部分手机的naigationbar是可以隐藏的,需要注意!
- 沉浸式的时候需要注意在onScrollChanged(…)中对吸顶的标题栏进行处理
- 处理方案一:动态扩展吸顶状态栏高度(paddingTop)
- 处理方案二:一直隐藏一个吸顶状态栏在顶部,到达顶部时显示,否则隐藏(很粗暴但很实用)
- 仿美团的条件筛选栏+recylerView的也可以通过此方式实现:(查看示例代码)
- MatchViewPager -> MatchFrameLayout
- 不使用BaseDetailFrag,直接为本页面RecycleView添加滑动监听
###4.附
END
–Nowy
–2019.02.19