自定義View1——BezierBottomBar

這個是在項目中運用的自定義View的第一篇,按照字母序,第一篇首先先講BezierBottomBar。這個控件是我從郭霖公眾號之前的一篇推送上學(xué)習(xí)來的,所以有些代碼是照搬的之前那篇推送,這篇文章也有很多地方直接引用了這篇推送的一些內(nèi)容,下面是那篇的推送地址

https://mp.weixin.qq.com/s?__biz=MzA5MzI3NjE2MA==&mid=2650243121&idx=1&sn=a3e3368758074d509691e531a927f2c8&chksm=8863715ebf14f848f4c648575cba1d26313ad88893fb3eaf3169aa7d10c44476b779a3438409&mpshare=1&scene=23&srcid=08207tLPp42M8JTyyGQcQrJm#rd

先看效果



這個是結(jié)合了ViewPager后的效果。
這個控件總共分為三個部分,可以把這部分看做是一個VVM模式

  • 控件本體——BezierBottomBarView
  • 控制View行為的——BezierBottomBarControl
  • 定制的ViewPager

BezierBottomBarView

測量View大小

由于我們是做的是一個底邊欄,所以我們要在onMeasure中設(shè)定最大高度,以防止控件占高度太高。所以在getHeight方法中,將控件最大高度設(shè)置為屏幕的1/8.

    private int getHeight(int heightMeasureSpec) {

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        DisplayMetrics dm = getResources().getDisplayMetrics();

        int height = (heightSize > dm.heightPixels) ? heightSize : dm.heightPixels;

        return (height / 8);

    }

對于單個圓的存放

在那篇推送中作者是將這個控件寫成了ViewGroup+ImgView的形式,但是在我們的應(yīng)用中我們想要加入手勢控制動畫,所以如果仍然使用作者的思路就會導(dǎo)致動畫實現(xiàn)起來相對來說比較的麻煩,故而我改用純View進(jìn)行繪制。
所以我們就需要一類數(shù)據(jù)來表示單個圓的各種參數(shù),在這里我將get和set方法省略,如果有需要可以去查看源碼。

public static class BarTag {

        private int icon;
        private String tag;
        private int color;

        private float centerX;
        private float centerY;

        private float radius;

        private RectF rectF;
        private RectF dst;
        private RectF tagRectF;
    }

在這里預(yù)留了tag和圖標(biāo)的color方法,在里面并沒有使用,如果未來有需要的話可以自行添加。這個類中主要持有的方法就是icon,radius和dst,dst表示圓的位置。

確定圓的擺放位置

在原文中,確定擺放位置是在onLayout中設(shè)置的,但是原文是將控件當(dāng)做一個ViewGroup去寫的,我在這里卻是將其當(dāng)做View去寫,所以我選擇在onSizeChanged中去確定圓的位置,并且存儲起來,在之后的onDraw方法后用canvas去繪制外面的圓形。

在這里有一點補(bǔ)充:
繼承與View和繼承與現(xiàn)有控件都是下面的順序,但是控件的大小是生成之后就固定的,不會再次改變。
onMeasure()→onSizeChanged()→onLayout()→onMeasure()→onLayout()→onDraw()

所以在onSizeChanged中我們可以這么確定一個圓的位置,interval表示兩個圓之間的間隔。

        interval = (width - 2 * tabNum * radius) / (tabNum + 1);

        for (int i = 0; i < tabNum; i++) {
            float cx = interval + radius + i * (interval + 2 * radius);
            RectF rectF = new RectF(cx - radius, startY - radius, cx + radius, startY + radius);
            barTags.get(i).setRectF(rectF);
            RectF dst = new RectF((int) (interval + (1 - scale * 1 / g2) * radius + i * (interval + 2 * radius)),
                    (int) (startY - scale * radius / g2),
                    (int) (interval + (1 + scale * 1 / g2) * radius + i * (interval + 2 * radius)),
                    (int) (startY + scale * radius / g2));
            barTags.get(i).setDst(dst);
            barTags.get(i).setRadius(radius);
        }

