最近在做新项目,需要实现一个淘宝商品详情页的效果,根据滚动的距离切换顶部的tab标签,点击tab标签可以滚动到指定位置。第一次看到这个效果想到的实现方案就是ScrollView和tabLayou的联动,但是因为对view的滑动事件不是很了解,很多方法也不知道,所以只能去百度、谷歌了。在查找的过程中发现RecyclerView也可以实现这个效果,想着RecyclerView实现的话,由于每个内容板块都是一个item,滑动定位应该会很准,不过由于多个请求,需要重新组装数据,有感觉很麻烦,放弃了,现在想起来是有点懒。回到ScrollView的实现方案后查到了一个自定义的ScrollView,只需要将每个内容板块的高度计算好后传到ScrollView,由他内部去处理就可以了,感觉使用起来很简单,就选择了他。一顿操作下载终于实现了这个效果,不过当去使用的时候发现好多问题,比如滑动切换标签的时机都是错的,(这个是因为请求数据后,对一些view隐藏显示导致的,后来选择在每次请求数据后都去重新计算高度,不过效果还是有点差),还有当ScrollView处于惯性滑动时,切换标签无法停止惯性滑动,还有tablayout的指示器和选中状态不统一各种问题,当时有点菜一个也没解决。这次新项目再次用到了这个效果,我觉得是时候去研究一下了原理了,就开始看那个自定义的ScrollView尝试去理解它,一个一个的去解决问题。首先看一下实现后的效果:
下面是遇到的问题,我们一个一个的去解决:
1.切换标签的滑动距离计算的不对:
因为项目中这个界面有3个请求,如果按照之前的写法,我需要在3个请求的成功回调中去重新计算高度,还有一些大图,可能无法立即显示,这个高度也无法去准确的计算出来,所以我干脆不再提前计算好,而是直接将内容板块的view传递过去,实时计算view距离顶部的距离,再根据距离顶部的距离去切换tab。
List views = new LinkedList<>();
views.add(view1);
views.add(view2);
views.add(view3);
views.add(view4);
scrollView.setAnchorList(views); // 设置视图集合
重写ScrollView的onScrollChanged方法,去循环views,然后使用getViewTop()获取view距离顶部的高度,如果滑动距离t大于该view距顶部的高度,就使用setSelectedTab()切换到该tab标签。
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (mViewList == null) {
return;
}
for (int i = mViewList.size() - 1; i >= 0; i--) {
if (t > getScrollDistance(i)) {
// 当达到该View的高度时去切换
setSelectedTab(i);
break;
}
}
}
/**
* 获取View距离顶部的高度(mTranslationY是距离顶部的偏移量)
*
* @param position
* @return
*/
private int getViewTop(int position) {
if (position >= mViewList.size() + 1) {
throw new IndexOutOfBoundsException("TabLayout的tab数量和视图View的数量不一致");
}
return mViewList.get(position).getTop() - mTranslationY;
}
/**
* 设置选中的tab标签
*
* @param position
*/
private void setSelectedTab(int position) {
if (mTabLayout != null && position != oldPosition) {
// 该方法不会走tabLayout的onTabSelected监听
mTabLayout.setScrollPosition(position, 0, true);
}
oldPosition = position;
}
这样切换标签时机就非常准确了。
问题2:惯性滑动时去切换tab无法终止惯性滑动,导致会自己滑动到下个标签。
造成这个问题的原因其实是因为调用方法错误的原因,之前调用的是scrollTo(x, y)方法,改成smoothScrollTo(x, y)就可以了。下面是两个方法的区别
scrollTo(x, y); // 该方法会立即滚动到指定的坐标,不会终止正在滑动的事件。
smoothScrollTo(x, y);// 该方法会平缓的滚动到指定的坐标,并且会终止正在滚动的事件。
问题3:tab标签的指示器和选择tab的字体颜色错乱
其实造成这个的原因和问题2有关系,在我点击tab切换标签的时候,scrollview又惯性滚动到了下个标签,导致tabLayou没有及时的切换所有的状态造成的,解决了问题2该问题也不再出现。这下问题都解决了,回过头来发现其实问题都很简单,只是当时有点摸不着头脑,而且从来没有去看里面的实现逻辑,就打了退堂鼓觉得自己不能理解,以后要拒绝当伸手党,应该多看看源码,理解里面的原理。
为了更方便的使用,我将tabLayout也传递进去了,这样只需要几行代码就可以实现这个效果了,实现起来就两步
1.第一步就是将内容板块的view集合传递进来
List views = new LinkedList<>();
views.add(llCommodity);
views.add(cardEvaluate);
views.add(llDetails);
views.add(llRecommend);
scrollView.setAnchorList(views); // 设置视图集合
2.第二部绑定tabLayout
scrollView.setupWithTabLayout(tabLayout); // 设置绑定的tabLayout
3.如果还想实现一个标题栏的渐变,可以设置滑动监听和距离顶部的偏差值
// 因为标题栏在ScrollView上面,所以需要设置一个距离顶部的偏差值(标题栏的高度+状态栏的高度),防止标题栏遮盖布局
scrollView.setTranslationY(SizeUtils.getMeasuredHeight(toolbar) + BarUtils.getStatusBarHeight());
scrollView.setOnScrollCallback(new TabWithScrollView.OnScrollCallback() {
@Override
public void onScrollCallback(int l, int t, int oldl, int oldt) {
setBgAlphaChange(t, height);
}
});
下面是整个类的代码以及源码:
/**
* Created by Hao on 2019/7/21.
* Describe ScrollView和TabLayout的联动
*/
public class TabWithScrollView extends ScrollView {
private static final String TAG = "TabWithScrollView";
/**
* 模块View的集合
*/
private List mViewList;
/**
* 是否是ScrollView引起的滑动,true-是,false-TabLayout引起的滑动
*/
private boolean isManualScroll;
/**
* 记录上一次点击的position,防止多次点击
*/
private int oldPosition = 0;
/**
* 需要联动的tabLayout
*/
private TabLayout mTabLayout;
/**
* ScrollView的滑动回调
*/
private OnScrollCallback onScrollCallback;
/**
* 距离顶部的偏移量,默认为10px;
*/
private int mTranslationY = 10;
public TabWithScrollView(Context context) {
super(context);
setOnTouchListener();
}
public TabWithScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
setOnTouchListener();
}
public TabWithScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOnTouchListener();
}
@SuppressLint("ClickableViewAccessibility")
public void setOnTouchListener() {
super.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
isManualScroll = true;
}
return false;
}
});
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (onScrollCallback != null) {
onScrollCallback.onScrollCallback(l, t, oldl, oldt);
}
if (isManualScroll) {
if (mViewList == null) {
return;
}
for (int i = mViewList.size() - 1; i >= 0; i--) {
if (t > getViewTop(i)) {
setSelectedTab(i);
break;
}
}
}
}
/**
* 获取View距离顶部的高度(mTranslationY是距离顶部的偏移量)
*
* @param position
* @return
*/
private int getViewTop(int position) {
if (position >= mViewList.size() + 1) {
throw new IndexOutOfBoundsException("TabLayout的tab数量和视图View的数量不一致");
}
return mViewList.get(position).getTop() - mTranslationY;
}
/**
* 设置选中的tab标签
*
* @param position
*/
private void setSelectedTab(int position) {
if (mTabLayout != null && position != oldPosition) {
// 该方法不会走tabLayout的onTabSelected监听
mTabLayout.setScrollPosition(position, 0, true);
}
oldPosition = position;
}
/**
* 设置绑定的tabLayout,并给tabLayout添加OnTabSelectedListener监听
*
* @param tabLayout
*/
public void setupWithTabLayout(TabLayout tabLayout) {
if (mTabLayout != null) {
mTabLayout.removeOnTabSelectedListener(mTabSelectedListener);
}
if (tabLayout != null) {
mTabLayout = tabLayout;
mTabLayout.addOnTabSelectedListener(mTabSelectedListener);
}
}
public void setAnchorList(List anchorList) {
this.mViewList = anchorList;
}
public void setOnScrollCallback(OnScrollCallback onScrollCallback) {
this.onScrollCallback = onScrollCallback;
}
public void setTranslationY(int translationY) {
this.mTranslationY = translationY;
}
TabLayout.OnTabSelectedListener mTabSelectedListener = new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
isManualScroll = false;
if (mViewList == null) {
Log.i(TAG, "onTabSelected: 未设置View集合");
return;
}
// smoothScrollTo可以平滑的滑动到指定位置,并打断惯性滑动
smoothScrollTo(0, getViewTop(tab.getPosition()));
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
};
/**
* ScrollView的滚动回调
*/
public interface OnScrollCallback {
void onScrollCallback(int l, int t, int oldl, int oldt);
}
}
源码地址:https://github.com/returntoorigin/CustomView