利用NestedScrolling实现悬浮式导航详情页

1.需要实现的效果图如下:


实现方式主要有两种:

1)、ScrollView内嵌软件介绍+ViewPager+ViewPager中是ScrollView,这种方式呢,纯原生,没有涉及到自定义控件,但是这样嵌套呢,涉及到测量以及事件的冲突处理。

2)、将做外层的ScrollView改为了自定义的一个控件,继承自LinearLayout,叫做StickyNavLayout,利用NestedScrolling特性:子view和父view共同消费滑动来实现。

详细代码如下:

import android.animation.ValueAnimator;
import android.content.Context;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.widget.LinearLayout;
import android.widget.OverScroller;

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

/*首先子View需要找到一个支持NestedScrollingParent的父View,
告知父View我准备开始和你一起处理滑动事件了,
一般情况下都是在onTouchEvent的ACTION_DOWN中调用public boolean startNestedScroll(int axes),
然后父View就会被回调public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
和public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes)

onStartNestedScroll可以理解是父View的一个验证机制,
父View可以在此方法中根据滑动方向等信息决定是否要和子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 nestedScrollAxes)
    {
        Log.e(TAG, "onNestedScrollAccepted");
    }
/*
* 随着ACTION_UP或者ACTION_CANCEL的到来,子View需要调用public void stopNestedScroll()
* 来告知父View本次NestedScrollig结束,父View对应的会被回调public void onStopNestedScroll(View target),
* 可以在此方法中做一些对应停止的逻辑操作比如资源释放等
* */
    @Override
    public void onStopNestedScroll(View target)
    {
        Log.e(TAG, "onStopNestedScroll");
    }
/*
* 父View处理完后,接下来子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就不应该再进行滑动了,这一步也就没有必要了
*
* */
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
    {
        Log.e(TAG, "onNestedScroll");
    }