在onSizeChanged方法中計算并且將圓的位置進(jìn)行存儲

對靜止圓的繪制

在計算完各個圓的位置之后,我們就可以在onDraw方法中進(jìn)行繪制??梢钥吹?,如果當(dāng)前圓被選中,那么就會有一個顏色填充。
所以大概就是這樣

    for (int i = 0; i < tabNum; i++) {
        float cx = barTags.get(i).centerX;
        float cy = barTags.get(i).centerY;
        canvas.drawCircle(cx, cy, radius, linePaint);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), barTags.get(i).icon);
        canvas.drawBitmap(bitmap, null, barTags.get(i).dst, fillPaint);
    }

動畫的繪制

(有關(guān)貝塞爾曲線及其內(nèi)容請直接參考開始給出的博客,我覺得講的不錯)
我們可以仔細(xì) 的觀察到,在這個動畫當(dāng)中,中間移動的圓分成了6個狀態(tài)
這里是起始的三個狀態(tài),分別是圓,開始向左移動,向左移動一定距離,還欠缺的是準(zhǔn)備結(jié)束向左移動,向左移動的過量和向左移動的回彈


這里只能說是三個狀態(tài)

那么我們可以假設(shè)動畫時間currentTime(0~1),所以我們可以將整個移動狀態(tài)分為以下幾個區(qū)間段:

  • 狀態(tài)1,圓
    currentTime = 0
  • 狀態(tài)2,即向左移動
    0 < currentTime <= 0.2
  • 狀態(tài)3,即開始進(jìn)入中間狀態(tài)
    0.2 < currentTime <= 0.5
  • 狀態(tài)4,即準(zhǔn)備結(jié)束這段運動,是狀態(tài)2的鏡面對稱
    0.5 < currentTime <= 0.8
  • 狀態(tài)5,結(jié)束這段運動時的回彈開始
    0.8 < currentTime <= 0.9
  • 狀態(tài)6,結(jié)束這段運動時候的回彈結(jié)束
    0.9 < currentTime < 1
  • 狀態(tài)1,圓
    currentTime = 1

由之前那篇博客我們可以知道,使用二階貝塞爾曲線去畫一個圓,受制于p1,p2,p3,p4四個點,所以我們就可以通過改變這四個點的參數(shù)去改變這個圓的參數(shù)


這張圖是原博客中的一張圖,我就直接拿來用了

