Behavior实现UC浏览器首页动画效果

老规矩,还是先上效果图
这里写图片描述

github地址

前面我也写过一篇关于UC浏览器首页滑动动画效果的文章UC浏览器首页滑动动画实现,只不过这篇文章是通过自定义View的方式实现这个滑动效果。最近在看Behavior相关的东西,所以使用Behavior又实现了一次UC浏览器主页的滑动效果,使用Behavior实现相比较自定义View的实现方式还是要简单方便很多。

View结构分析

UC首页滑动过程中可以分为四个View在参与滑动,具体的分析流程可以参见UC浏览器首页滑动动画实现这篇文章的分析,这里简要罗列下:
1. UCViewTitle:首页标题栏视图(UC首页显示UC头条)
2. UCViewHeader:首页头部导航视图(UC首页显示各个网站ICON入口)
3. UCViewContent:首页内容视图(UC首页显示新闻内容的列表)
4. UCViewTab:首页内容Tab导航视图(UC首页显示新闻分类的View)

Behavior

既然已经决定通过Behavior实现此效果,那下面几个概念就必须要弄清楚:
1. Behavior必须作用于CoordinatorLayout直接子View才会生效
2. Behavior其实是对嵌套滑动的应用,因为CoordinatorLayout其实是实现嵌套滑动,最终对嵌套滑动的执行交给Behavior来实现,所以Behavior的滑动处理必须要有能触发嵌套滑动的子View触发才会起作用

关于嵌套滑动

  1. Android实现嵌套滑动只需要实现NestedScrollingParentNestedScrollingChild这两个接口即可
  2. 在嵌套滑动过程中子View(实现NestedScrollingChild接口)会将自身的滑动情况通知父View(实现NestedScrollingParent接口),不一定是直接父View父View做完相关动作之后再通知子View,也就是子View其实是整个嵌套滑动的发起者
  3. CoordinatorLayout实现了NestedScrollingParent接口作为嵌套滑动的父View,因此如果要处理Behavior中对于滑动的相关处理,就需要有一个嵌套滑动的子View来触发这个Behavior

实现

  1. 上面分析UC首页时发现有个显示新闻的列表,因此我们可以用RecyclerView作为列表,因为RecyclerView实现了NestedScrollingChild接口,可以作为嵌套滑动的子View
  2. 因为是多个视图的同时滑动处理,所以在实现Behavior时需要选择一个依赖,这里我选择前面说过的UCViewHeader作为其他视图Behavior的依赖
  3. 在看了AppBarLayout的源码之后,发现其子类ScrollingViewBehavior继承至HeaderScrollingViewBehavior,在查看源码之后发现如下几个类可以抽出来为我们所用HeaderScrollingViewBehavior,ViewOffsetBehavior,ViewOffsetHelper
    1. HeaderScrollingViewBehavior:继承该类后,应用此BehaviorView布局时会自动在其依赖View的下方
    2. ViewOffsetBehavior:继承该类后,应用此BehaviorView在布局时会自动进行移动处理
UCViewTitleBehavior实现

UCViewTitle在初始时是不可见的,我采用设置其TopMargin为其-height让其不可见,然后在滑动过程中再慢慢滑动到可见,当其完全可见时滑动结束,因此滑动结束时UCViewTitle滑动的距离为UCViewTitle的高度值

下面的代码中涉及到Behavior在处理滑动过程中一些函数的实现及作用这里就不再说明,不清楚滑动过程中各个函数的作用可以参考我的上篇文章自定义Behavior实现快速返回效果,这篇文章里有介绍Behavior中各个函数的作用

同时UCViewTitle与其依赖UCViewHeader为反向滑动,关键代码如下:

public class UCViewTitleBehavior extends ViewOffsetBehavior<View> {

    ...
    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        //因为UCViewTitle默认是在屏幕外不可见,所以在UCViewTitle进行布局的时候设置其topMargin让其不可见
        ((CoordinatorLayout.LayoutParams) child.getLayoutParams()).topMargin = -child.getMeasuredHeight();
        return super.onLayoutChild(parent, child, layoutDirection);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return isDependOn(dependency);
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        //因为UCViewTitle与UCViewHeader的滑动方向相反
        //所以当依赖UCViewHeader发生变化时,只需要时设置反向的translationY即可
        child.setTranslationY(-dependency.getTranslationY());
        return false;
    }

    private boolean isDependOn(View dependency) {
        //确定UCViewHeader作为依赖
        return dependency != null && dependency.getId() == R.id.news_view_header_layout;
    }
}
UCViewTabBehavior实现

上面已经说了,当UCViewTitle完全可见时即代表整个滑动结束。因此在这个过程中UCViewTab整个滑动的距离即为UCViewHeader的高度减去UCViewTitle的高度。而且因为是同向滑动,所以在依赖位置发生变化时,我们只需要根据依赖视图因滑动而产生的translationY计算出UCViewTitletranslationY即可。计算方式见下面代码和注释:

