橢圓進度條的實現(xiàn)過程

簡介

模仿這個gif中SeekBar的功能和效果

由于這個控件的實現(xiàn)耗費了不少時間颈墅,也遇到了一些數(shù)學(xué),漸變相關(guān)的問題雾袱,過程較為復(fù)雜恤筛,所以我在這里整理了一下流程,希望能方便大家了解這個控件的功能和使用芹橡。

文中幾個容易混淆的概念

  • 角度和弧度:我們通常用角度degree描述叹俏,但Math中的三角函數(shù)用弧度radians計算。
    • 角度轉(zhuǎn)弧度:Math.toRadians --> angdeg / 180.0 * PI
    • 弧度轉(zhuǎn)角度:Math.toDegrees --> angrad * 180.0 / PI
  • 橢圓和弧線:Canvas中畫弧是按照矩形內(nèi)接橢圓畫出來的僻族。
  • 前后左右四段弧線,進度弧線是藍色弧線屡谐。

實現(xiàn)思路

  • 首先述么,在Canvas上畫四段弧線,這四段弧線在一個橢圓上愕掏。
  • 每一條弧線有一個起始角度startAngle度秘,和一個掃過角度sweepAngle。
  • 有進度變化饵撑,在前端的弧線上畫一段藍色的弧線剑梳。
  • 藍色弧線的長度也就是角度,根據(jù)當(dāng)前進度和前端弧線的角度計算出來滑潘。
  • 拖拽時垢乙,根據(jù)拖拽的角度,修改這四條弧線的起始角度语卤。
  • 旋轉(zhuǎn)時追逮,以旋轉(zhuǎn)時長為動畫時間酪刀,旋轉(zhuǎn)角度以動畫距離,使用插值器钮孵,計算出每次步進的角度骂倘,和拖拽一樣,修改起始角度巴席。

自定義View基礎(chǔ):

自定義的屬性存放在attrs.xml中历涝,主要是橢圓線條顏色,粗細漾唉,控制球半徑和顏色荧库,進度顏色,進度漸變起止顏色毡证,旋轉(zhuǎn)動畫時長电爹,具體參考注釋。

    <declare-styleable name="OvalSeekBar">
        <!--橢圓線條默認(rèn)顏色-->
        <attr name="oval_line_color" format="color|reference"></attr>
        <!--橢圓線條默認(rèn)粗細-->
        <attr name="oval_stroke_width" format="dimension|reference"></attr>
        <!--前端線條開始角度料睛,x軸正向是0度丐箩,順時針計算,前面的這段弧線要繪制進度恤煞,所以通過屬性確定下來-->
        <attr name="oval_start_angle" format="integer"></attr>
        <!--前端線條結(jié)束角度-->
        <attr name="oval_end_angle" format="integer"></attr>
        <!--進度條漸變顏色初值-->
        <attr name="oval_progress_start_color" format="color|reference"></attr>
        <!--進度條漸變顏色終值-->
        <attr name="oval_progress_end_color" format="color|reference"></attr>
        <!--節(jié)目時長文字顏色-->
        <attr name="oval_duration_text_color" format="color|reference"></attr>
        <!--節(jié)目時長文字大小-->
        <attr name="oval_duration_text_size" format="dimension|reference"></attr>
        <!--進度球半徑-->
        <attr name="oval_progress_thumb_radius" format="dimension|reference"></attr>
        <!--進度球顏色-->
        <attr name="oval_progress_thumb_color" format="color|reference"></attr>
        <!--弧線之間間隔度數(shù)-->
        <attr name="oval_arc_gap_angle" format="integer"></attr>
        <!--旋轉(zhuǎn)時長屎勘,如果把一次旋轉(zhuǎn)當(dāng)成一個動畫,那就是動畫時長-->
        <attr name="oval_rotate_duration" format="integer"></attr>
        <!--旋轉(zhuǎn)角度居扒,如果把一次旋轉(zhuǎn)當(dāng)成一個動畫概漱,那就是動畫距離,
                建議是90度喜喂,因為有4條弧線同時旋轉(zhuǎn)瓤摧,90度正好是一次位置替換-->
        <attr name="oval_rotate_angle" format="integer"></attr>
    </declare-styleable>

在view的構(gòu)造方法中讀取這些屬性,保存在我們view的成員變量中玉吁。

初始化各種畫筆照弥,這些畫筆包括:

  • 前端(mPaint)
  • 左側(cè)(mLeftPaint)
  • 后側(cè)(mBackPaint)
  • 右側(cè)(mRightPaint)
  • 進度(mProgressPaint)
  • 進度球(mThumbPaint)
  • 開始時長文字(mDurationStartPaint)
  • 調(diào)試(mDebugPaint)