那么我們可以將這六個部分的代碼變成這個樣子

  • 狀態(tài)1
        if (currentTime == 0) {
            resetP();
            canvas.drawCircle(interval + radius + (currentPos) * (interval + 2 * radius), startY, 0, clickPaint);
            fillPaint.setColor(startColor);
            canvas.translate(startX, startY);
            if (toPos > currentPos) {
                p2.setX(radius);
            } else {
                p4.setX(-radius);
            }
        }
  • 狀態(tài)2
        if (currentTime > 0 && currentTime <= 0.2) {
            direction = toPos > currentPos ? true : false;
            if (animating) {
                canvas.drawCircle(interval + radius + (toPos) * (interval + 2 * radius),
                        startY,
                        radius * 1.0f * 5 * currentTime,
                        clickPaint);
            }
            canvas.translate(startX, startY);
            if (toPos > currentPos) {
                p2.setX(radius + 2 * 5 * currentTime * radius / 2);
            } else {
                p4.setX(-radius - 2 * 5 * currentTime * radius / 2);
            }
        }
  • 狀態(tài)3
        if (currentTime > 0.2 && currentTime <= 0.5) {
            float cx = startX + (currentTime - 0.2f) * distance / 0.7f;
            canvas.translate(cx, startY);
            if (toPos > currentPos) {
                p1.setX(0.5f * radius * (currentTime - 0.2f) / 0.3f);
                p2.setX(2 * radius);
                p3.setX(0.5f * radius * (currentTime - 0.2f) / 0.3f);

                p2.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
                p4.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
            } else {
                p1.setX(-0.5f * radius * (currentTime - 0.2f) / 0.3f);
                p3.setX(-0.5f * radius * (currentTime - 0.2f) / 0.3f);
                p4.setX(-2 * radius);

                p2.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
                p4.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
            }
        }
  • 狀態(tài)4
        if (currentTime > 0.5 && currentTime <= 0.8) {
            float cx = startX + (currentTime - 0.2f) * distance / 0.7f;
            canvas.translate(cx, startY);
            if (toPos > currentPos) {
                p1.setX(0.5f * radius + 0.5f * radius * (currentTime - 0.5f) / 0.3f);
                p3.setX(0.5f * radius + 0.5f * radius * (currentTime - 0.5f) / 0.3f);

                p2.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
                p4.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
            } else {
                p1.setX(-0.5f * radius - 0.5f * radius * (currentTime - 0.5f) / 0.3f);
                p3.setX(-0.5f * radius - 0.5f * radius * (currentTime - 0.5f) / 0.3f);

                p2.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
                p4.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
            }
        }
  • 狀態(tài)5
       if (currentTime > 0.8 && currentTime <= 0.9) {
            p2.setMc(mc);
            p4.setMc(mc);
            float cx = startX + (currentTime - 0.2f) * distance / 0.7f;
            canvas.translate(cx, startY);
            if (toPos > currentPos) {
                p4.setX(-radius + 1.6f * radius * (currentTime - 0.8f) / 0.1f);
            } else {
                p2.setX(radius - 1.6f * radius * (currentTime - 0.8f) / 0.1f);
            }
        }
  • 狀態(tài)6
      if (currentTime > 0.9 && currentTime < 1) {
            if (toPos > currentPos) {
                p1.setX(radius);
                p3.setX(radius);
                canvas.translate(startX + distance, startY);
                p4.setX(0.6f * radius - 0.6f * radius * (currentTime - 0.9f) / 0.1f);
            } else {
                p1.setX(-radius);
                p3.setX(-radius);
                canvas.translate(startX + distance, startY);
                p2.setX(-0.6f * radius + 0.6f * radius * (currentTime - 0.9f) / 0.1f);
            }
        }

View的hide和show

show

可以看到,這個show的動畫有一個稍微過一些然后再回彈的效果(雖然可能真的不太明顯)并且圓是依次上升的,所以就需要讓它有一個上升的次序。



我們可以看到,在sin圖像中,在頂點下任取一個y值,都會有兩個x值使得sin(x) = y(→_→好像講的有點啰嗦)那么我們亦可以通過給不同的圓設(shè)置不同的初始值,來實現(xiàn)階梯式上升的效果。

    void show() {
        if (!valueRunning && !hideRunning) {
            float cy = (startY + radius) / (float) Math.sin(Math.toRadians(angle));
            for (int i = 0; i < tabNum; i++) {
                hideHeight = cy + startY / (float) Math.sin(Math.toRadians(angle));
                angles[i] = i * (-10);
                dsts[i] = new RectF((int) (interval + (1 - scale * 1 / g2) * radius + i * (interval + 2 * radius)),
                        (int) (startY - scale * radius / g2) + cy,
                        (int) (interval + (1 + scale * 1 / g2) * radius + i * (interval + 2 * radius)),
                        (int) (startY + scale * radius / g2) + cy);
            }
            animState = AnimState.Show;
            handler.postDelayed(showRunnable, showTime);
        }
    }
hide

