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();
}

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

Android自定义View实例-QQBubbleView实现_第1张图片

效果差不多,不过♥️的运动轨迹还是一条倾斜的直线,怎么才能像原项目那样不规则的运行呢,接下来我们试着给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();
}

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


Android自定义View实例-QQBubbleView实现_第2张图片

这样♥️运行的轨迹就不规则了,但是看起来还不是那么自然,那么下面我们运用一下三阶贝塞尔曲线看看吧(原项目中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{
    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);
    }
}

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


Android自定义View实例-QQBubbleView实现_第3张图片

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


Android自定义View实例-QQBubbleView实现_第4张图片

这个自定义的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 {
        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



    

你可能感兴趣的:(Android自定义View实例-QQBubbleView实现)