Android进阶之自定义View实战贝塞尔曲线应用
Posted kakacxicm
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android进阶之自定义View实战贝塞尔曲线应用相关的知识,希望对你有一定的参考价值。
android进阶之自定义View实战(三)贝塞尔曲线应用
一、引言
在自定义View中,常常看到这样一些非常规的UI效果,如水滴、心型、水波、仿真书页翻动、弹射床等效果,这里面都包含一个重要的要素:贝塞尔曲线(Bézier curve)。
贝塞尔曲线依据n(n>=3)个点位置任意的点坐标绘制出的一条光滑曲线。贝塞尔曲线的有趣之处更在于它的“皮筋效应”。常用的有二阶和三阶曲线。下面分别是二阶和三阶曲线的实现效果和方程。
二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数,它们分别称为起点、锚点(控制点)、终点。B(t)追踪:
方程:
三次贝塞尔曲线由P0、P1、P2、P3四个点确定。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P2之前,走向P1方向的“长度有多长”。B(t)追踪:
方程:
关于贝塞尔曲线的数学描述,大家有个直观的感觉就好。关键在于如何灵活的应用它。
二、入门API
Android提供了二阶与三阶贝塞尔曲线的绘制方法:Path的quadTo和cubicTo方法。后者和前者的区别仅仅是多了一个锚点而已。
二次贝塞尔曲线绘制代码
mPath.moveTo(100, 500);
mPath.quadTo(300, 100, 600, 500);
canvas.drawPath(mPath, mPaint);
效果
三次贝塞尔曲线绘制代码:
mPath.moveTo(100, 500);
mPath.cubicTo(100, 500, 300, 100, 600, 500);
效果:
动态的绘制贝尔曲线的主要工作就是确定各个点的位置,如弹射床的效果只需要改变二次曲线的锚点即可。一些相对复杂的效果则需要多个点同时计算,如水滴效果、QQ上的滑动删除小气泡的粘性效果等等。曲线动态变化逻辑的实质是一些平面几何的运算,可能涉及到圆的切线、三角函数等基础知识。
三、范例分析
前段时间无意间在github上发现了一个有意思的ViewPagerIndicator:SpringIndicator
这里就运用到贝塞尔曲线(ps:真是佩服这设计师的脑洞..)。下面我就按照自己的思路简单山寨一下这个例子中的Indicator页面切换时的效果。
老套路,先分解绘制元素:
1.变化时左边的圆和右边的圆,它这里做了变换过程中的圆半径的变化,左边的圆半径由大变小,右边的圆半径又小变大,因为贝塞尔曲线的端点均在圆上,所以半径的变化会影响两条贝塞尔曲线的4个端点;
2.上下X轴对称的两条二阶贝塞尔曲线,起点和终点均与两个圆位置相关,X坐标分别为俩圆的圆心X,Y坐标为(cy+lr)、(cy-rr),其中cy为俩圆心坐标的y值,lr和rr分别为左右圆的半径;控制点的X坐标为两圆心坐标连线的中点,Y坐标做滑动的比率(0,1)—>圆半径(0,r)的线性映射。
3.变化开始时,左圆未进行平移,一段比率之后,才开始滑动,这里需要分段函数计算左圆的水平偏移;两圆的半径分别在阙值范围内做递增和递减的线性变化。
通过以上分析, 滑动过程中只要控制好两条贝塞尔曲线的6个点,实现这样看起来很酷的效果也并非难事儿!
下面是范例代码:
package com.star.springindicatordemo;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
/**
* Created by star on 16/6/28.
*/
public class SpringIndicator extends View
private Paint mStaticCirclePaint;//静态圆形的画笔
private Paint mDynamicBezierPaint;//贝塞尔曲线的画笔
private Path mBezierPath;//贝塞尔曲线
private final int MAXCIRCLERADIUS = 20;//最大圆半径
private final int MINCIRCLERADIUS = 10;//最小圆半径
private float mLeftCircleRadius;//左圆的半径
private float mRightCircleRadius;//右边圆半径
//贝塞尔原点
private float MORIGINCENTERX = 20;
private float MORIGINCENTERY = 20;
private float mCircleGap = 20*8;
private float mLeftX;//贝塞尔曲线的左边起点X
private float mRightX;//贝塞尔曲线的右边边终X
private float mLeftTopY;//贝塞尔曲线的左上起点y
private float mLeftBottomY;//贝塞尔曲线的右上边终y
private float mRightBottomY;//贝塞尔曲线的右上边终y
private float mRightTopY;//贝塞尔曲线的右上上起点y
private float mControlX;//贝塞尔曲线的控制点X
private float mControlTopY;//贝塞尔曲线的控制点
private float mControlBottomY;
private float bezierFraction;//控制点在水平方向相对小圆半径偏移比例
public SpringIndicator(Context context, AttributeSet attrs)
super(context, attrs);
init();
public float getBezierFraction()
return bezierFraction;
//动画测试,更新View逻辑
public void setBezierFraction(float bezierFraction)
this.bezierFraction = bezierFraction;
//更新右圆的位置
mRightX = MORIGINCENTERX + bezierFraction*mCircleGap;
//更新左圆的位置
mLeftX = MORIGINCENTERX + leftCircleDelayTranslationXLogic(bezierFraction);
//更新控制点
mControlX = (mLeftX + mRightX)/2;
mControlTopY = MORIGINCENTERY - MAXCIRCLERADIUS *(1-bezierFraction);
mControlBottomY = MORIGINCENTERY + MAXCIRCLERADIUS *(1-bezierFraction);
//更新俩圆半径
mLeftCircleRadius = MAXCIRCLERADIUS + (MINCIRCLERADIUS - MAXCIRCLERADIUS)*bezierFraction;
mRightCircleRadius = MINCIRCLERADIUS + (MAXCIRCLERADIUS - MINCIRCLERADIUS)*bezierFraction;
//更新起止点
mLeftTopY = MORIGINCENTERY - mLeftCircleRadius;
mLeftBottomY = MORIGINCENTERY + mLeftCircleRadius;
mRightTopY = MORIGINCENTERY - mRightCircleRadius;
mRightBottomY = MORIGINCENTERY + mRightCircleRadius;
invalidate();
//左边的球延时移动
private float leftCircleDelayTranslationXLogic(float elapsedFraction)
if(elapsedFraction < 0.6f)
return 0;
float leftTranslatinX = ((2*elapsedFraction - 1)*mCircleGap);
return leftTranslatinX;
private void init()
mStaticCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mStaticCirclePaint.setStyle(Paint.Style.FILL);
mDynamicBezierPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDynamicBezierPaint.setColor(Color.BLUE);
mDynamicBezierPaint.setStyle(Paint.Style.FILL);
mBezierPath = new Path();
//半径
mLeftCircleRadius = MAXCIRCLERADIUS;
mRightCircleRadius = MINCIRCLERADIUS;
//初始化贝塞尔曲线的6个点
//4个起止点
mLeftX = MORIGINCENTERX;
mRightX = MORIGINCENTERX;
mLeftTopY = MORIGINCENTERY - mLeftCircleRadius;
mLeftBottomY = MORIGINCENTERY + mLeftCircleRadius;
mRightTopY = MORIGINCENTERY - mRightCircleRadius;
mRightBottomY = MORIGINCENTERY + mRightCircleRadius;
//控制点
mControlX = (mLeftX + mRightX)/2;
mControlTopY = MORIGINCENTERY - MAXCIRCLERADIUS *(1-bezierFraction);
mControlBottomY = MORIGINCENTERY + MAXCIRCLERADIUS *(1-bezierFraction);
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wMode = MeasureSpec.getMode(widthMeasureSpec);
int hMode = MeasureSpec.getMode(heightMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
int resultWidth = wSize;
int resultHeight = hSize;
//lp = wrapcontent时 指定默认值
if(wMode == MeasureSpec.AT_MOST)
resultWidth = (int) (MORIGINCENTERX + MAXCIRCLERADIUS *2+mCircleGap);
if(hMode == MeasureSpec.AT_MOST)
resultHeight = (int) (MAXCIRCLERADIUS + MORIGINCENTERY);
setMeasuredDimension(resultWidth, resultHeight);
@Override
protected void onDraw(Canvas canvas)
mStaticCirclePaint.setColor(Color.BLUE);
//左右的圆
canvas.drawCircle(mLeftX, MORIGINCENTERY, mLeftCircleRadius, mStaticCirclePaint);
canvas.drawCircle(mRightX, MORIGINCENTERY, mRightCircleRadius, mStaticCirclePaint);
mBezierPath.reset();
//上下两条控制点
mBezierPath.moveTo(mLeftX, mLeftTopY);
mBezierPath.quadTo(mControlX, mControlTopY, mRightX, mRightTopY);
mBezierPath.lineTo(mRightX, mRightBottomY);
mBezierPath.quadTo(mControlX,mControlBottomY, mLeftX, mLeftBottomY);
canvas.drawPath(mBezierPath, mDynamicBezierPaint);
/**
* 启动变幻测试
*/
public void start()
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "bezierFraction", 0, 1.0f);
animator.setDuration(2000);
animator.start();
说明:
1.init()方法对圆半径及贝塞尔曲线的六个点做初试化,开始右圆与左圆位置重合,半径一大一小.
2.bezierFraction是模拟Viewpage的滑动offset,结合属性动画,模拟页面切换;start方法启动变幻,动画启动后,setBezierFraction会在动画更新时调用,更新绘制参量,做重绘操作.这里涉及的绘制参量有:两圆的半径变化影响贝塞尔曲线4个端点的Y坐标,俩圆心坐标的变化影响曲线2个控制点的X坐标;
3.leftCircleDelayTranslationXLogic是采用分段函数处理左圆的水平移动逻辑:0-0.5时,不移动;0.5-1.0内,做0-mCircleGap的线性映射.
DemoActivity:
package com.star.springindicatordemo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends AppCompatActivity
private SpringIndicator mVpi;
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mVpi = (SpringIndicator) findViewById(R.id.spring_vpi);
findViewById(R.id.btn_start).setOnClickListener(new View.OnClickListener()
@Override
public void onClick(View view)
mVpi.start();
);
总之,不要被贝塞尔曲线这中高大上的名词唬住,控制了那几个”痛点”,你想怎么玩就怎么玩!
以上是关于Android进阶之自定义View实战贝塞尔曲线应用的主要内容,如果未能解决你的问题,请参考以下文章
Android进阶之自定义View实战仿iOS UISwitch控件实现