最近网上看了一个模仿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>