Android自定义View实例-QQBubbleView实现

参考项目地址 https://github.com/Yasic/QQBubbleView

最近网上看了一个模仿QQ空间直播页面右下角的礼物冒泡特效,感觉效果不错所以参考源码实现了一下。

本次自定义View主要运用的是属性动画的知识,不熟悉的可参考:
1.Android属性动画完全解析(上),初识属性动画的基本用法
2.Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
3.Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法
4.属性动画中的插值器和估值器

首先来看一下最终要实现的效果

通过效果图,我们分解一下只看一个♥️的运动轨迹,可以发现:

  • 1.♥️运动的范围是整个屏幕(除状态栏和标题栏),并且可以重叠所以布局上面我们可以选择继承FrameLayout或RelativeLayout,因为FrameLayout复杂度比RelativeLayout低所以我选择的是FrameLayout
  • 2.冒出♥️的地方位于屏幕最下方,并且大体水平居中,所以我选的位置是(getWidth()/2, getHeight())这个点
  • 3.♥️y轴运动范围是固定的(从最上方),x轴运动范围是随机的(但都在屏幕可视范围内所以是0到getWidth()),所以y轴的偏移范围为getHeight()到0,x轴偏移范围为getWidth()/2到getWidth()* Math.random(),所以可以定义两个偏移动画:
ObjectAnimator riseTranslationYAnimator = ObjectAnimator.ofFloat(tempImageView, "translationY", Float.valueOf(getHeight()), 0f);
ObjectAnimator riseTranslationXAnimator = ObjectAnimator.ofFloat(tempImageView, "translationX", getWidth() / 2f, (float) (getWidth()* Math.random()));
  • 4.桃心有一个慢慢变浅的效果,所以再定义一个渐变动画:
ObjectAnimator riseAlphaAnimator = ObjectAnimator.ofFloat(tempImageView, "alpha", 1.0f, 0.0f);

接下来我们可以开始写代码了

首先定义一个类继承自FrameLayoutpublic class CustomView extends FrameLayout,并实现一下构造方法。
接着对外定义一个方法,触发动画效果public void startAnimator(),每次触发方法内我们每次创建一个ImageView,背景设置为准备好的♥️图片(我直接从原项目中copy过来的),添加到布局中,并播放动画包含上述的三个动画),即:

public void startAnimator() {
    ImageView tempImageView = new ImageView(getContext());  //创建一个ImageView
    tempImageView.setImageResource(R.mipmap.ic_favorite_red_900_24dp);  //设置背景
    addView(tempImageView, new RelativeLayout.LayoutParams(viewWidth, viewHeight));//添加到布局中

    ObjectAnimator riseAlphaAnimator = ObjectAnimator.ofFloat(tempImageView, "alpha", 1.0f, 0.0f);  //渐渐变淡动画
    ObjectAnimator riseTranslationXAnimator = ObjectAnimator.ofFloat(tempImageView, "translationX", getWidth() / 2, (float) (getWidth()* Math.random()));  //x轴偏移动画
    ObjectAnimator riseTranslationYAnimator = ObjectAnimator.ofFloat(tempImageView, "translationY", getHeight(), 0);  //y轴偏移动画

    AnimatorSet animatorSet = new AnimatorSet();  //动画集合,集中动画一起播放
    animatorSet.play(riseTranslationYAnimator).with(riseTranslationXAnimator).with(riseAlphaAnimator);
    animatorSet.setDuration(5000);
    animatorSet.start();
}

好了,效果差不多实现了,我们在项目中运行一下试试吧(由于每次点击只会添加一个♥️,所以可以快速点击查看一下效果)


效果差不多,不过♥️的运动轨迹还是一条倾斜的直线,怎么才能像原项目那样不规则的运行呢,接下来我们试着给x轴的偏移动画加个插值器试试看吧:

public void startAnimator() {
    ImageView tempImageView = new ImageView(getContext());  //创建一个ImageView
    tempImageView.setImageResource(R.mipmap.ic_favorite_red_900_24dp);  //设置背景
    addView(tempImageView, new RelativeLayout.LayoutParams(viewWidth, viewHeight));//添加到布局中

    ObjectAnimator riseAlphaAnimator = ObjectAnimator.ofFloat(tempImageView, "alpha", 1.0f, 0.0f);  //渐渐变淡动画
    ObjectAnimator riseTranslationXAnimator = ObjectAnimator.ofFloat(tempImageView, "translationX", getWidth() / 2, (float) (getWidth()* Math.random()));  //x轴偏移动画
    riseTranslationXAnimator.setInterpolator(new BounceInterpolator());  //为x轴动画设置插值器
    ObjectAnimator riseTranslationYAnimator = ObjectAnimator.ofFloat(tempImageView, "translationY", getHeight(), 0);  //y轴偏移动画

    AnimatorSet animatorSet = new AnimatorSet();  //动画集合,集中动画一起播放
    animatorSet.play(riseTranslationYAnimator).with(riseTranslationXAnimator).with(riseAlphaAnimator);
    animatorSet.setDuration(5000);
    animatorSet.start();
}

再次运行试试看看效果(系统为我们提供了好几种插值器,大家可以都试试看):



这样♥️运行的轨迹就不规则了,但是看起来还不是那么自然,那么下面我们运用一下三阶贝塞尔曲线看看吧(原项目中x轴y轴的点都用了三阶贝塞尔曲线,这里我只针对x轴),下面我门在估值器中利用三阶贝塞尔曲线计算x轴的路径

