贝塞尔曲线(Bezier)之水波纹的手机充电动画效果(二)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/smile_Running/article/details/98996364

博主声明:

转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。

本文首发于此   博主威威喵  |  博客主页https://blog.csdn.net/smile_running

    对于博主上一次写的一个水波纹的充电动画效果,我个人还并不是特别满意,也就是写着玩的。这次想认真的写一个,能够用到实际的项目中去,并且可以很方便的使用,像 TextView 一样简单,定制性高一些。本来呢,想着在充电时,添加一个泡泡一直往上冒的效果,无奈啊,写了好一会儿,效果不太理想。我考虑一下,这次就不加入冒泡的效果了,下次在换另一种方式,实现一下冒泡的效果。

    对于这次的效果,会比上一篇水波纹充电动画好那么一丢丢吧,看看上篇的效果,点击:贝塞尔曲线(Bezier)之水波纹的手机充电动画效果(一)

    上一次没有理解那个 path 绘制三次贝塞尔曲线的方法,动画不太行。这次,我发现用二次贝塞尔也可以绘制出三角函数的图像来,是我学的太好了,见识短浅。来吧,先看几波效果图再说:

    这个是有电池盖子的,就是上面突了一个方形,然后是水波纹的效果:

 你可以选择不要上面突出来的那部分

 你也可以选择关闭水波纹的动画效果,也可以关闭电量进度的百分比值文本,我想没人愿意这么干吧,不就是为了炫酷嘛

你可以任意的切换电池外圈的颜色,充电进度的颜色,上面突出方形的颜色,以及文本的颜色和大小等

 

    好了,具体的功能就这些了,下面我们来看看关键实现的代码,首先肯定是自定义属性,你看这个属性,就知道它有什么样的功能了,代码如下:

    <declare-styleable name="ChargeBubbleView">
        //电池四周边框的宽度
        <attr name="charge_battery_width" format="dimension" />
        //电池边框的颜色
        <attr name="charge_battery_color" format="color" />
        //电池文本的颜色
        <attr name="charge_battery_text_color" format="color" />
        //电池文本的大小
        <attr name="charge_battery_text_size" format="dimension" />
        //当前电量的颜色
        <attr name="charge_battery_current_color" format="color" />
        //电池是否需要帽子
        <attr name="charge_battery_cap" format="boolean" />
        //是否开启电量文本
        <attr name="charge_battery_has_text" format="boolean" />
        //电池盖的填充颜色
        <attr name="charge_battery_cap_fill_color" format="color" />
        //是否开启水波纹效果
        <attr name="charge_battery_water_bezier" format="boolean" />
    </declare-styleable>

 1、电池的绘制,这部分需要用到 path 来绘制,很简单,就是计算各个点的坐标即可。

    private Path getBatteryStroke(boolean hasCap, float width, float height) {
        Path path = new Path();
        if (hasCap) {
            float capWidth = width / 3f;
            float capHeight = capWidth / 2f;
            path.moveTo(0, capHeight);
            path.lineTo(0, height);
            path.lineTo(width, height);
            path.lineTo(width, capHeight);
            path.lineTo(capWidth * 2, capHeight);
            path.lineTo(capWidth * 2, 0);
            path.lineTo(capWidth, 0);
            path.lineTo(capWidth, capHeight);
            path.close();
        } else {
            path.moveTo(0, 0);
            path.lineTo(0, height);
            path.lineTo(width, height);
            path.lineTo(width, 0);
            path.close();
        }
        return path;
    }

