使用三階貝塞爾曲線實(shí)現(xiàn)直播中點(diǎn)贊效果

前言

完整代碼,請(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è)贊哦!_

郵箱:819418850@qq.com

更多精彩內(nèi)容,請(qǐng)關(guān)注我的微信公眾號(hào)——Android機(jī)動(dòng)車

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,113評(píng)論 25 709
  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果,實(shí)現(xiàn)這些動(dòng)畫的過(guò)程并不復(fù)雜,今天將帶大家一窺ios動(dòng)畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,696評(píng)論 6 30
  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果,實(shí)現(xiàn)這些動(dòng)畫的過(guò)程并不復(fù)雜,今天將帶大家一窺iOS動(dòng)畫全貌。在這里你可以看...
    F麥子閱讀 5,270評(píng)論 5 13
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,410評(píng)論 4 61
  • 早就應(yīng)該學(xué)習(xí)網(wǎng)絡(luò)了,一直拖延,讀了《自品牌》這本書我決定要讓自己成為即用型專家。沒有開始就用遠(yuǎn)沒有進(jìn)步。閱讀就是為...
    自在心靈空間閱讀 1,970評(píng)論 4 5

友情鏈接更多精彩內(nèi)容