前言
完整代碼,請(qǐng)查看我的github:https://github.com/shuaijia/LiveLike,喜歡的話就給點(diǎn)個(gè)贊嘍_
視頻直播想必大家都不謀生,從2015年左右開始,視頻直播開始大量普及,市面上的大中型APP基本上都有直播功能,比如專做直播的斗魚、花椒等。大家都可能看過(guò)別人直播甚至參與過(guò)直播,那么對(duì)精彩的內(nèi)容總?cè)滩蛔↑c(diǎn)贊、送禮物!
那作為開發(fā)的我們,總是以技術(shù)的角度看待世界,看到酷炫的點(diǎn)贊效果,當(dāng)然也免不了自己實(shí)現(xiàn)一下子。
先看效果:
根據(jù)效果先分析一波:
根據(jù)效果,確定解決思路和關(guān)鍵技術(shù):
- 自定義View當(dāng)然少不了,這是基礎(chǔ)
- 多種愛心隨機(jī)出現(xiàn)、路徑也都不同,所以隨機(jī)數(shù)也是必要的
- 每個(gè)愛心的運(yùn)動(dòng)速度、變化快慢是不同的,所以用到了插值器
- 愛心的運(yùn)動(dòng)軌跡是平滑的曲線,而且曲線都不一樣,所以我們想到了使用貝塞爾函數(shù)
- 應(yīng)用貝塞爾函數(shù)計(jì)算運(yùn)動(dòng)中點(diǎn)的位置,就需要使用估值器來(lái)實(shí)現(xiàn)平滑的動(dòng)畫效果
這些很重要!
有了實(shí)現(xiàn)思路,那么接下來(lái)我們根據(jù)分析的它的特點(diǎn),一步步得來(lái)實(shí)現(xiàn):
一、創(chuàng)建基礎(chǔ)View,愛心出現(xiàn)在底部并居中
這樣使用RelativeLayout最為合適,所以自定義View需繼承RelativeLayout:
public class FavorLayout extends RelativeLayout {
private static final String TAG = "FavorLayout";
// 實(shí)現(xiàn)隨機(jī)效果
private Random random = new Random();
// 愛心高度
private int iHeight = 120;
// 愛心寬度
private int iWidth = 120;
// FavorLayout高度
private int mHeight;
// FavorLayout寬度
private int mWidth;
// 來(lái)控制子view的位置
private LayoutParams lp;
}
當(dāng)然了,構(gòu)造也少不了
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()方法進(jìn)行一些初始化,接下來(lái)的過(guò)程中我們會(huì)慢慢講解和一步步完善init方法。
首先在init方法中設(shè)置子View的LayoutParams,使其能夠?qū)崿F(xiàn)底部居中。
//底部 并且 水平居中
lp = new LayoutParams(iWidth, iHeight);
lp.addRule(CENTER_HORIZONTAL, TRUE); //這里的TRUE 要注意 不是true
lp.addRule(ALIGN_PARENT_BOTTOM, TRUE);
注意:
控件的寬度高度應(yīng)在onMeasure方法中獲取
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 獲取控件寬高(應(yīng)在onMeasure方法中獲?。? mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
}
二、愛心類型實(shí)現(xiàn)隨機(jī)
在自定義的View中創(chuàng)建 愛心 Drawable對(duì)象和數(shù)組
private Drawable aLove;
private Drawable bLove;
private Drawable cLove;
private Drawable dLove;
private Drawable eLove;
private Drawable[] loves;
在init方法中,將愛心創(chuàng)建并存入數(shù)組
//初始化顯示的圖片
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;
默認(rèn)提供了五種不同顏色的愛心,當(dāng)然,愛心數(shù)組可有開發(fā)者由外部設(shè)置,進(jìn)行拓展。
三、愛心進(jìn)入時(shí)候有一個(gè)縮放并漸變的動(dòng)畫
先看效果:
說(shuō)到Android動(dòng)畫,我們以前常用Animation,它通常情況下能滿足我們的需求,但是它的功能比較弱,并不是很好用。好在3.0后,強(qiáng)大的屬性動(dòng)畫的出現(xiàn),讓動(dòng)畫在Android中實(shí)現(xiàn)起來(lái)變得非常容易。如果你還不知道屬性動(dòng)畫怎么使用,趕緊去了解一下吧!
上代碼
/**
* 設(shè)置初始動(dòng)畫
* 漸變 并且橫縱向放大
*
* @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;
}
給傳進(jìn)來(lái)的target(就是愛心的ImageView)設(shè)置屬性動(dòng)畫集,漸變的同時(shí)橫縱向放大。
對(duì)外提供點(diǎn)贊的方法(其實(shí)是創(chuàng)建愛心ImageView并添加)
/**
* 點(diǎn)贊
* 對(duì)外暴露的方法
*/
public void addFavor() {
ImageView imageView = new ImageView(getContext());
// 隨機(jī)選一個(gè)
imageView.setImageDrawable(loves[random.nextInt(loves.length)]);
// 設(shè)置底部 水平居中
imageView.setLayoutParams(lp);
addView(imageView);
Log.e(TAG, "addFavor: " + "add后子view數(shù):" + getChildCount());
Animator set = getAnimator(imageView);
set.addListener(new AnimEndListener(imageView));
set.start();
}
點(diǎn)贊其實(shí)就是:在愛心數(shù)組中隨機(jī)抽取一個(gè)創(chuàng)建ImageView,添加給付控件并設(shè)置漸變和放大動(dòng)畫。
那么這樣我們?cè)诎粹o的點(diǎn)擊事件中調(diào)用addFavor方法就可以實(shí)現(xiàn)如上圖的愛心效果了。
四、使用貝塞爾函數(shù)實(shí)現(xiàn)曲線運(yùn)動(dòng)軌跡
我們?cè)趺醋寪坌陌凑涨€移動(dòng)?而且還有隨機(jī)呢?
接下來(lái)就是本文的主角貝塞爾曲線登場(chǎng)的時(shí)刻啦,這也是我實(shí)現(xiàn)這個(gè)效果學(xué)到的最重要的知識(shí)。不了解貝塞爾曲線的可以閱讀我寫的另一篇文章開發(fā)中的動(dòng)效設(shè)計(jì)與實(shí)現(xiàn) —— 貝塞爾曲線動(dòng)畫的插值法
簡(jiǎn)單來(lái)說(shuō):就是給定一個(gè)起點(diǎn),一個(gè)終點(diǎn),一個(gè)及一個(gè)以上的控制點(diǎn),計(jì)算出一個(gè)曲線.
簡(jiǎn)單了解貝塞爾曲線后,發(fā)現(xiàn) 三次方貝塞爾曲線 符合我們的要求。
公式:
公式中需要四個(gè)P、P0是我們的起點(diǎn),P3是終點(diǎn),P1、P2是曲線的兩個(gè)控制點(diǎn)。而t是一個(gè)因子,取值范圍是0-1,熟悉動(dòng)畫的同學(xué)應(yīng)該就明白,0-1,對(duì)動(dòng)畫的作用有多么重大。
因?yàn)樾枰约簩?shí)現(xiàn)貝塞爾,所以想到了屬性動(dòng)畫中的TypeEvaluator,它就是我們需要的。
上代碼:
/**
* Description: 動(dòng)畫估值器,以實(shí)現(xiàn)平滑動(dòng)畫
* Created by jia on 2017/10/13.
* 人之所以能,是相信能
*/
public class BezierEvaluator implements TypeEvaluator<PointF> {
// 兩個(gè)控制點(diǎn)
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;
//結(jié)果
PointF point = new PointF();
PointF point0 = (PointF)startValue;//起點(diǎn)
PointF point3 = (PointF)endValue;//終點(diǎn)
// 貝塞爾公式
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;
}
}
先認(rèn)識(shí)一下兩個(gè)類:
- TypeEvaluator:在獲取動(dòng)畫對(duì)象時(shí)只需要傳入起始和結(jié)束值系統(tǒng)就會(huì)自動(dòng)完成值的平滑過(guò)渡,這個(gè)平滑過(guò)渡的完成就是靠TypeEvaluator這個(gè)類
- PointF:點(diǎn)類,與Point一樣,區(qū)別是其x和y值是float類型
由于我們view的移動(dòng)需要控制x y 所以就傳入PointF 作為參數(shù)。
核心就是在動(dòng)畫變化過(guò)程中,實(shí)時(shí)根據(jù)貝塞爾三階方程計(jì)算點(diǎn)的位置并返回。
到這一步,只要我們傳入兩個(gè)PonitF就能得到一個(gè)貝塞爾曲線了。
接下來(lái)我們?cè)贔avorLayout中定義獲取一個(gè)貝塞爾動(dòng)畫的方法:
/**
* 獲取一條路徑的兩個(gè)控制點(diǎn)
* @param scale
*/
private PointF getPointF(int scale) {
PointF pointF = new PointF();
//減去100 是為了控制 x軸活動(dòng)范圍
pointF.x = random.nextInt((mWidth - 100));
//再Y軸上 為了確保第二個(gè)控制點(diǎn) 在第一個(gè)點(diǎn)之上,我把Y分成了上下兩半
pointF.y = random.nextInt((mHeight - 100)) / scale;
return pointF;
}
根據(jù)貝塞爾曲線方程可知:兩個(gè)控制點(diǎn)才是真正控制曲線路徑的關(guān)鍵!為了使愛心的運(yùn)動(dòng)軌跡不同,所以我們隨機(jī)生成兩個(gè)控制點(diǎn),就可以使得曲線軌跡隨機(jī)。
/**
* 獲取貝塞爾曲線動(dòng)畫
* @param target
* @return
*/
private ValueAnimator getBezierValueAnimator(View target) {
//初始化一個(gè)BezierEvaluator
BezierEvaluator evaluator = new BezierEvaluator(getPointF(2), getPointF(1));
// 起點(diǎn)固定,終點(diǎn)隨機(jī)
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;
}
可能你已經(jīng)發(fā)現(xiàn),我給曲線動(dòng)畫設(shè)置了一個(gè)監(jiān)聽BezierListener
/**
* Description: 動(dòng)畫監(jiān)聽,這里控制位置,真正實(shí)現(xiàn)動(dòng)畫
* 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) {
//這里獲取到貝塞爾曲線計(jì)算出來(lái)的的x y值 賦值給view 這樣就能讓愛心隨著曲線走啦
PointF pointF = (PointF) animation.getAnimatedValue();
target.setX(pointF.x);
target.setY(pointF.y);
// 這里偷個(gè)懶,順便做一個(gè)alpha動(dòng)畫,這樣alpha漸變也完成啦
target.setAlpha(1-animation.getAnimatedFraction());
}
}
只有在回調(diào)里使用了計(jì)算的值,才能真正做到曲線運(yùn)動(dòng),否則沒有效果哦。
我們?cè)谖恢酶聲r(shí)給愛心的ImageView設(shè)置x、y值,使其按計(jì)算的貝塞爾路徑運(yùn)動(dòng)起來(lái)。
并且同時(shí)設(shè)置了逐漸變淡動(dòng)畫,也就是在運(yùn)動(dòng)過(guò)程中逐漸消失的效果。
修改一下addFavor方法:將動(dòng)畫更換為 貝塞爾動(dòng)畫
public void addFavor() {
ImageView imageView = new ImageView(getContext());
//隨機(jī)選一個(gè)
imageView.setImageDrawable(drawables[random.nextInt(3)]);
imageView.setLayoutParams(lp);
addView(imageView);
Log.v(TAG, "add后子view數(shù):"+getChildCount());
getBezierValueAnimator(imageView).start();
}
看下效果:
五、收尾,效果合成
1、實(shí)現(xiàn)變速
// 為了實(shí)現(xiàn) 變速效果 挑選了幾種插補(bǔ)器
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;
隨機(jī)選用插值器,使得愛心運(yùn)動(dòng)有變化。
2、動(dòng)畫合并
/**
* 設(shè)置動(dòng)畫
*
* @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)]);//實(shí)現(xiàn)隨機(jī)變速
finalSet.setTarget(target);
return finalSet;
}
3、修改點(diǎn)贊方法
/**
* 點(diǎn)贊
* 對(duì)外暴露的方法
*/
public void addFavor() {
ImageView imageView = new ImageView(getContext());
// 隨機(jī)選一個(gè)
imageView.setImageDrawable(loves[random.nextInt(loves.length)]);
// 設(shè)置底部 水平居中
imageView.setLayoutParams(lp);
addView(imageView);
Log.e(TAG, "addFavor: " + "add后子view數(shù):" + getChildCount());
Animator set = getAnimator(imageView);
set.addListener(new AnimEndListener(imageView));
set.start();
}
聰明的伙伴可能又看出來(lái)了,我給動(dòng)畫集設(shè)置了結(jié)束監(jiān)聽,又是為什么呢?
4、設(shè)置消失監(jiān)聽
private class AnimEndListener extends AnimatorListenerAdapter {
private View target;
public AnimEndListener(View target) {
this.target = target;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
//因?yàn)椴煌5腶dd 導(dǎo)致子view數(shù)量只增不減,所以在view動(dòng)畫結(jié)束后remove掉
removeView((target));
Log.v(TAG, "removeView后子view數(shù):" + getChildCount());
}
}
我們之前代碼其實(shí)已經(jīng)實(shí)現(xiàn)點(diǎn)贊效果,但每次點(diǎn)擊都在創(chuàng)建新的愛心的ImageView并且添加到父布局中,所以增加了一個(gè)監(jiān)聽,目的是為了在動(dòng)畫結(jié)束后,把愛心移除,不然,子view只增不減!
六、總結(jié)
總結(jié)沒想好說(shuō)什么,由于時(shí)間倉(cāng)促,不免有bug或不足的地方,大家發(fā)現(xiàn)可以告訴我,有好的建議也可以告訴我,我們一起進(jìn)步哦!如果您喜歡我的文章,可以去https://github.com/shuaijia/LiveLike點(diǎn)個(gè)贊哦!_