/*
* 每次子View在滑动前都需要将滑动细节传递给父View,
* 一般情况下是在ACTION_MOVE中调用public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow),
* 然后父View就会被回调public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
* dx dy代表本次滑动 x y方向的距离,consumed需要子View创建并传递给父View,
* 如果父View选择要消耗掉滑动的值就需要通过此数组传递给子View,consumed[0]:x轴消费的距离;consumed[1]:y轴消费的距离
* */
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
    {
        Log.e(TAG, "onNestedPreScroll");
        Log.e(TAG, "onNestedPreScroll scrollY=" + getScrollY());
        Log.e(TAG, "onNestedPreScroll dy=" + dy);
        boolean hiddenTop = dy > 0 && getScrollY() < mTopViewHeight;
        boolean showTop = dy < 0 && getScrollY() >= 0 && !ViewCompat.canScrollVertically(target, -1);

        if (hiddenTop || showTop)
        {
            scrollBy(0, dy);
            consumed[1] = dy;
        }
    }

    private int TOP_CHILD_FLING_THRESHOLD = 3;

    /*
    * 如果产生了fling,就需要子View在stopNestedScroll前调用public boolean dispatchNestedPreFling(View target, float velocityX, float velocityY)
    * 和public boolean dispatchNestedFling(View target, float velocityX, float velocityY, boolean consumed),
    * 父View对应的会被回调public boolean onNestedPreFling(View target, float velocityX, float velocityY)
    * 和public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed)
    *参数consumed代表子View是否消耗掉了fling,fling不存在部分消耗,一旦被消耗就是指全部
    *返回值代表父View是否消耗掉了fling
     *  */
    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed)
    {
        Log.e(TAG, "onNestedFling, velocityX=" + velocityX + ",velocityY=" + velocityY);
        //如果是recyclerView 根据判断第一个元素是哪个位置可以判断是否消耗
        //这里判断如果第一个元素的位置是大于TOP_CHILD_FLING_THRESHOLD的
        //认为已经被消耗,在animateScroll里不会对velocityY<0时做处理
        if (target instanceof RecyclerView && velocityY < 0) {
            final RecyclerView recyclerView = (RecyclerView) target;
            final View firstChild = recyclerView.getChildAt(0);
            final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);
            Log.d(TAG,"onNestedFling childAdapterPosition" + childAdapterPosition);
            consumed = childAdapterPosition > TOP_CHILD_FLING_THRESHOLD;
        }
        if (!consumed) {
            animateScroll(velocityY, computeDuration(0),consumed);
        } else {
            animateScroll(velocityY, computeDuration(velocityY),consumed);
        }
        return true;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY)
    {
       //不做拦截 可以传递给子View
       return false;
    }

    @Override
    public int getNestedScrollAxes()
    {
        Log.e(TAG, "getNestedScrollAxes");
        return 0;
    }

    /**
     * 根据速度计算滚动动画持续时间
     * @param velocityY
     * @return
     */
    private int computeDuration(float velocityY) {
        final int distance;
        if (velocityY > 0) {
            distance = Math.abs(mTop.getHeight() - getScrollY());
        } else {
            distance = Math.abs(mTop.getHeight() - (mTop.getHeight() - getScrollY()));
        }


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

        return duration;

    }

    private void animateScroll(float velocityY, final int duration,boolean consumed) {
        final int currentOffset = getScrollY();
        final int topHeight = mTop.getHeight();
        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, topHeight);
            mOffsetAnimator.start();
        }else {
            //如果子View没有消耗down事件 那么就让自身滑倒0位置
            if(!consumed){
                mOffsetAnimator.setIntValues(currentOffset, 0);
                mOffsetAnimator.start();
            }

        }
    }

    private View mTop;
    private View mNav;
    private ViewPager mViewPager;

    private int mTopViewHeight;

    private OverScroller mScroller;
    private VelocityTracker mVelocityTracker;
    private ValueAnimator mOffsetAnimator;
    private Interpolator mInterpolator;
    private int mTouchSlop;
    private int mMaximumVelocity, mMinimumVelocity;

    private float mLastY;
    private boolean mDragging;

    public StickyNavLayout(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        setOrientation(LinearLayout.VERTICAL);

        mScroller = new OverScroller(context);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mMaximumVelocity = ViewConfiguration.get(context)
                .getScaledMaximumFlingVelocity();
        mMinimumVelocity = ViewConfiguration.get(context)
                .getScaledMinimumFlingVelocity();

    }

    private void initVelocityTrackerIfNotExists()
    {
        if (mVelocityTracker == null)
        {
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    private void recycleVelocityTracker()
    {
        if (mVelocityTracker != null)
        {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }


//    @Override
//    public boolean onTouchEvent(MotionEvent event)
//    {
//        initVelocityTrackerIfNotExists();
//        mVelocityTracker.addMovement(event);
//        int action = event.getAction();
//        float y = event.getY();
//
//        switch (action)
//        {
//            case MotionEvent.ACTION_DOWN:
//                if (!mScroller.isFinished())
//                    mScroller.abortAnimation();
//                mLastY = y;
//                return true;
//            case MotionEvent.ACTION_MOVE:
//                float dy = y - mLastY;
//
//                if (!mDragging && Math.abs(dy) > mTouchSlop)
//                {
//                    mDragging = true;
//                }
//                if (mDragging)
//                {
//                    scrollBy(0, (int) -dy);
//                }
//
//                mLastY = y;
//                break;
//            case MotionEvent.ACTION_CANCEL:
//                mDragging = false;
//                recycleVelocityTracker();
//                if (!mScroller.isFinished())
//                {
//                    mScroller.abortAnimation();
//                }
//                break;
//            case MotionEvent.ACTION_UP:
//                mDragging = false;
//                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
//                int velocityY = (int) mVelocityTracker.getYVelocity();
//                if (Math.abs(velocityY) > mMinimumVelocity)
//                {
//                    fling(-velocityY);
//                }
//                recycleVelocityTracker();
//                break;
//        }
//
//        return super.onTouchEvent(event);
//    }


    @Override
    protected void onFinishInflate()
    {
        super.onFinishInflate();
        mTop = findViewById(R.id.id_stickynavlayout_topview);
        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 show used by ViewPager !");
        }
        mViewPager = (ViewPager) view;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        //不限制顶部的高度
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        getChildAt(0).measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
        ViewGroup.LayoutParams params = mViewPager.getLayoutParams();
        params.height = getMeasuredHeight() - mNav.getMeasuredHeight();
        setMeasuredDimension(getMeasuredWidth(), mTop.getMeasuredHeight() + mNav.getMeasuredHeight() + mViewPager.getMeasuredHeight());

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh)
    {
        super.onSizeChanged(w, h, oldw, oldh);
        mTopViewHeight = mTop.getMeasuredHeight();
    }


    public void fling(int velocityY)
    {
        mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
        invalidate();
    }

    @Override
    public void scrollTo(int x, int y)
    {
        if (y < 0)
        {
            y = 0;
        }
        if (y > mTopViewHeight)
        {
            y = mTopViewHeight;
        }
        if (y != getScrollY())
        {
            super.scrollTo(x, y);
        }
    }

    @Override
    public void computeScroll()
    {
        Log.d(TAG,"computeScroll getCurrY():" + mScroller.getCurrY());
        if (mScroller.computeScrollOffset())
        {
            scrollTo(0, mScroller.getCurrY());
            invalidate();
        }
    }


}

主要原理就是,子View和父view一起处理一个滑动事件。在public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) 中获取父view在x,y轴上要滑动的距离,并调用scrollBy(0, dy); 不断进行滑动。

对Fling的滑动,则在public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed)中获取到y轴的滑动速度,并利用ValueAnimator .addUpdateListener 获取不断变化的y轴值,调用scrollTo来进行滑动操作。

2.使用

详情页布局如下:

    <com.zhy.view.StickyNavLayout xmlns:tools="http://schemas.android.com/tools"  
        xmlns:android="http://schemas.android.com/apk/res/android"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent"  
        android:orientation="vertical" >  
      
        <RelativeLayout  
            android:id="@id/id_stickynavlayout_topview"  
            android:layout_width="match_parent"  
            android:layout_height="300dp"  
            android:background="#4400ff00" >  
      
            <TextView  
                android:layout_width="match_parent"  
                android:layout_height="match_parent"  
                android:gravity="center"  
                android:text="软件介绍"  
                android:textSize="30sp"  
                android:textStyle="bold" />  
        </RelativeLayout>  
      
        <com.zhy.view.SimpleViewPagerIndicator  
            android:id="@id/id_stickynavlayout_indicator"  
            android:layout_width="match_parent"  
            android:layout_height="50dp"  
            android:background="#ffffffff" >  
        </com.zhy.view.SimpleViewPagerIndicator>  
      
        <android.support.v4.view.ViewPager  
            android:id="@id/id_stickynavlayout_viewpager"  
            android:layout_width="match_parent"  
            android:layout_height="wrap_content"  
            android:background="#44ff0000" >  
        </android.support.v4.view.ViewPager>  
      
    </com.zhy.view.StickyNavLayout>  

最外层是父view StickyNavLayout 再往下,依次是header, viewpageIndicator, 和viewpager



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值