畫筆初始化值得一提的是,給畫筆添加了LightingColorFilter, 如果不加這個进副,則畫出來的線顯得灰暗这揣,像是陷下去了,加了這個就會有發(fā)光效果影斑,顯得飽滿有光澤给赞。

在onMeasure中,確定一些關(guān)鍵變量:

  • view的寬高和它的padding (mWidth, mHeight)
  • 根據(jù)view的寬高和padding計算出橢圓的寬高(mOvalRectWith, mOvalRectHeight)
  • 進而計算出橢圓的區(qū)域(mOvalRectLeft, mOvalRectTop, mOvalRectRight, mOvalRectBottom)矫户,對應(yīng)矩形變量(mOvalRect)
  • 計算出橢圓的長半徑和短半徑(mA, mB). 橢圓方程:
    x^2 / a^2 + y^2 / b^2 = 1 (a > b > 0)
  • 計算出控制球拖拽區(qū)域片迅,因為用戶不會沿著弧線拖,還是會沿著直線拖皆辽,我們設(shè)定一個矩形的區(qū)域障涯,只有在這個區(qū)域的觸摸事件當(dāng)成seek,計算這個區(qū)域的代碼是:
            mSeekRect = new RectF(
                    (float) (mA + mA * Math.cos(Math.toRadians(mEndAngle)) - touchExpendDelta) + getPaddingLeft(),
                    mB + mB / 2 + touchExpendDelta,
                    (float) (mA + mA * Math.cos(Math.toRadians(mStartAngle)) + touchExpendDelta + getPaddingLeft()),
                    mHeight);
  • 初始進度是0罐旗, 計算橢圓圓心點(mOvalRect),例如橢圓矩形左頂點x是20, 寬是120唯蝶, 則橢圓中心的x在(120 + 20) / 2 = 70
mOvalCenter.x = (mOvalRectRight + mOvalRectLeft) / 2;
mOvalCenter.y = (mOvalRectBottom + mOvalRectTop) / 2;

弧線角度相關(guān)計算

計算4條弧線的開始角度和掃過角度九秀,對應(yīng)Canvas的drawArc方法的startAngle和sweepAngle:

    public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
            @NonNull Paint paint) {
        super.drawArc(oval, startAngle, sweepAngle, useCenter, paint);
    }

這4條弧線分別是被縫隙隔開的前后左右4條弧(mFrontArc, mLeftArc, mBackArc, mRightArc)我們定義了一個非常簡單的內(nèi)部類封裝startAngle和sweepAngle:

    protected class ArcData {
        float startAngle, sweepAngle;

        ArcData(float startAngle, float sweepAngle) {
            this.startAngle = startAngle;
            this.sweepAngle = sweepAngle;
        }

        void setValue(float startAngle, float sweepAngle) {
            this.startAngle = startAngle;
            this.sweepAngle = sweepAngle;
        }
    }

還定義了拖拽時的弧線信息(mRotateFrontArc, mRotateLeftArc, mRotateBackArc, mRotateRightArc)以及旋轉(zhuǎn)時的弧線信息(mDragFrontArc, mDragLeftArc, mDragBackArc, mDragRightArc)
這里的計算用到另外一個橢圓方程,主要是根據(jù)角度計算橢圓上具體一點的坐標(biāo)粘我,也就是橢圓參數(shù)方程鼓蜒。方程是:

    x = a * cosθ
    y = b * sinθ

a是橢圓長軸,b是橢圓短軸征字,θ計算比較特殊都弹,但我們這里把角度轉(zhuǎn)成弧度以后,用Math的三角函數(shù)就可以了]例如計算前端弧線的起始角度和掃過角度, 起始角度是布局文件中指定的角度加上縫隙角度的二分之一匙姜,掃過角度是結(jié)束角度減去縫隙的二分之一再減去起始角度:

    int startAngle = mStartAngle + mArcGapAngle / 2;
        int sweepAngle = mEndAngle - mArcGapAngle / 2 - mStartAngle;
        mFrontArc = new ArcData(startAngle, sweepAngle);

然后計算前端這段弧線起始位置和結(jié)束位置的坐標(biāo)畅厢,起始位置在第四象限,結(jié)束位置在第三象限氮昧。x, y 是按照橢圓圓心為原點計算出來的位置框杜,View的原點在左上頂點。如果對canvas做平移袖肥,也就是Canvas.translate,這個坐標(biāo)可以直接使用咪辱,但這里并沒有做,所以要用這個坐標(biāo)和View的寬高進行換算椎组,得到它在View中的坐標(biāo)油狂。因為橢圓的圓心也是view的中心,所以這個點在view中的坐標(biāo)是:橫向坐標(biāo)是寬度的一半減去或者加上x寸癌,縱向坐標(biāo)是高度的一半減去或者加上y专筷,借助正弦/余弦三角函數(shù)的周期性,可以寫成下面的樣子蒸苇。

  • 起始位置坐標(biāo):
    double x = mA * Math.cos(Math.toRadians(mStartAngle));
        double y = mB * Math.sin(Math.toRadians(mStartAngle));
        mFrontStartX =  x + mWidth / 2;
        mFrontStartY =  y + mHeight / 2;
  • 結(jié)束位置坐標(biāo):
    x = mA * Math.cos(Math.toRadians(mEndAngle));
        y = mB * Math.sin(Math.toRadians(mEndAngle));
        mFrontEndX = x + mWidth / 2;
        mFrontEndY =  y + mHeight / 2;

