一、关于贝塞尔曲线
在工业设计方面贝塞尔曲线有很多用途,比如字体设计,模型设计,任务构型等方方面都有使用,因此,对于绘制感兴趣的开发者,掌握贝塞尔曲线是非常必要的。
在 Android 中,贝塞尔曲线结合 Path 类可以实现更复杂的图形,这里我们给一个案例,来实现一种旋转的花朵。
二、自定义 View 的误区
今天我们自定义的 View 效果如下:
对于花朵而言,首先要构建花瓣,花瓣这里使用了三阶贝赛尔曲线,绘制更加精确,二阶贝赛尔曲线用来画树叶,也是不错的。
private void buildLeaf(Canvas canvas){
mPaint.setColor(0xff40835e);
int width = getWidth();
int height = getHeight();
if(width==0 || height==0){
return;
}
int centerX = width/2;
int centerY = height/2;
int Radius = Math.max(width,height)/2;
Path path = new Path();
path.moveTo(0,0);
// float leftctrY = - (Radius*4)/5.0f;
float leftctrY = - (Radius*7)/10.0f;
float leftctrX = -(float) (Math.abs(leftctrY) * Math.tan(Math.toRadians(30)));
// path.lineTo(leftctrX,leftctrY);
int lastX = 0;
int lastY = -Radius;
float rightctrY = - (Radius*7)/10.0f;
float rightctrX = (float) (Math.abs(rightctrY) * Math.tan(Math.toRadians(30)));
path.quadTo(leftctrX,leftctrY,lastX,lastY);
path.quadTo(rightctrX,rightctrY,0,0);
path.close();
path.setFillType(Path.FillType.WINDING);
int restoreId = canvas.save();
Paint.Style style = mPaint.getStyle();
// mPaint.setStyle(Paint.Style.STROKE);
canvas.translate(centerX,centerY);
canvas.drawPath(path,mPaint);
mPaint.setStyle(style);
canvas.restoreToCount(restoreId);
}
而我们需要的带有弧度的花瓣,因此二阶显然不行,花瓣的画法难度主要集中于三角函数的计算,此外还有第二个控制点的确定,第二个控制点与原点的举例实际上和离原点最远的边平行,此外过最远的点作垂线相交,否则可能产生如下效果。
private void buildHeart(Canvas canvas){
mPaint.setColor(0xffa7324a);
int width = getWidth();
int height = getHeight();
if(width==0 || height==0){
return;
}
int centerX = width/2;
int centerY = height/2;
int Radius = Math.max(width,height)/2;
Path path = new Path();
path.moveTo(0,0);
// float leftctrY = - (Radius*4)/5.0f;
float leftctrY = - (Radius*5)/10.0f;
float leftctrX = -(float) (Math.abs(leftctrY) * Math.tan(Math.toRadians(60)));
// path.lineTo(leftctrX,leftctrY);
int lastX = 0;
float lastY = -Radius * 8f/10;
float rightctrY = - (Radius*5)/10.0f;
float rightctrX = (float) (Math.abs(rightctrY) * Math.tan(Math.toRadians(60)));
path.cubicTo(leftctrX,leftctrY,leftctrX,-Radius,lastX,lastY);
path.cubicTo(rightctrX,-Radius,rightctrX,rightctrY,0,0);
path.close();
path.setFillType(Path.FillType.WINDING);
int restoreId = canvas.save();
Paint.Style style = mPaint.getStyle();
canvas.translate(centerX,centerY);
canvas.drawPath(path,mPaint);
mPaint.setStyle(style);
canvas.restoreToCount(restoreId);
}
三、实现自定义 View
旋转的七色花朵
3.1 颜色表定义
我们这里定义颜色表,目的是为每个花瓣都有不同的颜色
private static int[] COLOR_TABLE = new int[]{
argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
argb((float) Math.random(), (float) Math.random(), (float) Math.random())
};
3.2 旋转
旋转流程我们使用动画来实现
public void startRotate() {
stopRotate();
isPlaying = true;
if (animator == null) {
animator = ValueAnimator.ofFloat(0, 360);
animator.setEvaluator(new TypeEvaluator<Float>() {
@Override
public Float evaluate(float fraction, Float startValue, Float endValue) {
return fraction * 360;
}
});
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(new LinearInterpolator());
animator.setDuration(3000)
.setRepeatMode(ValueAnimator.RESTART);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setDegree((Float) animation.getAnimatedValue());
}
});
}
animator.start();
}
3.3 代码逻辑
下面是完整的实现逻辑
public class FlowerView extends View {
private TextPaint mPaint;
private int strokeWidth = 1;
private int textSize = 12;
private int minContentSize = 0;
private Path mPath;
private int mPetalNumbers = 7;
private float degree;
private int defaultPetalColor = 0xFFFF1493;
private static int[] COLOR_TABLE = new int[]{
argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
argb((float) Math.random(), (float) Math.random(), (float) Math.random()),
argb((float) Math.random(), (float) Math.random(), (float) Math.random())
};
private boolean isPlaying = false;
private int[] colorSet = COLOR_TABLE;
public FlowerView(Context context) {
this(context, null);
}
public FlowerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlowerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setClickable(true);
initPaint();
minContentSize = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
mPath = new Path();
}
public boolean isPlaying() {
return isPlaying;
}
private void initPaint() {
// 实例化画笔并打开抗锯齿
mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mPaint.setAntiAlias(true);
mPaint.setPathEffect(new CornerPathEffect(10)); //设置线条类型
mPaint.setStrokeWidth(dip2px(strokeWidth));
mPaint.setTextSize(dip2px((textSize)));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
width = (int) dip2px(210);
}
if (heightMode != MeasureSpec.EXACTLY) {
height = (int) dip2px(210);
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int width = getWidth();
final int height = getHeight();
if (width < minContentSize || height < minContentSize) return;
int contentSize = Math.min(width, height); //取最小边长,防止画出边界
int centerX = width / 2;
int centerY = height / 2;
final int restoreId = canvas.save();
canvas.translate(centerX, centerY); //将坐标系移到中心
mPaint.setStyle(Paint.Style.STROKE);
drawFlower(canvas, contentSize);
canvas.restoreToCount(restoreId);
}
public void setPetalNumber(int num, int[] colorSet) {
this.mPetalNumbers = num;
if(colorSet != null){
this.colorSet = colorSet;
}else{
this.colorSet = COLOR_TABLE;
}
invalidate();
}
ValueAnimator animator = null;
public void startRotate() {
stopRotate();
isPlaying = true;
if (animator == null) {
animator = ValueAnimator.ofFloat(0, 360);
animator.setEvaluator(new TypeEvaluator<Float>() {
@Override
public Float evaluate(float fraction, Float startValue, Float endValue) {
return fraction * 360;
}
});
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(new LinearInterpolator());
animator.setDuration(3000)
.setRepeatMode(ValueAnimator.RESTART);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setDegree((Float) animation.getAnimatedValue());
}
});
}
animator.start();
}
public void stopRotate() {
isPlaying = false;
if (animator != null) {
animator.cancel();
animator = null;
}
}
private void drawFlower(Canvas canvas, int contentSize) {
int N = this.mPetalNumbers;
for (int i = 0; i < N; i++) {
drawFlowerPath(canvas, contentSize, N, i);
}
}
//花瓣算法
private void drawFlowerPath(Canvas canvas, int contentSize, int N, int pos) {
float perDegree = 360f / N;
final float R = contentSize / 2f;
final float degree = perDegree * pos + this.degree;
float endY = (float) (R * Math.sin(Math.toRadians(degree)));
float endX = (float) (R * Math.cos(Math.toRadians(degree)));
float firstCtlLength = (float) ((R / 2f) / Math.cos(Math.PI / N));
float leftY = (float) ((firstCtlLength) * Math.sin(degree * Math.PI / 180 - Math.PI / N));
float leftX = (float) ((firstCtlLength) * Math.cos(degree * Math.PI / 180 - Math.PI / N));
float rightY = (float) ((firstCtlLength) * Math.sin(degree * Math.PI / 180 + Math.PI / N));
float rightX = (float) ((firstCtlLength) * Math.cos(degree * Math.PI / 180 + Math.PI / N));
float topLeftY = leftY + (float) (R / 2f * Math.sin(degree * Math.PI / 180)); //左侧第二控制点
float topLeftX = leftX + (float) (R / 2f * Math.cos(degree * Math.PI / 180));
float topRightY = rightY + (float) (R / 2f * Math.sin(degree * Math.PI / 180)); //右侧第二控制点
float topRightX = rightX + (float) (R / 2f * Math.cos(degree * Math.PI / 180));
mPath.reset();
mPath.moveTo(0, 0);
mPath.cubicTo(leftX, leftY, topLeftX, topLeftY, endX, endY);
mPath.cubicTo(topRightX, topRightY, rightX, rightY, 0, 0);
mPath.close();
if (colorSet == null || colorSet.length == 0) {
mPaint.setColor(0x9aFB2222);
} else {
mPaint.setColor(colorSet[pos % N]);
}
mPath.setFillType(Path.FillType.WINDING);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(mPath, mPaint);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawPath(mPath, mPaint);
}
public float dip2px(int dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
public static int argb(float red, float green, float blue) {
return ((int) (1 * 255.0f + 0.5f) << 24) |
((int) (red * 255.0f + 0.5f) << 16) |
((int) (green * 255.0f + 0.5f) << 8) |
(int) (blue * 255.0f + 0.5f);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
public void setDegree(float degree) {
this.degree = degree;
invalidate();
}
}
四、总结
和上一篇一样,都是关于贝塞尔曲线的应用,因此掌握贝塞尔曲线是非常重要的,比如自己去设计字体,手写签名都会用到。