又是一年毕业季,今年终于轮到我了,最近一边忙着公司的项目,一边赶着毕设和论文,还私下和朋友搞了些小外包,然后还要抽出时间写博客,真是忙的不要不要的。
好了,言归正传,前几天写了一篇关于贝塞尔曲线的基础篇,如果你对贝塞尔曲线还不是很了解,建议你先去阅读下:Android开发之贝塞尔曲线初体验
,今天这篇文章主要来讲讲关于贝塞尔曲线的实际应用。
国际惯例,先来看下今天要实现的效果图:
上面两张图分别是仿直播平台送礼动画和饿了么商品加入购物车动画。
1、小试牛刀
我们先来热热身,这里我打算用二阶贝塞尔曲线画出动态波浪的效果,效果如下:
效果还是不错的,很自然的动画呈现,平滑的过渡。
我们来一步步分析下:
1、首先,我们先单纯的思考屏幕内的可见区域,可以把它理解成近似一个周期的sin函数,只是它的幅度没有那么高,类似下图:
根据上面的图,其实我们可以发现它的起始点分别是(0,0)和(2π,0),控制点分别是(π/2,1)和(3π/2,-1),由于有两个控制点,所以这里可以用三阶贝塞尔曲线来画,不过我暂时打算先用二阶贝塞尔曲线来画,也就是把上面的图拆分成两部分:
第一部分:起始点为(0,0)和(π,0),控制点为(π/2,1)
第二部分:起始点为(π,0)和(2π,0),控制点为(3π/2,-1)
然后我们把2π的距离当成是屏幕的宽度,那么π的位置就是屏幕宽度的一半,这样分解下来,配合谷歌官方给我们提供的API,我们就可以很好的实现这2段曲线的绘制,我们先暂定波浪的高度为100px,实现代码也就是:
mPath.moveTo(0, mScreenHeight / 2);
mPath.quadTo(mScreenWidth / 4, mScreenHeight / 2 - 100, mScreenWidth / 2 , mScreenHeight / 2);
mPath.quadTo(mScreenWidth * 3 / 4, mScreenHeight / 2 + 100, mScreenWidth , mScreenHeight / 2);
然后我们把下面的空白区域铺满:
mPath.lineTo(mScreenWidth, mScreenHeight);
mPath.lineTo(0, mScreenHeight);
来看下此时的效果图:
2、实现了初步的效果,那现在我们就应该来思考如何让这个波浪动起来,其实很简单,只需要我们在屏幕外再画出另一周期的曲线,然后让它做平移动画这样就可以了,熟悉sin函数的朋友,肯定能想到下面这幅图:
现在我们把屏幕外的另一半也曲线也画出来(具体坐标这里就不再写出来了,大家画下图就能清楚):
mPath.moveTo(-mScreenWidth + mOffset, mScreenHeight / 2);
mPath.quadTo(-mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 - 100, -mScreenWidth / 2 + mOffset, mScreenHeight / 2);
mPath.quadTo(-mScreenWidth / 4 + mOffset, mScreenHeight / 2 + 100, 0 + mOffset, mScreenHeight / 2);
mPath.quadTo(mScreenWidth / 4 + mOffset, mScreenHeight / 2 - 100, mScreenWidth / 2 + mOffset, mScreenHeight / 2);
mPath.quadTo(mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 + 100, mScreenWidth + mOffset, mScreenHeight / 2);
3、平移动画的实现,这里我们利用到了Android3.0以后给我们提供的属性动画,然后平移长度即为一个周期长度(屏幕宽度):
/**
* 设置动画效果
*/
private void setViewanimator() {
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, mScreenWidth);
valueAnimator.setDuration(1200);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mOffset = (int) animation.getAnimatedValue();//当前平移的值
invalidate();
}
});
valueAnimator.start();
}
拿到平移的值后,我们只需要在各点的x轴动态的加上值,这样就会呈现出动态波浪了。
mPath.quadTo(-mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 - 100, -mScreenWidth / 2 + mOffset, mScreenHeight / 2);
mPath.quadTo(-mScreenWidth / 4 + mOffset, mScreenHeight / 2 + 100, 0 + mOffset, mScreenHeight / 2);
mPath.quadTo(mScreenWidth / 4 + mOffset, mScreenHeight / 2 - 100, mScreenWidth / 2 + mOffset, mScreenHeight / 2);
mPath.quadTo(mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 + 100, mScreenWidth + mOffset, mScreenHeight / 2);
可以简化写成
for (int i = 0; i < 2; i++) {
mPath.quadTo(-mScreenWidth * 3 / 4 + (mScreenWidth * i) + mOffset, mScreenHeight / 2 - 100, -mScreenWidth / 2 + (mScreenWidth * i) + mOffset, mScreenHeight / 2);
mPath.quadTo(-mScreenWidth / 4 + (mScreenWidth * i) + mOffset, mScreenHeight / 2 + 100, +(mScreenWidth * i) + mOffset, mScreenHeight / 2);
}
2、仿饿了么商品加入动画效果:
如果你理解了上面的“小试牛刀”例子,要实现这个效果就非常容易了,首先我们要确定添加购物车“+”的位置,然后确定购物车的位置,也就是我们贝塞尔曲线的起始点了,然后再给出一个控制点,只需要让它比“+”的位置高一些,让它成抛物线的效果即可。
1、要确定一个View所在屏幕内的位置,我们可以利用谷歌官方给我们提供的API(具体根据界面中的布局来确定):
getLocationInWindow(一个控件在其父窗口中的坐标位置)
getLocationOnScreen(一个控件在其整个屏幕上的坐标位置)
/**
* <p>Computes the coordinates of this view on the screen. The argument
* must be an array of two integers. After the method returns, the array
* contains the x and y location in that order.</p>
*
* @param outLocation an array of two integers in which to hold the coordinates
*/
public void getLocationOnScreen(@Size(2) int[] outLocation) {
getLocationInWindow(outLocation);
final AttachInfo info = mAttachInfo;
if (info != null) {
outLocation[0] += info.mWindowLeft;
outLocation[1] += info.mWindowTop;
}
}
/**
* <p>Computes the coordinates of this view in its window. The argument
* must be an array of two integers. After the method returns, the array
* contains the x and y location in that order.</p>
*
* @param outLocation an array of two integers in which to hold the coordinates
*/
public void getLocationInWindow(@Size(2) int[] outLocation) {
if (outLocation == null || outLocation.length < 2) {
throw new IllegalArgumentException("outLocation must be an array of two integers");
}
outLocation[0] = 0;
outLocation[1] = 0;
transformFromViewToWindowSpace(outLocation);
}
这里可以获取到一个int类型的数组,数组下标0和1分别代表着x和y坐标,需要注意的一点是,别在onCreate里去调用这个方法(点击事件内可以),否则获取到的坐标只会是(0,0),这个方法需要在Activity获取到焦点后调用才有效果。
2、当我们拿到了这3点坐标,我们就可以画出对应的贝塞尔曲线。然后我们只需要让这个小红点在这条曲线路径里去做平滑移动就可以了,由于小红点是带有x,y坐标的,曲线的每一个点也是带有x,y坐标的,聪明的你应该已经想到这里还是一样用到了属性动画,动态的去改变当前小红点的x,y坐标即可。
由于谷歌官方只给我们提供了一些比较基础的插值器,比如Int,Float,Argb等,并没有给我们提供关于坐标的插值器,不过好在它给我们开放了相关接口,我们只需要对应的去实现它即可,这个接口叫TypeEvaluator:
/**
* Interface for use with the {@link ValueAnimator#setEvaluator(TypeEvaluator)} function. Evaluators
* allow developers to create animations on arbitrary property types, by allowing them to supply
* custom evaluators for types that are not automatically understood and used by the animation
* system.
*
* @see ValueAnimator#setEvaluator(TypeEvaluator)
*/
public interface TypeEvaluator<T> {
/**
* This function returns the result of linearly interpolating the start and end values, with
* <code>fraction</code> representing the proportion between the start and end values. The
* calculation is a simple parametric calculation: <code>result = x0 + t * (x1 - x0)</code>,
* where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
* and <code>t</code> is <code>fraction</code>.
*
* @param fraction The fraction from the starting to the ending values
* @param startValue The start value.
* @param endValue The end value.
* @return A linear interpolation between the start and end values, given the
* <code>fraction</code> parameter.
*/
public T evaluate(float fraction, T startValue, T endValue);
}
从注释里我们可以得到这些信息,首先我们需要去实现evaluate方法,然后这里提供了3个回调参数,它们分别代表:
float fraction:动画的完成程度,0~1
T startValue:动画开始值
T endValue: 动画结束值(这里而外补充一点,要想得到当前的动画值其实也很简单,只需要用(动画开始值+动画完成程度*动画结束值))
这里贴下关于小红点移动坐标的插值器代码:(Point是系统自带的类,可以用来记录X,Y坐标点)
/**
* 自定义Evaluator
*/
public class CirclePointEvaluator implements TypeEvaluator {
/**
* @param t 当前动画进度
* @param startValue 开始值
* @param endValue 结束值
* @return
*/
@Override
public Object evaluate(float t, Object startValue, Object endValue) {
Point startPoint = (Point) startValue;
Point endPoint = (Point) endValue;
int x = (int) (Math.pow((1-t),2)*startPoint.x+2*(1-t)*t*mCircleConPoint.x+Math.pow(t,2)*endPoint.x);
int y = (int) (Math.pow((1-t),2)*startPoint.y+2*(1-t)*t*mCircleConPoint.y+Math.pow(t,2)*endPoint.y);
return new Point(x,y);
}
}
这里的x和y是根据二阶贝塞尔曲线计算出来的,对应的公式为:
然后我们在值变化监听器中去不断绘制这个小红点的位置就可以了。
//设置值动画
ValueAnimator valueAnimator = ValueAnimator.ofObject(new CirclePointEvaluator(), mCircleStartPoint, mCircleEndPoint);
valueAnimator.setDuration(600);
valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Point goodsViewPoint = (Point) animation.getAnimatedValue();
mCircleMovePoint.x = goodsViewPoint.x;
mCircleMovePoint.y = goodsViewPoint.y;
invalidate();
}
});
3、仿直播送礼物:
有了前两个例子的基础,现在要做类似于这种运动轨迹的效果是不是很有感觉了?打铁要趁热,我们接着来说直播送礼这个效果。
首先,我们先简化一下,看下图:
1、首先我们需要知道这条曲线的路径要怎么画,这里我应该不需要我再说了,三阶贝塞尔曲线,起始点和结束点分别为(屏幕宽度的一半,屏幕高度)和(屏幕宽度的一半,0),然后控制点有2个,分别是(屏幕宽度,四分之三屏幕高度)和(0,四分之一屏幕高度)
mPath.moveTo(mStartPoint.x, mStartPoint.y);
mPath.cubicTo(mConOnePoint.x, mConOnePoint.y, mConTwoPoint.x, mConTwoPoint.y, mEndPoint.x, mEndPoint.y);
canvas.drawPath(mPath, mPaint);
2、然后我们来说下关于这个星星的实现,这里是用到一张星星的图片,通过资源文件转Bitmap对象,再赋予给所创建的Canvas画布,然后通过Xfermodes将图片进行渲染变色,最后通过ImageView来加载。
这里我们取SrcIn模式,也就是我们先绘制Dst(资源文件),然后再绘制Src(画笔颜色),当我们设置SrcIn模式时,自然就剩下的Dst的形状+Src的颜色,也就是不同颜色的星星。
/**
* 画星星并随机赋予不同的颜色
*
* @param color
* @return
*/
private Bitmap drawStar(int color) {
//创建和资源文件Bitmap相同尺寸的Bitmap填充Canvas
Bitmap outBitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(outBitmap);
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
//利用Graphics中的XferModes对Canvas进行着色
canvas.drawColor(color, PorterDuff.Mode.SRC_IN);
canvas.setBitmap(null);
return outBitmap;
}
3、接下来就是让星星动起来,老套路,我们利用属性动画,去获取贝塞尔曲线上的各点坐标位置,然后动态的给ImageView设置坐标即可。这里的坐标点我们需要通过三阶贝塞尔曲线公式来计算:
public class StarTypeEvaluator implements TypeEvaluator<Point> {
@Override
public Point evaluate(float t, Point startValue, Point endValue) {
//利用三阶贝塞尔曲线公式算出中间点坐标
int x = (int) (startValue.x * Math.pow((1 - t), 3) + 3 * mConOnePoint.x * t * Math.pow((1 - t), 2) + 3 *
mConTwoPoint.x * Math.pow(t, 2) * (1 - t) + endValue.x * Math.pow(t, 3));
int y = (int) (startValue.y * Math.pow((1 - t), 3) + 3 * mConOnePoint.y * t * Math.pow((1 - t), 2) + 3 *
mConTwoPoint.y * Math.pow(t, 2) * (1 - t) + endValue.y * Math.pow(t, 3));
return new Point(x, y);
}
}
4、然后再带上一个渐隐(透明度)的属性动画动画即可。
//设置属性动画
ValueAnimator valueAnimator = ValueAnimator.ofObject(new StarTypeEvaluator(pointFFirst, pointFSecond), pointFStart,
pointFEnd);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Point point = (Point) animation.getAnimatedValue();
imageView.setX(point.x);
imageView.setY(point.y);
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
StarViewGroup.this.removeView(imageView);
}
});
//透明度动画
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(imageView, "alpha", 1.0f, 0f);
//组合动画
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(3500);
animatorSet.play(valueAnimator).with(objectAnimator);
animatorSet.start();
valueAnimator.start();
这样我们就实现了上面简化版的效果,然后我们来完成下最终满屏星星。
1、首先,这个星星我们是通过资源文件加载到Canvas画布,然后再装载到ImageView里去显示,现在屏幕里有很多星星,所以我们考虑自定义一个ViewGroup,让其继承于RelativeLayout。
2、再来观察下效果图,发现这些星星大致是往一定的轨迹在飘动,但是位置好像又不是一层不变的,所以这里我们可以知道,这4个关键点(起始点,结束点,2个控制点)是会变化的,所以我们只可以监听下这个ViewGroup的onTouch事件,在用户触摸屏幕的时候,去动态生成这几个点的坐标,其他的就没变化了,根据三阶贝塞尔曲线公式就可以星星当前所在的位置,然后进行绘制。
/**
* 监听onTouch事件,动态生成对应坐标
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
mStartPoint = new Point(mScreenWidth / 2, mScreenHeight);
mEndPoint = new Point((int) (mScreenWidth / 2 + 150 * mRandom.nextFloat()), 0);
mConOnePoint = new Point((int) (mScreenWidth * mRandom.nextFloat()), (int) (mScreenHeight * 3 * mRandom.nextFloat() / 4));
mConTwoPoint = new Point(0, (int) (mScreenHeight * mRandom.nextFloat() / 4));
addStar();
return true;
}
好了,文章到这里就结束了,由于篇幅限制,这里不能对一些东西讲的太细,比如一些自定义View的基础,还有属性动画的用法,大家自行查阅相关资料哈。
源码下载:
这里附上源码地址(欢迎Star,欢迎Fork):源码下载