Android 自定义View之烧瓶loading动画

作者: Samlss | 来源:发表于2018-10-09 19:41 被阅读6次

    我们首先看下效果

    FlaskView FlaskView

    画瓶子

    首先,创建一个自定义view,我们知道,在view的大小发生改变后,会回调接口

    /**
    * This is called during layout when the size of this view has changed. If
    * you were just added to the view hierarchy, you're called with the old
    * values of 0.
    *
    * @param w Current width of this view.
    * @param h Current height of this view.
    * @param oldw Old width of this view.
    * @param oldh Old height of this view.
    */
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    
    }
    

    因此,我们可以在该方法里面,取得view的宽高后进行瓶子的初始化,大概的思路是:

    • 计算瓶底圆半径大小
    • 计算瓶颈高度大小
    • 计算瓶盖高度大小
    • path添加瓶底圆轨迹
    • path添加瓶颈轨迹
    • path添加瓶盖轨迹

    在Android中,默认的0°为数学上圆的90°,这里不明白的请百度

    关于瓶底扇形圆弧,这里测试得出取-70° 到 250°,即瓶底圆和屏颈相交的两个点,为比较美观的,因此这里取了这个角度。

    关于path.addArc,首先这里的参数代表

    • oval 圆弧形状边界,可以当做是一个矩形边界
    • startAngle 起始角度
    • sweepAngle 旋转角度(注意,这里不是最终的角度,而是要旋转的角度)
    /**
    * Add the specified arc to the path as a new contour.
    *
    * @param oval The bounds of oval defining the shape and size of the arc
    * @param startAngle Starting angle (in degrees) where the arc begins
    * @param sweepAngle Sweep angle (in degrees) measured clockwise
    */
    public void addArc(RectF oval, float startAngle, float sweepAngle) {
        addArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle);
    }
    

    详细可查看以下代码注释

    //获取view的中心点
    float centerX = w / 2;
    float centerY = h / 2;
    
    //瓶底圆半径为view的宽度的1/5
    float flaskBottomCircleRadius = w / 5f;
    
    //瓶颈高度为半径的2/3
    float neckHeight = flaskBottomCircleRadius * 2f / 3;
    
    //瓶盖高度为瓶颈高度的3/10
    float headHeight = 0.3f * neckHeight;
    
    //重置path
    mFlaskPath.reset();
    
    //计算瓶子在view中的中心点y坐标
    float flaskCenterY = centerY + (neckHeight + headHeight) / 2;
    
    //**********************************************************瓶底部分******************************************************
    //瓶底和瓶颈的左边和右边的相交的两个点的坐标
    float[] leftEndPos = new float[2];
    float[] rightEndPos = new float[2];
    
    //瓶底圆底部点的坐标
    float[] bottomPos = new float[2];
    
    //计算三个点的坐标
    leftEndPos[0] = (float) (flaskBottomCircleRadius * Math.cos(250 * Math.PI / 180f) + centerX);
    leftEndPos[1] = (float) (flaskBottomCircleRadius * Math.sin(250 * Math.PI / 180f) + flaskCenterY);
    
    rightEndPos[0] = (float) (flaskBottomCircleRadius * Math.cos(-70 * Math.PI / 180f) + centerX);
    rightEndPos[1] = (float) (flaskBottomCircleRadius * Math.sin(-70 * Math.PI / 180f) + flaskCenterY);
    
    bottomPos[0] = (float) (flaskBottomCircleRadius * Math.cos(90 * Math.PI / 180f) + centerX);
    bottomPos[1] = (float) (flaskBottomCircleRadius * Math.sin(90 * Math.PI / 180f) + flaskCenterY);
    
    //计算出圆弧所在的区域
    RectF flaskArcRect = new RectF(centerX - flaskBottomCircleRadius, flaskCenterY - flaskBottomCircleRadius,
    centerX + flaskBottomCircleRadius, flaskCenterY + flaskBottomCircleRadius);
    
    //添加底部圆弧轨迹
    mFlaskPath.addArc(flaskArcRect, -70, 320);
    //***********************************************************************************************************************
    
    //首先将path移至左边相交点
    mFlaskPath.moveTo(leftEndPos[0], leftEndPos[1]);
    
    //添加左边的瓶颈线
    mFlaskPath.lineTo(leftEndPos[0], leftEndPos[1] - neckHeight);
    
    //通过贝塞尔曲线添加左边瓶盖轨迹
    mFlaskPath.quadTo(leftEndPos[0] - flaskBottomCircleRadius / 8, leftEndPos[1] - neckHeight - headHeight / 2,
        leftEndPos[0], leftEndPos[1] - neckHeight - headHeight);
    
    //移动至右边瓶盖定点
    mFlaskPath.lineTo(rightEndPos[0],rightEndPos[1] - neckHeight - headHeight);
    
    //通过贝塞尔曲线添加右边瓶盖轨迹
    mFlaskPath.quadTo(rightEndPos[0] + flaskBottomCircleRadius / 8, rightEndPos[1] - neckHeight - headHeight / 2,
        rightEndPos[0], rightEndPos[1] - neckHeight);
    
    //添加右边的瓶颈线
    mFlaskPath.lineTo(rightEndPos[0], rightEndPos[1]);
    

    View的onDraw中描绘瓶子

    canvas.drawPath(mFlaskPath, mStrokePaint);
    

    画水位

    根据以上代码,我们已经计算获得了整个瓶子的path,那么我们如何去计算和画水位呢?

    • 计算瓶子path所占的区域
    • 对整个瓶子的path进行canvas裁剪

    我们可以通过path.computeBounds()计算出瓶子所占的整个区域

    mFlaskPath.computeBounds(mFlaskBoundRect, false);
    mFlaskBoundRect.bottom -= (mFlaskBoundRect.bottom - bottomPos[1]);
    

    但是我们这里为什么还要减去一个差值呢?

    这是因为,path.addArc()后,如果圆被截断即addArc的并不是一个完整的圆(我们这里瓶底就是一个弧度圆,瓶底与瓶颈之间的交点使瓶底圆截断),会导致path.computeBounds()计算出来的区域多出来一定的空间,这里贴两张示例图:

    以下为不减去该差值的效果:


    FlaskView

    以下为减去该差值的效果:


    FlaskView

    计算出瓶子的区域后,我们就可以获取水位的区域了

    mWaterRect.set(mFlaskBoundRect.left, mFlaskBoundRect.bottom - mFlaskBoundRect.height() * mWaterHeightPercent,mFlaskBoundRect.right, mFlaskBoundRect.bottom);
    

    利用canvas的裁剪功能,进行水位的绘制

    //裁剪整个瓶子的画布
    canvas.clipPath(mFlaskPath);
    
    //画水位
    canvas.drawRect(mWaterRect, mWaterPaint);
    

    画水泡

    水泡生成和描绘的思路

    • 根据水位区域,在水位底部,随机产生水泡
    • 产生水泡后,将该水泡记录下来,并且根据一个speed进行位移
    • 当水泡离开水位区域,将其在记录中移除
    private void createBubble() {
        //若水泡数量达到上限或者水位区域为空的时候,不产生水泡
        if (mBubbles.size() >= mBubbleMaxNumber
            || mWaterRect.isEmpty()) {
            return;
        }
        
        //根据时间间隔,判断是否已到达水泡产生的时间
        long current = System.currentTimeMillis();
        if ((current - mBubbleCreationTime) < mBubbleCreationInterval){
            return;
        }
        
        mBubbleCreationTime = current;
        
        //以下代码为随机计算水泡坐标 + 半径+ 速度
        Bubble bubble = obtainBubble();
        int radius = mBubbleMinRadius + mOnlyRandom.nextInt(mBubbleMaxRadius - mBubbleMinRadius);
        
        bubble.radius = radius;
        bubble.speed = mBubbleMinSpeed + mOnlyRandom.nextFloat() * mBubbleMaxSpeed;
        
        bubble.x = mWaterRect.left + mOnlyRandom.nextInt((int) mWaterRect.width()); //random x coordinate
        bubble.y = mWaterRect.bottom - radius - mStrokeWidth / 2; //the fixed y coordinate
        
        mBubbles.add(bubble);
    }
    

    利用canvas的裁剪功能,进行水泡的绘制

    //裁剪水位画布
    canvas.clipRect(mWaterRect);
    
    //描绘水泡
    drawBubbles(canvas);
    

    优化

    我们知道,在进行频繁的创建水泡的时候,如果每次都创建新对象的话, 可能会增加不必要的内存使用,而且很容易引起频繁的gc,甚至是内存抖动。

    因此这里我增加了一个回收功能

    //首先判断栈中是否存在回收的对象,若存在,则直接复用,若不存在,则创建一个新的对象
    private Bubble obtainBubble(){
        if (mRecycler.isEmpty()){
             return new Bubble();
        }
        
        return mRecycler.pop();
    }
    
    //回收到一个栈里面,若这个栈数量超过最大可显示数量,则pop
    private void recycle(Bubble bubble){
        if (bubble == null){
            return;
        }
        
        if (mRecycler.size() >= mBubbleMaxNumber){
            mRecycler.pop();
        }
        
        mRecycler.push(bubble);
    }
    

    Github

    相关文章

      网友评论

      本文标题:Android 自定义View之烧瓶loading动画

      本文链接:https://www.haomeiwen.com/subject/bgvyaftx.html