淺析NestedScrolling嵌套滑動(dòng)機(jī)制之實(shí)踐篇-仿寫?zhàn)I了么商家詳情頁

預(yù)覽

嵌套系列導(dǎo)航

本文已在公眾號(hào)鴻洋原創(chuàng)發(fā)布纠拔。未經(jīng)許可注竿,不得以任何形式轉(zhuǎn)載俏拱!

概述

之前的《淺析NestedScrolling嵌套滑動(dòng)機(jī)制之基礎(chǔ)篇》帶大家了解NestedScrolling的原理和使用還有它的改進(jìn)等等,這篇文章手把手基于NestedScrolling嵌套滑動(dòng)機(jī)制現(xiàn)實(shí)餓了么v8.27.6商家詳情頁。github地址:https://github.com/pengguanming/ElemeNestedScrolling

效果預(yù)覽

需要仿寫的有兩部分,第一個(gè)是商家頁的嵌套滑動(dòng),第二個(gè)是商家點(diǎn)餐頁的嵌套滑動(dòng)良瞧。

商家滑動(dòng)

商家點(diǎn)餐滑動(dòng)

apk下載地址:
github
百度云 密碼:jm39

商家頁的嵌套滑動(dòng)實(shí)現(xiàn)

效果分析

商家滑動(dòng)頁層級(jí)

商家滑動(dòng)頁布局主要有上圖五部分組成陪汽,邏輯上的層級(jí)如圖所示有3層。

商家滑動(dòng)TransY

滑動(dòng)Content部分時(shí)利用View的TransitionY屬性改變位置來消耗滑動(dòng)值褥蚯,根據(jù)Content部分的TransitionY設(shè)定各種范圍從而計(jì)算百分比來執(zhí)行顏色漸變掩缓、位移、Scale遵岩、Alpha效果你辣。下面來說明上圖中變量的意義:

topBarHeight;//topBar高度
contentTransY;//滑動(dòng)內(nèi)容初始化TransY
upAlphaScaleY;//上滑時(shí)logo,收藏icon縮放尘执、搜索icon舍哄、分享icon透明度臨界值
upAlphaGradientY;//上滑時(shí)搜索框、topBar背景誊锭,返回icon表悬、拼團(tuán)icon顏色漸變臨界值
downFlingCutOffY;//從折疊狀態(tài)下滑產(chǎn)生fling時(shí)回彈到初始狀態(tài)的最大值
downCollapsedAlphaY;//下滑時(shí)折疊內(nèi)容透明度臨界值
downShopBarTransY;//下滑時(shí)購物內(nèi)容位移臨界值
downContentAlphaY;//下滑時(shí)收起按鈕和滑動(dòng)內(nèi)容透明度臨界值
ivCloseHeight;//收起按鈕的高度
content部分的上滑范圍=[topBarHeight,contentTransY]
content部分的下滑范圍=[contentTransY,滿屏高度-ivCloseHeight]

舉個(gè)根據(jù)Content部分的TransitionY計(jì)算百分比例子(請(qǐng)注意:根據(jù)Android的坐標(biāo)系contentTransY>upAlphaScaleY):

    /**
    *計(jì)算上滑時(shí)logo,收藏icon縮放丧靡、搜索icon蟆沫、分享icon透明度的百分比其范圍控制在[0.0f,1.0f],
    而Content部分的TranslationY值控制在[upAlphaScaleY,contentTransY],v4包的MathUtils類的clamp方法可以幫助控制值的范圍温治,超過范圍時(shí)百分下限為0饭庞,上限為1。
    */
    upAlphaScalePro= (contentTransY - MathUtils.clamp(Content部分的TranslationY, upAlphaScaleY, 
    contentTransY)) / (contentTransY-upAlphaScaleY);

    //根據(jù)upAlphaScalePro設(shè)置icon的透明度
    float alpha = 1 - upAlphaScalePro;
    setAlpha(mIvShare, alpha);

ElemeNestedScrollLayout

商家頁的嵌套滑的自定義View繼承FrameLayout熬荆,命名為ElemeNestedScrollLayout舟山,在Content部分能夠滑動(dòng)的View只有ViewPager里Fragment的RecyclerView和NestedScrollView,它們都實(shí)現(xiàn)了NestedScrollingChild2接口卤恳,故自定義View要實(shí)現(xiàn)NestedScrollingParent2接口根據(jù)具體邏輯來消費(fèi)NestedScrollingChild2的滑動(dòng)值累盗。

布局

下面是布局要點(diǎn),側(cè)重于控件的尺寸和位置突琳,完整布局請(qǐng)參考:ElemeNestedScrollLayout布局

<ElemeNestedScrollLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <!--Header部分-->
            <android.support.constraint.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="@dimen/header_height"
                ... >
        <!--Header部分-->
        <!--Collasp Content部分-->
            <<android.support.constraint.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1" 
                ... >
        <!--Collasp Content部分-->
     </LinearLayout>
    
    <!--Top Bar部分-->//自定義View若债,其作用是在折疊狀態(tài)時(shí)適配沉浸式狀態(tài)欄效果。
    <TopBarLayout
        android:layout_width="match_parent"
        android:layout_height="@dimen/top_bar_height"
        ...>
    <!--Top Bar部分-->

    <!--Content部分-->
    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:translationY="@dimen/content_trans_y">
            <com.flyco.tablayout.SlidingTabLayout
                android:layout_width="match_parent"
                android:layout_height="@dimen/stl_height"/>
            <android.support.v4.view.ViewPager
                android:layout_width="match_parent"
                android:layout_height="0dp"/>
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="@dimen/iv_close_height"/>
    </android.support.constraint.ConstraintLayout>
    <!--Content部分-->

    <!--Shop Bar部分-->
        <android.support.constraint.ConstraintLayout
            android:layout_gravity="bottom"
            android:layout_width="match_parent"
            android:layout_height="@dimen/shop_bar_height"
            ... >
    <!--Shop Bar部分-->
</ElemeNestedScrollLayout>

綁定需要做效果的View拆融、引入Dimens蠢琳、測(cè)量Content部分的高度

從上面圖片能夠分析出:折疊狀態(tài)時(shí),Content部分高度=滿屏高度-TopBar部分的高度

public class ElemeNestedScrollLayout extends FrameLayout implements NestedScrollingParent2 {
    //Header部分
    private View mIvLogo;
    private View mVCollect;

    //Collaps Content部分
    private View mClCollapsedHeader;
    private View mCollapsedContent;
    private View mRvCollapsed;

    //TopBar部分
    private View mIvSearch;
    private View mIvShare;
    private View mTvSearch;
    private View mTopBar;
    private ImageView mIvBack;
    private ImageView mIvAssemble;

    //Content部分
    private View mLlContent;
    private View mIvClose;
    private View mViewPager;
    private View mStl;

    //ShopBar部分
    private View mShopBar;

