椭圆进度条的实现过程

简介

模仿这个gif中SeekBar的功能和效果

由于这个控件的实现耗费了不少时间,也遇到了一些数学,渐变相关的问题,过程较为复杂,所以我在这里整理了一下流程,希望能方便大家了解这个控件的功能和使用。

文中几个容易混淆的概念

  • 角度和弧度:我们通常用角度degree描述,但Math中的三角函数用弧度radians计算。
    • 角度转弧度:Math.toRadians --> angdeg / 180.0 * PI
    • 弧度转角度:Math.toDegrees --> angrad * 180.0 / PI
  • 椭圆和弧线:Canvas中画弧是按照矩形内接椭圆画出来的。
  • 前后左右四段弧线,进度弧线是蓝色弧线。

实现思路

  • 首先,在Canvas上画四段弧线,这四段弧线在一个椭圆上。
  • 每一条弧线有一个起始角度startAngle,和一个扫过角度sweepAngle。
  • 有进度变化,在前端的弧线上画一段蓝色的弧线。
  • 蓝色弧线的长度也就是角度,根据当前进度和前端弧线的角度计算出来。
  • 拖拽时,根据拖拽的角度,修改这四条弧线的起始角度。
  • 旋转时,以旋转时长为动画时间,旋转角度以动画距离,使用插值器,计算出每次步进的角度,和拖拽一样,修改起始角度。

自定义View基础:

自定义的属性存放在attrs.xml中,主要是椭圆线条颜色,粗细,控制球半径和颜色,进度颜色,进度渐变起止颜色,旋转动画时长,具体参考注释。

    
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    

在view的构造方法中读取这些属性,保存在我们view的成员变量中。

初始化各种画笔,这些画笔包括:

  • 前端(mPaint)
  • 左侧(mLeftPaint)
  • 后侧(mBackPaint)
  • 右侧(mRightPaint)
  • 进度(mProgressPaint)
  • 进度球(mThumbPaint)
  • 开始时长文字(mDurationStartPaint)
  • 调试(mDebugPaint)

画笔初始化值得一提的是,给画笔添加了LightingColorFilter, 如果不加这个,则画出来的线显得灰暗,像是陷下去了,加了这个就会有发光效果,显得饱满有光泽。

在onMeasure中,确定一些关键变量:

  • view的宽高和它的padding (mWidth, mHeight)
  • 根据view的宽高和padding计算出椭圆的宽高(mOvalRectWith, mOvalRectHeight)
  • 进而计算出椭圆的区域(mOvalRectLeft, mOvalRectTop, mOvalRectRight, mOvalRectBottom),对应矩形变量(mOvalRect)
  • 计算出椭圆的长半径和短半径(mA, mB). 椭圆方程:
    x^2 / a^2 + y^2 / b^2 = 1 (a > b > 0)
  • 计算出控制球拖拽区域,因为用户不会沿着弧线拖,还是会沿着直线拖,我们设定一个矩形的区域,只有在这个区域的触摸事件当成seek,计算这个区域的代码是:
            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;

弧线角度相关计算

计算4条弧线的开始角度和扫过角度,对应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)我们定义了一个非常简单的内部类封装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)以及旋转时的弧线信息(mDragFrontArc, mDragLeftArc, mDragBackArc, mDragRightArc)
这里的计算用到另外一个椭圆方程,主要是根据角度计算椭圆上具体一点的坐标,也就是椭圆参数方程。方程是:

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

a是椭圆长轴,b是椭圆短轴,θ计算比较特殊,但我们这里把角度转成弧度以后,用Math的三角函数就可以了]例如计算前端弧线的起始角度和扫过角度, 起始角度是布局文件中指定的角度加上缝隙角度的二分之一,扫过角度是结束角度减去缝隙的二分之一再减去起始角度:

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

然后计算前端这段弧线起始位置和结束位置的坐标,起始位置在第四象限,结束位置在第三象限。x, y 是按照椭圆圆心为原点计算出来的位置,View的原点在左上顶点。如果对canvas做平移,也就是Canvas.translate,这个坐标可以直接使用,但这里并没有做,所以要用这个坐标和View的宽高进行换算,得到它在View中的坐标。因为椭圆的圆心也是view的中心,所以这个点在view中的坐标是:横向坐标是宽度的一半减去或者加上x,纵向坐标是高度的一半减去或者加上y,借助正弦/余弦三角函数的周期性,可以写成下面的样子。

  • 起始位置坐标:
    double x = mA * Math.cos(Math.toRadians(mStartAngle));
        double y = mB * Math.sin(Math.toRadians(mStartAngle));
        mFrontStartX =  x + mWidth / 2;
        mFrontStartY =  y + mHeight / 2;
  • 结束位置坐标:
    x = mA * Math.cos(Math.toRadians(mEndAngle));
        y = mB * Math.sin(Math.toRadians(mEndAngle));
        mFrontEndX = x + mWidth / 2;
        mFrontEndY =  y + mHeight / 2;