2、水波纹的动画效果,这部分比较难。首先,我们需要这样考虑,来看这张草图:

    从这张图就很形成的表明了我的意思,我们在绘制贝塞尔曲线的时候,其实是绘制了两条,就如下面的代码,我用了一个 Path[] 数组来保存两条贝塞尔曲线绘制出来的封闭区域,形成一种海水的效果。让曲线上的点 x 的坐标一直增加,它将会移动到屏幕的右侧,那么后面那条隐藏的贝塞尔曲线也会随之显现,一直不停的循环,就达到了水波纹动画的效果了。

    private Path[] getRealBatteryPath() {
        Path[] path = new Path[2];

        float realBatteryHeight = mCurrentBattery * mBatteryPercent;

        if (hasWaterBezier) {
            path[0] = new Path();
            path[1] = new Path();

            float realY = mHeight - realBatteryHeight;
            float p0x = 0;
            float p0y = realY;
            float pc1x = mWidth / 4;
            float pc1y = realY - 50;
            float p1x = mWidth / 2;
            float p1y = realY;
            float pc2x = p1x + pc1x;
            float pc2y = realY + 50;
            float p2x = mWidth;
            float p2y = realY;
            path[0].moveTo(p0x + diff, p0y);
            path[0].quadTo(pc1x + diff, pc1y, p1x + diff, p1y);
            path[0].quadTo(pc2x + diff, pc2y, p2x + diff, p2y);
            path[0].lineTo(mWidth + diff, mHeight);
            path[0].lineTo(0 + diff, mHeight);
            path[0].close();

            float p0x2 = 0;
            float p0y2 = realY;
            float pc1x2 = mWidth / 4;
            float pc1y2 = realY - 50;
            float p1x2 = mWidth / 2;
            float p1y2 = realY;
            float pc2x2 = p1x + pc1x;
            float pc2y2 = realY + 50;
            float p2x2 = mWidth;
            float p2y2 = realY;
            path[1].moveTo(p0x2 - mWidth + diff, p0y2);
            path[1].quadTo(pc1x2 - mWidth + diff, pc1y2, p1x2 - mWidth + diff, p1y2);
            path[1].quadTo(pc2x2 - mWidth + diff, pc2y2, p2x2 - mWidth + diff, p2y2);
            path[1].lineTo(diff, mHeight);
            path[1].lineTo(-mWidth + diff, mHeight);
            path[1].close();

            if (mWaterAnimator == null) {
                mWaterAnimator = ObjectAnimator.ofFloat(0, mWidth);
                mWaterAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        diff = (float) animation.getAnimatedValue();
                    }
                });
                mWaterAnimator.setDuration(2000);
                mWaterAnimator.setRepeatCount(-1);
                mWaterAnimator.start();
            }

        } else {
            path[0] = new Path();
            path[0].moveTo(0, mHeight - realBatteryHeight);
            path[0].lineTo(0, mHeight);
            path[0].lineTo(mWidth, mHeight);
            path[0].lineTo(mWidth, mHeight - realBatteryHeight);
            path[0].close();
        }
        return path;
    }

3、充电完成了,当然需要绘制一个提示文本,这个比较简单吧,就是 canvas 的基本操作,主要还是计算坐标位置,代码如下

    private void drawBatteryText(Canvas canvas) {
        if (hasText) {
            String batteryText = mCurrentBattery >= 100 ? "充电完成" : (String.valueOf(mCurrentBattery) + "%");
            Rect bounnds = new Rect();
            mPaints[3].getTextBounds(batteryText, 0, batteryText.length(), bounnds);
            if (mCurrentBattery >= 100) {
                canvas.drawText(batteryText, mWidth / 2 - bounnds.width() / 2, mHeight / 2, mPaints[3]);
            } else {
                canvas.drawText(batteryText, mWidth / 2 - bounnds.width() / 2, mHeight - mCurrentBattery * mBatteryPercent - mTextHeight, mPaints[3]);
            }
        }
    }

4、代码中还有一些简单的我就不说了,都是一些属性的赋值操作,自己看看代码吧。下面是本效果的全部代码

package nd.no.xww.qqmessagedragview;

import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * @author xww
 * @desciption : 气泡曲线上升与水波纹综合的充电效果(备注:气泡效果还没实现,等下次再搞)
 * @date 2019/8/9
 * @time 10:40
 * 博主:威威喵
 * 博客:https://blog.csdn.net/smile_Running
 */
public class ChargeBubbleView extends View {

    private float density;

    private float mWidth;
    private float mHeight;

    private Paint mPaints[] = new Paint[5];

    private Path mBatteryStrokePath;
    private Path[] mRealBatteryPath;

    private float mBatteryWidth;
    private int mBatteryStrokeColor;
    private int mCurrentBatteryColor;
    private int mCapFillColor;
    private boolean hasBatteryCap;
    private int mBatteryTextColor;
    private float mBatteryTextSize;
    private boolean hasText;
    private boolean hasWaterBezier;

    private float mTextHeight;//文本高度

    private int mCurrentBattery;// 电量
    private float mBatteryPercent;// 百分比

    private Random mRandom;
    private ValueAnimator mWaterAnimator = null;
    private float diff = 0;


    private List<PointF> mBubblePonits;

    private void init() {
        //绘制当前电量
        mPaints[0] = getPaint();
        mPaints[0].setColor(mCurrentBatteryColor);
        mPaints[0].setStyle(Paint.Style.FILL);
        //绘制电池
        mPaints[1] = getPaint();
        mPaints[1].setStyle(Paint.Style.STROKE);
        mPaints[1].setStrokeWidth(mBatteryWidth);
        mPaints[1].setColor(mBatteryStrokeColor);
        //绘制泡泡
        mPaints[2] = getPaint();
        mPaints[2].setStyle(Paint.Style.STROKE);
        mPaints[2].setStrokeWidth(8f);
        //绘制电量的文本
        mPaints[3] = getPaint();
        mPaints[3].setTextSize(mBatteryTextSize);
        mPaints[3].setColor(mBatteryTextColor);
        Paint.FontMetrics metrics = mPaints[3].getFontMetrics();

        mPaints[4] = getPaint();
        mPaints[4].setColor(mCapFillColor);
        mTextHeight = (metrics.bottom - metrics.top) / 2 - metrics.descent / 2;

        mRandom = new Random();
        mBubblePonits = new ArrayList<>();
    }