hide方法和show方法相同,也需要幾個圓依次向下。所以思路也同show——賦予幾個圓不同的負(fù)向初始量,然后在handle中對其進(jìn)行改變

    void hide() {
        if (!valueRunning && !showRunning) {
            hideSpeed = (startY + radius + (tabNum - 1) * (radius * 2 / tabNum)) / hideTime;
            for (int i = 0; i < tabNum; i++) {
                changeHeight[i] = i * (-10);
                dsts[i] = new RectF((int) (interval + (1 - scale * 1 / g2) * radius + i * (interval + 2 * radius)),
                        (int) (startY - scale * radius / g2),
                        (int) (interval + (1 + scale * 1 / g2) * radius + i * (interval + 2 * radius)),
                        (int) (startY + scale * radius / g2));
            }
            animState = AnimState.Hide;
            handler.postDelayed(hideRunnable, hideTime);
        }
    }

控件的單擊事件

在這個控件中,我們有兩個手勢操作,以及單擊操作。但是手勢操作我們需要在整個屏幕,也就是在Activity中去操作這個全局手勢,所以我們就只需要在控件的onTouchEvent中處理單擊操作即可。
由于控件有show和hide兩種狀態(tài),所以我們只需要讓其在show的時候處理Event即可,在hide的時候我們可以選擇無視。
所以在這部分就可以這么去處理TouchEvent

            if (x > interval + 2 * radius && x < (interval + 2 * radius) * tabNum) {
                if (animator != null) {
                    animator.cancel();
                }
                int toPos = (int) (x / (interval + 2 * radius));
                if (toPos != currentPos && toPos <= tabNum) {
                    startAniTo(currentPos, toPos);
                }
            } else if (x > interval && x < interval + 2 * radius) {
                if (animator != null) {
                    animator.cancel();
                }
                if (currentPos != 0)
                    startAniTo(currentPos, 0);
            }

我們可以看到,在這里就只需要判斷單擊的x,y值即可。

BezierBottomBarControl

由于需要在全局設(shè)置一個手勢操作,并且需要在一定時間過后對BottomBar進(jìn)行一個隱藏,所以需要一個ViewControl來統(tǒng)一的對View的狀態(tài)進(jìn)行操作。

設(shè)置手勢操作

由于這個控件在App中僅存在于MainActivity中,所以只需要在MainActivity的onTouchEvent中將MotionEvent傳入control就可以了。在Control中對其進(jìn)行處理。由于我們并不需要實時的使得控件對于TouchEvent進(jìn)行反饋,所以我們只需獲取到Down和Up的坐標(biāo)即可。

    public void setTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                if (lastX == 0) {
                    lastX = viewPager.getLastX();
                }
                if (lastY == 0) {
                    lastY = viewPager.getLastY();
                }
                float deltaY = lastY - y;
                if (deltaY > 0) {
                    if (bottomBar.getState() == BezierBottomBarView.AnimState.Hide) {
                        show();
                    }
                }
                if (deltaY < 0) {
                    if (bottomBar.getState() != BezierBottomBarView.AnimState.Hide) {
                        hide();
                    }
                }
                break;
        }
    }

show和hide

在control中起了一個定時器,當(dāng)控件顯示一段時間過后,就會自動調(diào)用hide方法,使得控件進(jìn)行隱藏。

setViewPagerListener

由于我們的控件可以和ViewPager進(jìn)行一個聯(lián)動,在Control的構(gòu)造方法中已經(jīng)將ViewPager的實例傳入,所以我們只需要設(shè)置ViewPager的Listener即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,040評論 25 709
  • 流程圖 1、視頻編碼 1、1>初始化視頻編碼類初始化調(diào)用:VTCompressionSessionCreate( ...
    xgou閱讀 2,979評論 0 2
  • 時光荏苒,我們也經(jīng)歷了很多感情上的變幻莫測,但始終,最基本的友情信任還是患得患失,好像從未有過,有時我只是很失望的...
    大君22閱讀 339評論 1 6
  • 姓名:母光艷 公司:寧波貞觀電器 寧波盛和塾第235期,利他二組 【日精進(jìn)打卡第302天】 【知-學(xué)習(xí)】 誦讀《六...
    母光焱閱讀 140評論 0 0

友情鏈接更多精彩內(nèi)容