Android | 自定義上拉抽屜+組合動(dòng)畫效果

話不多說先來個(gè)效果圖看一下


效果圖

實(shí)現(xiàn)的主要功能就是上拉抽屜(解決了子view的滑動(dòng)沖突)+ 邊緣動(dòng)畫 + 中間小球和seekbar效果動(dòng)畫兜材。黃色部分就是上拉抽屜整體,綠色部分是橫向的recyclerview据沈。有個(gè)朋友說有阻尼效果就完美了 ... 因?yàn)樾Ч麍D沒有阻尼效果,所以就沒有去研究 - -!

先總結(jié)一下主要用到的技術(shù)

  • ScrollView + NestedScrollingParent + NestedScrollingChild (主要做上拉抽屜解決內(nèi)部和外部滑動(dòng)沖突的)
  • 自定義view戒悠,貝塞爾曲線、lineTo钞螟、drawCircle兔甘、drawPath等一些常用的
    emmmm 好像就沒了,其實(shí)主要就是自定義view畫圖而已啦鳞滨,也沒有很復(fù)雜洞焙。

頂部也可以放個(gè)圖片,像醬紫


picture1.png

圓形中間也可以放圖片和文字拯啦,上下滑動(dòng)的時(shí)候內(nèi)部圖片和文字也會(huì)隨之改變澡匪,其實(shí)原理都是一樣的,一個(gè)會(huì)了你放啥都行提岔,文章后面也會(huì)介紹仙蛉。
效果就是醬紫


picture2.png

picture3.png

抽屜里我放的是LinearLayout,然后動(dòng)態(tài)添加了多個(gè)可以橫向滾動(dòng)的RecyclerView碱蒙,上滑下滑左滑右滑輕松無壓力~~就是這么刺激

效果介紹完了荠瘪,下面我們看一下如何實(shí)現(xiàn)的

一、 上滑抽屜+抽屜內(nèi)部滾動(dòng) 解決上下滾動(dòng)沖突

  1. 首先你得先了解NestedScrollingParent & NestedScrollingChild
    主要就是父視圖和子視圖關(guān)于滾動(dòng)的監(jiān)聽和相互之間滾動(dòng)信號(hào)的傳遞赛惩。

  2. 整理一下滾動(dòng)的需求:
    上滑
    滾動(dòng)父視圖 - > 監(jiān)聽到頂之后 -> 滾動(dòng)子視圖
    下滑
    先滾動(dòng)子視圖 -> 子視圖到頂后 -> 滾動(dòng)父視圖

  3. 整體布局
    父布局里是需要有三個(gè)子布局的

// 父布局的滾動(dòng)
<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>

        //子布局 內(nèi)層滑動(dòng)部分
        <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>

