├── 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 | ![](https://cloud.githubusercontent.com/assets/7321351/15959979/1a9a51bc-2f30-11e6-9ec9-c052cec47923.gif) 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 | ![f603918fa0ec08fad54f8dff58ee3d6d55fbda1f](https://cloud.githubusercontent.com/assets/7321351/15960401/349b2c38-2f32-11e6-9ed8-2ca0ed7041a6.jpg) 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 | --------------------------------------------------------------------------------