進度對應(yīng)的角度可以根據(jù)前端弧線對應(yīng)的總時長和前端弧線的總度數(shù)計算出來磷蛹, 例如前端弧線是80度,時長是4分鐘填渠,現(xiàn)在進度是2分鐘,則進度對應(yīng)的角度是 80 / 4 * 2 = 40度鸟辅,當(dāng)然這里是按毫秒計算的

        mProgressAngle = (float) progress / mPlayDuration * (mEndAngle - mStartAngle);

因為進度球是從第三象限向第四象限移動氛什,也就是從左往右,所以用結(jié)束角度減去進度角度匪凉。同理枪眉,進度球的位置:

    float arcStart = mEndAngle - mProgressAngle;
        x = mA * Math.cos(Math.toRadians(arcStart));
        y = mB * Math.sin(Math.toRadians(arcStart));
        mThumbX = (int) x + mWidth / 2;
        mThumbY = (int) y + mHeight / 2;

一個較為繁瑣的計算是漸變的起止位置,用左側(cè)弧線的漸變舉例說明再层,因為左側(cè)弧線開頭的部分不透明度高贸铜,結(jié)尾的部分不透明度低堡纬,漸變開始的坐標(biāo)按它的開始角度算出來,結(jié)束坐標(biāo)按它的開始角度和掃過角度求和計算出來蒿秦。右側(cè)漸變和左側(cè)漸變略有不同烤镐。下面是左側(cè)漸變計算的詳細過程:

        float la = startAngle;
        double lx = mA * Math.cos(Math.toRadians(la));
        double ly = mB * Math.sin(Math.toRadians(la));
        float lsx = (int) lx + mWidth / 2;
        float lsy = (int) ly + mHeight / 2;
        la = sweepAngle + startAngle;
        lx = mA * Math.cos(Math.toRadians(la));
        ly = mB * Math.sin(Math.toRadians(la));
        float lex = (int) lx + mWidth / 2;
        float ley = (int) ly + mHeight / 2;
        mLeftLineGradient = new LinearGradient(
                (int) lsx, (int) lsy, lex, ley,
                mLeftArcGradientColors, null, Shader.TileMode.CLAMP);

下面介紹靜止?fàn)顟B(tài)下進度球位置計算, 我給這個計算寫了詳細的注釋,理解這個計算就理解了我們這個進度條的計算過程棍鳖,因為進度的變化會導(dǎo)致進度弧線藍色漸變的變化炮叶,所以還增加了一點漸變的計算《纱Γ可以打開對應(yīng)的debug開關(guān)镜悉,在進度變化的時候關(guān)注log的輸出,找到mThumbX和mThumbY的變化規(guī)律:

    /*
     * 計算進度球的x医瘫, y坐標(biāo)侣肄,同時根據(jù)進度的長度設(shè)計進度漸變的寬度.
     *
     */
    private void calculateThumbPoint(int progress) {
        if (progress == 0 || mPlayDuration == 0 || mEndAngle - mStartAngle == 0) {
            mProgressAngle = 0;
        } else {
            mProgressAngle = (float) progress / mPlayDuration * (mEndAngle - mStartAngle);
        }
        //修改旋轉(zhuǎn)弧線的sweep角度,這樣拖拽開始時醇份,控制球的初始位置和進度條上的位置一致稼锅,
        mRotateProgressArc.sweepAngle = (int) mProgressAngle;
        //修改拖拽弧線的sweep角度,這樣拖拽開始時被芳,控制球的初始位置和進度條上的位置一致缰贝,
        mDragProgressArc.sweepAngle = (int) mProgressAngle;
        //橢圓標(biāo)準(zhǔn)方程, x = a * cos(弧度), y = b * sin(弧度)畔濒,首先求角度剩晴,然后將角度轉(zhuǎn)弧度
        //角度求法:以3點鐘方向為x正向坐標(biāo),9點鐘方向為180度侵状,由于進度是從結(jié)束角度mEndAngle開始赞弥,
        //所以球的位置與x軸的初始夾角是mEndAngle - mProgressAngle,
        // 簡單來看是180 - mEndAngle + mProgressAngle,但由于余弦函數(shù)的周期性趣兄,我們可以直接寫成
        //mEndAngle - mProgressAngle, 如果有進度角绽左,這個夾角會變大進度角度數(shù)
        float arcStart = mEndAngle - mProgressAngle;
        double x = mA * Math.cos(Math.toRadians(arcStart));
        double y = mB * Math.sin(Math.toRadians(arcStart));
        //位置從布局左上為原點,上面計算出來的x艇潭, y是以橢圓圓心為原點的值拼窥,所以要用畫布一半的寬度減去x
        mThumbX = (int) x + mWidth / 2; //相當(dāng)于mWidth / 2 - x
        mThumbY = (int) y + mHeight / 2;
        //進度漸變
        if (mProgressAngle > 0) {
            mProgressGradient = new LinearGradient(
                    (int) mDragDurationStartX, (int) mDragDurationStartY, mThumbX, mThumbY,
                    mProgressGradientColors, null, Shader.TileMode.CLAMP);
            if (DEBUG_PROCESS) {
                Log.d(TAG, "LinearGradient mDragDurationStartX: " + mDragDurationStartX
                        + ", mDragDurationStartY: " + mDragDurationStartY + ", mThumbX: "
                        + mThumbX + ", mThumbY: " + mThumbY);
            }
        }
        if (DEBUG_PROCESS) {
            Log.d(TAG, "calculateThumbPoint progress: " + progress
                    + ", mProgressAngle: " + mProgressAngle);
        }

        if (DEBUG_PROCESS) {
            Log.d(TAG, "calculateThumbPoint mThumbX: " + mThumbX
                    + ", mThumbY: " + mThumbY);
        }
    }