在當(dāng)前demo里

  • 上滑隱藏的部分 :頂部透明
  • 上滑到頂吸附的部分 :中間的弧度和圓
  1. ScrollParentView
  • onStartNestedScroll 是否接受嵌套滾動(dòng),只有它返回true,后面 的其他方法才會(huì)被調(diào)用
  • onNestedPreScroll 在內(nèi)層view處理滾動(dòng)事件前先被調(diào)用,可以讓外層view先消耗部分滾動(dòng)
  • onNestedScroll 在內(nèi)層view將剩下的滾動(dòng)消耗完之后調(diào)用,可以在這里處理最后剩下的滾動(dòng)
  • onNestedPreFling 在內(nèi)層view的Fling事件處理之前被調(diào)用
  • onNestedFling 在內(nèi)層view的Fling事件處理完之后調(diào)用
    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();
    }

    /**
     * 初始化內(nèi)部三個(gè)子視圖
     */
    @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) {
    }

    /**
     * 處理上滑和下滑 頂部需要滾動(dòng)的距離
     * @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
    子布局的滾動(dòng)就相對(duì)比較簡單哀墓,主要是通過代理處理和父布局的一些滾動(dòng)事件
 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());
    }

到這里就可以實(shí)現(xiàn)如效果圖一樣的滾動(dòng)效果了

二、 類似水波紋的動(dòng)畫

picture4.gif

這樣看就比較直觀些
這個(gè)就是用貝塞爾曲線畫的簡單的一個(gè)效果

  • 首先 -> 了解貝塞爾曲線
    已經(jīng)有過很多人寫了貝塞爾曲線的詳解文章喷兼,學(xué)一下篮绰,這里不做詳細(xì)介紹。

我這里是用了兩個(gè)三階貝塞爾曲線季惯,從中間分開吠各,左邊一個(gè)右邊一個(gè),然后吧這個(gè)視圖上下分為一半勉抓,中間的點(diǎn)不變贾漏,兩邊的高度增加,兩邊是扇形畫的圓角藕筋,然后lineto畫成封閉圖形纵散,這樣就出現(xiàn)了如上圖所示的動(dòng)畫效果。


分解圖.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);


    }

三隐圾、圓形和圓環(huán)

這部分大家應(yīng)該就比較熟悉伍掀,自定義view經(jīng)常會(huì)用到,用法就不多說了暇藏,記錄一下中間圖片隨之縮放和透明改變的寫法

  • Bitmap.createScaledBitmap 將當(dāng)前存在的一個(gè)位圖按一定比例(尺寸)構(gòu)建一個(gè)新位圖
  • paint.setAlpha(mAlpha); 設(shè)置畫筆的透明度

然后再動(dòng)畫中不斷改變圓和圓環(huán)的半徑蜜笤、圖的尺寸、畫筆透明度盐碱,就能達(dá)到效果

四把兔、整體上滑效果

抽屜的弧度啊胶、圓、圓環(huán)和圖片這些的改變主要是監(jiān)聽當(dāng)前上滑的距離和需要上滑的距離做的百分比計(jì)算的然后相應(yīng)的隨之改變垛贤。

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的滾動(dòng)監(jiān)聽里做的改變,topHeight就是抽屜需要滾動(dòng)的距離趣倾。

結(jié)語

之前接觸的動(dòng)畫都是單獨(dú)的模塊聘惦,直接開始結(jié)束的那種,像這次這樣需要?jiǎng)討B(tài)改變而且多個(gè)結(jié)合的還是第一次遇到(渣渣本渣沒錯(cuò)了)儒恋,所以也是在邊學(xué)邊寫善绎,可能有很多地方寫的不是很恰當(dāng),也是希望大佬可以指出诫尽,共同學(xué)習(xí)共同進(jìn)步禀酱。其實(shí)現(xiàn)在的效果是大改過一次的,最初貝塞爾曲線高度取的整個(gè)高度牧嫉,然后改變中間的那個(gè)點(diǎn)向下凹剂跟,但是外面的圓又要正好一半在他的上方一半在下方,這樣的位置其實(shí)是不好做適配的酣藻,所以就改成了現(xiàn)在的這樣曹洽。通過這個(gè)動(dòng)畫的實(shí)現(xiàn),自己不僅是在自定義view辽剧、動(dòng)畫還是一些思考方式上都有所進(jìn)步送淆,這是挺重要的。項(xiàng)目中還有另一個(gè)動(dòng)畫怕轿,就下篇再講吧~

gitee項(xiàng)目地址
https://gitee.com/yoyo666/TopScrollView.git

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末偷崩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子撞羽,更是在濱河造成了極大的恐慌阐斜,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件放吩,死亡現(xiàn)場(chǎng)離奇詭異智听,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)渡紫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門到推,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人惕澎,你說我怎么就攤上這事莉测。” “怎么了唧喉?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵捣卤,是天一觀的道長忍抽。 經(jīng)常有香客問我,道長董朝,這世上最難降的妖魔是什么鸠项? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮子姜,結(jié)果婚禮上祟绊,老公的妹妹穿的比我還像新娘。我一直安慰自己哥捕,他們只是感情好牧抽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著遥赚,像睡著了一般扬舒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上凫佛,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天讲坎,我揣著相機(jī)與錄音,去河邊找鬼御蒲。 笑死衣赶,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的厚满。 我是一名探鬼主播府瞄,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼碘箍!你這毒婦竟也來了遵馆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤丰榴,失蹤者是張志新(化名)和其女友劉穎货邓,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體四濒,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡换况,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盗蟆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片戈二。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖喳资,靈堂內(nèi)的尸體忽然破棺而出觉吭,到底是詐尸還是另有隱情,我是刑警寧澤仆邓,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布鲜滩,位于F島的核電站伴鳖,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏徙硅。R本人自食惡果不足惜榜聂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嗓蘑。 院中可真熱鬧峻汉,春花似錦、人聲如沸脐往。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽业簿。三九已至,卻和暖如春阳懂,著一層夾襖步出監(jiān)牢的瞬間梅尤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來泰國打工岩调, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留巷燥,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓号枕,卻偏偏與公主長得像缰揪,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子葱淳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354