public class UCViewTabBehavior extends HeaderScrollingViewBehavior {
    private int mTitleViewHeight = 0;
    ... 
    @Override
    protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        //UCViewTitle高度
        mTitleViewHeight = parent.findViewById(R.id.news_view_title_layout).getMeasuredHeight();
        super.layoutChild(parent, child, layoutDirection);
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

        //UCViewTab要滑动的距离为Header的高度减去TitleView的高度
        float offsetRange = mTitleViewHeight - dependency.getMeasuredHeight();
        //当Header向上滑动mTitleViewHeight高度后,即滑动完成
        int headerOffsetRange = -mTitleViewHeight;
        if(dependency.getTranslationY() == headerOffsetRange) {//Header已经上滑结束
            child.setTranslationY(offsetRange);
        } else if(dependency.getTranslationY() == 0) {//下滑结束,也是初始化的状态
            child.setTranslationY(0);
        } else {
            //UCViewTab与UCViewHeader为同向滑动
            //根据依赖UCViewHeader的滑动比例计算当前UCViewTab应该要滑动的值translationY,依赖的translationY为正值则其也为正值反之亦然
            child.setTranslationY(dependency.getTranslationY() / (headerOffsetRange * 1.0f) * offsetRange);
        }
        return false;
    }
    ...
}
UCViewContentBehavior实现

UCViewTab一样,UCViewContent与依赖UCViewHeader也是同向滑动,其滑动过程中translationY的计算方式也是一样的。只是滑动过程中UCViewContent的滑动总距离为依赖UCViewHeader的高度减去UCViewTab的高度和UCViewTitle高度,其对应的Behavior实现关键代码如下:

public class UCViewContentBehavior extends HeaderScrollingViewBehavior {
    private int mTitleViewHeight = 0;
    private int mTabViewHeight = 0;
    ...
    @Override
    protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {

        mTitleViewHeight = parent.findViewById(R.id.news_view_title_layout).getMeasuredHeight();
        mTabViewHeight = parent.findViewById(R.id.news_view_tab_layout).getMeasuredHeight();
        super.layoutChild(parent, child, layoutDirection);
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

        int headerOffsetRange = -mTitleViewHeight;
        //因为UCViewContent与依赖UCViewHeader为同向滑动
        //所以UCViewHeader向上滑即translationY为负数时,UCViewContent也向上滑其translationY也为负数
        //所以UCViewHeader向上滑即translationY为正数时,UCViewContent也向上滑其translationY也为正数
        //而headerOffsetRange为负数,getScrollRange(dependency)为正数,所以最前面要加上一个负号

        //计算方式与UCViewTab的计算方式一样
        child.setTranslationY(-dependency.getTranslationY() / (headerOffsetRange * 1.0f) * getScrollRange(dependency));
        return false;
    }

    @Override
    protected int getScrollRange(View dependency) {

        if(isDependency(dependency)) {
            //UCViewHeader的高度,减去UCViewTab和UCViewTitle的高度就是UCViewContent要滑动的高度
            return dependency.getMeasuredHeight() - mTitleViewHeight - mTabViewHeight;
        }
        return super.getScrollRange(dependency);
    }
    ...
}
UCViewHeaderBehavior实现

UCViewHeader需要处理好何时滑动结束,何时可以滑动,松开手指时该如何处理

在我的实现中以每次实际滑动距离的1/4作为UCViewHeader的滑动值,而在松开手指时如果滑动达到整个滑动距离的1/4则会自动滑动到结束,否则则会自动滑动到初始位置,下面是该Behavior的完整代码

public class UCViewHeaderBehavior extends ViewOffsetBehavior<View> {
    private int mTitleViewHeight = 0;
    private OverScroller mOverScroller;
    private WeakReference<View> mChild;

    public static final int STATE_OPENED = 0;
    public static final int STATE_CLOSED = 1;
    public static final int DURATION_SHORT = 300;
    public static final int DURATION_LONG = 600;

    private int mCurState = STATE_OPENED;

    public UCViewHeaderBehavior() {

        super();
    }

    public UCViewHeaderBehavior(Context context, AttributeSet attrs) {

        super(context, attrs);
        mOverScroller = new OverScroller(context);
    }