    private float topBarHeight;//topBar高度
    private float shopBarHeight;//shopBar高度
    private float contentTransY;//滑動(dòng)內(nèi)容初始化TransY
    private float upAlphaScaleY;//上滑時(shí)logo冠息,收藏icon縮放挪凑、搜索icon孕索、分享icon透明度臨界值
    private float upAlphaGradientY;//上滑時(shí)搜索框逛艰、topBar背景,返回icon搞旭、拼團(tuán)icon顏色漸變臨界值
    private float downFlingCutOffY;//從折疊狀態(tài)下滑產(chǎn)生fling時(shí)回彈到初始狀態(tài)的臨界值
    private float downCollapsedAlphaY;//下滑時(shí)折疊內(nèi)容透明度臨界值
    private float downShopBarTransY;//下滑時(shí)購物內(nèi)容位移臨界值
    private float downContentAlphaY;//下滑時(shí)收起按鈕和滑動(dòng)內(nèi)容透明度臨界值
    private float downEndY;//下滑時(shí)終點(diǎn)值
  
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mLlContent = findViewById(R.id.cl_content);
        mCollapsedContent = findViewById(R.id.cl_collapsed_content);
        mIvSearch = findViewById(R.id.iv_search);
        mIvShare = findViewById(R.id.iv_share);
        mIvBack = findViewById(R.id.iv_back);
        mIvAssemble = findViewById(R.id.iv_assemble);
        mIvLogo = findViewById(R.id.iv_logo);
        mVCollect = findViewById(R.id.iv_collect);
        mTvSearch = findViewById(R.id.tv_search);
        mTopBar = findViewById(R.id.cl_top_bar);
        mClCollapsedHeader = findViewById(R.id.cl_collapsed_header);
        mRvCollapsed = findViewById(R.id.rv_collasped);
        mIvClose = findViewById(R.id.iv_close);
        mViewPager = findViewById(R.id.vp);
        mStl = findViewById(R.id.stl);
        mShopBar = findViewById(R.id.cl_shop_bar);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //設(shè)置Content部分高度
        topBarHeight= mTopBar.getMeasuredHeight();
        ViewGroup.LayoutParams params = mLlContent.getLayoutParams();
        params.height = (int) (getMeasuredHeight() - topBarHeight);
        //重新測(cè)量
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

     @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        shopBarHeight = getResources().getDimension(R.dimen.shop_bar_height);
        contentTransY = getResources().getDimension(R.dimen.content_trans_y);
        downShopBarTransY = contentTransY+ shopBarHeight;
        upAlphaScaleY = contentTransY - dp2px(32);
        upAlphaGradientY = contentTransY - dp2px(64);
        downFlingCutOffY = contentTransY + dp2px(28);
        downCollapsedAlphaY = contentTransY + dp2px(32);
        downContentAlphaY = getResources().getDimension(R.dimen.donw_content_alpha_y);
        downEndY = getHeight() - getResources().getDimension(R.dimen.iv_close_height);
    }
}

現(xiàn)實(shí)NestedScrollingParent2接口

onStartNestedScroll()

ElemeNestedScrollLayout只處理Content部分里可滑動(dòng)View的垂直方向的滑動(dòng)。

    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
        return child.getId() == mLlContent.getId()&&
        axes==ViewCompat.SCROLL_AXIS_VERTICAL;
    }

onNestedPreScroll()

接下來就是處理滑動(dòng),上面效果分析提過:Content部分的上滑范圍=[topBarHeight,contentTransY]永乌、
下滑范圍=[contentTransY,downEndY(即滿屏高度-ivCloseHeight)]即滑動(dòng)范圍為[topBarHeight,downEndY];ElemeNestedScrollLayout要控制Content部分的TransitionY值要在范圍內(nèi)望几,具體處理如下惕味。

Content部分里可滑動(dòng)View往上滑動(dòng)時(shí):

1慌随、如果Content部分當(dāng)前TransitionY+View滑動(dòng)的dy > topBarHegiht,ElemeNestedScrollLayout設(shè)置Content部分的TransitionY為Content部分當(dāng)前TransitionY+View滑動(dòng)的dy達(dá)到移動(dòng)的效果來消費(fèi)View的dy剃袍。

2涛救、如果Content部分當(dāng)前TransitionY+View滑動(dòng)的dy = topBarHegiht舒萎,ElemeNestedScrollLayout同上操作。

3咆贬、如果Content部分當(dāng)前TransitionY+View滑動(dòng)的dy < topBarHegiht皱蹦,ElemeNestedScrollLayout只消費(fèi)部分dy(即Content部分當(dāng)前TransitionY到topBarHeight差值),剩余的dy讓View滑動(dòng)消費(fèi)。

Content部分里可滑動(dòng)View往下滑動(dòng)并且View已經(jīng)不能往下滑動(dòng)
(比如RecyclerView已經(jīng)到頂部還往下滑)時(shí):

1嫌拣、如果Content部分當(dāng)前TransitionY+View滑動(dòng)的dy >= topBarHeight 并且 Content部分當(dāng)前TransitionY+View滑動(dòng)的dy <= downEndY,ElemeNestedScrollLayout設(shè)置Content部分的TransitionY為Content部分當(dāng)前TransitionY+View滑動(dòng)的dy達(dá)到移動(dòng)的效果來消費(fèi)View的dy。

2酝润、Content部分當(dāng)前TransitionY+View滑動(dòng)的dy > downEndY,ElemeNestedScrollLayout只消費(fèi)部分dy(即Content部分當(dāng)前TransitionY到downEndY差值)。

下面是代碼實(shí)現(xiàn):

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        float contentTransY = mLlContent.getTranslationY() - dy;
        //處理上滑
        if (dy > 0) {
            if (contentTransY >= topBarHeight) {
                translationByConsume(mLlContent, contentTransY, consumed, dy);
            } else {
                translationByConsume(mLlContent, topBarHeight, consumed, (mLlContent.getTranslationY() - topBarHeight));
            }
        }
        if (dy < 0 && !target.canScrollVertically(-1)) {
            //處理下滑
            if (contentTransY >= topBarHeight && contentTransY <= downEndY) {
                translationByConsume(mLlContent, contentTransY, consumed, dy);
            } else {
                translationByConsume(mLlContent, downEndY, consumed, downEndY - mLlContent.getTranslationY());
            }
        }
    }

    private void translationByConsume(View view, float translationY,
     int[] consumed, float consumedDy) {
        consumed[1] = (int) consumedDy;
        view.setTranslationY(translationY);
    }

就這樣處理好了Content部分的滑動(dòng),接下來就處理Content部分的滑動(dòng)過程中各種View的效果并對(duì)外暴露百分比監(jiān)聽接口,比如上滑折疊過程改變狀態(tài)欄字體就需要用到酌壕。下面的范圍值是針對(duì)Content部分的TransitionY印蓖,根據(jù)之前效果分析具體如下:

1、設(shè)置logo船侧、收藏icon縮放队塘,搜索icon、分享icon透明度范圍[upAlphaScaleY,contentTransY]

2锯梁、設(shè)置搜索框、topBar背景,返回icon蛤肌、拼團(tuán)icon顏色漸范圍[upAlphaGradientY,contentTransY]