靜止?fàn)顟B(tài)下的繪制就已經(jīng)差不多了,我們有畫筆蹋凝,有橢圓矩形鲁纠,有角度,文字根據(jù)進度和時長計算一下鳍寂,文字的位置就是前端弧線的開始和結(jié)束位置改含,進度球位置也根據(jù)進度計算出來了,半徑通過讀取自定義屬性初始化了迄汛。繪制在drawIdle方法中進行捍壤,當(dāng)然骤视,如果想看參考線,可以打開debug開關(guān)和DEBUG_REFER_LINE開關(guān)鹃觉。

處理進度Seek

由于是SeekBar专酗,那么就要能seek,所以需要重寫onTouchEvent帜慢,在onTouchEvent這個方法中笼裳,首先在down事件中判斷觸摸是不是落在進度球范圍之內(nèi),如果不在粱玲,說明用戶沒有點進度球躬柬,所以不想seek,我們return false抽减,不再接收后面的事件允青。如果在,我們標(biāo)記一下卵沉,準(zhǔn)備seek:

            case MotionEvent.ACTION_DOWN:
                float xd = event.getX(), yd = event.getY();
                boolean onThumb = eventDownOnThumb(xd, yd);
                if (onThumb) {
                    startTrackingTouch();
                    return true;
                } else {
                    return false;
                }

在move事件中颠锉,如果事件發(fā)生在我們前面提到的Seek 矩形區(qū)域,認(rèn)為是用戶在做Seek操作史汗,否則琼掠,說明用戶已經(jīng)離開了seek區(qū)域, 如果在做Seek操作我們就根據(jù)拖拽的距離更新進度:

            float xm = event.getX(), ym = event.getY();
                boolean onSeekArea = eventMoveOnProgressBar(xm, ym);
                if (onSeekArea) {
                    int progress = getSeekProgress(xm, ym);
                    int nowProgress = mProgress + progress;
                    if (DEBUG_TOUCH) {
                        Log.d(TAG, "----onTouchEvent, nowProgress " + nowProgress);
                    }
                    setProgress(mProgress + progress, true);
                    return true;
                } else {
                    stopTrackingTouch();
                    return false;
                }

這里根據(jù)距離計算進度的實現(xiàn)比較粗糙,但它能正常工作停撞,根據(jù)事件的橫坐標(biāo)瓷蛙,減去進度球的橫坐標(biāo),得到一個偏移量戈毒,然后根據(jù)Seek 矩形的長度和總的進度計算seek的進度:

    private int getSeekProgress(float x, float y) {
        float deltaX = x - mThumbX;
        float progress = deltaX / (mSeekRect.right - mSeekRect.left) * mPlayDuration;
        if (DEBUG_PROCESS) {
            Log.d(TAG, "getSeekProgress deltaX: " + deltaX + ", progress: " + progress);
        }
        return (int) progress;
    }

處理旋轉(zhuǎn)