    @Override
    protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {

        super.layoutChild(parent, child, layoutDirection);
        mTitleViewHeight = parent.findViewById(R.id.news_view_title_layout).getMeasuredHeight();
        mChild = new WeakReference<>(child);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {

        //开始滑动的条件,垂直方向滑动,滑动未结束
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL && canScroll(child, 0) && !isClosed(child);
    }

    /**
     * 当前是否可以滑动
     * @param child
     * @param pendingDy     Y轴方向滑动的translationY
     * @return
     */
    private boolean canScroll(View child, float pendingDy) {

        int pendingTranslationY = (int) (child.getTranslationY() - pendingDy);
        if (pendingTranslationY >= getHeaderOffsetRange() && pendingTranslationY <= 0) {
            return true;
        }
        return false;
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {

        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        //开始滑动之前的逻辑处理

        //dy>0 向上滑
        //dy<0 向下滑
        float halfOfDis = dy / 4.0f;//每次以滑动的1/4作为滑动距离进行滑动
        if (!canScroll(child, halfOfDis)) {//滑动结束
            if(halfOfDis > 0) {
                child.setVisibility(View.GONE);//滑动结束后,隐藏此视图
                child.setTranslationY(getHeaderOffsetRange());
            } else {
                child.setTranslationY(0);
            }
        } else {//滑动未结束
            if(halfOfDis <= 0) {
                child.setVisibility(View.VISIBLE);
            }
            //滑动
            child.setTranslationY(child.getTranslationY() - halfOfDis);
        }
        //消耗掉当前垂直方向上的滑动距离
        consumed[1] = dy;
    }

    /**
     * 向上滑动过程时translationY的最小值
     * @return
     */
    private int getHeaderOffsetRange() {

        return -mTitleViewHeight;
    }

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {

        if(ev.getAction() == MotionEvent.ACTION_UP) {
            //对松开手指时进行处理,如果松开时滑动滑动了1/4则自动滑动到结束,否则则回归原位
            handlerActionUp(child);
        }
        return super.onInterceptTouchEvent(parent, child, ev);
    }

    private void handlerActionUp(View child) {
        if (mFlingRunnable != null) {
            child.removeCallbacks(mFlingRunnable);
            mFlingRunnable = null;
        }
        mFlingRunnable = new FlingRunnable(child);
        if (child.getTranslationY() < getHeaderOffsetRange() / 4.0f) {
            mFlingRunnable.scrollToClosed(DURATION_SHORT);
        } else {
            mFlingRunnable.scrollToOpen(DURATION_SHORT);
        }
    }

    private void onFlingFinished(View layout) {

        boolean isClosed = isClosed(layout);
        mCurState = isClosed ? STATE_CLOSED : STATE_OPENED;
        if(isClosed) {
            layout.setVisibility(View.GONE);
        }
    }

    /**
     * 是否滑动结束
     * @param child
     * @return
     */
    private boolean isClosed(View child) {

        return child.getTranslationY() == getHeaderOffsetRange();
    }

    public boolean isClosed() {
        return mCurState == STATE_CLOSED;
    }

    public void openPager() {
        openPager(DURATION_LONG);
    }

    /**
     * @param duration open animation duration
     */
    public void openPager(int duration) {
        View child = mChild.get();
        if (isClosed() && child != null) {
            if(child.getVisibility() == View.GONE) {
                child.setVisibility(View.VISIBLE);
            }
            if (mFlingRunnable != null) {
                child.removeCallbacks(mFlingRunnable);
                mFlingRunnable = null;
            }
            mFlingRunnable = new FlingRunnable(child);
            mFlingRunnable.scrollToOpen(duration);
        }
    }

    public void closePager() {
        closePager(DURATION_LONG);
    }

    /**
     * @param duration close animation duration
     */
    public void closePager(int duration) {
        View child = mChild.get();
        if (!isClosed()) {
            if (mFlingRunnable != null) {
                child.removeCallbacks(mFlingRunnable);
                mFlingRunnable = null;
            }
            mFlingRunnable = new FlingRunnable(child);
            mFlingRunnable.scrollToClosed(duration);
        }
    }


    private FlingRunnable mFlingRunnable;
    private class FlingRunnable implements Runnable {
        private final View mLayout;

        FlingRunnable(View layout) {
            mLayout = layout;
        }

        public void scrollToClosed(int duration) {
            float curTranslationY = ViewCompat.getTranslationY(mLayout);
            float dy = getHeaderOffsetRange() - curTranslationY;
            mOverScroller.startScroll(0, Math.round(curTranslationY - 0.1f), 0, Math.round(dy + 0.1f), duration);
            start();
        }

        public void scrollToOpen(int duration) {
            float curTranslationY = ViewCompat.getTranslationY(mLayout);
            mOverScroller.startScroll(0, (int) curTranslationY, 0, (int) -curTranslationY, duration);
            start();
        }

        private void start() {
            if (mOverScroller.computeScrollOffset()) {
                mFlingRunnable = new FlingRunnable(mLayout);
                ViewCompat.postOnAnimation(mLayout, mFlingRunnable);
            } else {
                onFlingFinished(mLayout);
            }
        }


        @Override
        public void run() {
            if (mLayout != null && mOverScroller != null) {
                if (mOverScroller.computeScrollOffset()) {
                    ViewCompat.setTranslationY(mLayout, mOverScroller.getCurrY());
                    ViewCompat.postOnAnimation(mLayout, this);
                } else {
                    onFlingFinished(mLayout);
                }
            }
        }
    }
}

上面就是涉及到的四个视图对应的Behavior,从上面代码的介绍可以看出使用Behavior实现该效果比自定义View实现该效果要简单省事很多而且难度也不大,可见掌握Behavior对开发者来说是很有必要的。

完整代码戳这里


大家如果有问题可以加QQ群交流:106510493

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值