博主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
本文首发于此 博主:威威喵 | 博客主页:https://blog.csdn.net/smile_running
对于博主上一次写的一个水波纹的充电动画效果,我个人还并不是特别满意,也就是写着玩的。这次想认真的写一个,能够用到实际的项目中去,并且可以很方便的使用,像 TextView 一样简单,定制性高一些。本来呢,想着在充电时,添加一个泡泡一直往上冒的效果,无奈啊,写了好一会儿,效果不太理想。我考虑一下,这次就不加入冒泡的效果了,下次在换另一种方式,实现一下冒泡的效果。
对于这次的效果,会比上一篇水波纹充电动画好那么一丢丢吧,看看上篇的效果,点击:贝塞尔曲线(Bezier)之水波纹的手机充电动画效果(一)
上一次没有理解那个 path 绘制三次贝塞尔曲线的方法,动画不太行。这次,我发现用二次贝塞尔也可以绘制出三角函数的图像来,是我学的太好了,见识短浅。来吧,先看几波效果图再说:
这个是有电池盖子的,就是上面突了一个方形,然后是水波纹的效果:
你可以选择不要上面突出来的那部分
你也可以选择关闭水波纹的动画效果,也可以关闭电量进度的百分比值文本,我想没人愿意这么干吧,不就是为了炫酷嘛
你可以任意的切换电池外圈的颜色,充电进度的颜色,上面突出方形的颜色,以及文本的颜色和大小等
好了,具体的功能就这些了,下面我们来看看关键实现的代码,首先肯定是自定义属性,你看这个属性,就知道它有什么样的功能了,代码如下:
//电池四周边框的宽度
//电池边框的颜色
//电池文本的颜色
//电池文本的大小
//当前电量的颜色
//电池是否需要帽子
//是否开启电量文本
//电池盖的填充颜色
//是否开启水波纹效果
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 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、使用方式就像我说的一样,非常简单,如下:
好了,这样的话,你只要拿到该 View 的实例,就可以从过 set 方法,设置电量的值了,还可以调用 clear 来清除动画效果,那么本效果就实现到这里,也算是一个挺完整的自定义 View,有什么额外的功能,可以基于此自己改进。
不过有点可惜,没有达到我预想的效果,这个效果还是太简单了,没什么难度,我的冒泡泡的动画还没实现,下一次再研究研究,接着写到这个效果里面去。