3赔硫、設(shè)置Collapse Content部分透明度范圍[contentTransY,downCollapsedAlphaY]

4砸王、設(shè)置Shop Bar位移范圍[contentTransY,downShopBarTransY]

5榔昔、設(shè)置收起按鈕和滑動(dòng)內(nèi)容透明度[downContentAlphaY,downEndY]

下面是代碼實(shí)現(xiàn):

    ...
    private ArgbEvaluator iconArgbEvaluator;//返回icon、拼團(tuán)icon顏色漸變的Evaluator
    private ArgbEvaluator topBarArgbEvaluator;//topbar顏色漸變的Evaluator

    public ElemeNestedScrollLayout(@NonNull Context context) {
        this(context, null);
    }

    public ElemeNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public ElemeNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mParentHelper = new NestedScrollingParentHelper(this);

        iconArgbEvaluator = new ArgbEvaluator();
        topBarArgbEvaluator = new ArgbEvaluator();
        ...
    }

     @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        ...

        //根據(jù)upAlphaScalePro,設(shè)置logo怔檩、收藏icon縮放,搜索icon蝴猪、分享icon透明度
        float upAlphaScalePro = getUpAlphaScalePro();
        alphaScaleByPro(upAlphaScalePro);

        //根據(jù)upAlphaGradientPro,設(shè)置topBar背景、返回icon膊爪、拼團(tuán)icon顏色漸變值自阱,搜索框透明度
        float upAlphaGradientPro = getUpAlphaGradientPro();
        alphaGradientByPro(upAlphaGradientPro);

        //根據(jù)downCollapsedAlphaPro,設(shè)置折疊內(nèi)容透明度
        float downCollapsedAlphaPro = getDownCollapsedAlphaPro();
        alphaCollapsedContentByPro(downCollapsedAlphaPro);

        //根據(jù)downContentAlphaPro,設(shè)置滑動(dòng)內(nèi)容、收起按鈕的透明度
        float downContentAlphaPro = getDownContentAlphaPro();
        alphaContentByPro(downContentAlphaPro);

        //根據(jù)downShopBarTransPro,設(shè)置購物內(nèi)容內(nèi)容位移
        float downShopBarTransPro = getDownShopBarTransPro();
        transShopBarByPro(downShopBarTransPro);

        //根據(jù)upCollapsedContentTransPro,設(shè)置折疊內(nèi)容位移
        float upCollapsedContentTransPro = getUpCollapsedContentTransPro();
        transCollapsedContentByPro(upCollapsedContentTransPro);

        if (mProgressUpdateListener!=null){
            mProgressUpdateListener.onUpAlphaScaleProUpdate(upAlphaScalePro);
            mProgressUpdateListener.onUpAlphaGradientProUpdate(upAlphaGradientPro);
            mProgressUpdateListener.onDownCollapsedAlphaProUpdate(downCollapsedAlphaPro);
            mProgressUpdateListener.onDownContentAlphaProUpdate(downContentAlphaPro);
            mProgressUpdateListener.onDownShopBarTransProUpdate(downShopBarTransPro);
            mProgressUpdateListener.onUpCollapsedContentTransProUpdate(upCollapsedContentTransPro);
        }
    }

    /**
     * 根據(jù)upCollapsedContentTransPro,設(shè)置折疊內(nèi)容位移
     */
    private void transCollapsedContentByPro(float upCollapsedContentTransPro) {
        float collapsedContentTranY = - (upCollapsedContentTransPro * (contentTransY - topBarHeight));
        translation(mCollapsedContent,collapsedContentTranY);
    }

    /**
     * 根據(jù)downShopBarTransPro,設(shè)置購物內(nèi)容內(nèi)容位移
     */
    private void transShopBarByPro(float downShopBarTransPro) {
        float shopBarTransY = (1-downShopBarTransPro) * shopBarHeight;
        translation(mShopBar,shopBarTransY);
    }

    /**
     * 根據(jù)downContentAlphaPro,設(shè)置滑動(dòng)內(nèi)容米酬、收起按鈕的透明度
     */
    private void alphaContentByPro(float downContentAlphaPro) {
        setAlpha(mViewPager,downContentAlphaPro);
        setAlpha(mStl,downContentAlphaPro);
        setAlpha(mIvClose,1-downContentAlphaPro);
        if (mIvClose.getAlpha()==0){
            mIvClose.setVisibility(GONE);
        }else {
            mIvClose.setVisibility(VISIBLE);
        }
    }

    /**
     * 根據(jù)downCollapsedAlphaPro,設(shè)置折疊內(nèi)容透明度
     */
    private void alphaCollapsedContentByPro(float downCollapsedAlphaPro) {
        setAlpha(mClCollapsedHeader,downCollapsedAlphaPro);
        setAlpha(mRvCollapsed,1 - downCollapsedAlphaPro);
    }

    /**
     * 根據(jù)upAlphaGradientPro,設(shè)置topBar背景加派、返回icon、拼團(tuán)icon顏色漸變值,搜索框透明度
     */
    private void alphaGradientByPro(float upAlphaGradientPro) {
        setAlpha(mTvSearch, upAlphaGradientPro);
        int iconColor = (int) iconArgbEvaluator.evaluate(
                upAlphaGradientPro,
                getContext().getResources().getColor(R.color.white),
                getContext().getResources().getColor(R.color.black)
        );
        int topBarColor = (int) topBarArgbEvaluator.evaluate(
                upAlphaGradientPro,
                getContext().getResources().getColor(R.color.trans_white),
                getContext().getResources().getColor(R.color.white)
        );
        mTopBar.setBackgroundColor(topBarColor);
        mIvBack.getDrawable().mutate().setTint(iconColor);
        mIvAssemble.getDrawable().mutate().setTint(iconColor);
    }

    /**
     * 根據(jù)upAlphaScalePro,設(shè)置logo软吐、收藏icon縮放提佣,搜索icon、分享icon透明度
     */
    private void alphaScaleByPro(float upAlphaScalePro) {
        float alpha = 1 - upAlphaScalePro;
        float scale = 1 - upAlphaScalePro;
        setAlpha(mIvSearch, alpha);
        setAlpha(mIvShare, alpha);
        setScaleAlpha(mIvLogo, scale, scale, alpha);
        setScaleAlpha(mVCollect, scale, scale, alpha);
    }

    private float getDownContentAlphaPro() {
        return (downEndY - MathUtils.clamp(mLlContent.getTranslationY(), downContentAlphaY, downEndY)) / (downEndY - downContentAlphaY);
    }

    private float getDownCollapsedAlphaPro() {
        return (downCollapsedAlphaY - MathUtils.clamp(mLlContent.getTranslationY(), contentTransY, downCollapsedAlphaY)) / (downCollapsedAlphaY -contentTransY);
    }

    private float getDownShopBarTransPro() {
        return (downShopBarTransY - MathUtils.clamp(mLlContent.getTranslationY(), contentTransY, downShopBarTransY)) / (downShopBarTransY -contentTransY);
    }

    private float getUpAlphaGradientPro() {
        return (upAlphaScaleY - MathUtils.clamp(mLlContent.getTranslationY(), upAlphaGradientY, upAlphaScaleY)) / (upAlphaScaleY-upAlphaGradientY);
    }

    private float getUpAlphaScalePro() {
        return (contentTransY - MathUtils.clamp(mLlContent.getTranslationY(), upAlphaScaleY, contentTransY)) / (contentTransY-upAlphaScaleY);
    }

    private float getUpCollapsedContentTransPro() {
        return (contentTransY - MathUtils.clamp(mLlContent.getTranslationY(), topBarHeight, contentTransY)) / (contentTransY-topBarHeight);
    }

    private void setAlpha(View view, float alpha){
        view.setAlpha(alpha);
    }

    private void setScale(View view ,float scaleY,float scaleX){
        view.setScaleY(scaleY);
        view.setScaleX(scaleX);
    }
    private void setScaleAlpha(View view ,float scaleY,float scaleX,float alpha){
        setAlpha(view,alpha);
        setScale(view,scaleY,scaleX);
    }

    private void translation(View view, float translationY) {
            view.setTranslationY(translationY);
    }

    public interface ProgressUpdateListener{

        void onUpCollapsedContentTransProUpdate(float pro);

        void onUpAlphaScaleProUpdate(float pro);

        void onUpAlphaGradientProUpdate(float pro);

        void onDownCollapsedAlphaProUpdate(float pro);

        void onDownContentAlphaProUpdate(float pro);

        void onDownShopBarTransProUpdate(float pro);
    }

