Android-仿网易云歌手资料页面的实现-NestedScrolling

一、简介

先来看看效果图:

按照上图:

按照传统的事件分发去理解,我们滑动的是下面的内容区域,而移动的却是外部的ViewGroup,如果采用传统的事件分发,是外部的Parent拦截了(Parent的onInterceptTouchEvent返回true)内部的Child的事件,但是,上面的效果中,当Parent滑动到一定的距离时,Child又开始滑动,整个过程是同一个事件序列。传统的事件分发中,当Parent拦截了事件后(Parent的onInterceptTouchEvent返回true),是无法再把事件交给Child的。

注意:某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话)并且它的onInterceptTouchEvent不会再被调用。

但是NestedScrolling机制来处理这个事情就很好办,不了解的可以先了解一下再回来。

NestedScrolling 推荐这篇文章:https://www.jianshu.com/p/f09762df81a5

接下来上代码,首先是布局文件:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<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"
    tools:context=".MainActivity">
    <ImageView
        android:id="@+id/id_stickynavlayout_avatar"
        android:layout_width="match_parent"
        android:layout_height="220dp"
        android:src="@drawable/taylor_swift"
        android:scaleType="centerCrop"/>

    <com.example.hp.android_stickynavlayout.custom.StickNavLayout
        android:id="@+id/id_stickynavlayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:fillViewport="true">
        <com.example.hp.android_stickynavlayout.custom.SimpleViewPagerIndicator
            android:id="@+id/id_stickynavlayout_indicator"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_marginTop="220dp"
            android:background="@android:color/white">
        </com.example.hp.android_stickynavlayout.custom.SimpleViewPagerIndicator>

        <android.support.v4.view.ViewPager
            android:id="@+id/id_stickynavlayout_viewpager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_toEndOf="@id/id_stickynavlayout_indicator"
            android:layout_toRightOf="@id/id_stickynavlayout_indicator">
        </android.support.v4.view.ViewPager>

    </com.example.hp.android_stickynavlayout.custom.StickNavLayout>

    <include layout="@layout/online_search_bar"/>

</RelativeLayout>

最外层是RelativeLayout,然后是顶部图片,然后是我们的自定义的控件StickyNavLayout,注意它的宽高都是match_parent,然后是Vp的指示器(SimpleViewPagerIndicator),最后是ViewPager。

注意这里StickyNavLayout 在顶部图片得上层,要为顶部图片留出空, SimpleViewPagerIndicator 设置了marginTop。

扫描二维码关注公众号,回复: 2403263 查看本文章

还有 ViewPager 的父布局 StickyNavLayout 要添加 android:fillViewport="true" ,否则Viewpager无法显示。

接下来是MainActivity:

public class MainActivity extends AppCompatActivity implements SimpleViewPagerIndicator.IndicatorClickListener, StickNavLayout.MyStickyListener{

    public static final String UID = "UID";
    public static final String[] titles = new String[]{"单曲","专辑","MV","歌手信息"};

    @Bind(R.id.id_stickynavlayout)
    StickNavLayout mStickNavLayout;

    @Bind(R.id.id_stickynavlayout_avatar)
    ImageView iv_avatar;
    @Bind(R.id.id_stickynavlayout_indicator)
    SimpleViewPagerIndicator mIndicator;
    @Bind(R.id.id_stickynavlayout_viewpager)
    ViewPager mViewPager;
    private TabFragmentPagerAdapter mAdapter;

    private List<Fragment> mFragments = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        if(Build.VERSION.SDK_INT >= 21){
            View decorView = getWindow().getDecorView();
            //int option = View.SYSTEM_UI_FLAG_VISIBLE|View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
            int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE|View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
            decorView.setSystemUiVisibility(option);
//            getWindow().setStatusBarColor(Color.parseColor("#9C27B0"));
            getWindow().setStatusBarColor(Color.TRANSPARENT);
        }

        initView();
        initData();
    }

    protected void initData() {
    }

    protected void initView() {
        mIndicator.setIndicatorClickListener(this);
        mIndicator.setTitles(titles);
        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                mIndicator.scroll(position,positionOffset);
            }
            @Override
            public void onPageSelected(int position) {
            }
            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });
        for(int i=0;i<titles.length;i++){
            mFragments.add(ADetailSongFragment.newInstance());
        }
        mAdapter = new TabFragmentPagerAdapter(getSupportFragmentManager(),mFragments);
        mViewPager.setAdapter(mAdapter);
        mViewPager.setCurrentItem(0);

        mStickNavLayout.setScrollListener(this);

        int height = DisplayUtil.getScreenHeight(MainActivity.this)-DisplayUtil.dip2px(MainActivity.this,65)-DisplayUtil.dip2px(MainActivity.this,40);
        LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) mViewPager.getLayoutParams();
        layoutParams.height = height;
        mViewPager.setLayoutParams(layoutParams);
    }

    public static void toArtistDetailActivity(Context context, String uid){
        Intent intent = new Intent(context,MainActivity.class);
        intent.putExtra(UID,uid);
        context.startActivity(intent);
    }

    @Override
    public void onClickItem(int k) {
        mViewPager.setCurrentItem(k);
    }

    //获取手机屏幕宽度,像素为单位
    private float getMobileWidth() {
        DisplayMetrics dm = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(dm);
        int width = dm.widthPixels;
        return width;
    }

    //改变顶部图片的大小,参数为导航栏相对于其父布局的top
    @Override
    public void imageScale(float bottom) {
        float height = DisplayUtil.dip2px(MainActivity.this,220);
        float mScale = bottom/height;
        float width = getMobileWidth()*mScale;
        float dx = (width-getMobileWidth())/2;
        iv_avatar.layout((int)(0-dx),0,(int)(getMobileWidth()+dx),(int)bottom);
    }
}

