欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-06-11 11:17:54
...

一、简介

先来看看效果图:

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。

还有 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

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