相比拖拽艰猬,旋轉(zhuǎn)容易一些,所以先處理埋市,理解旋轉(zhuǎn)的邏輯以后冠桃,拖拽就容易多了,因為拖拽可以理解成可控制的旋轉(zhuǎn)道宅,并且拖拽引起的角度變化不會大于旋轉(zhuǎn)的角度食听。我們在onMeasure的時候,用靜止的角度變量初始化了旋轉(zhuǎn)弧線的初值:

    private void setRotateInitialData() {
        mRotateFrontArc = new ArcData(mFrontArc.startAngle, mFrontArc.sweepAngle);
        mRotateLeftArc = new ArcData(mLeftArc.startAngle, mLeftArc.sweepAngle);
        mRotateBackArc = new ArcData(mBackArc.startAngle, mBackArc.sweepAngle);
        mRotateRightArc = new ArcData(mRightArc.startAngle, mRightArc.sweepAngle);
        mRotateProgressArc = new ArcData(mEndAngle, (int) mProgressAngle);
    }

旋轉(zhuǎn)通過public void rotate(boolean clockwise) 方法觸發(fā)污茵,boolean參數(shù)表示是否是順時針旋轉(zhuǎn)樱报。一般切換下一頁,順時針轉(zhuǎn)一次省咨,反之逆時針轉(zhuǎn)一次肃弟。在旋轉(zhuǎn)之前玷室,我們修改一下進度球的旋轉(zhuǎn)初始位置零蓉,否則就會出現(xiàn)跳動的問題笤受。如果不是拖拽引起的旋轉(zhuǎn),則正常進度確定的位置就是旋轉(zhuǎn)開始的初始位置,然后我們開始旋轉(zhuǎn)動畫:

    public void rotate(boolean clockwise) {
        if (bIsRotating) {
            //如果正在旋轉(zhuǎn)敌蜂,則忽略新的旋轉(zhuǎn)調(diào)用
            return;
        }
        if (bIsDragging) {
            mRotateThumbX = mDragThumbX;
            mRotateThumbY = mDragThumbY;
        } else {
            mRotateThumbX = mThumbX;
            mRotateThumbY = mThumbY;
        }
        startRotateAnimation(clockwise);
    }

這個動畫實際上不是一個動畫箩兽,而是反復(fù)的invalidate產(chǎn)生的動畫效果。為了產(chǎn)生和動畫相同的效果章喉,我們創(chuàng)建了一個ValueAnimator汗贫,并且給他一個減速插值器DecelerateInterpolator,并創(chuàng)建一個ValueAnimator.AnimatorUpdateListener監(jiān)聽動畫事件秸脱,在onAnimationUpdate中繪制新弧線落包。
在旋轉(zhuǎn)動畫完成后,重置旋轉(zhuǎn)弧線的變量值:

            @Override
            public void onAnimationEnd(Animator animation) {
                //旋轉(zhuǎn)結(jié)束時將狀態(tài)還原
                restoreAfterRotate();
            }           

在onAnimationUpdate中摊唇,我們拿到動畫的值咐蝇,然后和前值(mAnimatorPreValue)比較,得到偏移量巷查,本次拿到的值變成前值.然后根據(jù)偏移量旋轉(zhuǎn)有序,旋轉(zhuǎn)代碼非常簡單,就是不停地增加或者減小(mRotateFrontArc, mRotateLeftArc, mRotateBackArc, mRotateRightArc)這幾個旋轉(zhuǎn)弧線的開始角度岛请,再計算一下進度球的旋轉(zhuǎn)過程中的坐標(biāo)旭寿,然后invalidate一下view就可以了,startAngle的變化不用說了崇败,簡單介紹一下旋轉(zhuǎn)過程中進度球坐標(biāo)的計算盅称。首先是確定角度,旋轉(zhuǎn)過程中改變的只是起始角度僚匆,掃過角度是保持不變的微渠,所以用起始角度減去掃過角度,代入橢圓標(biāo)準(zhǔn)方程的三角函數(shù)就可以了:

    private void calculateRotateThumbXY() {
        float arcStart = mRotateProgressArc.startAngle - mRotateProgressArc.sweepAngle;
        double x = mA * Math.cos(Math.toRadians(arcStart));
        double y = mB * Math.sin(Math.toRadians(arcStart));
        mRotateThumbX = (int) x + mWidth / 2;
        mRotateThumbY = (int) y + mHeight / 2;
        if (DEBUG_TOUCH) {
            Log.d(TAG, "calculateRotateThumbXY mRotateThumbX: " + mRotateThumbX
                    + ", mRotateThumbY: " + mRotateThumbY);
        }

        transferThumbPaintAlpha(mThumbPaint.getAlpha() - mThumbPaintAlphaStep);
    }

