仿网易云音乐Android端歌手资料页面的实现

一种嵌套滑动Tab悬停布局的实现方法

Posted by lx8421bcd on May 21, 2016

原文由本人于2016年创作于CSDN,后续修改完善提交于此。

最近项目首页需要用到Banner + Tab + ViewPager切换的效果,在思考实现的过程中我突然发现,这个效果与网易云音乐Android端的歌手资料页面十分相似,因此好好把玩了一下网易云音乐,然后模仿出了一个效果类似的页面,这里就将界面元素完全替换为模仿网易云音乐,作为一个demo拿来分享。

项目地址: 网易云音乐歌手资料页面Demo

demo

说一下实现思路:

页面分为三个部分,最上方是透明的自定义导航栏(Toolbar),下面是带有歌手图片和PagerSlidingTabStrip的Header,最下方是带有各个子页面的ViewPager。
最开始没有什么实现思路,后来看到网易云音乐界面的嵌套滑动效果,列表的滚动条被Header遮挡,恍然大悟,然后按照这个思路做了实现。

在滑动的过程中是不能够改变ViewPager里面滑动组件的高度的,不然会导致滑动距离判断出现异常,因此ViewPager实际上是占满整个屏幕的。为了保证上面的Header不会把列表项挡住,在各个Fragment中加入一个与Header高度一样的空View作为占位。这样基本上就完成了基本页面的样式。水平滑动基本上是没什么问题了。

接下来就要解决上下滑动时Header的伸缩距离与ViewPager内滑动元素的滚动同步的问题。

列表页面使用RecyclerView,各种控件组合的页面使用ScrollView,这些控件的滑动距离与Header伸缩的距离是1:1的,而滑动距离与Header变色实际上也是一次函数关系,因此只需要检测到RecyclerView和ScrollView的滑动距离,就可以根据这个值的变化来改变头部,RecyclerView可以通过addOnScrollChangedListener来监测滑动距离:

rcvGoodsList.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, final int dx, final int dy) {
        super.onScrolled(recyclerView, dx, dy);
        scrolledX += dx;
        scrolledY += dy;
        if(HomeListFragment.this.isResumed()) {
            doOnScrollChanged(scrolledX, scrolledY, dx, dy);

        }
    }
});

scrolledX和scrolledY分别记录了RecyclerView的滑动距离,并通过设定在Fragment中的Listener传递给上层。

ScrollView则需要继承重写,并为其添加监测滑动距离的方法

public class ObservableScrollView extends ScrollView {
 
    public interface OnScrollChangedListener {
 
        void onScrollChanged(ScrollView scrollView, int scrolledX, int scrolledY, int dx, int dy);
    }
 
    private int scrolledX;
 
    private int scrolledY;
 
    private OnScrollChangedListener listener;
 
    public ObservableScrollView(Context context) {
        super(context);
    }
 
    public ObservableScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
 
    public ObservableScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
 
    public void setOnScrollChangedListener(OnScrollChangedListener listener) {
        this.listener = listener;
    }
 
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        int dl = l - oldl;
        int dt = t - oldt;
        scrolledX += dl;
        scrolledY += dt;
        if(listener != null) {
            listener.onScrollChanged(this, getScrollX(), getScrollY(), dl, dt);
        }
 
    }
}

在Header所属的页面,对ViewPager所属的Fragment加入监测滑动距离的Listener,设定好计算颜色变化和滑动距离的初始值即可

/**
* 初始化滑动参数,k值
* */
private void initSlidingParams() {
    int headerSize = getResources().getDimensionPixelOffset(R.dimen.home_header_size);
    int navBarHeight = getResources().getDimensionPixelOffset(R.dimen.nav_bar_height);
    int tabStripHeight = getResources().getDimensionPixelOffset(R.dimen.tabstrip_height);
    slidingDistance = headerSize - navBarHeight - tabStripHeight;
    Log.d("HomeFragment", "slidingDistance" + slidingDistance);
}


/**
* 根据页面滑动距离改变Header方法
* */
private void scrollChangeHeader(int scrolledY) {
    if (scrolledY < 0) {
        scrolledY = 0;
    }
    if (scrolledY < slidingDistance) {
        rlNavBar.setBackgroundColor(Color.argb(scrolledY * 192 / slidingDistance, 0x00, 0x00, 0x00));
        llHeader.setPadding(0, -scrolledY, 0, 0);
        currScrollY = scrolledY;
    } else {
        rlNavBar.setBackgroundColor(Color.argb(192, 0x00, 0x00, 0x00));
        llHeader.setPadding(0, -slidingDistance, 0, 0);
        currScrollY = slidingDistance;
    }
}

这样单个列表滑动就已经实现了,但是还有一个很重要的问题没有解决,试想这样一个场景:我滑动了第一页,把Header滑到了最小值,此时我切到了另一个页面,而这个页面的列表还是未滑动的,这时占位的空View就显示出来了,或者说我在一个页面把Header滑到了最大,切到了另一个页面,就把列表给挡住了。
对于这个问题,我又看了一眼网易云音乐的页面,发现它的做法是在切换时把各个列表的滑动距离都设定为当前Header的滑动距离,so,我也采取了这样的方法

viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}

    @Override
    public void onPageSelected(int position) {
        currentPosition = position;
        displayFragments.get(position).setScrolledY(currScrollY);
    }

    @Override
    public void onPageScrollStateChanged(int state) {}
});

在Fragment基类中定义设置滑动距离的抽象方法

public abstract void setScrolledY(int scrolledY);

在含有RecyclerView的Fragment中实现

@Override
public void setScrolledY(int scrolledY) {
    if(rcvGoodsList != null) {
        if (this.scrolledY >= scrolledY) {
            int scrollDistance = (this.scrolledY - scrolledY) * -1;
            rcvGoodsList.scrollBy(0, scrollDistance);
        }
        else {
            rcvGoodsList.scrollBy(0, scrolledY);
        }
    }
}

在含有ScrollView的Fragment中实现

@Override
public void setScrolledY(int scrolledY) {
    if (osvHomeRecommend != null) {
        osvHomeRecommend.scrollTo(0, scrolledY);

    }
}

这样就实现了滑动距离的同步,最后一个问题是下拉刷新与列表上滑手势的冲突,在这里我使用的是SwipeRefreshLayout来实现下拉刷新,因此对于此冲突的解决办法就是自定义SwipeRefreshLayout,重写onInterceptTouchEvent,加入监听,如果Header伸缩距离没有恢复到0,就不触发下拉刷新。

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {

    if (listener != null && !listener.onInterceptTouchEvent(event)) {
        return false;
    }
    return super.onInterceptTouchEvent(event);
}

//使用方法
srlRefresh.setOnInterceptTouchEventListener(new CusSwipeRefreshLayout.OnInterceptTouchEventListener() {
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return currScrollY == 0;
    }
});

这样基本的效果就已经实现了,在网易云音乐的页面中还有一个可以横向滑动的列表,这里也需要在ViewPager中对水平滑动的事件进行拦截判断,只不过我暂时用不上就没有实现了,原理同下拉刷新,代码都在Github上,这里只列出了一些实现思路相关的代码,仅供参考