    private Paint getPaint() {
        Paint paint = new Paint();
        paint.setDither(true);
        paint.setAntiAlias(true);
        return paint;
    }

    public ChargeBubbleView(Context context) {
        this(context, null);
    }

    public ChargeBubbleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ChargeBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        initAttrs(context, attrs);
        init();
    }

    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ChargeBubbleView);
        if (array != null) {
            mBatteryWidth = array.getDimension(R.styleable.ChargeBubbleView_charge_battery_width, 30f);
            mBatteryStrokeColor = array.getColor(R.styleable.ChargeBubbleView_charge_battery_color, Color.LTGRAY);
            mCurrentBatteryColor = array.getColor(R.styleable.ChargeBubbleView_charge_battery_current_color, Color.GREEN);
            hasBatteryCap = array.getBoolean(R.styleable.ChargeBubbleView_charge_battery_cap, true);
            hasWaterBezier = array.getBoolean(R.styleable.ChargeBubbleView_charge_battery_water_bezier, false);
            hasText = array.getBoolean(R.styleable.ChargeBubbleView_charge_battery_has_text, true);
            mBatteryTextColor = array.getColor(R.styleable.ChargeBubbleView_charge_battery_text_color, Color.RED);
            mCapFillColor = array.getColor(R.styleable.ChargeBubbleView_charge_battery_cap_fill_color, Color.CYAN);
            mBatteryTextSize = array.getDimension(R.styleable.ChargeBubbleView_charge_battery_text_size, 60f);
            array.recycle();
        }

        density = getResources().getDisplayMetrics().density;
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidth = MeasureSpec.getSize(widthMeasureSpec);
        mHeight = MeasureSpec.getSize(heightMeasureSpec);

        mBatteryPercent = hasBatteryCap ? (mHeight - mWidth / 6f) / 100f : mHeight / 100f;

        // 电池边框,初始化绘制一次即可
        mBatteryStrokePath = getBatteryStroke(hasBatteryCap, mWidth, mHeight);
    }

    private Path getBatteryStroke(boolean hasCap, float width, float height) {
        Path path = new Path();
        if (hasCap) {
            float capWidth = width / 3f;
            float capHeight = capWidth / 2f;
            path.moveTo(0, capHeight);
            path.lineTo(0, height);
            path.lineTo(width, height);
            path.lineTo(width, capHeight);
            path.lineTo(capWidth * 2, capHeight);
            path.lineTo(capWidth * 2, 0);
            path.lineTo(capWidth, 0);
            path.lineTo(capWidth, capHeight);
            path.close();
        } else {
            path.moveTo(0, 0);
            path.lineTo(0, height);
            path.lineTo(width, height);
            path.lineTo(width, 0);
            path.close();
        }
        return path;
    }

    private Path[] getRealBatteryPath() {
        Path[] path = new Path[2];

        float realBatteryHeight = mCurrentBattery * mBatteryPercent;

        if (hasWaterBezier) {
            path[0] = new Path();
            path[1] = new Path();

            float realY = mHeight - realBatteryHeight;
            float p0x = 0;
            float p0y = realY;
            float pc1x = mWidth / 4;
            float pc1y = realY - 50;
            float p1x = mWidth / 2;
            float p1y = realY;
            float pc2x = p1x + pc1x;
            float pc2y = realY + 50;
            float p2x = mWidth;
            float p2y = realY;
            path[0].moveTo(p0x + diff, p0y);
            path[0].quadTo(pc1x + diff, pc1y, p1x + diff, p1y);
            path[0].quadTo(pc2x + diff, pc2y, p2x + diff, p2y);
            path[0].lineTo(mWidth + diff, mHeight);
            path[0].lineTo(0 + diff, mHeight);
            path[0].close();

            float p0x2 = 0;
            float p0y2 = realY;
            float pc1x2 = mWidth / 4;
            float pc1y2 = realY - 50;
            float p1x2 = mWidth / 2;
            float p1y2 = realY;
            float pc2x2 = p1x + pc1x;
            float pc2y2 = realY + 50;
            float p2x2 = mWidth;
            float p2y2 = realY;
            path[1].moveTo(p0x2 - mWidth + diff, p0y2);
            path[1].quadTo(pc1x2 - mWidth + diff, pc1y2, p1x2 - mWidth + diff, p1y2);
            path[1].quadTo(pc2x2 - mWidth + diff, pc2y2, p2x2 - mWidth + diff, p2y2);
            path[1].lineTo(diff, mHeight);
            path[1].lineTo(-mWidth + diff, mHeight);
            path[1].close();

            if (mWaterAnimator == null) {
                mWaterAnimator = ObjectAnimator.ofFloat(0, mWidth);
                mWaterAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        diff = (float) animation.getAnimatedValue();
                    }
                });
                mWaterAnimator.setDuration(2000);
                mWaterAnimator.setRepeatCount(-1);
                mWaterAnimator.start();
            }

        } else {
            path[0] = new Path();
            path[0].moveTo(0, mHeight - realBatteryHeight);
            path[0].lineTo(0, mHeight);
            path[0].lineTo(mWidth, mHeight);
            path[0].lineTo(mWidth, mHeight - realBatteryHeight);
            path[0].close();
        }
        return path;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        drawBattery(canvas);
        drawBatteryStroke(canvas);

        drawBatteryText(canvas);
        drawBatteryCap(canvas);
    }

    private void drawBatteryStroke(Canvas canvas) {
        canvas.drawPath(mBatteryStrokePath, mPaints[1]);
    }

    private void drawBatteryText(Canvas canvas) {
        if (hasText) {
            String batteryText = mCurrentBattery >= 100 ? "充电完成" : (String.valueOf(mCurrentBattery) + "%");
            Rect bounnds = new Rect();
            mPaints[3].getTextBounds(batteryText, 0, batteryText.length(), bounnds);
            if (mCurrentBattery >= 100) {
                canvas.drawText(batteryText, mWidth / 2 - bounnds.width() / 2, mHeight / 2, mPaints[3]);
            } else {
                canvas.drawText(batteryText, mWidth / 2 - bounnds.width() / 2, mHeight - mCurrentBattery * mBatteryPercent - mTextHeight, mPaints[3]);
            }
        }
    }

    private void drawBatteryCap(Canvas canvas) {
        if (hasBatteryCap) { // 如果有电池盖子,画一个颜色填充一下,否则有点丑
            if (mCurrentBattery >= 100) {
                Path path = new Path();
                float capWidth = mWidth / 3f;
                float capHeight = capWidth / 2f;
                path.moveTo(capWidth, capHeight);
                path.lineTo(capWidth, 0);
                path.lineTo(capWidth * 2, 0);
                path.lineTo(capWidth * 2, capHeight);
                path.close();
                canvas.drawPath(path, mPaints[4]);
            }
        }
    }

    private void drawBattery(Canvas canvas) {
        //默认电量为 0
        mRealBatteryPath = getRealBatteryPath();

        if (mCurrentBattery == 100 || mRealBatteryPath[0].isEmpty()) {
            if (mWaterAnimator != null)
                mWaterAnimator.cancel();
            Path path = getBatteryStroke(hasBatteryCap, mWidth, mHeight);
            canvas.drawPath(path, mPaints[0]);
            return;
        }

        if (hasWaterBezier) {
            canvas.drawPath(mRealBatteryPath[0], mPaints[0]);
            canvas.drawPath(mRealBatteryPath[1], mPaints[0]);
        } else {
            canvas.drawPath(mRealBatteryPath[0], mPaints[0]);
        }
    }

    public void setBatteryCapacity(int batteryCapacity) {
        this.mCurrentBattery = batteryCapacity;
        invalidate();
    }

    public void clearAnimator() {
        if (mWaterAnimator != null) {
            mWaterAnimator.cancel();
        }
    }

}

5、使用方式就像我说的一样,非常简单,如下:
 

    <nd.no.xww.qqmessagedragview.ChargeBubbleView
        android:id="@+id/battery"
        android:layout_width="160dp"
        app:charge_battery_width="2dp"
        app:charge_battery_cap="true"
        app:charge_battery_color="#6A5ACD"
        app:charge_battery_text_size="20sp"
        app:charge_battery_water_bezier="true"
        app:charge_battery_cap_fill_color="#6495ED"
        app:charge_battery_current_color="#BA55D3"
        app:charge_battery_text_color="#54FF9F"
        app:charge_battery_has_text="true"
        android:layout_height="300dp"
        android:layout_gravity="center" />

     好了,这样的话,你只要拿到该 View 的实例,就可以从过 set 方法,设置电量的值了,还可以调用 clear 来清除动画效果,那么本效果就实现到这里,也算是一个挺完整的自定义 View,有什么额外的功能,可以基于此自己改进。

    不过有点可惜,没有达到我预想的效果,这个效果还是太简单了,没什么难度,我的冒泡泡的动画还没实现,下一次再研究研究,接着写到这个效果里面去。

猜你喜欢

转载自blog.csdn.net/smile_Running/article/details/98996364