自定义view仿写今日头条点赞动画

前言

平时喜欢看今日头条,上面的财经、科技和NBA栏目都很喜欢,无意中发现他的点赞动画还不错,一下子就吸引到了我。遂即想要不自己实现一下。
最终效果对比如下:
头条:


20210218120920417.gif

仿写效果:


whl.gif

一、导读

学习的过程中发现,每个知识点都是一个小小的体系。比如Glide源码解析,我看到有作者写了10篇文章一个系列来解析(Glide源码解析 https://www.jianshu.com/nb/45157164);又比如自定义view,扔物线凯哥也是从三个方面(绘制、布局、动画)11篇文章来叙述,Carson_Ho也是写了一个系列来描述;
所以掌握一个知识点里面的知识体系还是需要下一些功夫的。

二、效果分析

1 点击一次会撒出五个随机表情和点击音效;
2 连续点击会连续撒出表情并播放音效;
3 长按会一直撒;
4 连续撒时会出现次数和标语(0-20 鼓励,20-40加油,>40太棒了);

三、实现过程

3.1 外层布局

因为今日头条里面底部评论框和资讯列表页都会有点赞按钮,那么点赞效果的表情机会满屏幕都存在,所以最外层继承了RelativeLayout。然后宽高都设置match_parent。
在点击按钮的时候触发OnTouch事件:

        ivThumbBottom.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (event.getAction() == MotionEvent.ACTION_DOWN) {
                    lastDownTime = System.currentTimeMillis();
                    //获取到 x y的坐标来确定动画撒表情的起点
                    x = (int) event.getRawX();
                    y = (int) event.getRawY();
                    Log.i("aaa", (System.currentTimeMillis() - lastDownTime) + "");
                    handler.postDelayed(mLongPressed, 100);
                }
                if (event.getAction() == MotionEvent.ACTION_UP) {
                    Log.i("aaa", (System.currentTimeMillis() - lastDownTime) + "");
                    if (System.currentTimeMillis() - lastDownTime < 100) {//判断为单击事件
                        articleThumbRl.setVisibility(View.VISIBLE);
                        articleThumbRl.setThumb(true, x, y, articleThumbRl);
                        handler.removeCallbacks(mLongPressed);
                    } else {//判断为长按事件后松开
                        handler.removeCallbacks(mLongPressed);
                    }
                }
                return true;
            }
        });

其中通过如下方式实现,长按循环撒表情。

    final Runnable mLongPressed = new Runnable() {
        @Override
        public void run() {
            articleThumbRl.setVisibility(View.VISIBLE);
            articleThumbRl.setThumb(x, y, articleThumbRl);
            handler.postDelayed(mLongPressed, 100);
        }
    };

3.2 setThumb方法 处理点击事件

    public void setThumb(float x, float y, ArticleRl articleThumbRl) {
        //这里处理音效播放
        if (mMediaPlayer.isPlaying()) {
            mMediaPlayer.seekTo(0);//重复点击时,从头开始播放
        } else {
            mMediaPlayer.start();
        }
        if (System.currentTimeMillis() - lastClickTime > 800) {//单次点击
            addThumbImage(mContext, x, y, this);
            lastClickTime = System.currentTimeMillis();
            for (int i = getChildCount() - 5; i < getChildCount(); i++) {
                if (getChildAt(i) instanceof ThumbEmoji) {
                    ((ThumbEmoji) getChildAt(i)).setThumb(true, articleThumbRl);
                }
            }
            currentNumber = 0;
            if (thumbNumber != null) {
                removeView(thumbNumber);
                thumbNumber = null;
            }
        } else {//连续点击
            lastClickTime = System.currentTimeMillis();
            Log.i(TAG, "当前动画化正在执行");
            addThumbImage(mContext, x, y, this);
            for (int i = getChildCount() - 5; i < getChildCount(); i++) {
                if (getChildAt(i) instanceof ThumbEmoji) {
                    ((ThumbEmoji) getChildAt(i)).setThumb(true, articleThumbRl);
                }
            }
            currentNumber++;
            //这里添加数字连击view
            LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
            layoutParams.setMargins(600, (int) (y) - 300, 0, 150);
            if (thumbNumber == null) {
                thumbNumber = new ThumbNumber(mContext);
                addView(thumbNumber, layoutParams);//第二个参数 让数字连击始终保持在最上层
            }
            thumbNumber.setNumber(currentNumber);
        }
    }

其中,数字连击view中的数字有一个颜色渐变和描边效果,颜色渐变用LinearGradient(扔物线课程里面有),描边用重叠绘制方式。

textPaint = new Paint();
        textPaint.setTextSize(TEXT_SIZE);
        textPaint.setTextAlign(Paint.Align.LEFT);
        textPaint.setStrokeWidth(STROKE_WIDTH);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setTypeface(Typeface.DEFAULT_BOLD);
        //这里为了做成上面和下面颜色各一半
        LinearGradient mLinearGradient = new LinearGradient(0, 0, 0, 90f,
                new int[]{0xFFFF9641, 0xFFFF9641, 0xFFFF9641, 0xFFFF9641, 0xFFff0000, 0xFFff0000},
                null, Shader.TileMode.CLAMP);
        textPaint.setShader(mLinearGradient);
        //描边画笔
        textPaintStroke = new Paint();
        textPaintStroke.setColor(Color.BLACK);
        textPaintStroke.setTextSize(TEXT_SIZE);
        textPaintStroke.setTextAlign(Paint.Align.LEFT);
        textPaintStroke.setStrokeWidth(4);
        textPaintStroke.setStyle(Paint.Style.STROKE);
        textPaintStroke.setTypeface(Typeface.DEFAULT_BOLD);