onStopNestedScroll()

在下滑Content部分從初始狀態(tài)轉(zhuǎn)換到展開狀態(tài)的過程中松手就會(huì)執(zhí)行收起或展開的動(dòng)畫,這邏輯在onStopNestedScroll()實(shí)現(xiàn)颜价,但注意如果動(dòng)畫未執(zhí)行完畢手指再落下滑動(dòng)時(shí),應(yīng)該在onNestedScrollAccepted()取消當(dāng)前執(zhí)行中的動(dòng)畫。


收起動(dòng)畫

如果Content部分的TransitionY沒有超過downCollapsedAlphaY榛臼,執(zhí)行收起Content部分動(dòng)畫效果媳友,恢復(fù)到初始轉(zhuǎn)態(tài)芳杏。

展開動(dòng)畫

如果Content部分的TransitionY超過了downCollapsedAlphaY秕铛,執(zhí)行展開Content部分動(dòng)畫效果缩挑,轉(zhuǎn)換到展開轉(zhuǎn)態(tài)物遇。
代碼實(shí)現(xiàn)如下:

    ...
    public static final long ANIM_DURATION_FRACTION = 200L;
    private ValueAnimator restoreOrExpandAnimator;//收起或展開折疊內(nèi)容時(shí)執(zhí)行的動(dòng)畫
    
    public ElemeNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        ...
        restoreOrExpandAnimator = new ValueAnimator();
        restoreOrExpandAnimator.setInterpolator(new AccelerateInterpolator());
        restoreOrExpandAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                translation(mLlContent, (float) animation.getAnimatedValue());

                //根據(jù)downShopBarTransPro,設(shè)置購物內(nèi)容內(nèi)容位移
                float downShopBarTransPro = getDownShopBarTransPro();
                transShopBarByPro(downShopBarTransPro);

                //根據(jù)downCollapsedAlphaPro,設(shè)置折疊內(nèi)容透明度
                float downCollapsedAlphaPro = getDownCollapsedAlphaPro();
                alphaCollapsedContentByPro(downCollapsedAlphaPro);

                //根據(jù)downContentAlphaPro,設(shè)置滑動(dòng)內(nèi)容蹄衷、收起按鈕的透明度
                float downContentAlphaPro = getDownContentAlphaPro();
                alphaContentByPro(downContentAlphaPro);

                if (mProgressUpdateListener!=null){
                    mProgressUpdateListener.onDownCollapsedAlphaProUpdate(downCollapsedAlphaPro);
                    mProgressUpdateListener.onDownContentAlphaProUpdate(downContentAlphaPro);
                    mProgressUpdateListener.onDownShopBarTransProUpdate(downShopBarTransPro);
                }
            }
        });
    }

    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
        mParentHelper.onNestedScrollAccepted(child, target, axes, type);
        if (restoreOrExpandAnimator.isStarted()) {
            restoreOrExpandAnimator.cancel();
        }
    }

    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {
        mParentHelper.onStopNestedScroll(target, type);
        //如果不是從初始狀態(tài)轉(zhuǎn)換到展開狀態(tài)過程觸發(fā)返回
        if (mLlContent.getTranslationY() <= contentTransY) {
            return;
        }

        //根據(jù)百分比計(jì)算動(dòng)畫執(zhí)行的時(shí)長
        float downCollapsedAlphaPro = getDownCollapsedAlphaPro();
        float downContentAlphaYPro = getDownContentAlphaPro();
        if (downCollapsedAlphaPro == 0) {
            expand((long) (downContentAlphaYPro * ANIM_DURATION_FRACTION));
        } else {
            restore((long) (downCollapsedAlphaPro * ANIM_DURATION_FRACTION));
        }
    }

     public void restore(long dur){
        if (restoreOrExpandAnimator.isStarted()) {
            restoreOrExpandAnimator.cancel();
        }
        restoreOrExpandAnimator.setFloatValues(mLlContent.getTranslationY(), contentTransY);
        restoreOrExpandAnimator.setDuration(dur);
        restoreOrExpandAnimator.start();
    }

    public void expand(long dur){
        if (restoreOrExpandAnimator.isStarted()) {
            restoreOrExpandAnimator.cancel();
        }
        restoreOrExpandAnimator.setFloatValues(mLlContent.getTranslationY(), downEndY);
        restoreOrExpandAnimator.setDuration(dur);
        restoreOrExpandAnimator.start();
    }

處理慣性滑動(dòng)

NestedScrollingParent2處理慣性滑動(dòng)的方式主要有兩種:
一忧额、在onNestedPreFling()或者onNestedFling()返回值為true消費(fèi)掉。
二愧口、在onNestedPreFling()和onNestedFling()返回值都為false的前提下睦番,在onNestedPreScroll()或者onNestedScroll()消費(fèi)掉,這種方式可以和普通的滑動(dòng)共用邏輯代碼耍属。


上滑F(xiàn)ling到折疊狀態(tài)

場景1:快速往上滑動(dòng)Content部分的可滑動(dòng)View產(chǎn)生慣性滑動(dòng)托嚣,這和前面onNestedPreScroll()處理上滑的效果一模一樣,因此可以復(fù)用邏輯厚骗,使用第二種方式處理示启。

折疊狀態(tài)下滑F(xiàn)ling

場景2:在折疊狀態(tài)并Content部分的可滑動(dòng)View沒有滑動(dòng)到頂部盡頭時(shí),快速往下滑動(dòng)Content部分的可滑動(dòng)View產(chǎn)生慣性滑動(dòng)滑到頂部盡頭就停止了领舰,這和前面onNestedPreScroll()處理下滑的效果類似夫嗓,但多了個(gè)慣性滑動(dòng)滑到頂部盡頭就停止的條件判斷酗昼,使用第二種方式處理犀概。