进度对应的角度可以根据前端弧线对应的总时长和前端弧线的总度数计算出来, 例如前端弧线是80度,时长是4分钟,现在进度是2分钟,则进度对应的角度是 80 / 4 * 2 = 40度,当然这里是按毫秒计算的

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

因为进度球是从第三象限向第四象限移动,也就是从左往右,所以用结束角度减去进度角度。同理,进度球的位置:

    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;

一个较为繁琐的计算是渐变的起止位置,用左侧弧线的渐变举例说明,因为左侧弧线开头的部分不透明度高,结尾的部分不透明度低,渐变开始的坐标按它的开始角度算出来,结束坐标按它的开始角度和扫过角度求和计算出来。右侧渐变和左侧渐变略有不同。下面是左侧渐变计算的详细过程:

        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);

下面介绍静止状态下进度球位置计算, 我给这个计算写了详细的注释,理解这个计算就理解了我们这个进度条的计算过程,因为进度的变化会导致进度弧线蓝色渐变的变化,所以还增加了一点渐变的计算。可以打开对应的debug开关,在进度变化的时候关注log的输出,找到mThumbX和mThumbY的变化规律:

    /*
     * 计算进度球的x, y坐标,同时根据进度的长度设计进度渐变的宽度.
     *
     */
    private void calculateThumbPoint(int progress) {
        if (progress == 0 || mPlayDuration == 0 || mEndAngle - mStartAngle == 0) {
            mProgressAngle = 0;
        } else {
            mProgressAngle = (float) progress / mPlayDuration * (mEndAngle - mStartAngle);
        }
        //修改旋转弧线的sweep角度,这样拖拽开始时,控制球的初始位置和进度条上的位置一致,
        mRotateProgressArc.sweepAngle = (int) mProgressAngle;
        //修改拖拽弧线的sweep角度,这样拖拽开始时,控制球的初始位置和进度条上的位置一致,
        mDragProgressArc.sweepAngle = (int) mProgressAngle;
        //椭圆标准方程, x = a * cos(弧度), y = b * sin(弧度),首先求角度,然后将角度转弧度
        //角度求法:以3点钟方向为x正向坐标,9点钟方向为180度,由于进度是从结束角度mEndAngle开始,
        //所以球的位置与x轴的初始夹角是mEndAngle - mProgressAngle,
        // 简单来看是180 - mEndAngle + mProgressAngle,但由于余弦函数的周期性,我们可以直接写成
        //mEndAngle - mProgressAngle, 如果有进度角,这个夹角会变大进度角度数
        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; //相当于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);
        }
    }

静止状态下的绘制就已经差不多了,我们有画笔,有椭圆矩形,有角度,文字根据进度和时长计算一下,文字的位置就是前端弧线的开始和结束位置,进度球位置也根据进度计算出来了,半径通过读取自定义属性初始化了。绘制在drawIdle方法中进行,当然,如果想看参考线,可以打开debug开关和DEBUG_REFER_LINE开关。

处理进度Seek

由于是SeekBar,那么就要能seek,所以需要重写onTouchEvent,在onTouchEvent这个方法中,首先在down事件中判断触摸是不是落在进度球范围之内,如果不在,说明用户没有点进度球,所以不想seek,我们return false,不再接收后面的事件。如果在,我们标记一下,准备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事件中,如果事件发生在我们前面提到的Seek 矩形区域,认为是用户在做Seek操作,否则,说明用户已经离开了seek区域, 如果在做Seek操作我们就根据拖拽的距离更新进度:

            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;
                }

这里根据距离计算进度的实现比较粗糙,但它能正常工作,根据事件的横坐标,减去进度球的横坐标,得到一个偏移量,然后根据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;
    }

处理旋转

相比拖拽,旋转容易一些,所以先处理,理解旋转的逻辑以后,拖拽就容易多了,因为拖拽可以理解成可控制的旋转,并且拖拽引起的角度变化不会大于旋转的角度。我们在onMeasure的时候,用静止的角度变量初始化了旋转弧线的初值:

    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);
    }

旋转通过public void rotate(boolean clockwise) 方法触发,boolean参数表示是否是顺时针旋转。一般切换下一页,顺时针转一次,反之逆时针转一次。在旋转之前,我们修改一下进度球的旋转初始位置,否则就会出现跳动的问题。如果不是拖拽引起的旋转,则正常进度确定的位置就是旋转开始的初始位置,然后我们开始旋转动画:

    public void rotate(boolean clockwise) {
        if (bIsRotating) {
            //如果正在旋转,则忽略新的旋转调用
            return;
        }
        if (bIsDragging) {
            mRotateThumbX = mDragThumbX;
            mRotateThumbY = mDragThumbY;
        } else {
            mRotateThumbX = mThumbX;
            mRotateThumbY = mThumbY;
        }
        startRotateAnimation(clockwise);
    }

这个动画实际上不是一个动画,而是反复的invalidate产生的动画效果。为了产生和动画相同的效果,我们创建了一个ValueAnimator,并且给他一个减速插值器DecelerateInterpolator,并创建一个ValueAnimator.AnimatorUpdateListener监听动画事件,在onAnimationUpdate中绘制新弧线。
在旋转动画完成后,重置旋转弧线的变量值:

            @Override
            public void onAnimationEnd(Animator animation) {
                //旋转结束时将状态还原
                restoreAfterRotate();
            }           