動畫結(jié)束后咧擂,執(zhí)行restoreAfterRotate逞盆,將旋轉(zhuǎn)變量值還原,并將進度設(shè)置為0松申,時長設(shè)置為0云芦。否則,由于上一次遺留的進度贸桶,會使得旋轉(zhuǎn)完畢以后舅逸,立即產(chǎn)生一個不合適的藍色進度線。
這樣皇筛,動畫每更新一次琉历,我們就計算一次,旋轉(zhuǎn)弧線的值就確定了,調(diào)用drawRotating畫一下就可以了旗笔。

處理拖拽

相比旋轉(zhuǎn)的處理彪置,拖拽較為復(fù)雜一些,主要是我們要根據(jù)ViewPager或者RecyclerView的變化而變化蝇恶,先不考慮和這些控件聯(lián)動的問題拳魁,我們假設(shè)橢圓向左或者向右拖拽了一個角度,應(yīng)該怎么處理呢撮弧,其實和旋轉(zhuǎn)過程中的一個片段是一樣的潘懊,對比代碼也非常相似,只是旋轉(zhuǎn)的時候修改的是mRotate開頭的弧線變量贿衍,這里修改的是mDrag開頭的弧線變量授舟。在拖拽過程中修改旋轉(zhuǎn)弧線起始角度的原因是,在拖拽完成以后需要旋轉(zhuǎn)時贸辈,直接接著拖拽的角度旋轉(zhuǎn)岂却,而不是跑到初始位置旋轉(zhuǎn)。由于拖拽的過程中裙椭,文字是跟著動的躏哩,所以比旋轉(zhuǎn)多了一個計算拖拽的文字位置的調(diào)用。

我們在一開始就用靜止?fàn)顟B(tài)下的弧線起始角度和掃過角度初始化拖拽弧線相應(yīng)角度:

    private void setDragInitialData() {
        mDragFrontArc = new ArcData(mFrontArc.startAngle, mFrontArc.sweepAngle);
        mDragLeftArc = new ArcData(mLeftArc.startAngle, mLeftArc.sweepAngle);
        mDragBackArc = new ArcData(mBackArc.startAngle, mBackArc.sweepAngle);
        mDragRightArc = new ArcData(mRightArc.startAngle, mRightArc.sweepAngle);
        mDragProgressArc = new ArcData(mEndAngle, (int) mProgressAngle);
        mDragAngle = 0;
        mDragDurationStartX = mFrontEndX;
        mDragDurationStartY = mFrontEndY;
    }
    private void calculateDragThumbXY() {
        mDragProgressArc = new ArcData(mDragProgressArc.startAngle, mDragProgressArc.sweepAngle);
        float arcStart = mDragProgressArc.startAngle - mDragProgressArc.sweepAngle;
        double x = mA * Math.cos(Math.toRadians(arcStart));
        double y = mB * Math.sin(Math.toRadians(arcStart));
        mDragThumbX = (int) x + mWidth / 2;
        mDragThumbY = (int) y + mHeight / 2;
        mRotateThumbX = mDragThumbX;
        mRotateThumbY = mDragThumbY;

        if (DEBUG_PROCESS) {
            Log.d(TAG, "calculateDragThumbXY mDragThumbX: " + mDragThumbX
                    + ", mDragThumbY: " + mDragThumbY);
        }
    }

計算完成以后invalidate一下揉燃,就會按照新的結(jié)果進行繪制扫尺,為了簡化繪制的處理情況,我們定義了三種繪制方法炊汤,分別是idle正驻,drag, rotate抢腐,對應(yīng)靜止姑曙,拖拽和旋轉(zhuǎn)的繪制,各種繪制互不干擾:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (bIsDragging) { // 拖拽
            drawDragging(canvas);
        } else if (bIsRotating) { // 旋轉(zhuǎn)
            drawRotating(canvas);
        } else { // 靜止
            drawIdle(canvas);
        }
    }

這樣拖拽效果的處理就差不多了迈倍,接下來就是把拖拽和ViewPager或者recyclerview結(jié)合起來伤靠。為了簡化代碼結(jié)構(gòu),我們分兩個類各自繼承當(dāng)前類, 分別配合ViewPager或者RecyclerView使用啼染,這樣做的原因是:卡片之前是用ViewPager實現(xiàn)的宴合,后來改成了RecyclerView了,提供兩個實現(xiàn)迹鹅,方便卡片選擇合適的控件:

public class ViewPagerOvalSeekBar extends OvalSeekBar
public class RecyclerViewOvalSeekBar extends OvalSeekBar