下滑F(xiàn)ling到展開狀態(tài)

場景3:快速往下滑動(dòng)Content部分的可滑動(dòng)View轉(zhuǎn)化展開狀態(tài)產(chǎn)生慣性滑動(dòng),這和前面onNestedPreScroll()處理下滑的效果類似嫉髓,使用第二種方式處理劳跃,但注意在慣性滑動(dòng)沒有完全消費(fèi)掉的時(shí)候谎仲,會(huì)不斷觸發(fā)onNestedPreScroll()來消費(fèi)直到慣性滑動(dòng)完全消費(fèi)掉,所以當(dāng)滑動(dòng)到展開狀態(tài)的時(shí)候要停止Content部分的View滑動(dòng)因?yàn)檫@時(shí)已經(jīng)是展開狀態(tài)了刨仑,不需View繼續(xù)滑動(dòng)觸發(fā)onNestedPreScroll(),注意NestedScrollView并沒有暴露對(duì)外停止滑動(dòng)的方法郑诺,只能反射獲取它的OverScroller停止滑動(dòng)夹姥。
下面是代碼實(shí)現(xiàn):

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        float contentTransY = mLlContent.getTranslationY() - dy;
        //處理上滑和場景1
        if (dy > 0) {
            if (contentTransY >= topBarHeight) {
                translationByConsume(mLlContent, contentTransY, consumed, dy);
            } else {
                translationByConsume(mLlContent, topBarHeight, consumed, (mLlContent.getTranslationY() - topBarHeight));
            }
        }

        if (dy < 0 && !target.canScrollVertically(-1)) {
            //下滑時(shí)處理Fling,完全折疊時(shí),下滑Recycler(或NestedScrollView) Fling滾動(dòng)到列表頂部(或視圖頂部)停止Fling辙诞,對(duì)應(yīng)場景2
            if (type == ViewCompat.TYPE_NON_TOUCH&&mLlContent.getTranslationY() == topBarHeight) {
                stopViewScroll(target);
                return;
            }

            //處理下滑
            if (contentTransY >= topBarHeight && contentTransY <= downEndY) {
                translationByConsume(mLlContent, contentTransY, consumed, dy);
            } else {
                translationByConsume(mLlContent, downEndY, consumed, downEndY - mLlContent.getTranslationY());
                //對(duì)應(yīng)場景3
                if (target instanceof NestedScrollView) {
                    stopViewScroll(target);
                }
            }
        }
        ...
    }

    /**
     * 停止View的滑動(dòng)
     */
    private void stopViewScroll(View target){
        if (target instanceof RecyclerView) {
            ((RecyclerView) target).stopScroll();
        }
        if (target instanceof NestedScrollView) {
            try {
                Class<? extends NestedScrollView> clazz = ((NestedScrollView) target).getClass();
                Field mScroller = clazz.getDeclaredField("mScroller");
                mScroller.setAccessible(true);
                OverScroller overScroller = (OverScroller) mScroller.get(target);
                overScroller.abortAnimation();
            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
下滑F(xiàn)ling到初始狀態(tài)回彈

場景4:快速往下滑動(dòng)Content部分的可滑動(dòng)View辙售,從非折疊狀態(tài)轉(zhuǎn)化展開狀態(tài)產(chǎn)生慣性滑動(dòng),因?yàn)橛谢貜椥Ч赏浚赃壿嬏幚砗蚾nNestedPreScroll()不一樣旦部,使用第一種方式處理。

    ...
    private ValueAnimator reboundedAnim;//回彈動(dòng)畫

    public ElemeNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        ...
        reboundedAnim = new ValueAnimator();
        reboundedAnim.setInterpolator(new DecelerateInterpolator());
        reboundedAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                translation(mLlContent, (float) animation.getAnimatedValue());

                //根據(jù)upAlphaScalePro,設(shè)置logo较店、收藏icon縮放士八,搜索icon、分享icon透明度
                float upAlphaScalePro = getUpAlphaScalePro();
                alphaScaleByPro(upAlphaScalePro);

                //根據(jù)upAlphaGradientPro,設(shè)置topBar背景梁呈、返回icon婚度、拼團(tuán)icon顏色漸變值,搜索框透明度
                float upAlphaGradientPro = getUpAlphaGradientPro();
                alphaGradientByPro(upAlphaGradientPro);

                //根據(jù)downCollapsedAlphaPro,設(shè)置折疊內(nèi)容透明度
                float downCollapsedAlphaPro = getDownCollapsedAlphaPro();
                alphaCollapsedContentByPro(downCollapsedAlphaPro);

                //根據(jù)downShopBarTransPro,設(shè)置購物內(nèi)容內(nèi)容位移
                float downShopBarTransPro = getDownShopBarTransPro();
                transShopBarByPro(downShopBarTransPro);

                //根據(jù)upCollapsedContentTransPro,設(shè)置折疊內(nèi)容位移
                float upCollapsedContentTransPro = getUpCollapsedContentTransPro();
                transCollapsedContentByPro(upCollapsedContentTransPro);

                if (mProgressUpdateListener!=null){
                    mProgressUpdateListener.onUpAlphaScaleProUpdate(upAlphaScalePro);
                    mProgressUpdateListener.onUpAlphaGradientProUpdate(upAlphaGradientPro);
                    mProgressUpdateListener.onDownCollapsedAlphaProUpdate(downCollapsedAlphaPro);
                    mProgressUpdateListener.onDownShopBarTransProUpdate(downShopBarTransPro);
                    mProgressUpdateListener.onUpCollapsedContentTransProUpdate(upCollapsedContentTransPro);
                }
            }
        });
        reboundedAnim.setDuration(ANIM_DURATION_FRACTION);
    }

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

    @Override
    public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
        if (velocityY<0){//往下滑動(dòng)的慣性滑動(dòng)
            float translationY = mLlContent.getTranslationY();
            float dy = translationY - velocityY;
            if (translationY >topBarHeight && translationY<= downFlingCutOffY) {//非折疊狀態(tài)
                //根據(jù)dy設(shè)置動(dòng)畫結(jié)束值官卡,只有dy>contentTransY才會(huì)有回彈蝗茁,downFlingCutOffY是回彈的最大值
                if (dy<=contentTransY){
                    reboundedAnim.setFloatValues(translationY,dy);
                }else if (dy>contentTransY&&dy<downFlingCutOffY){
                    reboundedAnim.setFloatValues(translationY,dy,contentTransY);
                }else {
                    reboundedAnim.setFloatValues(translationY,downFlingCutOffY,contentTransY);
                }
                reboundedAnim.start();
                return true;
            }
        }
        return false;
    }

釋放資源