注意在 initView 中,为 ViewPager动态设置了高度,因为当布局向上滚动到 导航栏 贴到 标题栏 时,ViewPager达到最大高度,而在布局时我们并不知道这个高度是多少,如果设置为match_parent(注释动态设置的代码),则viewpager无法填满屏幕,如果设置为wrap_content(注释动态设置的代码),则会导致ViewPager里面的RecyclerView显示不全,读者可以试试。

fragment的代码就不贴了,只有一个recyclerView列表

二、StickyNavLayout解析

1、代码如下

public class StickNavLayout extends LinearLayout implements NestedScrollingParent {
    public static final String TAG = "StickNavLayout";

    private View mNav;
    private ViewPager mViewPager;

    private ValueAnimator mOffsetAnimator;
    private Interpolator mInterpolator;

    private MyStickyListener listener;

    //scroll表示手指滑动多少距离,界面跟着显示多少距离,而fling是根据你的滑动方向与轻重,还会自动滑动一段距离。

    public StickNavLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setOrientation(LinearLayout.VERTICAL);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mNav = findViewById(R.id.id_stickynavlayout_indicator);
        View view = findViewById(R.id.id_stickynavlayout_viewpager);
        if(!(view instanceof ViewPager)){
            throw new RuntimeException("id_stickynavlayout_viewpager should used by ViewPager!");
        }
        mViewPager = (ViewPager) view;
    }

    /**
     * 只有在onStartNestedScroll返回true的时候才会接着调用onNestedScrollAccepted,
     * 这个判断是需要我们自己来处理的,
     * 不是直接的父子关系一样可以正常进行
     */
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        Log.e(TAG, "onStartNestedScroll");
        return true;
    }

    /**
     * 字面意思可以理解出来父View接受了子View的邀请,可以在此方法中做一些初始化的操作。
     */
    @Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
        Log.e(TAG, "onNestedScrollAccepted");
    }

    /**
     * 每次子View在滑动前都需要将滑动细节传递给父View,
     * 一般情况下是在ACTION_MOVE中调用
     * public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow),
     * dispatchNestedPreScroll在ScrollView、ListView的Action_Move中被调用
     * 然后父View就会被回调public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)。
     */

    private int mNavTop = -1;
    private int mViewPagerTop = -1;

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        Log.e(TAG, "onNestedPreScroll is call");
        //dy:鼠标往上走是正,往下走是负
        //方法一
        if(mNavTop == -1){
            mNavTop = mNav.getTop();
        }
        if(mViewPagerTop == -1){
            mViewPagerTop = mViewPager.getTop();
        }
        int moveY = (int) Math.sqrt(Math.abs(dy)*2);
        if(dy < 0){
            //往下拉
            if(getScrollY() == 0 && mNav.getTop() >= mNavTop) {
                mNav.layout(mNav.getLeft(), mNav.getTop() + moveY, mNav.getRight(), mNav.getBottom() + moveY);
                mViewPager.layout(mViewPager.getLeft(), mViewPager.getTop() + moveY, mViewPager.getRight(), mViewPager.getBottom() + moveY);
                listener.imageScale(mNav.getTop());
                consumed[1] = dy;
            }else if(getScrollY() > 0 && !ViewCompat.canScrollVertically(target,-1)){
                if(getScrollY()+dy<0){
                    scrollTo(0,0);
                }else {
                    scrollTo(0, getScrollY() + dy);
                    consumed[1] = dy;
                }
            }
        }else if(dy > 0){
            if(mNav.getTop() > mNavTop){
                if(mNav.getTop()-moveY < mNavTop){
                    mNav.layout(mNav.getLeft(),mNavTop,mNav.getRight(),mNavTop+mNav.getHeight());
                    mViewPager.layout(mViewPager.getLeft(),mViewPagerTop,mViewPager.getRight(),mViewPagerTop+mViewPager.getHeight());
                    listener.imageScale(mNavTop);
                    consumed[1] = dy;
                }else {
                    mNav.layout(mNav.getLeft(), mNav.getTop() - moveY, mNav.getRight(), mNav.getBottom() - moveY);
                    mViewPager.layout(mViewPager.getLeft(), mViewPager.getTop() - moveY, mViewPager.getRight(), mViewPager.getBottom() - moveY);
                    listener.imageScale(mNav.getTop());
                    consumed[1] = dy;
                }
            }else if(getScrollY()<DisplayUtil.dip2px(getContext(),155)){
                if(getScrollY()+dy>DisplayUtil.dip2px(getContext(),155)){
                    scrollTo(0,DisplayUtil.dip2px(getContext(),155));
                    consumed[1] = dy;
                }else {
                    scrollTo(0, getScrollY() + dy);
                    consumed[1] = dy;
                }
            }
        }
    }

    /**
     * 接下来子View就要进自己的滑动操作了,滑动完成后子View还需要调用
     * public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)
     * 将自己的滑动结果再次传递给父View,父View对应的会被回调
     * public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed),
     * 但这步操作有一个前提,就是父View没有将滑动值全部消耗掉,因为父View全部消耗掉,子View就不应该再进行滑动了
     * 子View进行自己的滑动操作时也是可以不全部消耗掉这些滑动值的,剩余的可以再次传递给父View,
     * 使父View在子View滑动结束后还可以根据子View剩余的值再次执行某些操作。
     */
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {

    }

    /**
     * ACTION_UP或者ACTION_CANCEL的到来,
     * 子View需要调用public void stopNestedScroll()来告知父View本次NestedScrollig结束,
     * 父View对应的会被回调public void onStopNestedScroll(View target),
     */
    @Override
    public void onStopNestedScroll(View child) {
        if(mNav.getTop() != mNavTop) {
            mNav.layout(mNav.getLeft(),mNavTop,mNav.getRight(),mNavTop+mNav.getHeight());
            mViewPager.layout(mViewPager.getLeft(),mViewPagerTop,mViewPager.getRight(),mViewPagerTop+mViewPager.getHeight());
            listener.imageScale(mNavTop);
        }
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        //鼠标向下拉,velocityY为负
        if(target instanceof RecyclerView && velocityY < 0){
            final RecyclerView recyclerView = (RecyclerView) target;
            final View firstChild = recyclerView.getChildAt(0);
            final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);
            consumed = childAdapterPosition > 3;
        }
        if(!consumed){
            animateScroll(velocityY,computeDuration(0),consumed);
        }else{
            animateScroll(velocityY,computeDuration(velocityY),consumed);

        }
        return true;
    }

    private int computeDuration(float velocityY) {
        final int distance;
        if(velocityY > 0){
            //鼠标往上
            distance = Math.abs(mNav.getTop() - getScrollY());
        }else{
            //鼠标往下
            distance = Math.abs(getScrollY());
        }

        final int duration;
        velocityY = Math.abs(velocityY);
        if(velocityY > 0){
            duration = 3 * Math.round(1000 * (distance / velocityY));
        }else{
            final float distanceRadtio = distance/getHeight();
            duration = (int) ((distanceRadtio+1)*150);
        }
        return duration;
    }

    private void animateScroll(float velocityY, int duration, boolean consumed) {
        final int currentOffset = getScrollY();
        final int topHeight = mNav.getTop();
        if(mOffsetAnimator == null){
            mOffsetAnimator = new ValueAnimator();
            mOffsetAnimator.setInterpolator(mInterpolator);
            mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    if(animation.getAnimatedValue() instanceof Integer){
                        scrollTo(0, (Integer) animation.getAnimatedValue());
                    }
                }
            });
        }else{
            mOffsetAnimator.cancel();
        }
        mOffsetAnimator.setDuration(Math.min(duration,600));

        if(velocityY >= 0){
            mOffsetAnimator.setIntValues(currentOffset,mNav.getTop()-DisplayUtil.dip2px(getContext(),65));
            mOffsetAnimator.start();
        }else{
            if(!consumed){
                mOffsetAnimator.setIntValues(currentOffset,0);
                mOffsetAnimator.start();
            }
        }
    }

    @Override
    public int getNestedScrollAxes() {
        return 0;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_UP:
                if(mNav.getTop()>mNavTop){
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                mNav.layout(mNav.getLeft(),mNavTop,mNav.getRight(),mNavTop+mNav.getHeight());
                break;
        }
        return super.onTouchEvent(event);
    }

    public void setScrollListener(MyStickyListener myOnScrollListener){
        this.listener = myOnScrollListener;
    }

    public interface MyStickyListener{
        void imageScale(float v);
    }
}

继承自LinearLayout,实现NestedScrollingParent接口,NestedScrollingParent 的方法就是你要做的事情,方法也加了注释,这个没什么好讲的。。。

这里通过回调方法改变图片的大小,通过layout进行布局的调整

注意这里NestedScrollingParent 接口的方法,参数dy等和平时使用的dy有所不同,比如

onNestedPreScroll中按住,鼠标往上拉时,dy为正,鼠标往下拉时,dy为负。

完整项目地址:https://github.com/wuxiaogui593/AndroidStickyNavLayout

有什么错误或问题欢迎骚扰!!!

猜你喜欢

转载自blog.csdn.net/xg1057415595/article/details/81155391