這里用RecyclerViewOvalSeekBar說明一下聯(lián)動的過程卦洽。我們通過getRecyclerViewListener返回一個RecyclerView.OnScrollListener對象,把這個對象通過RecyclerView.addOnScrollListener添加給控件斜棚,我們就能獲取到RecyclerView的滑動事件了阀蒂,在收到onScrollStateChanged的SCROLL_STATE_DRAGGING的事件中该窗,我們標(biāo)記為開始拖拽,然后在onScrolled中調(diào)用dragging(dx):

    @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            if (DEBUG_TOUCH) {
                Log.d(TAG, "onScrolled dx: " + dx + ", dy: " + dy);
            }
            if (!bIsRotating) {
                if (bIsDragging) {
                    dragging(dx);
                }
            }

        }

這里如何將拖拽的距離換算成橢圓旋轉(zhuǎn)的弧度了蚤霞,正確的做法應(yīng)該是尋找dx的變化規(guī)律挪捕,然后通過三角函數(shù)計算,但這樣比較復(fù)雜争便。我們假設(shè)偏移角度在某一個數(shù)值左右波動,所以這里沒有計算断医,用2試了一下滞乙,感覺表現(xiàn)得還好。所以就這樣做了:

    /**
     * 橫向拖拽
     *
     * @param dx
     */
    public void dragging(int dx) {
        if (DEBUG_TOUCH) {
            Log.d(TAG, "--drag-- dragging dx: " + dx);
        }
        boolean left;
        if (dx > 0) {
            left = true;
        } else {
            left = false;
        }
        dragInner(left, 2);
    }
}

我們還可以根據(jù)拖拽的角度換算出一個alpha鉴嗤,把它用在拖拽時前端弧線和進度球上斩启,使它們在拖拽時有漸變的效果:

        float alpha = Math.abs((mDragFrontArc.startAngle - mStartAngle)) / mRotateAngle;
        if (alpha > 1f) {
            alpha = 1f;
        }

拖拽時,進度弧線漸變區(qū)域也會發(fā)生變化醉锅,所以需要重新創(chuàng)建, 無論向左還是向右兔簇,可以用mDragProgressArc.startAngle計算出漸變開始的坐標(biāo),用mDragProgressArc.startAngle - mDragProgressArc.sweepAngle計算出漸變結(jié)束的坐標(biāo)硬耍,我們順便還給它做了個debug的參考線

        if (mProgressAngle > 0) {
            int[] colors = new int[2];
            float da = mDragProgressArc.startAngle;
            double dx = mA * Math.cos(Math.toRadians(da));
            double dy = mB * Math.sin(Math.toRadians(da));
            float dsx = (int) dx + mWidth / 2;
            float dsy = (int) dy + mHeight / 2;
            da = mDragProgressArc.startAngle - mDragProgressArc.sweepAngle;
            dx = mA * Math.cos(Math.toRadians(da));
            dy = mB * Math.sin(Math.toRadians(da));
            float dex = (int) dx + mWidth / 2;
            float dey = (int) dy + mHeight / 2;
            mDragAlpha = (float) Math.max(1 - alpha, 0.4); //避免變成0
            colors[0] = adjustAlpha(mProgressGradientColors[0], mDragAlpha);
            colors[1] = adjustAlpha(mProgressGradientColors[1], mDragAlpha);
            mDragProgressGradient = new LinearGradient((int) dsx, (int) dsy, dex, dey,
                    colors, null, Shader.TileMode.CLAMP);
            if (DEBUG_REFER_LINE) {
                debugDragProcessStartX = dsx;
                debugDragProcessStartY = dsy;
                debugDragProcessEndX = dex;
                debugDragProcessEndY = dey;
                Log.d(TAG, "dragInner drag process mDragProgressArc.startAngle:" +
                        mDragProgressArc.startAngle + ", mDragProgressArc.sweepAngle: " +
                        mDragProgressArc.sweepAngle + ", mDragProgressArc.endAngle: " +
                        (mDragProgressArc.startAngle - mDragProgressArc.sweepAngle));
                Log.d(TAG, "dragInner process debugDragProcessStartX: " + debugDragProcessStartX
                        + ", debugDragProcessStartY: " + debugDragProcessStartY + ", debugDragProcessEndX: "
                        + debugDragProcessEndX + ", debugDragProcessEndY: " + debugDragProcessEndY
                        + " alpha: " + alpha);
            }
            if (DEBUG_PROCESS) {
                Log.d(TAG, "mDragProgressGradient dsx: " + dsx + ", dsy: " + dsy + ", dex: "
                        + dex + ", dey: " + dey);
            }
        }

如何使用:

