├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ └── styles.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values-w820dp
│ │ │ │ └── dimens.xml
│ │ │ └── layout
│ │ │ │ └── activity_main.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── anenn
│ │ │ └── flowlikeview
│ │ │ └── MainActivity.java
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── anenn
│ │ │ └── flowlikeview
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── anenn
│ │ └── flowlikeview
│ │ └── ApplicationTest.java
├── proguard-rules.pro
└── build.gradle
├── lib
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ └── strings.xml
│ │ │ └── drawable-xhdpi
│ │ │ │ ├── heart0.png
│ │ │ │ ├── heart1.png
│ │ │ │ ├── heart2.png
│ │ │ │ ├── heart3.png
│ │ │ │ ├── heart4.png
│ │ │ │ ├── heart5.png
│ │ │ │ ├── heart6.png
│ │ │ │ ├── heart7.png
│ │ │ │ └── heart8.png
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── anenn
│ │ │ └── flowlikeviewlib
│ │ │ └── FlowLikeView.java
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── anenn
│ │ │ └── flowlikeviewlib
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── anenn
│ │ └── flowlikeviewlib
│ │ └── ApplicationTest.java
├── build.gradle
└── proguard-rules.pro
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
└── README.md
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/lib/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':lib'
2 |
--------------------------------------------------------------------------------
/lib/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Lib
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | FlowLikeView
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/lib/src/main/res/drawable-xhdpi/heart0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/lib/src/main/res/drawable-xhdpi/heart0.png
--------------------------------------------------------------------------------
/lib/src/main/res/drawable-xhdpi/heart1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/lib/src/main/res/drawable-xhdpi/heart1.png
--------------------------------------------------------------------------------
/lib/src/main/res/drawable-xhdpi/heart2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/lib/src/main/res/drawable-xhdpi/heart2.png
--------------------------------------------------------------------------------
/lib/src/main/res/drawable-xhdpi/heart3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/lib/src/main/res/drawable-xhdpi/heart3.png
--------------------------------------------------------------------------------
/lib/src/main/res/drawable-xhdpi/heart4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/lib/src/main/res/drawable-xhdpi/heart4.png
--------------------------------------------------------------------------------
/lib/src/main/res/drawable-xhdpi/heart5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/lib/src/main/res/drawable-xhdpi/heart5.png
--------------------------------------------------------------------------------
/lib/src/main/res/drawable-xhdpi/heart6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/lib/src/main/res/drawable-xhdpi/heart6.png
--------------------------------------------------------------------------------
/lib/src/main/res/drawable-xhdpi/heart7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/lib/src/main/res/drawable-xhdpi/heart7.png
--------------------------------------------------------------------------------
/lib/src/main/res/drawable-xhdpi/heart8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/lib/src/main/res/drawable-xhdpi/heart8.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anenn/FlowLikeView/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Dec 28 10:00:20 PST 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
7 |
--------------------------------------------------------------------------------
/lib/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/test/java/com/anenn/flowlikeview/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.anenn.flowlikeview;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
9 | */
10 | public class ExampleUnitTest {
11 | @Test
12 | public void addition_isCorrect() throws Exception {
13 | assertEquals(4, 2 + 2);
14 | }
15 | }
--------------------------------------------------------------------------------
/lib/src/test/java/com/anenn/flowlikeviewlib/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.anenn.flowlikeviewlib;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
9 | */
10 | public class ExampleUnitTest {
11 | @Test
12 | public void addition_isCorrect() throws Exception {
13 | assertEquals(4, 2 + 2);
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/anenn/flowlikeview/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.anenn.flowlikeview;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
--------------------------------------------------------------------------------
/lib/src/androidTest/java/com/anenn/flowlikeviewlib/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.anenn.flowlikeviewlib;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
--------------------------------------------------------------------------------
/lib/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 23
5 | buildToolsVersion "23.0.3"
6 |
7 | defaultConfig {
8 | minSdkVersion 8
9 | targetSdkVersion 23
10 | versionCode 1
11 | versionName "1.0"
12 | }
13 | buildTypes {
14 | release {
15 | minifyEnabled false
16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
17 | }
18 | }
19 | }
20 |
21 | dependencies {
22 | compile fileTree(dir: 'libs', include: ['*.jar'])
23 | testCompile 'junit:junit:4.12'
24 | compile 'com.android.support:appcompat-v7:23.4.0'
25 | compile 'com.nineoldandroids:library:2.4.0'
26 | }
27 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/zhangxiaoqin/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/lib/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/zhangxiaoqin/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 23
5 | buildToolsVersion "23.0.3"
6 |
7 | defaultConfig {
8 | applicationId "com.anenn.flowlikeview"
9 | minSdkVersion 8
10 | targetSdkVersion 23
11 | versionCode 1
12 | versionName "1.0"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | }
21 |
22 | dependencies {
23 | compile fileTree(include: ['*.jar'], dir: 'libs')
24 | testCompile 'junit:junit:4.12'
25 | compile 'com.android.support:appcompat-v7:23.4.0'
26 | compile project(':lib')
27 | }
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 | !/*.apk
5 |
6 | # Files for the Dalvik VM
7 | *.dex
8 |
9 | # Java class files
10 | *.class
11 |
12 | # Generated files
13 | bin/
14 | gen/
15 | out/
16 |
17 | # Gradle files
18 | .gradle/
19 | build/
20 |
21 | # Local configuration file (sdk path, etc)
22 | local.properties
23 |
24 | # Proguard folder generated by Eclipse
25 | proguard/
26 |
27 | # Log Files
28 | *.log
29 |
30 | # User-specific configurations
31 | .idea/
32 |
33 | # Intellij project files
34 | *.iml
35 | *.ipr
36 | *.iws
37 |
38 | # OS-specific files
39 | .DS_Store
40 | .DS_Store?
41 | ._*
42 | .Spotlight-V100
43 | .Trashes
44 | ehthumbs.db
45 | Thumbs.db
46 |
47 | # other setting
48 | gradlew
49 | gradlew.bat
50 | gradle.properties
51 | /captures
--------------------------------------------------------------------------------
/app/src/main/java/com/anenn/flowlikeview/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.anenn.flowlikeview;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.app.AppCompatActivity;
5 | import android.view.View;
6 |
7 | import com.anenn.flowlikeviewlib.FlowLikeView;
8 |
9 | public class MainActivity extends AppCompatActivity {
10 |
11 | private FlowLikeView likeViewLayout;
12 |
13 | @Override
14 | protected void onCreate(Bundle savedInstanceState) {
15 | super.onCreate(savedInstanceState);
16 | setContentView(R.layout.activity_main);
17 |
18 | initView();
19 | }
20 |
21 | private void initView() {
22 | likeViewLayout = (FlowLikeView) findViewById(R.id.flowLikeView);
23 | }
24 |
25 | public void addLikeView(View view) {
26 | likeViewLayout.addLikeView();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
18 |
19 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FlowLikeView
2 |
3 | 现在市面上直播类的应用可以说是一抓一大把,随随便便就以什么主题来开发个直播App。在直播界面中,当然少不了的就是各种打赏、各种点赞。今天自己就针对点赞功能敲了一下,代码不多,主要是涉及到动画运动轨迹运算,这里借助 [贝塞尔曲线](http://baike.baidu.com/link?url=CAQk_7L5i-MgWoo-JHG_TFaKG8Q6qHNd50WQ1dUc5_wR52XCPBZ1aDvgwE36pc8l_w7NKOgHmOJyyNVIHNKF-q),使用三阶贝塞尔曲线来实现轨迹动画。效果如下:
4 |
5 | 
6 |
7 | ### 一、具体实现流程
8 | 仔细分析整个点赞过程可以发现,首先是“爱心”的出现动画,然后是“爱心”以类似气泡的形式向上运动。
9 |
10 | **“爱心”的出现动画**
11 |
12 | ```
13 | private AnimatorSet generateEnterAnimation(View target) {
14 | ObjectAnimator alpha = ObjectAnimator.ofFloat(target, "alpha", 0.2f, 1f);
15 | ObjectAnimator scaleX = ObjectAnimator.ofFloat(target, "scaleX", 0.5f, 1f);
16 | ObjectAnimator scaleY = ObjectAnimator.ofFloat(target, "scaleY", 0.5f, 1f);
17 | AnimatorSet enterAnimation = new AnimatorSet();
18 | enterAnimation.playTogether(alpha, scaleX, scaleY);
19 | enterAnimation.setDuration(150);
20 | enterAnimation.setTarget(target);
21 | return enterAnimation;
22 | }
23 | ```
24 |
25 | **“爱心“的上浮动画**
26 |
27 | ```
28 | private ValueAnimator generateCurveAnimation(View target) {
29 | CurveEvaluator evaluator = new CurveEvaluator(generateCTRLPointF(1), generateCTRLPointF(2));
30 | ValueAnimator valueAnimator = ValueAnimator.ofObject(evaluator,
31 | new PointF((mViewWidth - mPicWidth) / 2, mViewHeight - mChildViewHeight - mPicHeight),
32 | new PointF((mViewWidth) / 2 + (mRandom.nextBoolean() ? 1 : -1) * mRandom.nextInt(100), 0));
33 | valueAnimator.setDuration(3000);
34 | valueAnimator.addUpdateListener(new CurveUpdateLister(target));
35 | valueAnimator.setTarget(target);
36 |
37 | return valueAnimator;
38 | }
39 | ```
40 |
41 | 这里需要自定义一个估值算法 CurveEveluator,因为“爱心”在上浮的过程中并不是以某一直线运动的,而是通过一条不规则的曲线往上浮,而我们知道 TypeEveluator 的作用就是根据动画的变化率来设置控件属性的当前值,具体算法实现就是使用三阶贝塞尔曲线公式:
42 |
43 | 
44 |
45 | 其中 P0 是动画的起点,P3 是动画的终点,而另外两点P1、P2是则作为控制点。具体P1、P2要去什么值,这个凭经验,感觉差不多就行哈 ^_^
46 |
47 | ```
48 | private class CurveEvaluator implements TypeEvaluator {
49 |
50 | // 由于这里使用的是三阶的贝塞儿曲线, 所以我们要定义两个控制点
51 | private PointF ctrlPointF1;
52 | private PointF ctrlPointF2;
53 |
54 | public CurveEvaluator(PointF ctrlPointF1, PointF ctrlPointF2) {
55 | this.ctrlPointF1 = ctrlPointF1;
56 | this.ctrlPointF2 = ctrlPointF2;
57 | }
58 |
59 | @Override
60 | public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
61 |
62 | // 这里运用了三阶贝塞儿曲线的公式, 请自行上网查阅
63 | float leftTime = 1.0f - fraction;
64 | PointF resultPointF = new PointF();
65 |
66 | // 三阶贝塞儿曲线
67 | resultPointF.x = (float) Math.pow(leftTime, 3) * startValue.x
68 | + 3 * (float) Math.pow(leftTime, 2) * fraction * ctrlPointF1.x
69 | + 3 * leftTime * (float) Math.pow(fraction, 2) * ctrlPointF2.x
70 | + (float) Math.pow(fraction, 3) * endValue.x;
71 | resultPointF.y = (float) Math.pow(leftTime, 3) * startValue.y
72 | + 3 * (float) Math.pow(leftTime, 2) * fraction * ctrlPointF1.y
73 | + 3 * leftTime * fraction * fraction * ctrlPointF2.y
74 | + (float) Math.pow(fraction, 3) * endValue.y;
75 |
76 | // 二阶贝塞儿曲线
77 | // resultPointF.x = (float) Math.pow(leftTime, 2) * startValue.x + 2 * fraction * leftTime * ctrlPointF1.x
78 | // + ((float) Math.pow(fraction, 2)) * endValue.x;
79 | // resultPointF.y = (float) Math.pow(leftTime, 2) * startValue.y + 2 * fraction * leftTime * ctrlPointF1.y
80 | // + ((float) Math.pow(fraction, 2)) * endValue.y;
81 |
82 | return resultPointF;
83 | }
84 | }
85 | ```
86 |
87 | ### 二、使用操作
88 |
89 | ```
90 |
94 |
95 |
107 |
108 | ```
109 |
110 | 然后在点击响应事件中调用 FlowLikeView.addLikeView() 方法可以啦
111 |
112 | 好了,有什么问题欢迎 issues,喜欢的来个 Star ~~~
--------------------------------------------------------------------------------
/lib/src/main/java/com/anenn/flowlikeviewlib/FlowLikeView.java:
--------------------------------------------------------------------------------
1 | package com.anenn.flowlikeviewlib;
2 |
3 | import android.content.Context;
4 | import android.graphics.PointF;
5 | import android.graphics.drawable.Drawable;
6 | import android.support.v4.content.ContextCompat;
7 | import android.support.v4.view.ViewCompat;
8 | import android.util.AttributeSet;
9 | import android.view.View;
10 | import android.widget.ImageView;
11 | import android.widget.RelativeLayout;
12 |
13 | import com.nineoldandroids.animation.Animator;
14 | import com.nineoldandroids.animation.AnimatorListenerAdapter;
15 | import com.nineoldandroids.animation.AnimatorSet;
16 | import com.nineoldandroids.animation.ObjectAnimator;
17 | import com.nineoldandroids.animation.TypeEvaluator;
18 | import com.nineoldandroids.animation.ValueAnimator;
19 |
20 | import java.util.ArrayList;
21 | import java.util.List;
22 | import java.util.Random;
23 |
24 | /**
25 | * Created by Anenn on 6/10/16.
26 | */
27 | public class FlowLikeView extends RelativeLayout {
28 |
29 | private List mLikeDrawables; // 图片的集合
30 | private LayoutParams mLayoutParams; // 用于设置动画对象的位置参数
31 | private Random mRandom; // 用于产生随机数,如生成随机图片
32 |
33 | private int mViewWidth; // 控件的宽度
34 | private int mViewHeight; // 控件的高度
35 |
36 | private int mPicWidth; // 图片的宽度
37 | private int mPicHeight; // 图片的高度
38 |
39 | private int mChildViewHeight; // 在 XML 布局文件中添加的子View的总高度
40 |
41 | public FlowLikeView(Context context) {
42 | this(context, null);
43 | }
44 |
45 | public FlowLikeView(Context context, AttributeSet attrs) {
46 | this(context, attrs, 0);
47 | }
48 |
49 | public FlowLikeView(Context context, AttributeSet attrs, int defStyleAttr) {
50 | super(context, attrs, defStyleAttr);
51 |
52 | initParams();
53 | }
54 |
55 | private void initParams() {
56 | mLikeDrawables = new ArrayList<>();
57 | mLikeDrawables.add(generateDrawable(R.drawable.heart0));
58 | mLikeDrawables.add(generateDrawable(R.drawable.heart1));
59 | mLikeDrawables.add(generateDrawable(R.drawable.heart2));
60 | mLikeDrawables.add(generateDrawable(R.drawable.heart3));
61 | mLikeDrawables.add(generateDrawable(R.drawable.heart4));
62 | mLikeDrawables.add(generateDrawable(R.drawable.heart5));
63 | mLikeDrawables.add(generateDrawable(R.drawable.heart6));
64 | mLikeDrawables.add(generateDrawable(R.drawable.heart7));
65 | mLikeDrawables.add(generateDrawable(R.drawable.heart8));
66 |
67 | // 获取图片的宽高, 由于图片大小一致,故直接获取第一张图片的宽高
68 | mPicWidth = mLikeDrawables.get(0).getIntrinsicWidth();
69 | mPicHeight = mLikeDrawables.get(0).getIntrinsicHeight();
70 |
71 | // 初始化布局参数
72 | mLayoutParams = new RelativeLayout.LayoutParams(mPicWidth, mPicHeight);
73 | mLayoutParams.addRule(CENTER_HORIZONTAL);
74 | mLayoutParams.addRule(ALIGN_PARENT_BOTTOM);
75 |
76 | mRandom = new Random();
77 | }
78 |
79 | private Drawable generateDrawable(int resID) {
80 | return ContextCompat.getDrawable(getContext(), resID);
81 | }
82 |
83 | @Override
84 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
85 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
86 |
87 | if (mChildViewHeight <= 0) {
88 | for (int i = 0, size = getChildCount(); i < size; i++) {
89 | View childView = getChildAt(i);
90 | measureChild(childView, widthMeasureSpec, heightMeasureSpec);
91 | mChildViewHeight += childView.getMeasuredHeight();
92 | }
93 |
94 | // 设置底部间距
95 | mLayoutParams.bottomMargin = mChildViewHeight;
96 | }
97 | }
98 |
99 | @Override
100 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
101 | super.onSizeChanged(w, h, oldw, oldh);
102 |
103 | mViewWidth = getWidth();
104 | mViewHeight = getHeight();
105 | }
106 |
107 | /**
108 | * 动态添加 FlowView
109 | */
110 | public void addLikeView() {
111 | ImageView likeView = new ImageView(getContext());
112 | likeView.setImageDrawable(mLikeDrawables.get(mRandom.nextInt(mLikeDrawables.size())));
113 | likeView.setLayoutParams(mLayoutParams);
114 |
115 | addView(likeView);
116 | startAnimation(likeView);
117 | }
118 |
119 | private void startAnimation(View target) {
120 | // 设置进入动画
121 | AnimatorSet enterAnimator = generateEnterAnimation(target);
122 | // 设置路径动画
123 | ValueAnimator curveAnimator = generateCurveAnimation(target);
124 |
125 | // 设置动画集合, 先执行进入动画,最后再执行运动曲线动画
126 | AnimatorSet finalAnimatorSet = new AnimatorSet();
127 | finalAnimatorSet.setTarget(target);
128 | finalAnimatorSet.playSequentially(enterAnimator, curveAnimator);
129 | finalAnimatorSet.addListener(new AnimationEndListener(target));
130 | finalAnimatorSet.start();
131 | }
132 |
133 | /**
134 | * 生成进入动画
135 | *
136 | * @return 动画集合
137 | */
138 | private AnimatorSet generateEnterAnimation(View target) {
139 | ObjectAnimator alpha = ObjectAnimator.ofFloat(target, "alpha", 0.2f, 1f);
140 | ObjectAnimator scaleX = ObjectAnimator.ofFloat(target, "scaleX", 0.5f, 1f);
141 | ObjectAnimator scaleY = ObjectAnimator.ofFloat(target, "scaleY", 0.5f, 1f);
142 | AnimatorSet enterAnimation = new AnimatorSet();
143 | enterAnimation.playTogether(alpha, scaleX, scaleY);
144 | enterAnimation.setDuration(150);
145 | enterAnimation.setTarget(target);
146 | return enterAnimation;
147 | }
148 |
149 | /**
150 | * 生成曲线运动动画
151 | *
152 | * @return 动画集合
153 | */
154 | private ValueAnimator generateCurveAnimation(View target) {
155 | CurveEvaluator evaluator = new CurveEvaluator(generateCTRLPointF(1), generateCTRLPointF(2));
156 | ValueAnimator valueAnimator = ValueAnimator.ofObject(evaluator,
157 | new PointF((mViewWidth - mPicWidth) / 2, mViewHeight - mChildViewHeight - mPicHeight),
158 | new PointF((mViewWidth) / 2 + (mRandom.nextBoolean() ? 1 : -1) * mRandom.nextInt(100), 0));
159 | valueAnimator.setDuration(3000);
160 | valueAnimator.addUpdateListener(new CurveUpdateLister(target));
161 | valueAnimator.setTarget(target);
162 |
163 | return valueAnimator;
164 | }
165 |
166 | /**
167 | * 生成贝塞儿曲线的控制点
168 | *
169 | * @param value 设置控制点 y 轴上取值区域
170 | * @return 控制点的 x y 坐标
171 | */
172 | private PointF generateCTRLPointF(int value) {
173 | PointF pointF = new PointF();
174 | pointF.x = mViewWidth / 2 - mRandom.nextInt(100);
175 | pointF.y = mRandom.nextInt(mViewHeight / value);
176 |
177 | return pointF;
178 | }
179 |
180 | /**
181 | * 自定义估值算法, 计算对象当前运动的具体位置 Point
182 | */
183 | private class CurveEvaluator implements TypeEvaluator {
184 |
185 | // 由于这里使用的是三阶的贝塞儿曲线, 所以我们要定义两个控制点
186 | private PointF ctrlPointF1;
187 | private PointF ctrlPointF2;
188 |
189 | public CurveEvaluator(PointF ctrlPointF1, PointF ctrlPointF2) {
190 | this.ctrlPointF1 = ctrlPointF1;
191 | this.ctrlPointF2 = ctrlPointF2;
192 | }
193 |
194 | @Override
195 | public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
196 |
197 | // 这里运用了三阶贝塞儿曲线的公式, 请自行上网查阅
198 | float leftTime = 1.0f - fraction;
199 | PointF resultPointF = new PointF();
200 |
201 | // 三阶贝塞儿曲线
202 | resultPointF.x = (float) Math.pow(leftTime, 3) * startValue.x
203 | + 3 * (float) Math.pow(leftTime, 2) * fraction * ctrlPointF1.x
204 | + 3 * leftTime * (float) Math.pow(fraction, 2) * ctrlPointF2.x
205 | + (float) Math.pow(fraction, 3) * endValue.x;
206 | resultPointF.y = (float) Math.pow(leftTime, 3) * startValue.y
207 | + 3 * (float) Math.pow(leftTime, 2) * fraction * ctrlPointF1.y
208 | + 3 * leftTime * fraction * fraction * ctrlPointF2.y
209 | + (float) Math.pow(fraction, 3) * endValue.y;
210 |
211 | // 二阶贝塞儿曲线
212 | // resultPointF.x = (float) Math.pow(leftTime, 2) * startValue.x + 2 * fraction * leftTime * ctrlPointF1.x
213 | // + ((float) Math.pow(fraction, 2)) * endValue.x;
214 | // resultPointF.y = (float) Math.pow(leftTime, 2) * startValue.y + 2 * fraction * leftTime * ctrlPointF1.y
215 | // + ((float) Math.pow(fraction, 2)) * endValue.y;
216 |
217 | return resultPointF;
218 | }
219 | }
220 |
221 | /**
222 | * 动画曲线路径更新监听器, 用于动态更新动画作用对象的位置
223 | */
224 | private class CurveUpdateLister implements ValueAnimator.AnimatorUpdateListener {
225 | private View target;
226 |
227 | public CurveUpdateLister(View target) {
228 | this.target = target;
229 | }
230 |
231 | @Override
232 | public void onAnimationUpdate(ValueAnimator animation) {
233 | // 获取当前动画运行的状态值, 使得动画作用对象沿着曲线(涉及贝塞儿曲线)运动
234 | PointF pointF = (PointF) animation.getAnimatedValue();
235 | ViewCompat.setX(target, pointF.x);
236 | ViewCompat.setY(target, pointF.y);
237 | // 改变对象的透明度
238 | ViewCompat.setAlpha(target, 1 - animation.getAnimatedFraction());
239 | }
240 | }
241 |
242 | /**
243 | * 动画结束监听器,用于释放无用的资源
244 | */
245 | private class AnimationEndListener extends AnimatorListenerAdapter {
246 | private View target;
247 |
248 | public AnimationEndListener(View target) {
249 | this.target = target;
250 | }
251 |
252 | @Override
253 | public void onAnimationEnd(Animator animation) {
254 | super.onAnimationEnd(animation);
255 |
256 | removeView(target);
257 | }
258 | }
259 | }
260 |
--------------------------------------------------------------------------------