在onAnimationUpdate中,我们拿到动画的值,然后和前值(mAnimatorPreValue)比较,得到偏移量,本次拿到的值变成前值.然后根据偏移量旋转,旋转代码非常简单,就是不停地增加或者减小(mRotateFrontArc, mRotateLeftArc, mRotateBackArc, mRotateRightArc)这几个旋转弧线的开始角度,再计算一下进度球的旋转过程中的坐标,然后invalidate一下view就可以了,startAngle的变化不用说了,简单介绍一下旋转过程中进度球坐标的计算。首先是确定角度,旋转过程中改变的只是起始角度,扫过角度是保持不变的,所以用起始角度减去扫过角度,代入椭圆标准方程的三角函数就可以了:

    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);
    }

动画结束后,执行restoreAfterRotate,将旋转变量值还原,并将进度设置为0,时长设置为0。否则,由于上一次遗留的进度,会使得旋转完毕以后,立即产生一个不合适的蓝色进度线。
这样,动画每更新一次,我们就计算一次,旋转弧线的值就确定了,调用drawRotating画一下就可以了。

处理拖拽

相比旋转的处理,拖拽较为复杂一些,主要是我们要根据ViewPager或者RecyclerView的变化而变化,先不考虑和这些控件联动的问题,我们假设椭圆向左或者向右拖拽了一个角度,应该怎么处理呢,其实和旋转过程中的一个片段是一样的,对比代码也非常相似,只是旋转的时候修改的是mRotate开头的弧线变量,这里修改的是mDrag开头的弧线变量。在拖拽过程中修改旋转弧线起始角度的原因是,在拖拽完成以后需要旋转时,直接接着拖拽的角度旋转,而不是跑到初始位置旋转。由于拖拽的过程中,文字是跟着动的,所以比旋转多了一个计算拖拽的文字位置的调用。

我们在一开始就用静止状态下的弧线起始角度和扫过角度初始化拖拽弧线相应角度:

    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一下,就会按照新的结果进行绘制,为了简化绘制的处理情况,我们定义了三种绘制方法,分别是idle,drag, rotate,对应静止,拖拽和旋转的绘制,各种绘制互不干扰:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (bIsDragging) { // 拖拽
            drawDragging(canvas);
        } else if (bIsRotating) { // 旋转
            drawRotating(canvas);
        } else { // 静止
            drawIdle(canvas);
        }
    }

这样拖拽效果的处理就差不多了,接下来就是把拖拽和ViewPager或者recyclerview结合起来。为了简化代码结构,我们分两个类各自继承当前类, 分别配合ViewPager或者RecyclerView使用,这样做的原因是:卡片之前是用ViewPager实现的,后来改成了RecyclerView了,提供两个实现,方便卡片选择合适的控件:

public class ViewPagerOvalSeekBar extends OvalSeekBar
public class RecyclerViewOvalSeekBar extends OvalSeekBar

这里用RecyclerViewOvalSeekBar说明一下联动的过程。我们通过getRecyclerViewListener返回一个RecyclerView.OnScrollListener对象,把这个对象通过RecyclerView.addOnScrollListener添加给控件,我们就能获取到RecyclerView的滑动事件了,在收到onScrollStateChanged的SCROLL_STATE_DRAGGING的事件中,我们标记为开始拖拽,然后在onScrolled中调用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);
                }
            }

        }

这里如何将拖拽的距离换算成椭圆旋转的弧度了,正确的做法应该是寻找dx的变化规律,然后通过三角函数计算,但这样比较复杂。我们假设偏移角度在某一个数值左右波动,所以这里没有计算,用2试了一下,感觉表现得还好。所以就这样做了:

    /**
     * 横向拖拽
     *
     * @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);
    }
}

我们还可以根据拖拽的角度换算出一个alpha,把它用在拖拽时前端弧线和进度球上,使它们在拖拽时有渐变的效果:

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

拖拽时,进度弧线渐变区域也会发生变化,所以需要重新创建, 无论向左还是向右,可以用mDragProgressArc.startAngle计算出渐变开始的坐标,用mDragProgressArc.startAngle - mDragProgressArc.sweepAngle计算出渐变结束的坐标,我们顺便还给它做了个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一样,但由于没有提供设置自定义属性的set方法,所以只能在布局文件里面使用,并指定那些属性:

        

让它根据翻页旋转:

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

代码下载:

github:
OvalSeekBar

结语

以上是这个控件的总体实现过程,还有一些点没有提到,例如旋转完成以后,进度球渐变出现的实现,进度变化的监听,播放时长的格式转换,Log输出的控制,参考线的绘制细节等,但这些都不难理解。当然还有很多细节需要提高,比如拖拽和旋转过程中的渐变效果,使用自己的估值器(TypeEvaluator)使得动画的缓动更加明显(这个需要找一个数学公式,比较难),还可以考虑适配更多的卡片控件等。

你可能感兴趣的:(椭圆进度条的实现过程)