Android 自定義Path貝塞爾曲線View實踐——旋轉的花朵
阿新 • • 發佈:2019-03-24
一、關於貝塞爾曲線
在工業設計方面貝塞爾曲線有很多用途,同樣,在Android中,貝塞爾曲線結合Path類可以實現更復雜的圖形,這裡我們給一個案例,來實現一種旋轉的花朵。對於貝賽爾曲線的理解,建議參考《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-旋轉的花朵
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 int[] colorSet = new int[]{0xffFF1493,0xffFFD700,0xffFFFF00,0xff87CEFA,0xff00FA9A,0xffBA55D3,0xffE0FFFF};
private boolean isPlaying = false;
public FlowerView(Context context) {
this(context,null);
}
public FlowerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public FlowerView(Context context, @Nullable 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); //取最小邊長,防止畫出邊界
clearCanvas(canvas);
canvas.drawColor(0xffffffff);
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;
this.colorSet = colorSet;
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 new Float(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.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawPath(mPath,mPaint);
}
private synchronized void clearCanvas(Canvas canvas) {
final Xfermode xfermode = mPaint.getXfermode();
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawPaint(mPaint);
mPaint.setXfermode(xfermode);
}
public float dip2px(int dp){
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp,getResources().getDisplayMetrics());
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
public void setDegree(float degree) {
this.degree = degree;
invalidate();
}
}
以上就是整個View的實現,使用方法如下
myFlower = (FlowerView) findViewById(R.id.id_myflower);
myFlower.setPetalNumber(7,null);
myFlower.setOnClickListener(this);
//省略其他不需要給你們看的程式碼
@Override
public void onClick(View v) {
if(v.getId()==R.id.id_myflower){
myFlower.startRotate();
}
}