需要开发一个动画特效,欢迎界面切换时,底部的指示点需要有移动粘连的效果。由于对粘连特效感兴趣,所以决定自己动手来实现。
先来看一下效果图(不是动图):
自定义View,大家应该非常熟悉,这理解不做介绍了。以下代码是主要使用的方法:
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
粘连特效的实现,主要就是贝塞尔曲线的绘制。
贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。百度百科
贝塞尔曲线分为二阶,三阶。现在只需要实现二阶曲线就好。
在Android中,二阶贝塞尔曲线的实现利用的是Path类的quadTo方法。
void quadTo(float x1, float y1, float x2, float y2)
Add a quadratic bezier from the last point, approaching control point (x1,y1), and ending at (x2,y2).
就是说(x1,y1)是贝塞尔曲线的控制点,(x2,y2)是结束点。
绘制贝塞尔曲线至少需要起点,控制点,结束点三个点位。
对上图来说,我们只需要得到P1,P5,P4三个点的坐标,或者得到P2,P5,P3三个点的坐标,就能绘制出一条贝塞尔曲线。
我们可以利用Path类绘制一个闭合的路径图,所以我们还需要把P1,P2连接起来,把P3,P4连接起来。
求点的坐标之前,我们是知道两个圆的圆心坐标以及圆的半径大小。
所以利用三角函数可以得出五个点位的坐标,代码如下:
private void Calculation(float pax, float pay, float pbx, float pby) {
double a = Math.atan((pbx - pax) / (pby - pay));
double sin = Math.sin(a);
double cos = Math.cos(a);
p1.Y = (float) (pay + (sin * 100));
p1.X = (float) (pax - (cos * 100));
p2.X = (float) (pax + cos * 100);
p2.Y = (float) (pay - sin * 100);
p3.X = (float) (pbx - cos * 100);
p3.Y = (float) (pby + sin * 100);
p4.X = (float) (pbx + cos * 100);
p4.Y = (float) (pby - sin * 100);
p5.X = (pax + pbx) / 2;
p5.Y = (pay + pby) / 2;
}
得到五个点位的坐标,就可以开始组合Path的路径了。
在组合Path路径之前,需要介绍一个Path的方法:
void lineTo(float x, float y)
Add a line from the last point to the specified point (x,y).
意思就是说连接Path设置的上一个点的坐标,默认为(0,0)
path.reset();
path.moveTo(p1.X, p1.Y);
path.quadTo(p5.X, p5.Y, p3.X, p3.Y);
path.lineTo(p4.X, p4.Y);
path.quadTo(p5.X, p5.Y, p2.X, p2.Y);
path.lineTo(p1.X, p1.Y);
连接成一个闭合的路径。
//绘制贝塞尔曲线
canvas.drawPath(path, paint);
调用drawPath方法开始绘制Path,就能绘制出贝塞尔曲线。
以上介绍了主要的功能实现,粘连特效的形成。下面开始介绍滑动效果。
public class CirclePoint {
private P p;
private Paint paint;
public CirclePoint() {
initPaint();
}
public void setP(P p) {
this.p = p;
}
public P getP() {
return p;
}
//绘制方法,绘制的画布与颜色
public void onDraw(Canvas canvas, int color) {
if (p == null) return;
paint.setColor(color);
canvas.drawCircle(p.X, p.Y, p.radius, paint);
}
//初始化画笔
private void initPaint() {
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setStrokeWidth(2);
}
}
itemWidth = ViewWidth / count;
itemCenter = itemWidth / 2;
circlePoints.clear();
for (int i = 0; i < count; i++) {
CirclePoint circlePoint = new CirclePoint();
P p = new P();
p.X = itemCenter + itemWidth * i;
p.Y = pointY;
p.radius = circleRadius;
circlePoint.setP(p);
circlePoints.add(circlePoint);
}
然后在自定义View里面循环绘制每个定圆。
//绘制每个定圆
for (int i = 0; i < circlePoints.size(); i++) {
circlePoints.get(i).onDraw(canvas, normalColor);
}
animP.X = itemCenter + circleRadius;
animP.Y = pointY;
animP.radius = circleRadius;
animCirclePoint.setP(animP);
//计算方框Path
private void CalculationFramePath() {
path.moveTo(p1.X, p1.Y);
path.lineTo(p3.X, p3.Y);
path.lineTo(p4.X, p4.Y);
path.lineTo(p2.X, p2.Y);
path.close();
}
在滑动过程中,动圆需要不断切换与之一起绘制形成Path闭合路径(贝塞尔曲线)的定圆,所以就需要计算动圆与当前形成闭合路径的定圆的两圆心的距离,当距离超过一定值后(当前设置为两定圆之间距离的一半减去偏移量),切换到下一个定圆。偏移量两个定圆中心点,往左右的偏移量。当动圆处于此位置时,动圆不与任何定圆产生Path闭合路径。
两圆心的距离:
circleDistance = Math.abs(Math.sqrt(Math.pow(pax - pbx, 2) + Math.pow(pay - pby, 2)));
滑动逻辑处理:
path.reset();//重置路径
if (circleDistance <= circleRadius * 2) {
//绘制闭合方框
CalculationFramePath();
} else if (circleDistance > circleRadius * 2 && circleDistance < itemCenter - offSet) {
//绘制贝塞尔曲线
CalculationBezierPath();
} else if (circleDistance > itemCenter - offSet && circleDistance < itemCenter + offSet) {
//取消绘制贝塞尔曲线
} else if (circleDistance >= itemCenter + offSet) {
//切换下一个圆,以此圆为基础计算Path路径,然后绘制
if (currentIndex <= circlePoints.size() - 1) {
if (pax > pbx) {//动圆位于当前圆的左侧
currentIndex = currentIndex - 1;
} else {//动圆位于当前圆的右侧
currentIndex = currentIndex + 1;
}
}
}
然后在OnDraw方法里面开始绘制定圆与动圆,然后计算滑动逻辑,绘制路径即可。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (circlePoints == null || circlePoints.size() <= 0) return;
//绘制每个圆,根据当前的位置,处理绘制的颜色
for (int i = 0; i < circlePoints.size(); i++) {
if (currentIndex == i) {
circlePoints.get(i).onDraw(canvas, selectColor);
} else {
circlePoints.get(i).onDraw(canvas, normalColor);
}
}
//绘制移动的圆
animCirclePoint.onDraw(canvas, selectColor);
//计算Path路径
CalculationData();
//绘制Path路径
canvas.drawPath(path, paint);
}
在滑动过程中,只需要改变动圆的X坐标,并不会调用绘制方法就可以了。
public void setTranslateX(int x) {
if (animCirclePoint != null && animCirclePoint.getP() != null) {
animCirclePoint.getP().X = x / scale + itemCenter + circleRadius;
}
invalidate();
}
以上就是所有的内容,若有很多不当的地方,还请多多谅解(第一次写)。谢谢!
GitHub源码已上传 源码连接