Android | 自定义上拉抽屉+组合动画效果

话不多说先来个效果图看一下


效果图

实现的主要功能就是上拉抽屉(解决了子view的滑动冲突)+ 边缘动画 + 中间小球和seekbar效果动画。黄色部分就是上拉抽屉整体,绿色部分是横向的recyclerview。有个朋友说有阻尼效果就完美了 ... 因为效果图没有阻尼效果,所以就没有去研究 - -!

先总结一下主要用到的技术

  • ScrollView + NestedScrollingParent + NestedScrollingChild (主要做上拉抽屉解决内部和外部滑动冲突的)
  • 自定义view,贝塞尔曲线、lineTo、drawCircle、drawPath等一些常用的
    emmmm 好像就没了,其实主要就是自定义view画图而已啦,也没有很复杂。

顶部也可以放个图片,像酱紫


picture1.png

圆形中间也可以放图片和文字,上下滑动的时候内部图片和文字也会随之改变,其实原理都是一样的,一个会了你放啥都行,文章后面也会介绍。
效果就是酱紫


picture2.png

picture3.png

抽屉里我放的是LinearLayout,然后动态添加了多个可以横向滚动的RecyclerView,上滑下滑左滑右滑轻松无压力~~就是这么刺激

效果介绍完了,下面我们看一下如何实现的

一、 上滑抽屉+抽屉内部滚动 解决上下滚动冲突

  1. 首先你得先了解NestedScrollingParent & NestedScrollingChild
    主要就是父视图和子视图关于滚动的监听和相互之间滚动信号的传递。

  2. 整理一下滚动的需求:
    上滑
    滚动父视图 - > 监听到顶之后 -> 滚动子视图
    下滑
    先滚动子视图 -> 子视图到顶后 -> 滚动父视图

  3. 整体布局
    父布局里是需要有三个子布局的

// 父布局的滚动
<com.yoyo.topscrollview.scrollview.ScrollParentView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clickable="true"
        android:orientation="vertical"
        android:id="@+id/scrollParentView">
        //需要上滑隐藏的部分
        <RelativeLayout
            android:id="@+id/rl_transparentTop"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
        //上滑到顶需要吸附的部分
        <RelativeLayout
            android:id="@+id/center"
            android:layout_width="match_parent"
            android:layout_height="100dp">
            <com.yoyo.topscrollview.centerview.WaveView
                android:id="@+id/waveView"
                android:layout_centerInParent="true"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
            <com.yoyo.topscrollview.centerview.CircleView
                android:id="@+id/circleView"
                android:layout_centerInParent="true"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:ring_color="@color/lightPink"
                app:circle_color="@color/pink"/>
        </RelativeLayout>

        //子布局 内层滑动部分
        <com.yoyo.topscrollview.scrollview.ScrollChildView
            android:id="@+id/scrollChildView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:scrollbars="none"
            android:overScrollMode="never">
            <LinearLayout
                android:id="@+id/ll_content"
                android:background="@color/orange"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:paddingLeft="15dp"
                android:paddingRight="15dp">

            </LinearLayout>
        </com.yoyo.topscrollview.scrollview.ScrollChildView>
    </com.yoyo.topscrollview.scrollview.ScrollParentView>