在View從Window上移除時(shí)候,執(zhí)行要停止動(dòng)畫寻咒、釋放監(jiān)聽者的操作哮翘。

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (restoreOrExpandAnimator.isStarted()) {
            restoreOrExpandAnimator.cancel();
            restoreOrExpandAnimator.removeAllUpdateListeners();
            restoreOrExpandAnimator = null;
        }
        
        if(reboundedAnim.isStarted()){
            reboundedAnim.cancel();
            reboundedAnim.removeAllUpdateListeners();
            reboundedAnim = null;
        }

        if (mProgressUpdateListener!=null){
            mProgressUpdateListener=null;
        }
    }

商家點(diǎn)餐頁的嵌套滑動(dòng)實(shí)現(xiàn)

效果分析

商家點(diǎn)餐頁層級(jí)

商家點(diǎn)餐頁布局主要有上圖五部分組成,邏輯上的層級(jí)如圖所示有3層仔涩。

商家點(diǎn)餐頁TransY

滑動(dòng)Content部分時(shí)利用View的TransitionY屬性改變位置來消耗滑動(dòng)值忍坷,根據(jù)Content部分的TransitionY設(shè)定各種范圍從而計(jì)算百分比來執(zhí)行位移、Alpha效果熔脂。下面來說明上圖中變量的意義:

contentTransY;//滑動(dòng)內(nèi)容初始化TransY
iconTransY;//分享、關(guān)閉icon初始化transY
upEndIconTransY;//分享柑肴、關(guān)閉icon上滑最終transY
downFlingCutOffY;///從展開狀態(tài)下滑產(chǎn)生fling時(shí)回彈到初始狀態(tài)的最大值

ElemeFoodNestedScrollLayout

商家點(diǎn)餐頁的嵌套滑的自定義View繼承FrameLayout霞揉、實(shí)現(xiàn)NestedScrollingParent2接口,命名為ElemeFoodNestedScrollLayout晰骑。

布局

下面是布局要點(diǎn)适秩,側(cè)重于控件的尺寸和位置,完整布局請(qǐng)參考:ElemeFoodNestedScrollLayout布局

<ElemeFoodNestedScrollLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!--Mask部分-->
    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <!--Mask部分-->

    <!--Content部分-->
    <android.support.v4.widget.NestedScrollView
        android:id="@+id/ns"
        android:translationY="@dimen/food_content_trans_y"
        android:fillViewport="true"
        android:background="@android:color/white"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        ...
        >
    <!--Content部分-->

    <!--Icon部分-->
    <ImageView
        android:layout_gravity="right"
        android:layout_width="28dp"
        android:layout_height="28dp"
        android:translationY="@dimen/iv_food_icon_trans_y"
        android:layout_marginEnd="60dp"
        />
    <ImageView
        android:layout_gravity="right"
        android:layout_width="28dp"
        android:layout_height="28dp"
        android:translationY="@dimen/iv_food_icon_trans_y"
        android:layout_marginEnd="16dp"
        />
    <!--Icon部分-->

    <!--Expand部分-->
    <ImageView
        android:alpha="0"
        android:layout_gravity="bottom"
        android:layout_width="match_parent"
        android:layout_height="@dimen/iv_food_expand"
        />
    <!--Expand部分-->

    <!--Shop Bar部分-->
    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_gravity="bottom"
        android:layout_height="@dimen/shop_bar_height"
        android:translationY="@dimen/shop_bar_height"
        ...
        >
    <!--Shop Bar部分-->
</ElemeFoodNestedScrollLayout>

綁定需要做效果的View硕舆、引入Dimens秽荞、設(shè)置Content部分的初始化TransitionY

從上面圖片能夠分析出:關(guān)閉狀態(tài)時(shí),Content部分的TransY為滿屏高度

public class ElemeFoodNestedScrollLayout extends FrameLayout implements NestedScrollingParent2 {
    ...
    //shopBar部分
    private View mShopBar;

    //content部分
    private View mNestedScrollView;
    private View mTvComm;
    private View mTvGoodCommRate;
    private View mTvCommDetail;
    private View mTvCommCount;
    private View mVLine;
    private View mTvFoodDetail;
    
    //expand部分
    private View mIvExpand;
    
    //icon部分
    private View mIvShare;
    private View mIvClose;

    //mask部分
    private View mVMask;

    private float shopBarHeight;//shopBar部分高度
    private float ivExpandHegiht;//ivExpand部分高度
    private float statusBarHeight;//狀態(tài)欄高度
    private float iconTransY;//分享抚官、關(guān)閉icon初始化transY
    private float contentTransY;//滑動(dòng)內(nèi)容初始化TransY
    private float downFlingCutOffY;//下滑時(shí)fling上部分回彈臨界值
    private float upEndIconTransY;//分享扬跋、關(guān)閉icon上滑最終transY

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        shopBarHeight = getResources().getDimension(R.dimen.shop_bar_height);
        ivExpandHegiht = getResources().getDimension(R.dimen.iv_food_expand);
        contentTransY = getResources().getDimension(R.dimen.food_content_trans_y);
        iconTransY = getResources().getDimension(R.dimen.iv_food_icon_trans_y);
        statusBarHeight = getResources().getDimensionPixelSize(getResources().getIdentifier("status_bar_height", "dimen", "android"));
        downFlingCutOffY = contentTransY + dp2px(92);
        upEndIconTransY = statusBarHeight + dp2px(8);
        //因?yàn)殚_始就是關(guān)閉狀態(tài),設(shè)置Content部分的TransY為滿屏高度
        mNestedScrollView.setTranslationY(getMeasuredHeight());
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mNestedScrollView = findViewById(R.id.ns);
        mShopBar = findViewById(R.id.cl_food_shop_bar);

        mTvComm = findViewById(R.id.t_comm);
        mTvGoodCommRate = findViewById(R.id.t_good_comm_rate);
        mTvCommDetail = findViewById(R.id.t_comm_detail);
        mTvFoodDetail = findViewById(R.id.t_food_detail);

        mTvCommCount = findViewById(R.id.t_comm_count);
        mVLine = findViewById(R.id.view_line);

        mIvExpand = findViewById(R.id.iv_food_expand);
        mIvShare = findViewById(R.id.iv_small_share);
        mIvClose = findViewById(R.id.iv_small_close);
        mVMask = findViewById(R.id.v_mask);
    }
}

實(shí)現(xiàn)NestedScrollingParent2接口

onStartNestedScroll()

ElemeFoodNestedScrollLayout只處理Content部分里可滑動(dòng)View的垂直方向的滑動(dòng)凌节。

    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
        //處理Content部分里可滑動(dòng)View的垂直方向的滑動(dòng)
        return axes == ViewCompat.SCROLL_AXIS_VERTICAL && target.getId() == R.id.ns;
    }

onNestedPreScroll()

