前言
完整代码,请查看我的github:https://github.com/shuaijia/LiveLike,喜欢的话就给点个赞喽_
视频直播想必大家都不谋生,从2015年左右开始,视频直播开始大量普及,市面上的大中型APP基本上都有直播功能,比如专做直播的斗鱼、花椒等。大家都可能看过别人直播甚至参与过直播,那么对精彩的内容总忍不住点赞、送礼物!
那作为开发的我们,总是以技术的角度看待世界,看到酷炫的点赞效果,当然也免不了自己实现一下子。
先看效果:
根据效果先分析一波:
根据效果,确定解决思路和关键技术:
- 自定义View当然少不了,这是基础
- 多种爱心随机出现、路径也都不同,所以随机数也是必要的
- 每个爱心的运动速度、变化快慢是不同的,所以用到了插值器
- 爱心的运动轨迹是平滑的曲线,而且曲线都不一样,所以我们想到了使用贝塞尔函数
- 应用贝塞尔函数计算运动中点的位置,就需要使用估值器来实现平滑的动画效果
这些很重要!
有了实现思路,那么接下来我们根据分析的它的特点,一步步得来实现:
一、创建基础View,爱心出现在底部并居中
这样使用RelativeLayout最为合适,所以自定义View需继承RelativeLayout:
public class FavorLayout extends RelativeLayout {
private static final String TAG = "FavorLayout";
// 实现随机效果
private Random random = new Random();
// 爱心高度
private int iHeight = 120;
// 爱心宽度
private int iWidth = 120;
// FavorLayout高度
private int mHeight;
// FavorLayout宽度
private int mWidth;
// 来控制子view的位置
private LayoutParams lp;
}
当然了,构造也少不了
public FavorLayout(Context context) {
super(context);
init();
}
public FavorLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public FavorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
其中init()方法进行一些初始化,接下来的过程中我们会慢慢讲解和一步步完善init方法。
首先在init方法中设置子View的LayoutParams,使其能够实现底部居中。
//底部 并且 水平居中
lp = new LayoutParams(iWidth, iHeight);
lp.addRule(CENTER_HORIZONTAL, TRUE); //这里的TRUE 要注意 不是true
lp.addRule(ALIGN_PARENT_BOTTOM, TRUE);
注意:
控件的宽度高度应在onMeasure方法中获取
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取控件宽高(应在onMeasure方法中获取)
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
}
二、爱心类型实现随机
在自定义的View中创建 爱心 Drawable对象和数组
private Drawable aLove;
private Drawable bLove;
private Drawable cLove;
private Drawable dLove;
private Drawable eLove;
private Drawable[] loves;
在init方法中,将爱心创建并存入数组
//初始化显示的图片
loves = new Drawable[5];
aLove = getResources().getDrawable(R.mipmap.love_a);
bLove = getResources().getDrawable(R.mipmap.love_b);
cLove = getResources().getDrawable(R.mipmap.love_c);
dLove = getResources().getDrawable(R.mipmap.love_d);
eLove = getResources().getDrawable(R.mipmap.love_e);
//赋值给loves
loves[0] = aLove;
loves[1] = bLove;
loves[2] = cLove;
loves[3] = dLove;
loves[4] = eLove;
默认提供了五种不同颜色的爱心,当然,爱心数组可有开发者由外部设置,进行拓展。
三、爱心进入时候有一个缩放并渐变的动画
先看效果:
说到Android动画,我们以前常用Animation,它通常情况下能满足我们的需求,但是它的功能比较弱,并不是很好用。好在3.0后,强大的属性动画的出现,让动画在Android中实现起来变得非常容易。如果你还不知道属性动画怎么使用,赶紧去了解一下吧!
上代码
/**
* 设置初始动画
* 渐变 并且横纵向放大
*
* @param target
* @return
*/
private AnimatorSet getEnterAnimtor(final View target) {
ObjectAnimator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, 0.2f, 1f);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, 0.2f, 1f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, 0.2f, 1f);
AnimatorSet enter = new AnimatorSet();
enter.setDuration(500);
enter.setInterpolator(new LinearInterpolator());
enter.playTogether(alpha, scaleX, scaleY);
enter.setTarget(target);
return enter;
}
给传进来的target(就是爱心的ImageView)设置属性动画集,渐变的同时横纵向放大。
对外提供点赞的方法(其实是创建爱心ImageView并添加)
/**
* 点赞
* 对外暴露的方法
*/
public void addFavor() {
ImageView imageView = new ImageView(getContext());
// 随机选一个
imageView.setImageDrawable(loves[random.nextInt(loves.length)]);
// 设置底部 水平居中
imageView.setLayoutParams(lp);
addView(imageView);
Log.e(TAG, "addFavor: " + "add后子view数:" + getChildCount());
Animator set = getAnimator(imageView);
set.addListener(new AnimEndListener(imageView));
set.start();
}
点赞其实就是:在爱心数组中随机抽取一个创建ImageView,添加给付控件并设置渐变和放大动画。
那么这样我们在按钮的点击事件中调用addFavor方法就可以实现如上图的爱心效果了。
四、使用贝塞尔函数实现曲线运动轨迹
我们怎么让爱心按照曲线移动?而且还有随机呢?
接下来就是本文的主角贝塞尔曲线登场的时刻啦,这也是我实现这个效果学到的最重要的知识。不了解贝塞尔曲线的可以阅读我写的另一篇文章开发中的动效设计与实现 —— 贝塞尔曲线动画的插值法
简单来说:就是给定一个起点,一个终点,一个及一个以上的控制点,计算出一个曲线.
简单了解贝塞尔曲线后,发现 三次方贝塞尔曲线 符合我们的要求。
公式:
公式中需要四个P、P0是我们的起点,P3是终点,P1、P2是曲线的两个控制点。而t是一个因子,取值范围是0-1,熟悉动画的同学应该就明白,0-1,对动画的作用有多么重大。
因为需要自己实现贝塞尔,所以想到了属性动画中的TypeEvaluator,它就是我们需要的。
上代码:
/**
* Description: 动画估值器,以实现平滑动画
* Created by jia on 2017/10/13.
* 人之所以能,是相信能
*/
public class BezierEvaluator implements TypeEvaluator<PointF> {
// 两个控制点
private PointF pointF1;
private PointF pointF2;
public BezierEvaluator(PointF pointF1,PointF pointF2){
this.pointF1 = pointF1;
this.pointF2 = pointF2;
}
@Override
public PointF evaluate(float time, PointF startValue, PointF endValue) {
float timeLeft = 1.0f - time;
//结果
PointF point = new PointF();
PointF point0 = (PointF)startValue;//起点
PointF point3 = (PointF)endValue;//终点
// 贝塞尔公式
point.x = timeLeft * timeLeft * timeLeft * (point0.x)
+ 3 * timeLeft * timeLeft * time * (pointF1.x)
+ 3 * timeLeft * time * time * (pointF2.x)
+ time * time * time * (point3.x);
point.y = timeLeft * timeLeft * timeLeft * (point0.y)
+ 3 * timeLeft * timeLeft * time * (pointF1.y)
+ 3 * timeLeft * time * time * (pointF2.y)
+ time * time * time * (point3.y);
return point;
}
}
先认识一下两个类:
- TypeEvaluator:在获取动画对象时只需要传入起始和结束值系统就会自动完成值的平滑过渡,这个平滑过渡的完成就是靠TypeEvaluator这个类
- PointF:点类,与Point一样,区别是其x和y值是float类型
由于我们view的移动需要控制x y 所以就传入PointF 作为参数。
核心就是在动画变化过程中,实时根据贝塞尔三阶方程计算点的位置并返回。
到这一步,只要我们传入两个PonitF就能得到一个贝塞尔曲线了。
接下来我们在FavorLayout中定义获取一个贝塞尔动画的方法:
/**
* 获取一条路径的两个控制点
* @param scale
*/
private PointF getPointF(int scale) {
PointF pointF = new PointF();
//减去100 是为了控制 x轴活动范围
pointF.x = random.nextInt((mWidth - 100));
//再Y轴上 为了确保第二个控制点 在第一个点之上,我把Y分成了上下两半
pointF.y = random.nextInt((mHeight - 100)) / scale;
return pointF;
}
根据贝塞尔曲线方程可知:两个控制点才是真正控制曲线路径的关键!为了使爱心的运动轨迹不同,所以我们随机生成两个控制点,就可以使得曲线轨迹随机。
/**
* 获取贝塞尔曲线动画
* @param target
* @return
*/
private ValueAnimator getBezierValueAnimator(View target) {
//初始化一个BezierEvaluator
BezierEvaluator evaluator = new BezierEvaluator(getPointF(2), getPointF(1));
// 起点固定,终点随机
ValueAnimator animator = ValueAnimator.ofObject(evaluator, new PointF((mWidth - iWidth) / 2, mHeight - iHeight), new PointF(random.nextInt(getWidth()), 0));
animator.addUpdateListener(new BezierListener(target));
animator.setTarget(target);
animator.setDuration(3000);
return animator;
}
可能你已经发现,我给曲线动画设置了一个监听BezierListener
/**
* Description: 动画监听,这里控制位置,真正实现动画
* Created by jia on 2017/10/13.
* 人之所以能,是相信能
*/
public class BezierListener implements ValueAnimator.AnimatorUpdateListener {
private View target;
public BezierListener(View target) {
this.target = target;
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//这里获取到贝塞尔曲线计算出来的的x y值 赋值给view 这样就能让爱心随着曲线走啦
PointF pointF = (PointF) animation.getAnimatedValue();
target.setX(pointF.x);
target.setY(pointF.y);
// 这里偷个懒,顺便做一个alpha动画,这样alpha渐变也完成啦
target.setAlpha(1-animation.getAnimatedFraction());
}
}
只有在回调里使用了计算的值,才能真正做到曲线运动,否则没有效果哦。
我们在位置更新时给爱心的ImageView设置x、y值,使其按计算的贝塞尔路径运动起来。
并且同时设置了逐渐变淡动画,也就是在运动过程中逐渐消失的效果。
修改一下addFavor方法:将动画更换为 贝塞尔动画
public void addFavor() {
ImageView imageView = new ImageView(getContext());
//随机选一个
imageView.setImageDrawable(drawables[random.nextInt(3)]);
imageView.setLayoutParams(lp);
addView(imageView);
Log.v(TAG, "add后子view数:"+getChildCount());
getBezierValueAnimator(imageView).start();
}
看下效果:
五、收尾,效果合成
1、实现变速
// 为了实现 变速效果 挑选了几种插补器
private Interpolator line = new LinearInterpolator();//线性
private Interpolator acc = new AccelerateInterpolator();//加速
private Interpolator dce = new DecelerateInterpolator();//减速
private Interpolator accdec = new AccelerateDecelerateInterpolator();//先加速后减速
// 在init中初始化
private Interpolator[] interpolators;
在init方法中:
// 初始化插值器
interpolators = new Interpolator[4];
interpolators[0] = line;
interpolators[1] = acc;
interpolators[2] = dce;
interpolators[3] = accdec;
随机选用插值器,使得爱心运动有变化。
2、动画合并
/**
* 设置动画
*
* @param target
* @return
*/
private Animator getAnimator(View target) {
AnimatorSet set = getEnterAnimtor(target);
ValueAnimator bezierValueAnimator = getBezierValueAnimator(target);
AnimatorSet finalSet = new AnimatorSet();
finalSet.playSequentially(set);
finalSet.playSequentially(set, bezierValueAnimator);
finalSet.setInterpolator(interpolators[random.nextInt(4)]);//实现随机变速
finalSet.setTarget(target);
return finalSet;
}
3、修改点赞方法
/**
* 点赞
* 对外暴露的方法
*/
public void addFavor() {
ImageView imageView = new ImageView(getContext());
// 随机选一个
imageView.setImageDrawable(loves[random.nextInt(loves.length)]);
// 设置底部 水平居中
imageView.setLayoutParams(lp);
addView(imageView);
Log.e(TAG, "addFavor: " + "add后子view数:" + getChildCount());
Animator set = getAnimator(imageView);
set.addListener(new AnimEndListener(imageView));
set.start();
}
聪明的伙伴可能又看出来了,我给动画集设置了结束监听,又是为什么呢?
4、设置消失监听
private class AnimEndListener extends AnimatorListenerAdapter {
private View target;
public AnimEndListener(View target) {
this.target = target;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
//因为不停的add 导致子view数量只增不减,所以在view动画结束后remove掉
removeView((target));
Log.v(TAG, "removeView后子view数:" + getChildCount());
}
}
我们之前代码其实已经实现点赞效果,但每次点击都在创建新的爱心的ImageView并且添加到父布局中,所以增加了一个监听,目的是为了在动画结束后,把爱心移除,不然,子view只增不减!
六、总结
总结没想好说什么,由于时间仓促,不免有bug或不足的地方,大家发现可以告诉我,有好的建议也可以告诉我,我们一起进步哦!如果您喜欢我的文章,可以去https://github.com/shuaijia/LiveLike点个赞哦!_