├── .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 | ![image](https://github.com/CCY0122/bounceballview/blob/master/bounceball1%20(2).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 | ![image](https://github.com/CCY0122/bounceballview/blob/master/bounceball3%20(2).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 | 8 | 9 | 10 | 11 | 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 |