接下來就是處理滑動(dòng)钦听,上面效果分析提過:Content部分的上滑范圍=[0,contentTransY]洒试、
下滑范圍=[contentTransY,即滿屏高度]即滑動(dòng)范圍為[0,即滿屏高度],ElemeFoodNestedScrollLayout要控制Content部分的TransitionY值要在范圍內(nèi)朴上,之前的商家頁已經(jīng)說過了具體思路垒棋,這里不再贅述。

    ...
    private ProgressUpdateListener mProgressUpdateListener;
    
    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        float contentTransY = target.getTranslationY() - dy;
        //處理上滑
        if (dy > 0) {
            if (contentTransY >= 0) {
                translationByConsume(target, contentTransY, consumed, dy);
            } else {
                translationByConsume(target, 0, consumed, (target.getTranslationY() - 0));
            }
        }

        //處理下滑
        if (dy < 0 && !target.canScrollVertically(-1)) {
            if (contentTransY >= 0 && contentTransY < getMeasuredHeight()) {
                translationByConsume(target, contentTransY, consumed, dy);
            } else {
                translationByConsume(target, getMeasuredHeight(), consumed, getMeasuredHeight() - target.getTranslationY());
            }
        }

        alphaTransView(contentTransY);

        if (mProgressUpdateListener!=null){
            mProgressUpdateListener.onDownConetntCloseProUpdate(getDownConetntClosePro());
        }
    }

    private void alphaTransView(float transY) {
        float upCollapseTransPro = getUpExpandTransPro();
        //位移購物內(nèi)容
        float shopBarTransY = (1 - upCollapseTransPro) * shopBarHeight;
        translation(mShopBar, shopBarTransY);

        //設(shè)置商品信息View的透明度變化
        setAlpha(mTvComm, upCollapseTransPro);
        setAlpha(mTvGoodCommRate, upCollapseTransPro);
        setAlpha(mTvCommDetail, upCollapseTransPro);
        setAlpha(mTvFoodDetail, upCollapseTransPro);
        setAlpha(mTvCommCount, 1 - upCollapseTransPro);
        setAlpha(mVLine, 1 - upCollapseTransPro);

        //位移share close兩個(gè)Icon痪宰,設(shè)置展開icon透明度
        if (transY <= contentTransY) {
            float ivExpandUpTransY = upCollapseTransPro * -contentTransY;
            translation(mIvExpand, ivExpandUpTransY);
            setAlpha(mIvExpand, 1 - upCollapseTransPro);

            float iconTransY = upEndIconTransY + (1 - upCollapseTransPro) * (this.iconTransY - upEndIconTransY);
            translation(mIvShare, iconTransY);
            translation(mIvClose, iconTransY);

        } else if (transY > contentTransY && transY <= getMeasuredHeight()) {
            float ivExpandDowndTransY = (1 - getDownIvExpnadPro()) * ivExpandHegiht;
            translation(mIvExpand, ivExpandDowndTransY);

            float iconTransY = transY + dp2px(16);
            translation(mIvShare, iconTransY);
            translation(mIvClose, iconTransY);
        }
    }

    private float getDownConetntClosePro() {
        return (mNestedScrollView.getTranslationY() - contentTransY) / (getMeasuredHeight() - contentTransY);
    }

    private float getDownIvExpnadPro() {
        return ((contentTransY+ivExpandHegiht)-MathUtils.clamp(mNestedScrollView.getTranslationY(), contentTransY, contentTransY+ivExpandHegiht)) / ivExpandHegiht;
    }

    private float getUpExpandTransPro() {
        return (contentTransY - MathUtils.clamp(mNestedScrollView.getTranslationY(), 0, contentTransY)) / contentTransY;
    }

    public interface ProgressUpdateListener{
        void onDownConetntCloseProUpdate(float pro);
    }

onStopNestedScroll()

在從初始狀態(tài)到展開狀態(tài)的上滑過程中松手叼架,若上滑百分比小于等于50%則執(zhí)行恢復(fù)到初始狀態(tài)的動(dòng)畫,否則執(zhí)行轉(zhuǎn)化到展開狀態(tài)的動(dòng)畫衣撬;同理從初始狀態(tài)到關(guān)閉狀態(tài)下滑過程中松手碉碉,若下滑百分比小于等于50%則執(zhí)行恢復(fù)到初始狀態(tài)的動(dòng)畫,否則執(zhí)行轉(zhuǎn)化到關(guān)閉狀態(tài)的動(dòng)畫淮韭;

    ...
    private ValueAnimator restoreOrExpandOrCloseAnimator;//收起或展開折疊內(nèi)容時(shí)執(zhí)行的動(dòng)畫
    private NestedScrollingParentHelper mParentHelper;

    public ElemeFoodNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mParentHelper = new NestedScrollingParentHelper(this);
        ...
        restoreOrExpandOrCloseAnimator = new ValueAnimator();
        restoreOrExpandOrCloseAnimator.setInterpolator(new AccelerateInterpolator());
        restoreOrExpandOrCloseAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                translation(mNestedScrollView, (float) animation.getAnimatedValue());
                alphaTransView(mNestedScrollView.getTranslationY());
                if (mProgressUpdateListener!=null){
                    mProgressUpdateListener.onDownConetntCloseProUpdate(getDownConetntClosePro());
                }
            }
        });
        restoreOrExpandOrCloseAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                int alpha=mNestedScrollView.getTranslationY() >= getMeasuredHeight()?0:1;
                setAlpha(mIvClose,alpha);
                setAlpha(mIvShare,alpha);
            }
        });
    }

    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {
        mParentHelper.onStopNestedScroll(target, type);
        float translationY = target.getTranslationY();
        if (translationY == contentTransY|| reboundedAnim.isStarted()|| restoreOrExpandOrCloseAnimator.isStarted()) {
            return;
        }

        long dur;
        if (translationY < contentTransY) {
            if (getUpExpandTransPro() <= 0.5f) {
                dur = (long) (getUpExpandTransPro() *  ANIM_DURATION_FRACTION);
                restore(dur);
            } else {
                dur = (long) ((1 - getUpExpandTransPro()) *  ANIM_DURATION_FRACTION);
                expand(dur);
            }
        } else {
            if (getDownConetntClosePro() >= 0.5f) {
                dur = (long) (getDownConetntClosePro() *  ANIM_DURATION_FRACTION);
                close(dur);
            } else {
                dur = (long) ((1 - getDownConetntClosePro()) *  ANIM_DURATION_FRACTION);
                restore(dur);
            }
        }
    }

    public void restore(long dur) {
        mIvClose.setClickable(true);
        mVMask.setClickable(true);
        mIvExpand.setClickable(true);
        if (restoreOrExpandOrCloseAnimator.isStarted()) {
            restoreOrExpandOrCloseAnimator.cancel();
        }
        restoreOrExpandOrCloseAnimator.setFloatValues(mNestedScrollView.getTranslationY(), contentTransY);
        restoreOrExpandOrCloseAnimator.setDuration(dur);
        restoreOrExpandOrCloseAnimator.start();
    }

    public void expand(long dur) {
        if (restoreOrExpandOrCloseAnimator.isStarted()) {
            restoreOrExpandOrCloseAnimator.cancel();
        }
        restoreOrExpandOrCloseAnimator.setFloatValues(mNestedScrollView.getTranslationY(), 0);
        restoreOrExpandOrCloseAnimator.setDuration(dur);
        restoreOrExpandOrCloseAnimator.start();
    }

    public void close(long dur) {
        mNestedScrollView.scrollTo(0,0);
        mIvClose.setClickable(false);
        mVMask.setClickable(false);
        mIvExpand.setClickable(false);
        if (restoreOrExpandOrCloseAnimator.isStarted()) {
            restoreOrExpandOrCloseAnimator.cancel();
        }
        restoreOrExpandOrCloseAnimator.setFloatValues(mNestedScrollView.getTranslationY(), getMeasuredHeight());
        restoreOrExpandOrCloseAnimator.setDuration(dur);
        restoreOrExpandOrCloseAnimator.start();
    }