在当前demo里

  • 上滑隐藏的部分 :顶部透明
  • 上滑到顶吸附的部分 :中间的弧度和圆
  1. ScrollParentView
  • onStartNestedScroll 是否接受嵌套滚动,只有它返回true,后面 的其他方法才会被调用
  • onNestedPreScroll 在内层view处理滚动事件前先被调用,可以让外层view先消耗部分滚动
  • onNestedScroll 在内层view将剩下的滚动消耗完之后调用,可以在这里处理最后剩下的滚动
  • onNestedPreFling 在内层view的Fling事件处理之前被调用
  • onNestedFling 在内层view的Fling事件处理完之后调用
    private View topView ;
    private View centerView;
    private View contentView;
    private NestedScrollingParentHelper mParentHelper;
    private int imgHeight;
    private int tvHeight;


    public ScrollParentView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ScrollParentView(Context context) {
        super(context);
        init();
    }

    /**
     * 初始化内部三个子视图
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        topView = getChildAt(0);
        centerView =  getChildAt(1);
        contentView = getChildAt(2);
        topView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if(imgHeight<=0){
                    imgHeight =  topView.getMeasuredHeight();
                }
            }
        });
        centerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if(tvHeight<=0){
                    tvHeight =  centerView.getMeasuredHeight();
                }
            }
        });

    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(getMeasuredWidth(), topView.getMeasuredHeight() + centerView.getMeasuredHeight() + contentView.getMeasuredHeight());

    }
    public int  getTopViewHeight(){
        return topView.getMeasuredHeight();
    }

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

        return true;
    }
    private void init() {
        mParentHelper = new NestedScrollingParentHelper(this);

    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    }

    @Override
    public void onStopNestedScroll(View target) {
        mParentHelper.onStopNestedScroll(target);
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    }

    /**
     * 处理上滑和下滑 顶部需要滚动的距离
     * @param target
     * @param dx
     * @param dy
     * @param consumed
     */
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        boolean headerScrollUp = dy > 0 && getScrollY() < imgHeight;
        boolean headerScrollDown = dy < 0 && getScrollY() > 0 && !target.canScrollVertically(-1);
        if (headerScrollUp || headerScrollDown) {
            scrollBy(0, dy);
            consumed[1] = dy;
        }
    }

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

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

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



    @Override
    public void scrollTo(int x, int y) {
        if(y<0){
            y=0;
        }
        if(y>imgHeight){
            y=imgHeight;
        }

        super.scrollTo(x, y);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction()==MotionEvent.ACTION_DOWN){
            return true;
        }
        return super.onTouchEvent(event);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return super.onInterceptTouchEvent(event);
    }
  1. ScrollChildView
    子布局的滚动就相对比较简单,主要是通过代理处理和父布局的一些滚动事件
 private NestedScrollingChildHelper mScrollingChildHelper;

    public ScrollChildView(Context context) {
        super(context);
        init(context);
    }

    public ScrollChildView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public ScrollChildView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public void init(Context context) {
        final ViewConfiguration configuration = ViewConfiguration.get(context);

    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        getScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return getScrollingChildHelper().isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {

        boolean bl = getScrollingChildHelper().startNestedScroll(axes);
        return bl;
    }

    @Override
    public void stopNestedScroll() {
        getScrollingChildHelper().stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return getScrollingChildHelper().hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                        int dyUnconsumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
    }

    private NestedScrollingChildHelper getScrollingChildHelper() {
        if (mScrollingChildHelper == null) {
            mScrollingChildHelper = new NestedScrollingChildHelper(this);
            mScrollingChildHelper.setNestedScrollingEnabled(true);
        }
        return mScrollingChildHelper;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(getMeasuredWidth(),getMeasuredHeight()+((ScrollParentView)getParent()).getTopViewHeight());
    }

到这里就可以实现如效果图一样的滚动效果了

二、 类似水波纹的动画

picture4.gif

这样看就比较直观些
这个就是用贝塞尔曲线画的简单的一个效果

  • 首先 -> 了解贝塞尔曲线
    已经有过很多人写了贝塞尔曲线的详解文章,学一下,这里不做详细介绍。

我这里是用了两个三阶贝塞尔曲线,从中间分开,左边一个右边一个,然后吧这个视图上下分为一半,中间的点不变,两边的高度增加,两边是扇形画的圆角,然后lineto画成封闭图形,这样就出现了如上图所示的动画效果。


分解图.png
@Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        mPath.reset();
        // start point
        mPath.moveTo(mStartX, mViewHeightHalf);
        // 贝塞尔曲线
        mPath.rCubicTo(mViewWidthHalf / 4, 0, mViewWidthHalf / 4, Math.abs(mViewHeightHalf - mCenterRadius), mViewWidthHalf / 2, Math.abs(mViewHeightHalf - mCenterRadius));
        mPath.rCubicTo(mViewWidthHalf / 4, 0, mViewWidthHalf / 4, -Math.abs(mViewHeightHalf - mCenterRadius), mViewWidthHalf / 2, -Math.abs(mViewHeightHalf - mCenterRadius));
        
        // 两边的圆角扇形
        mPath.addArc(0, mViewHeightHalf, 200, mViewHeightHalf + 200, 180, 90);
        mPath.addArc(mViewWidthHalf * 2 - 200, mViewHeightHalf, mViewWidthHalf * 2, mViewHeightHalf + 200, 270, 90);


        // 图形边框
        mPath.lineTo(this.getMeasuredWidth() - 100, mViewHeightHalf);
        mPath.lineTo(this.getMeasuredWidth(), mViewHeightHalf + 100);
        mPath.lineTo(this.getMeasuredWidth(), this.getMeasuredHeight());

        mPath.lineTo(0, this.getMeasuredHeight());
        mPath.lineTo(0, mViewHeightHalf + 100);
        mPath.lineTo(100, mViewHeightHalf);
        mPath.lineTo(mStartX, mViewHeightHalf);
        mPath.lineTo(mStartX * 2 + mStartX, mViewHeightHalf);

        mPath.setFillType(Path.FillType.WINDING);
        //Close path
        mPath.close();
        canvas.drawPath(mPath, mCenterLinePaint);


    }

三、圆形和圆环

这部分大家应该就比较熟悉,自定义view经常会用到,用法就不多说了,记录一下中间图片随之缩放和透明改变的写法

  • Bitmap.createScaledBitmap 将当前存在的一个位图按一定比例(尺寸)构建一个新位图
  • paint.setAlpha(mAlpha); 设置画笔的透明度

然后再动画中不断改变圆和圆环的半径、图的尺寸、画笔透明度,就能达到效果

四、整体上滑效果

抽屉的弧度、圆、圆环和图片这些的改变主要是监听当前上滑的距离和需要上滑的距离做的百分比计算的然后相应的随之改变。

mScrollParentView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
            @Override
            public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                float v1 = scrollY / topHeight;
                if (0 <= v1 && v1 <= 1.1) {
                    mWaveView.changeWave(v1);
                    mCircleView.changeCircle(v1);
                }
            }
        });

是在父view的滚动监听里做的改变,topHeight就是抽屉需要滚动的距离。

结语

之前接触的动画都是单独的模块,直接开始结束的那种,像这次这样需要动态改变而且多个结合的还是第一次遇到(渣渣本渣没错了),所以也是在边学边写,可能有很多地方写的不是很恰当,也是希望大佬可以指出,共同学习共同进步。其实现在的效果是大改过一次的,最初贝塞尔曲线高度取的整个高度,然后改变中间的那个点向下凹,但是外面的圆又要正好一半在他的上方一半在下方,这样的位置其实是不好做适配的,所以就改成了现在的这样。通过这个动画的实现,自己不仅是在自定义view、动画还是一些思考方式上都有所进步,这是挺重要的。项目中还有另一个动画,就下篇再讲吧~

gitee项目地址
https://gitee.com/yoyo666/TopScrollView.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343