這是一個繼承View的自定義控件垄琐,用法和普通View一樣,但由于沒有提供設(shè)置自定義屬性的set方法经柴,所以只能在布局文件里面使用狸窘,并指定那些屬性:

        <com.kaolafm.widget.RecyclerViewOvalSeekBar
            android:id="@+id/oval_progress_bar"
            android:layout_width="@dimen/x840"
            android:layout_height="@dimen/y200"
            android:layout_above="@+id/player_audio_simple_info_layout"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="@dimen/y30"
            android:paddingLeft="10dip"
            android:paddingRight="10dip"
            android:paddingTop="20dip"
            android:visibility="invisible"
            kaola:oval_arc_gap_angle="6"
            kaola:oval_duration_text_size="@dimen/m20"
            kaola:oval_end_angle="135"
            kaola:oval_line_color="#AA3F4A6B"
            kaola:oval_progress_end_color="#FF06BDDB"
            kaola:oval_progress_start_color="#FF4C5BFE"
            kaola:oval_progress_thumb_color="#FF06BDDB"
            kaola:oval_progress_thumb_radius="@dimen/x6"
            kaola:oval_rotate_angle="90"
            kaola:oval_rotate_duration="600"
            kaola:oval_start_angle="45"
            kaola:oval_stroke_width="@dimen/x4" />

讓它根據(jù)翻頁旋轉(zhuǎn):

        int offset = realItem - position;
        boolean rotatingClockwise = offset > 0 ? false : true;
        mSeekBar.rotate(rotatingClockwise);

代碼下載:

github:
OvalSeekBar

結(jié)語

以上是這個控件的總體實現(xiàn)過程,還有一些點沒有提到坯认,例如旋轉(zhuǎn)完成以后翻擒,進度球漸變出現(xiàn)的實現(xiàn),進度變化的監(jiān)聽牛哺,播放時長的格式轉(zhuǎn)換陋气,Log輸出的控制,參考線的繪制細節(jié)等引润,但這些都不難理解巩趁。當(dāng)然還有很多細節(jié)需要提高,比如拖拽和旋轉(zhuǎn)過程中的漸變效果淳附,使用自己的估值器(TypeEvaluator)使得動畫的緩動更加明顯(這個需要找一個數(shù)學(xué)公式晶渠,比較難),還可以考慮適配更多的卡片控件等燃观。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末褒脯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子缆毁,更是在濱河造成了極大的恐慌番川,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異颁督,居然都是意外死亡践啄,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門沉御,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屿讽,“玉大人,你說我怎么就攤上這事吠裆》ヌ福” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵试疙,是天一觀的道長诵棵。 經(jīng)常有香客問我,道長祝旷,這世上最難降的妖魔是什么履澳? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮怀跛,結(jié)果婚禮上距贷,老公的妹妹穿的比我還像新娘。我一直安慰自己吻谋,他們只是感情好储耐,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著滨溉,像睡著了一般什湘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晦攒,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天闽撤,我揣著相機與錄音,去河邊找鬼脯颜。 笑死哟旗,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的栋操。 我是一名探鬼主播闸餐,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼矾芙!你這毒婦竟也來了舍沙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤剔宪,失蹤者是張志新(化名)和其女友劉穎拂铡,沒想到半個月后壹无,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡感帅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年斗锭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片失球。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡岖是,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出实苞,到底是詐尸還是另有隱情豺撑,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布硬梁,位于F島的核電站,受9級特大地震影響胞得,放射性物質(zhì)發(fā)生泄漏荧止。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一阶剑、第九天 我趴在偏房一處隱蔽的房頂上張望跃巡。 院中可真熱鬧,春花似錦牧愁、人聲如沸素邪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兔朦。三九已至猿涨,卻和暖如春波材,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驻仅。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工乏奥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留摆舟,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓邓了,卻偏偏與公主長得像恨诱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子骗炉,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 標(biāo)題說漸變進度條是為了方便理解照宝,這里本身的項目背景是一款表盤的分針。先上圖: 周圈藍色的漸變條(分針)就是本次要實...
    0晨鶴0閱讀 11,223評論 6 13
  • @(HTML5)[canvas與SVG] [TOC] 十一 句葵、SVG HTML體系中硫豆,最常用的繪制矢量圖的技術(shù)是S...
    踏浪free閱讀 4,574評論 0 2
  • 中國足球小將龙巨,實際上是一個既團結(jié)、又分散的存在熊响,很大程度上是通過人情來聯(lián)絡(luò)旨别。這個團隊最大的特色之處,在于他們彼此沒...
    黃瀾柒閱讀 914評論 0 0
  • 很久沒有自己半夜打開電腦敲點什么了汗茄,懷念秸弛。 開心的一件事情---三天前表姐和大石同一天生娃兒了,雖然不是自己生娃兒...
    小寒姐閱讀 307評論 0 0
  • 聊天聚友聽音樂,上半場涮起銅鍋瞳腌,下半場喝起小酒绞铃。完美合拍,你的時間你做主嫂侍,怎么舒服怎么來儿捧。 這個地兒很難定義:說是...
    咪咕江閱讀 244評論 0 1