處理慣性滑動(dòng)

這里有兩個(gè)場景需要使用onNestedPreFling()處理慣性滑動(dòng):
1垢粮、從展開狀態(tài)下滑時(shí)處理回彈Fling,執(zhí)行回彈動(dòng)畫;
2靠粪、從初始狀態(tài)到關(guān)閉狀態(tài)下滑百分比超過50%慣性滑動(dòng)關(guān)閉蜡吧;

    private ValueAnimator reboundedAnim;//回彈動(dòng)畫
    ...

    public ElemeFoodNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        ...
        reboundedAnim = new ValueAnimator();
        reboundedAnim.setInterpolator(new DecelerateInterpolator());
        reboundedAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                translation(mNestedScrollView, (float) animation.getAnimatedValue());
                alphaTransView(mNestedScrollView.getTranslationY());
                if (mProgressUpdateListener!=null){
                    mProgressUpdateListener.onDownConetntCloseProUpdate(getDownConetntClosePro());
                }
            }
        });
        reboundedAnim.setDuration(ANIM_DURATION_FRACTION);
    }

    @Override
    public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
        if (velocityY<0) {
            float translationY = target.getTranslationY();
            float dy = translationY - velocityY;
            //從展開狀態(tài)下滑時(shí)處理回彈Fling,執(zhí)行回彈動(dòng)畫
            if (translationY >= 0 && translationY <= downFlingCutOffY){
                if (dy<contentTransY){
                    reboundedAnim.setFloatValues(translationY,dy);
                }else if (dy>contentTransY&&dy<downFlingCutOffY){
                    reboundedAnim.setFloatValues(translationY,dy,contentTransY);
                }else {
                    reboundedAnim.setFloatValues(translationY,downFlingCutOffY,contentTransY);
                }
                target.scrollTo(0,0);
                reboundedAnim.start();
                return true;
            }

            //從初始狀態(tài)到關(guān)閉狀態(tài)下滑百分比超過50%慣性滑動(dòng)關(guān)閉
            float dur = (1- getDownConetntClosePro()) * ANIM_DURATION_FRACTION;
            if (translationY<=(getMeasuredHeight()/2f)&&translationY>downFlingCutOffY){
                restore((long) dur);
                return true;
            }else {
                close((long) dur);
                return true;
            }
        }
        return false;
    }

釋放資源

在View從Window上移除時(shí)候,執(zhí)行要停止動(dòng)畫占键、釋放監(jiān)聽者的操作昔善。

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (restoreOrExpandOrCloseAnimator.isStarted()) {
            restoreOrExpandOrCloseAnimator.cancel();
            restoreOrExpandOrCloseAnimator.removeAllUpdateListeners();
            restoreOrExpandOrCloseAnimator.removeAllListeners();
            restoreOrExpandOrCloseAnimator = null;
        }

        if (reboundedAnim.isStarted()) {
            reboundedAnim.cancel();
            reboundedAnim.removeAllUpdateListeners();
            restoreOrExpandOrCloseAnimator = null;
        }

        if (mProgressUpdateListener!=null){
            mProgressUpdateListener=null;
        }
    }

ElemeNestedScrollLayout和ElemeFoodNestedScrollLayout聯(lián)動(dòng)

從效果預(yù)覽看的出,在點(diǎn)擊商品時(shí)ElemeFoodNestedScrollLayout內(nèi)容彈出畔乙,而背后有蒙層和ElemeFoodNestedScrollLayout內(nèi)容的縮放效果君仆,它們兩都提供滑動(dòng)百分比監(jiān)聽者,所以這部分邏輯應(yīng)該在Activity處理:

    mElemeFoodNsLayout.setProgressUpdateListener(new ElemeFoodNestedScrollLayout.ProgressUpdateListener() {
        @Override
        public void onDownConetntCloseProUpdate(float pro) {
            mElemeNSLayout.setScaleX(0.9f+(pro*0.1f));
            mElemeNSLayout.setScaleY(0.9f+(pro*0.1f));
            mVMask.setAlpha(1-pro);
            if (pro==1){
                mVMask.setVisibility(View.GONE);
            }else {
                mVMask.setVisibility(View.VISIBLE);
            }
        }
    });

總結(jié)

NestedScrolling機(jī)制結(jié)合View的Alpha牲距、Scale返咱、Transitio、動(dòng)畫等等可以弄出各種神奇而體驗(yàn)好的交互牍鞠,當(dāng)自定義View遇上NestedScrolling機(jī)制咖摹,給你不一樣的精彩,只要理清思路难述、分析問題萤晴、一步一步來解決或許就會(huì)守得云開見月明,由于本人水平有限僅給各位提供參考胁后,希望能夠拋磚引玉店读,如果有什么可以討論的問題可以在評(píng)論區(qū)留言或聯(lián)系本人,本文餓了么例子圖片素材來源于h5餓了么阿里巴巴矢量素材庫攀芯。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末屯断,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌裹纳,老刑警劉巖择葡,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異剃氧,居然都是意外死亡敏储,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門朋鞍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來已添,“玉大人,你說我怎么就攤上這事滥酥「瑁” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵坎吻,是天一觀的道長缆蝉。 經(jīng)常有香客問我,道長瘦真,這世上最難降的妖魔是什么刊头? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮诸尽,結(jié)果婚禮上原杂,老公的妹妹穿的比我還像新娘。我一直安慰自己您机,他們只是感情好穿肄,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著际看,像睡著了一般咸产。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仿村,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天锐朴,我揣著相機(jī)與錄音,去河邊找鬼蔼囊。 笑死,一個(gè)胖子當(dāng)著我的面吹牛衣迷,可吹牛的內(nèi)容都是我干的畏鼓。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼壶谒,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼云矫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起汗菜,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤让禀,失蹤者是張志新(化名)和其女友劉穎挑社,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體巡揍,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡痛阻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了腮敌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阱当。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖糜工,靈堂內(nèi)的尸體忽然破棺而出弊添,到底是詐尸還是另有隱情,我是刑警寧澤捌木,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布油坝,位于F島的核電站,受9級(jí)特大地震影響刨裆,放射性物質(zhì)發(fā)生泄漏澈圈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一崔拥、第九天 我趴在偏房一處隱蔽的房頂上張望极舔。 院中可真熱鬧,春花似錦链瓦、人聲如沸拆魏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽渤刃。三九已至,卻和暖如春贴膘,著一層夾襖步出監(jiān)牢的瞬間卖子,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工刑峡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留洋闽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓突梦,卻偏偏與公主長得像诫舅,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宫患,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345