├── .gitattributes
├── .gitignore
├── README.md
├── bounceball1 (2).gif
├── bounceball2 (2).gif
├── bounceball3 (2).gif
├── bounceballview.iml
├── build.gradle
├── proguard-rules.pro
└── src
├── androidTest
└── java
│ └── com
│ └── example
│ └── ccy
│ └── bounceballview
│ └── ExampleInstrumentedTest.java
├── main
├── AndroidManifest.xml
├── java
│ └── com
│ │ └── example
│ │ └── ccy
│ │ └── bounceballview
│ │ ├── BallDialog.java
│ │ ├── BounceBallView.java
│ │ ├── MainActivity.java
│ │ └── MultiDecelerateAccelerateInterpolator.java
└── res
│ ├── drawable
│ ├── round_bg.xml
│ └── xm_logo.png
│ ├── layout
│ ├── activity_main.xml
│ └── bounce_ball_view.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ └── values
│ ├── attrs.xml
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
└── test
└── java
└── com
└── example
└── ccy
└── bounceballview
└── ExampleUnitTest.java
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 |
7 | # Standard to msysgit
8 | *.doc diff=astextplain
9 | *.DOC diff=astextplain
10 | *.docx diff=astextplain
11 | *.DOCX diff=astextplain
12 | *.dot diff=astextplain
13 | *.DOT diff=astextplain
14 | *.pdf diff=astextplain
15 | *.PDF diff=astextplain
16 | *.rtf diff=astextplain
17 | *.RTF diff=astextplain
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # bounceballview
2 | android小球自由落体弹跳动画效果的自定义控件
3 | 源码解析博客地址:[http://blog.csdn.net/ccy0122/article/details/77427795](http://blog.csdn.net/ccy0122/article/details/77427795)
4 |
5 | 效果图:
6 | ====
7 |
(因为gif图被压缩的原因动画看起来有点不流畅)
8 |
9 | .gif)
10 |
11 | 使用方法:
12 | ====
13 | **XML:**
14 | ```
15 |
24 | ```
25 |
可用的属性:
26 | ------
27 |
28 | * bounce_count :小球弹跳次数
29 | * ball_color:小球颜色
30 | * ball_count:小球数量
31 | * ball_radius:小球半径
32 | * ball_delay:小球出现时间间隔(当小球数大于1时)
33 | * anim_duration:小球一次动画时长
34 | * physic_mode : 开启物理效果(下落加速上升减速)
35 | * random_color: 开启小球颜色随机
36 | * random_radius: 开启小球大小随机(在基础大小上下浮动)
37 | * random_path: 开启小球路径随机(在基础路径坐标上下浮动)
38 |
39 |
40 | **也可以在代码中进行配置:**
41 |
42 | ```
43 | bbv1 = (BounceBallView) findViewById(R.id.bbv1);
44 | bbv.config()
45 | .ballCount(15)
46 | .bounceCount(3)
47 | .ballDelay(220)
48 | .duration(3300)
49 | .radius(15)
50 | .isPhysicMode(true)
51 | .isRamdomPath(true)
52 | .isRandomColor(true)
53 | .isRandomRadius(true)
54 | .apply();
55 |
56 | ```
57 |
58 |
59 |
60 | **最后开启动画:**
61 |
62 | ```
63 | bbv1.start();
64 | ```
65 |
66 | 欢迎star,欢迎交流。
67 |
68 | **将该控件应用于加载框,还是很美观的:**
69 | .gif)
70 |
71 |
--------------------------------------------------------------------------------
/bounceball1 (2).gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/bounceball1 (2).gif
--------------------------------------------------------------------------------
/bounceball2 (2).gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/bounceball2 (2).gif
--------------------------------------------------------------------------------
/bounceball3 (2).gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/bounceball3 (2).gif
--------------------------------------------------------------------------------
/bounceballview.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | generateDebugSources
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 25
5 | buildToolsVersion "26.0.0"
6 |
7 | defaultConfig {
8 | applicationId "com.example.ccy.bounceballview"
9 | minSdkVersion 16
10 | targetSdkVersion 25
11 | versionCode 1
12 | versionName "1.0"
13 |
14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
15 |
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | compile fileTree(dir: 'libs', include: ['*.jar'])
27 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
28 | exclude group: 'com.android.support', module: 'support-annotations'
29 | })
30 | compile 'com.android.support:appcompat-v7:25.3.1'
31 | compile 'com.android.support.constraint:constraint-layout:1.0.2'
32 | testCompile 'junit:junit:4.12'
33 | }
34 |
--------------------------------------------------------------------------------
/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 D:\AndroidSDK/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 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/src/androidTest/java/com/example/ccy/bounceballview/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.example.ccy.bounceballview;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.example.ccy.bounceballview", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/main/java/com/example/ccy/bounceballview/BallDialog.java:
--------------------------------------------------------------------------------
1 | package com.example.ccy.bounceballview;
2 |
3 | import android.app.DialogFragment;
4 | import android.app.FragmentTransaction;
5 | import android.os.Bundle;
6 | import android.support.annotation.Nullable;
7 | import android.view.LayoutInflater;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 |
11 | /**
12 | * Created by ccy on 2017-08-09.
13 | */
14 |
15 | public class BallDialog extends DialogFragment{
16 |
17 | BounceBallView bbv;
18 |
19 | @Override
20 | public void onCreate(Bundle savedInstanceState) {
21 | super.onCreate(savedInstanceState);
22 | setStyle(STYLE_NORMAL,R.style.custom_dialog);
23 |
24 | }
25 |
26 | @Nullable
27 | @Override
28 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
29 | View v = inflater.inflate(R.layout.bounce_ball_view,container);
30 | bbv = (BounceBallView) v.findViewById(R.id.ball_view);
31 | bbv.start();
32 | return v;
33 | }
34 |
35 | public BounceBallView getBBV(){
36 | return bbv;
37 | }
38 |
39 |
40 | @Override
41 | public int show(FragmentTransaction transaction, String tag) {
42 | return super.show(transaction, tag);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/com/example/ccy/bounceballview/BounceBallView.java:
--------------------------------------------------------------------------------
1 | package com.example.ccy.bounceballview;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorListenerAdapter;
5 | import android.animation.ValueAnimator;
6 | import android.content.Context;
7 | import android.content.res.TypedArray;
8 | import android.graphics.Canvas;
9 | import android.graphics.Color;
10 | import android.graphics.Paint;
11 | import android.graphics.Path;
12 | import android.graphics.PathMeasure;
13 | import android.graphics.PointF;
14 | import android.util.AttributeSet;
15 | import android.util.Log;
16 | import android.util.TypedValue;
17 | import android.view.View;
18 | import android.view.animation.Interpolator;
19 | import android.view.animation.LinearInterpolator;
20 |
21 | /**
22 | * Created by ccy on 2017-08-08.
23 | */
24 |
25 | public class BounceBallView extends View {
26 | /**
27 | * 常量
28 | */
29 | private final static String TAG = "BounceBallView";
30 | private final float DEFAULT_BALL_RADIUS = dp2px(5);
31 | private static final int DEFAULT_BALL_COLOR = 0xff000000;
32 | private static final int DEFAULT_BOUNCE_COUNT = 2;
33 | private static final int DEFAULT_ANIM_DURATION = 2400;
34 | private static final int DEFAULT_BALL_COUNT = 10;
35 | private static final int DEFAULT_BALL_DELAY = (int) (DEFAULT_ANIM_DURATION / DEFAULT_BALL_COUNT);
36 |
37 |
38 |
39 | /**
40 | * 数据
41 | */
42 | private float radius;
43 | private int ballColor;
44 | private int bounceCount; //回弹次数 >=0
45 | private int ballCount;
46 | private int ballDelay; //当ballCount>1时,相邻小球开始下落的时间间隔
47 | private float defaultPadding;//默认偏移(left、top、right)
48 | private float defaultPaddingBottom; //bottom偏移(比默认偏移稍大一点)
49 | private int defaultWidth;
50 | private int defaultHeight;
51 | private int viewWidth;
52 | private int viewHeight;
53 | private float skipLength; //起点需无视的路径长
54 | /**
55 | * 绘图
56 | */
57 | private Paint[] paint;
58 | private Path path;
59 | private PathMeasure pathMeasure;
60 | private float[] pos = new float[2]; //存储某点的坐标值
61 | private float[] tan = new float[2]; //存储某点正切值
62 | private float[] segmentLength;
63 | private boolean isRandomBallPath = true; // 是否开启小球轨迹略微随机偏移
64 | private boolean isRandomColor = true; //是否开启小球随机颜色
65 | private boolean isRandomRadius = true; //是否开启小球随机大小(基础大小上下浮动)
66 | private float[] randomTransRatioX;
67 | private float[] randomTransRatioY;
68 | private int[] randomBallColors;
69 | private float[] randomRadius;
70 | /**
71 | * 动画
72 | */
73 | private int defaultDuration;
74 | private boolean isPhysicsMode = true; //是否开启物理效果(下落加速,上弹减速)
75 | private Interpolator physicInterpolator; //物理效果插值器
76 | private ValueAnimator[] translateAnim; // 作用与小球位置变换
77 | private float[] translateFraction; //动画比例 [0,1]
78 | MultiDecelerateAccelerateInterpolator interCreater;
79 | private Interpolator defaultInterpolator = new LinearInterpolator();
80 |
81 | /**
82 | * 动态配置
83 | */
84 | private boolean isTransaction = false; //是否已开启动态配置事务
85 |
86 |
87 |
88 | public BounceBallView(Context context) {
89 | this(context, null);
90 | }
91 |
92 | public BounceBallView(Context context, AttributeSet attrs) {
93 | this(context, attrs, 0);
94 | }
95 |
96 | public BounceBallView(Context context, AttributeSet attrs, int defStyle) {
97 | super(context, attrs, defStyle);
98 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.BounceBallView);
99 | radius = ta.getDimension(R.styleable.BounceBallView_ball_radius, DEFAULT_BALL_RADIUS);
100 | ballColor = ta.getColor(R.styleable.BounceBallView_ball_color, DEFAULT_BALL_COLOR);
101 | bounceCount = ta.getInt(R.styleable.BounceBallView_bounce_count, DEFAULT_BOUNCE_COUNT);
102 | defaultDuration = ta.getInteger(R.styleable.BounceBallView_anim_duration, DEFAULT_ANIM_DURATION);
103 | ballCount = ta.getInteger(R.styleable.BounceBallView_ball_count, DEFAULT_BALL_COUNT);
104 | ballDelay = ta.getInteger(R.styleable.BounceBallView_ball_delay, DEFAULT_BALL_DELAY);
105 | isPhysicsMode = ta.getBoolean(R.styleable.BounceBallView_physic_mode, true);
106 | isRandomColor = ta.getBoolean(R.styleable.BounceBallView_random_color, true);
107 | isRandomRadius = ta.getBoolean(R.styleable.BounceBallView_random_radius, true);
108 | isRandomBallPath = ta.getBoolean(R.styleable.BounceBallView_random_path, true);
109 | ta.recycle();
110 |
111 | checkAttrs(); // 检查合法性
112 |
113 | initData();
114 | }
115 |
116 | private void checkAttrs() {
117 | radius = radius >= 0 ? radius : DEFAULT_BALL_RADIUS;
118 | // ballColor = ballColor >= 0 ? ballColor : DEFAULT_BALL_COLOR;
119 | bounceCount = bounceCount >= 0 ? bounceCount : DEFAULT_BOUNCE_COUNT;
120 | ballCount = ballCount >= 1 ? ballCount : DEFAULT_BALL_COUNT;
121 | ballDelay = ballDelay >= 0 ? ballDelay : DEFAULT_BALL_DELAY;
122 | defaultDuration = defaultDuration >= 0 ? defaultDuration : DEFAULT_ANIM_DURATION;
123 | }
124 |
125 |
126 | private void initData() {
127 |
128 | defaultPadding = 2 * radius + dp2px(2);
129 | defaultPaddingBottom = 2 * radius + dp2px(15);
130 | defaultWidth = (int) (2 * defaultPadding + dp2px(200));
131 | defaultHeight = (int) (defaultPadding + defaultPaddingBottom + dp2px(80));
132 |
133 | paint = new Paint[ballCount];
134 | for (int i = 0; i < paint.length; i++) {
135 | paint[i] = new Paint(Paint.ANTI_ALIAS_FLAG);
136 | paint[i].setColor(ballColor);
137 | paint[i].setStyle(Paint.Style.FILL);
138 | }
139 |
140 | path = new Path();
141 | pathMeasure = new PathMeasure();
142 | randomBallColors = new int[ballCount];
143 | randomRadius = new float[ballCount];
144 | randomTransRatioX = new float[ballCount];
145 | randomTransRatioY = new float[ballCount];
146 |
147 | translateFraction = new float[ballCount];
148 | translateAnim = new ValueAnimator[ballCount];
149 |
150 | }
151 |
152 |
153 | @Override
154 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
155 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
156 | int wSize = MeasureSpec.getSize(widthMeasureSpec);
157 | int wMode = MeasureSpec.getMode(widthMeasureSpec);
158 | int hSize = MeasureSpec.getSize(heightMeasureSpec);
159 | int hMode = MeasureSpec.getMode(heightMeasureSpec);
160 |
161 |
162 | if (wMode == MeasureSpec.EXACTLY) {
163 | viewWidth = wSize;
164 | } else {
165 | viewWidth = Math.min(defaultWidth, wSize);
166 | }
167 | if (hMode == MeasureSpec.EXACTLY) {
168 | viewHeight = hSize;
169 | } else {
170 | viewHeight = Math.min(defaultHeight, hSize);
171 | }
172 |
173 | setMeasuredDimension(viewWidth, viewHeight);
174 |
175 | initPath();
176 | }
177 |
178 | @Override
179 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
180 | super.onSizeChanged(w, h, oldw, oldh);
181 | if ((oldw != 0 && oldw != w) || (oldh != 0 && oldh != h)) {
182 | initData();
183 | initPath();
184 | }
185 | }
186 |
187 |
188 | /**
189 | * 初始化球体弹跳的路径
190 | */
191 | private void initPath() {
192 | path.reset();
193 |
194 | float intervalX = (viewWidth - 2 * defaultPadding) / (bounceCount + 1); //每次弹跳的间距
195 | PointF start = new PointF();//起点位置
196 | PointF control = new PointF(); //贝塞尔控制点
197 | PointF end = new PointF(); //贝塞尔结束点
198 | start.x = defaultPadding;
199 | start.y = viewHeight - defaultPaddingBottom;
200 |
201 | float controlOffsetY = viewHeight * 0.6f; //控制点向上偏移量,0.6为调试值
202 | float deltaY = (1.2f * viewHeight + controlOffsetY) / (bounceCount + 1); //控制点高度递减值,1.2为调试值
203 |
204 | PathMeasure tempPathMeasure = new PathMeasure();
205 | segmentLength = new float[bounceCount + 1];
206 |
207 | for (int i = 0; i <= bounceCount; i++) {
208 | control.x = start.x + intervalX * (i + 0.5f);
209 | control.y = -controlOffsetY + deltaY * i;
210 | end.x = start.x + intervalX * (i + 1);
211 | end.y = start.y;
212 | if (i == 0) {
213 | path.moveTo(start.x, start.y);
214 | }
215 | if (i == bounceCount) {
216 | end.y = viewHeight;
217 | }
218 | path.quadTo(control.x, control.y, end.x, end.y);
219 |
220 | tempPathMeasure.setPath(path, false);
221 | if (i == 0) { //第一次弹跳的上升阶段不画,记录弹跳一半长度(为效果更好,实际取值0.45
222 | skipLength = tempPathMeasure.getLength() * 0.45f;
223 | }
224 | segmentLength[i] = tempPathMeasure.getLength();
225 | }
226 |
227 | pathMeasure.setPath(path, false);
228 |
229 | if (interCreater == null) {
230 | interCreater = new MultiDecelerateAccelerateInterpolator();
231 | }
232 | physicInterpolator = interCreater.createInterpolator(segmentLength);
233 |
234 | // Log.d("ccy","total length = " + pathMeasure.getLength());
235 | // for (int i = 0; i < segmentLength.length; i++) {
236 | // Log.d("ccy","i = " + i +";length = " + segmentLength[i]);
237 | // }
238 | }
239 |
240 |
241 | @Override
242 | protected void onDraw(Canvas canvas) {
243 |
244 | drawBounceBall(canvas);
245 |
246 |
247 | }
248 |
249 |
250 | private void drawBounceBall(Canvas canvas) {
251 | for (int i = 0; i < ballCount; i++) {
252 | canvas.save();
253 |
254 | if (translateFraction[i] < (skipLength / pathMeasure.getLength())) {
255 | continue;
256 | }
257 | //根据当前动画进度获取path上对应点的坐标和正切
258 | pathMeasure.getPosTan(pathMeasure.getLength() * translateFraction[i],
259 | pos,
260 | tan);
261 |
262 | //路径随机
263 | if (isRandomBallPath) {
264 | pos[0] *= randomTransRatioX[i];
265 | pos[1] *= randomTransRatioY[i];
266 | }
267 |
268 | //颜色随机已在makeRandom里被应用
269 |
270 | canvas.drawCircle(pos[0],
271 | pos[1],
272 | isRandomRadius ? randomRadius[i] : radius,
273 | paint[i]);
274 | canvas.restore();
275 | }
276 | }
277 |
278 |
279 | /**
280 | * 启动动画
281 | */
282 | public void start() {
283 | start(defaultDuration);
284 | }
285 |
286 | /**
287 | * 启动动画
288 | *
289 | * @param duration 动画时长
290 | */
291 | public void start(final int duration) {
292 | post(new Runnable() { //放入队列(保证view已加载完成)
293 | @Override
294 | public void run() {
295 | createAnim(duration); //20170810备注:检查内部有没有重复创建实例
296 | startAnim();
297 | }
298 | });
299 | }
300 |
301 | private void startAnim() {
302 | for (int i = 0; i < translateAnim.length; i++) {
303 | translateAnim[i].start();
304 | }
305 | }
306 |
307 | private void createAnim(int duration) {
308 | for (int i = 0; i < ballCount; i++) {
309 | createTranslateAnim(i, duration, i * ballDelay);
310 | }
311 | }
312 |
313 | private void createTranslateAnim(final int index, int duration, final int delay) {
314 | if (translateAnim[index] == null) {
315 | translateAnim[index] = ValueAnimator.ofFloat(0.0f, 1.0f);
316 | translateAnim[index].setDuration(duration);
317 | translateAnim[index].setRepeatCount(ValueAnimator.INFINITE);
318 | translateAnim[index].setStartDelay(delay);
319 | if (isPhysicsMode) {
320 | translateAnim[index].setInterpolator(physicInterpolator);
321 | } else {
322 | translateAnim[index].setInterpolator(defaultInterpolator);
323 | }
324 | translateAnim[index].addListener(new AnimatorListenerAdapter() {
325 | @Override
326 | public void onAnimationStart(Animator animation) {
327 | super.onAnimationStart(animation);
328 | makeRandom(index);
329 | }
330 |
331 | @Override
332 | public void onAnimationRepeat(Animator animation) {
333 | super.onAnimationRepeat(animation);
334 | makeRandom(index);
335 | }
336 | });
337 |
338 | translateAnim[index].addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
339 | @Override
340 | public void onAnimationUpdate(ValueAnimator animation) {
341 | translateFraction[index] = animation.getAnimatedFraction();
342 |
343 | if (dealFromAlphaAnim(translateFraction[index]) != -1) {
344 | paint[index].setAlpha(dealFromAlphaAnim(translateFraction[index]));
345 | } else if (dealToAlphaAnim(translateFraction[index]) != -1) {
346 | paint[index].setAlpha(dealToAlphaAnim(translateFraction[index]));
347 | } else {
348 | paint[index].setAlpha(255);
349 | }
350 |
351 | invalidate();
352 | }
353 | });
354 | }
355 |
356 | }
357 |
358 | /**
359 | * 数据随机化
360 | *
361 | * @param index
362 | */
363 | private void makeRandom(int index) {
364 |
365 | if (isRandomBallPath) { //坐标是在ondraw里才获得的,故在ondraw里再去应用
366 | randomTransRatioX[index] = (float) (0.9f + (0.2f * Math.random())); //[0.9,1.1)
367 | randomTransRatioY[index] = (float) (0.8f + (0.4f * Math.random())); //[0.8,1.2)
368 | }
369 |
370 | if (isRandomColor) { //不要在ondraw里再应用,会同时覆盖透明度通道,透明动画会失效
371 | randomBallColors[index] = getRandomColor();
372 | paint[index].setColor(randomBallColors[index]);
373 | } else {
374 | paint[index].setColor(ballColor);
375 | }
376 |
377 | if (isRandomRadius) {
378 | randomRadius[index] = (float) (radius * (0.7 + (0.6 * Math.random()))); //[0.7,1.3]
379 | } else {
380 | randomRadius[index] = radius;
381 | }
382 | }
383 |
384 | /**
385 | * 传入一个值,和这个值的上下限,计算该值当前比例
386 | *
387 | * @param start 起点值
388 | * @param end 终点值
389 | * @param current 当前值
390 | * @return
391 | */
392 | private float getEvaluatedFraction(float start, float end, float current) {
393 | if (end - start == 0) {
394 | throw new RuntimeException("传值错误,分母为0: start = " + start + ";end = " + end);
395 | } else {
396 | return (current - start) / (end - start);
397 | }
398 | }
399 |
400 |
401 | /**
402 | * “透明-不透明”的透明值
403 | */
404 | private int dealFromAlphaAnim(float fraction) {
405 | float totalLength = pathMeasure.getLength();
406 | float beginFra = skipLength / totalLength;
407 | float endFrac = segmentLength[0] / totalLength;
408 | if (fraction > beginFra &&
409 | fraction < endFrac) {
410 | return (int) (255 * getEvaluatedFraction(beginFra, endFrac, fraction));
411 | }
412 | return -1;
413 | }
414 |
415 | /**
416 | * “不透明-透明”的透明值
417 | */
418 | private int dealToAlphaAnim(float fraction) {
419 | float totalLength = pathMeasure.getLength();
420 | if (segmentLength.length > 1) {
421 | float beginFra = segmentLength[segmentLength.length - 2] / totalLength;
422 | float endFrac = 1.0f;
423 | if (fraction > beginFra &&
424 | fraction < 1.0f) {
425 | return (int) (255 - 255 * getEvaluatedFraction(beginFra, endFrac, fraction));
426 | }
427 | }
428 | return -1;
429 | }
430 |
431 | private int getRandomColor() {
432 | return Color.argb(255,
433 | (int) (255 * Math.random()),
434 | (int) (255 * Math.random()),
435 | (int) (255 * Math.random()));
436 | }
437 |
438 | /**
439 | * 取消已有动画,释放资源
440 | */
441 | public void cancel(){
442 | if(translateAnim != null){
443 | for (int i = 0; i < translateAnim.length; i++) {
444 | if(translateAnim[i] != null){
445 | translateAnim[i].cancel();
446 | translateAnim[i] = null;
447 | }
448 | }
449 | translateAnim = null;
450 | }
451 | }
452 |
453 | @Override
454 | protected void onDetachedFromWindow() {
455 | // Log.d("ccy","-----------------------------detached");
456 | cancel();
457 | super.onDetachedFromWindow();
458 | }
459 |
460 |
461 | /*
462 | --------------以下为动态配置、各种setter/getter
463 | */
464 |
465 | /**
466 | * 开启配置事务,可连缀配置属性,最后调用{@link #apply()}使配置生效
467 | * @return
468 | */
469 | public BounceBallView config(){
470 | for (int i = 0; i < translateAnim.length; i++) {
471 | translateAnim[i].cancel();
472 | translateAnim[i] = null;
473 | }
474 | isTransaction = true;
475 | return this;
476 | }
477 |
478 | /**
479 | * 使应用配置,在这之前先调用{@link #config()}
480 | */
481 | public void apply(){
482 |
483 | if(isTransaction == true){
484 | Log.w(TAG,"调用apply()之前没有调用过config()函数!");
485 | }
486 | isTransaction = false;
487 | cancel();
488 |
489 | checkAttrs();
490 | initData();
491 | requestLayout();
492 | invalidate();
493 | }
494 |
495 | /**
496 | * 小球半径
497 | * @param radius 默认5dp
498 | * @return
499 | */
500 | public BounceBallView radius(float radius){
501 | check();
502 | this.radius = radius;
503 | return this;
504 | }
505 |
506 | /**
507 | * 小球颜色
508 | * @param ballColor 默认黑色,{@link #isRandomColor} 为true时无效
509 | * @return
510 | */
511 | public BounceBallView ballColor(int ballColor){
512 | check();
513 | this.ballColor = ballColor;
514 | return this;
515 | }
516 |
517 | /**
518 | * 小球数量
519 | * @param ballCount 默认10
520 | * @return
521 | */
522 | public BounceBallView ballCount(int ballCount){
523 | check();
524 | this.ballCount = ballCount;
525 | return this;
526 | }
527 |
528 | /**
529 | * 小球弹跳次数
530 | * @param bounceCount 默认2次
531 | * @return
532 | */
533 | public BounceBallView bounceCount(int bounceCount){
534 | check();
535 | this.bounceCount = bounceCount;
536 | return this;
537 | }
538 |
539 | /**
540 | * 相邻小球出现间隔
541 | * @param ballDelay 默认为(动画时长/小球数量)。单位ms
542 | * @return
543 | */
544 | public BounceBallView ballDelay(int ballDelay){
545 | check();
546 | this.ballDelay = ballDelay;
547 | return this;
548 | }
549 |
550 | /**
551 | * 一个小球一次完整的动画时长
552 | * @param defaultDuration 默认2400ms。单位ms
553 | * @return
554 | */
555 | public BounceBallView duration(int defaultDuration){
556 | check();
557 | this.defaultDuration = defaultDuration;
558 | return this;
559 | }
560 |
561 | /**
562 | * 是否颜色随机
563 | * @param isRandomColor 默认true
564 | * @return
565 | */
566 | public BounceBallView isRandomColor(boolean isRandomColor){
567 | check();
568 | this.isRandomColor = isRandomColor;
569 | return this;
570 | }
571 |
572 | /**
573 | * 是否路径稍微随机偏移
574 | * @param isRandomBallPath 默认true
575 | * @return
576 | */
577 | public BounceBallView isRamdomPath(boolean isRandomBallPath){
578 | check();
579 | this.isRandomBallPath = isRandomBallPath;
580 | return this;
581 | }
582 |
583 | /**
584 | * 小球大小是否稍微随机偏移
585 | * @param isRandomRadius 默认true
586 | * @return
587 | */
588 | public BounceBallView isRandomRadius(boolean isRandomRadius){
589 | check();
590 | this.isRandomRadius = isRandomRadius;
591 | return this;
592 | }
593 |
594 | /**
595 | * 是否开启仿物理效果(下落加速上弹减速)
596 | * @param isPhysicsMode 默认true
597 | * @return
598 | */
599 | public BounceBallView isPhysicMode(boolean isPhysicsMode){
600 | check();
601 | this.isPhysicsMode = isPhysicsMode;
602 | return this;
603 | }
604 |
605 | private void check(){
606 | if(isTransaction){
607 | return;
608 | }else{
609 | throw new RuntimeException("请先调用config()来开启配置,调用apply()来应用配置");
610 | }
611 | }
612 |
613 | public float getRadius() {
614 | return radius;
615 | }
616 |
617 | public int getBallColor() {
618 | return ballColor;
619 | }
620 |
621 | public int getBounceCount() {
622 | return bounceCount;
623 | }
624 |
625 | public int getBallCount() {
626 | return ballCount;
627 | }
628 |
629 | public int getBallDelay() {
630 | return ballDelay;
631 | }
632 |
633 | public boolean isRandomBallPath() {
634 | return isRandomBallPath;
635 | }
636 |
637 | public boolean isRandomColor() {
638 | return isRandomColor;
639 | }
640 |
641 | public boolean isRandomRadius() {
642 | return isRandomRadius;
643 | }
644 |
645 | public int getDefaultDuration() {
646 | return defaultDuration;
647 | }
648 |
649 | public boolean isPhysicsMode() {
650 | return isPhysicsMode;
651 | }
652 |
653 |
654 | private float dp2px(float dp) {
655 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
656 | }
657 | }
658 |
--------------------------------------------------------------------------------
/src/main/java/com/example/ccy/bounceballview/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.example.ccy.bounceballview;
2 |
3 | import android.animation.ValueAnimator;
4 | import android.os.Bundle;
5 | import android.support.v7.app.AppCompatActivity;
6 | import android.util.Log;
7 | import android.view.View;
8 | import android.view.animation.LinearInterpolator;
9 | import android.widget.Button;
10 | import android.widget.CheckBox;
11 | import android.widget.EditText;
12 | import android.widget.Toast;
13 |
14 | import static com.example.ccy.bounceballview.R.id.bbv1;
15 | import static com.example.ccy.bounceballview.R.id.radius;
16 | import static com.example.ccy.bounceballview.R.id.random_color;
17 |
18 | public class MainActivity extends AppCompatActivity {
19 |
20 | private BounceBallView bbv1;
21 | private EditText bounceCount;
22 | private EditText ballCount;
23 | private EditText ballDelay;
24 | private EditText duration;
25 | private EditText radius;
26 | private CheckBox physicMode;
27 | private CheckBox randomPath;
28 | private CheckBox randomColor;
29 | private CheckBox randomRadius;
30 | private Button b1;
31 | private Button b2;
32 | private BallDialog dialog;
33 |
34 | @Override
35 | protected void onCreate(Bundle savedInstanceState) {
36 | super.onCreate(savedInstanceState);
37 | setContentView(R.layout.activity_main);
38 | dialog = new BallDialog();
39 |
40 | bbv1 = (BounceBallView) findViewById(R.id.bbv1);
41 | ballCount = (EditText) findViewById(R.id.ball_count);
42 | ballDelay = (EditText) findViewById(R.id.ball_delay);
43 | bounceCount = (EditText) findViewById(R.id.bounce_count);
44 | radius = (EditText) findViewById(R.id.radius);
45 | duration = (EditText) findViewById(R.id.duration);
46 | physicMode = (CheckBox) findViewById(R.id.physic_mode);
47 | randomColor = (CheckBox) findViewById(random_color);
48 | randomPath = (CheckBox) findViewById(R.id.random_path);
49 | randomRadius = (CheckBox) findViewById(R.id.random_radius);
50 |
51 | bbv1.post(new Runnable() {
52 | @Override
53 | public void run() {
54 | initText();
55 | }
56 | });
57 | bbv1.start();
58 |
59 | b2 = (Button) findViewById(R.id.b2);
60 | b2.setOnClickListener(new View.OnClickListener() {
61 | @Override
62 | public void onClick(View v) {
63 | apply(bbv1);
64 | initText();
65 | bbv1.start();
66 | }
67 | });
68 |
69 | b1 = (Button) findViewById(R.id.b1);
70 | b1.setOnClickListener(new View.OnClickListener() {
71 | @Override
72 | public void onClick(View v) {
73 | dialog.show(getFragmentManager(),"1");
74 | }
75 | });
76 | }
77 |
78 | private void initText(){
79 | ballCount.setText(bbv1.getBallCount()+"");
80 | ballDelay.setText(bbv1.getBallDelay()+"");
81 | bounceCount.setText(bbv1.getBounceCount()+"");
82 | duration.setText(bbv1.getDefaultDuration()+"");
83 | radius.setText(bbv1.getRadius()+"");
84 | physicMode.setChecked(bbv1.isPhysicsMode());
85 | randomRadius.setChecked(bbv1.isRandomRadius());
86 | randomPath.setChecked(bbv1.isRandomBallPath());
87 | randomColor.setChecked(bbv1.isRandomColor());
88 | }
89 |
90 | public void apply(BounceBallView bbv){
91 | if(bbv == null){
92 | Toast.makeText(this,"BounceBallView is null",Toast.LENGTH_LONG).show();
93 | return;
94 | }
95 | try{
96 | bbv.config()
97 | .ballCount(Integer.parseInt(ballCount.getText().toString()))
98 | .bounceCount(Integer.parseInt(bounceCount.getText().toString()))
99 | .ballDelay(Integer.parseInt(ballDelay.getText().toString()))
100 | .duration(Integer.parseInt(duration.getText().toString()))
101 | .radius(Float.parseFloat(radius.getText().toString()))
102 | .isPhysicMode(physicMode.isChecked())
103 | .isRamdomPath(randomPath.isChecked())
104 | .isRandomColor(randomColor.isChecked())
105 | .isRandomRadius(randomRadius.isChecked())
106 | .apply();
107 | }catch (Exception e){
108 | Toast.makeText(this,"错误",Toast.LENGTH_LONG).show();
109 | }
110 | }
111 |
112 | }
113 |
--------------------------------------------------------------------------------
/src/main/java/com/example/ccy/bounceballview/MultiDecelerateAccelerateInterpolator.java:
--------------------------------------------------------------------------------
1 | package com.example.ccy.bounceballview;
2 |
3 | import android.graphics.Path;
4 | import android.graphics.PointF;
5 | import android.support.v4.view.animation.PathInterpolatorCompat;
6 | import android.util.Log;
7 | import android.view.animation.Interpolator;
8 |
9 | /**
10 | * Created by ccy on 2017-08-09.
11 | * 模拟球体弹起时减速下落时加速动画插值器
12 | * 通过绘制若干次减速-加速path路径的构造器。
13 | * 之后利用PathInterpolator将路径转为对应插值器函数
14 | */
15 |
16 | public class MultiDecelerateAccelerateInterpolator {
17 |
18 |
19 | private PointF originStart; //起点,用于构造PathInterpolator时须[0,0]
20 | private PointF originEnd; //终点,用于构造PathInterpolator时须[1,1]
21 | private float intervalX;
22 | private float intervalY;
23 |
24 | private float bezierControlRatioX;
25 | private float bezierControlRatioY;
26 |
27 | /**
28 | * ratiox = 0.2, ratioy = 0.55 为调试值
29 | * 单次路径效果图: http://cubic-bezier.com/#.2,.55,.8,.45
30 | * 可自行调整,配合动画的整体时长,调出比较接近自由落体的效果
31 | */
32 | public MultiDecelerateAccelerateInterpolator() {
33 | this(new PointF(0,0),
34 | new PointF(1,1),
35 | 0.2f ,
36 | 0.55f );
37 | }
38 |
39 |
40 | /**
41 | * 用于构造PathInterpolator时,起点必须为[0,0]终点必须为[1,1]
42 | * 用于构造“先减速后加速”效果时,建议ratiox取值[0,0.5];ratioy取值范围[0,1]且ratiox < ratioy,
43 | * @param start 起点
44 | * @param end 终点
45 | * @param ratiox x比例值,用于控制贝塞尔控制点位置
46 | * @param ratioy y比例值,用于控制贝塞尔控制点位置
47 | */
48 | public MultiDecelerateAccelerateInterpolator(PointF start,PointF end,float ratiox,float ratioy){
49 | originStart = start;
50 | originEnd = end;
51 | intervalX = Math.abs(originEnd.x - originStart.x);
52 | intervalY = Math.abs(originEnd.y - originStart.y);
53 | bezierControlRatioX = ratiox;
54 | bezierControlRatioY = ratioy;
55 | // Log.d("ccy","intervalx,y = " + intervalX + ";" + intervalY);
56 | }
57 |
58 |
59 | /**
60 | * 利用三次贝塞尔构造减速加速函数
61 | * @param segmentLength 从起点到每一段终点的长度集合
62 | * @return
63 | */
64 | public Path createPath(float[] segmentLength){
65 | Path path = new Path();
66 | float ratio;
67 | PointF start = new PointF();
68 | PointF con1 = new PointF();
69 | PointF con2 = new PointF();
70 | PointF end = new PointF();
71 |
72 | float totalLength = segmentLength[segmentLength.length - 1];
73 |
74 | for (int i = 0; i < segmentLength.length; i++) {
75 | ratio = segmentLength[i] / totalLength;
76 | if(i == 0){
77 | start.x = originStart.x;
78 | start.y = originStart.y;
79 | path.moveTo(originStart.x,originStart.y);
80 | }
81 | end.x = intervalX * ratio;
82 | end.y = intervalY * ratio;
83 | con1.x = start.x + (end.x - start.x) * bezierControlRatioX;
84 | con1.y = start.y + (end.y - start.y) * bezierControlRatioY;
85 | con2.x = end.x - (end.x - start.x) * (bezierControlRatioX );
86 | con2.y = end.y - (end.y - start.y) * (bezierControlRatioY );
87 |
88 | path.cubicTo(con1.x,con1.y,
89 | con2.x,con2.y,
90 | end.x,end.y);
91 |
92 | // Log.d("ccy","startx,y = "+start.x+";"+start.y);
93 | // Log.d("ccy","con1x,y = "+con1.x+";"+con1.y);
94 | // Log.d("ccy","con2x,y = "+con2.x+";"+con2.y);
95 | // Log.d("ccy","endx,y = "+end.x+";"+end.y);
96 | start.x = end.x;
97 | start.y = end.y;
98 |
99 | }
100 | return path;
101 | }
102 |
103 | /**
104 | * 构造PathInterpolator
105 | * @param segmentLength
106 | * @return
107 | */
108 | public Interpolator createInterpolator(float[] segmentLength){
109 | Path p = createPath(segmentLength);
110 | Interpolator inter =PathInterpolatorCompat.create(p);
111 | return inter;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/res/drawable/round_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/main/res/drawable/xm_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/src/main/res/drawable/xm_logo.png
--------------------------------------------------------------------------------
/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
23 |
24 |
28 |
29 |
34 |
35 |
40 |
41 |
46 |
47 |
48 |
52 |
53 |
57 |
58 |
63 |
64 |
69 |
70 |
75 |
76 |
77 |
78 |
82 |
83 |
87 |
88 |
93 |
94 |
98 |
103 |
108 |
109 |
113 |
118 |
123 |
124 |
125 |
132 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/src/main/res/layout/bounce_ball_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
14 |
24 |
25 |
--------------------------------------------------------------------------------
/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCY0122/bounceballview/0ac18290f4494cc5f3ff082848c760d566e46bcf/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BounceBallView
3 |
4 |
--------------------------------------------------------------------------------
/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/test/java/com/example/ccy/bounceballview/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.example.ccy.bounceballview;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------