3.3 添加表情的自定义view ThumbEmoji

     private void addThumbImage(Context context, float x, float y, ThumbEmoji.AnimatorListener animatorListener) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 8; i++) {
            list.add(i);
        }
        Collections.shuffle(list);//打乱顺序
        for (int i = 0; i < 5; i++) {
            LayoutParams layoutParams = new LayoutParams(100, 100);
            layoutParams.setMargins((int) x, (int) y - 50, 0, 0);
            ThumbEmoji articleThumb = new ThumbEmoji(context);
            articleThumb.setEmojiType(list.get(i));
            articleThumb.setmAnimatorListener(animatorListener);
            if (getChildCount() > 1)
                this.addView(articleThumb, getChildCount() - 1, layoutParams);
            else {
                this.addView(articleThumb, layoutParams);
            }
        }
    }

其中这里的addview方法给他设置index为 childcount-1后,就可以让它保持在数字连击view的下方,但是我设置成1会出现bug,的原因我还得再去看看。

 if (getChildCount() > 1)
                this.addView(articleThumb, getChildCount() - 1, layoutParams);
            else {
                this.addView(articleThumb, layoutParams);
            }

3.4 撒花效果的动画(也就是抛物线动画)的实现

抛物线动画 分为上升和下降两部分,
上升时,x轴匀速左移或右移,y轴减速向上,表情图片宽高从0变到100;
下降时,x变为1.2倍x,高度变为最高处的0.8,透明度在最后1/8时间段里从1变为0。

       private void showThumbDownAni(ArticleRl articleThumbRl) {
        float topX = -(1080 - 200) + (float) ((2160 - 400) * Math.random());
        float topY = -300 + (float) (-700 * Math.random());
        //上升动画
        //抛物线动画 x方向
        ObjectAnimator translateAnimationX = ObjectAnimator.ofFloat(this, "translationX",
                0, topX);
        translateAnimationX.setDuration(DURATION);
        translateAnimationX.setInterpolator(new LinearInterpolator());
        //y方向
        ObjectAnimator translateAnimationY = ObjectAnimator.ofFloat(this, "translationY",
                0, topY);
        translateAnimationY.setDuration(DURATION);
        translateAnimationY.setInterpolator(new DecelerateInterpolator());
        //表情图片的大小变化
        ObjectAnimator translateAnimationRightLength = ObjectAnimator.ofInt(this, "rightLength",
                0, 100, 100, 100, 100, 100);
        translateAnimationRightLength.setDuration(DURATION);
        ObjectAnimator translateAnimationBottomLength = ObjectAnimator.ofInt(this, "bottomLength",
                0, 100, 100, 100, 100, 100);
        translateAnimationBottomLength.setDuration(DURATION);
        translateAnimationRightLength.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                invalidate();//ondraw会在什么情况下执行?
            }
        });
        //动画集合
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.play(translateAnimationX).with(translateAnimationY).with(translateAnimationRightLength).with(translateAnimationBottomLength);

        //下降动画
        //抛物线动画,原理:两个位移动画,一个横向匀速移动,一个纵向变速移动,两个动画同时执行,就有了抛物线的效果。
        ObjectAnimator translateAnimationXDown = ObjectAnimator.ofFloat(this, "translationX", topX, topX * 1.2f);
        translateAnimationXDown.setDuration(DURATION / 5);
        translateAnimationXDown.setInterpolator(new LinearInterpolator());

        ObjectAnimator translateAnimationYDown = ObjectAnimator.ofFloat(this, "translationY", topY, topY * 0.8f);
        translateAnimationYDown.setDuration(DURATION / 5);
        translateAnimationYDown.setInterpolator(new AccelerateInterpolator());
        //透明度
        ObjectAnimator alphaAnimation = ObjectAnimator.ofFloat(this, "alpha", 1f, 1f, 1f, 1f, 1f, 1f, 1f, 0f);
        alphaAnimation.setDuration(DURATION / 5);
        AnimatorSet animatorSetDown = new AnimatorSet();//设置动画播放顺序
        //播放上升动画
        animatorSet.start();
        animatorSet.addListener(new Animator.AnimatorListener() {
        
            @Override
            public void onAnimationEnd(Animator animation) {
                animatorSetDown.play(translateAnimationXDown).with(translateAnimationYDown).with(alphaAnimation);
                animatorSetDown.start();
            }
        });
        animatorSetDown.addListener(new Animator.AnimatorListener() {

            @Override
            public void onAnimationEnd(Animator animation) {
                articleThumbRl.removeView(ThumbEmoji.this);
                mAnimatorListener.onAnimationEmojiEnd();
            }
        });
    }

四、总结

项目github地址:https://github.com/honglei92/toutiaothumb
view是应用开发时常会接触得的东西,从使用概念原理几个方面我们需要深学细悟、研机析理,做到融会贯通。
apk地址:https://github.com/honglei92/toutiaothumb/blob/master/app/release/app-release.apk

五、参考文献

[1]https://mp.weixin.qq.com/s/PlNtRiowKe9jUXhzPA-CNg
[2]https://github.com/arvinljw/ThumbUpSample
[3]https://mp.weixin.qq.com/s/JjeYAESAI8NnwFdJJUXqQA
[4]https://rengwuxian.com/105.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容