public void startAnimator() {
    ImageView tempImageView = new ImageView(getContext());  //创建一个ImageView
    tempImageView.setImageResource(R.mipmap.ic_favorite_red_900_24dp);  //设置背景
    addView(tempImageView, new RelativeLayout.LayoutParams(viewWidth, viewHeight));//添加到布局中

    ObjectAnimator riseAlphaAnimator = ObjectAnimator.ofFloat(tempImageView, "alpha", 1.0f, 0.0f);  //渐渐变淡动画

    BesselEvaluator besselEvaluator = new BesselEvaluator((float) (getWidth()* Math.random()), (float) (getWidth()* Math.random()));//三阶贝塞尔曲线需要四个值,起点、重点我们确定了,我们再随机取两个中间点
    ObjectAnimator riseTranslationXAnimator = ObjectAnimator.ofObject(tempImageView, "translationX", besselEvaluator, getWidth() / 2f, (float) (getWidth()* Math.random()));  //x轴偏移动画
    ObjectAnimator riseTranslationYAnimator = ObjectAnimator.ofFloat(tempImageView, "translationY", getHeight(), 0);  //y轴偏移动画

    AnimatorSet animatorSet = new AnimatorSet();  //动画集合,集中动画一起播放
    animatorSet.play(riseTranslationYAnimator).with(riseTranslationXAnimator).with(riseAlphaAnimator);
    animatorSet.setDuration(5000);
    animatorSet.start();
}

public class BesselEvaluator implements TypeEvaluator<Float>{
    private Float secondX;
    private Float thirdX;
    public BesselEvaluator(Float sencodX, Float thirdX){
        this.secondX = sencodX;
        this.thirdX = thirdX;
    }
    @Override
    public Float evaluate(float fraction, Float start, Float end) {
        return Float.valueOf(start * (1 - fraction) * (1 - fraction) * (1 - fraction)
                + secondX * 3 * fraction * (1 - fraction) * (1 - fraction)
                + thirdX * 3 * (1 - fraction) * fraction * fraction
                + end * fraction * fraction * fraction);
    }
}

我们再来看一下实现的效果吧:



这次看起来舒服多了,接下来我们只需要为每个♥️随机设置背景图片就可以实现原项目中的效果了:



这个自定义的View主要运用的是属性动画的知识,不涉及测量、布局、事件分发等,所以实现起来比较容易学,下面把我的代码贴出来,方便大家理解,如需可运行的代码请到原项目中获取(实现稍微有点不一样,大同小异):
CustomView.java
public class CustomView extends FrameLayout {

    private int[] imgResids = {R.mipmap.ic_favorite_indigo_900_24dp
            , R.mipmap.ic_favorite_deep_purple_900_24dp
            , R.mipmap.ic_favorite_cyan_900_24dp
            , R.mipmap.ic_favorite_deep_purple_900_24dp
            , R.mipmap.ic_favorite_light_blue_900_24dp
            , R.mipmap.ic_favorite_lime_a200_24dp
            , R.mipmap.ic_favorite_purple_900_24dp
            , R.mipmap.ic_favorite_pink_900_24dp
            , R.mipmap.ic_favorite_red_900_24dp};

    private int viewWidth = dp2pix(16), viewHeight = dp2pix(16);

    public CustomView(Context context) {
        super(context);
    }
    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void startAnimator() {
        ImageView tempImageView = new ImageView(getContext());
        tempImageView.setImageResource(imgResids[(int) (Math.random() * imgResids.length)]);
        addView(tempImageView, new RelativeLayout.LayoutParams(viewWidth, viewHeight));
        ObjectAnimator riseAlphaAnimator = ObjectAnimator.ofFloat(tempImageView, "alpha", 1.0f, 0.0f);
        BesselEvaluator besselEvaluator = new BesselEvaluator((float) (getWidth() * Math.random()), (float) (getWidth() * Math.random()));
        ObjectAnimator riseTranslationXAnimator = ObjectAnimator.ofObject(tempImageView, "translationX", besselEvaluator, getWidth() / 2f, (float) (getWidth() * Math.random()));
        ObjectAnimator riseTranslationYAnimator = ObjectAnimator.ofFloat(tempImageView, "translationY", getHeight(), 0);
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.play(riseTranslationYAnimator).with(riseTranslationXAnimator).with(riseAlphaAnimator);
        animatorSet.setDuration(5000);
        animatorSet.start();
    }

    public class BesselEvaluator implements TypeEvaluator<Float> {
        private Float secondX;
        private Float thirdX;
        public BesselEvaluator(Float sencodX, Float thirdX) {
            this.secondX = sencodX;
            this.thirdX = thirdX;
        }
        @Override
        public Float evaluate(float fraction, Float start, Float end) {
            return Float.valueOf(start * (1 - fraction) * (1 - fraction) * (1 - fraction)
                    + secondX * 3 * fraction * (1 - fraction) * (1 - fraction)
                    + thirdX * 3 * (1 - fraction) * fraction * fraction
                    + end * fraction * fraction * fraction);
        }
    }

    private int dp2pix(int dp) {
        float scale = getResources().getDisplayMetrics().density;
        int pix = (int) (dp * scale + 0.5f);
        return pix;
    }
}

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private CustomView mCustomView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCustomView = (CustomView) findViewById(R.id.bubbleView);
    }
    public void clickButton(View view) {
        mCustomView.startAnimator();
    }
}

activity_main

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.zhong.qqlivebubble.MainActivity">
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:onClick="clickButton"
        android:text="开始"/>
    <com.zhong.qqlivebubble.CustomView
        android:id="@+id/bubbleView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"/>
</RelativeLayout>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容