先上效果图
洒豆子的效果,突发奇想,觉得这个动画挺有意思的,就抽空写了一个玩玩
绘制流程:
定义6个‘’豆子‘’,每个豆子有各自的属性,大小,抛出的速度等,然后控制每个的方向和状态,回弹效果使用差值器 BounceInterpolator
package com.fragmentapp.view.beans; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.BounceInterpolator; import com.fragmentapp.R; import com.fragmentapp.helper.RandomUtil; /** * Created by liuzhen on 2017/1/17. */ public class BeansView extends View { private Paint paint; private int mWidth; private int mHeight; private int top; private ValueAnimator va; private Beans beans1,beans2,beans3,beans4,beans5,beans6; public BeansView(Context context) { this(context, null); } public BeansView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public BeansView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { Log.e("tag","init"); setWillNotDraw(false); paint = new Paint(); paint.setAntiAlias(true); paint.setStyle(Paint.Style.FILL); paint.setColor(getResources().getColor(R.color.color_ff9c19)); //随机生成球体的大小、 beans1 = new Beans(RandomUtil.random(5,15)); beans2 = new Beans(RandomUtil.random(5,15)); beans3 = new Beans(RandomUtil.random(5,15)); beans4 = new Beans(RandomUtil.random(5,15)); beans5 = new Beans(RandomUtil.random(5,15)); beans6 = new Beans(RandomUtil.random(5,15)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { mWidth = getWidth(); mHeight = getHeight(); this.top = top; startAnim(); } } @Override protected void onDraw(Canvas canvas) { //正常向右掉落,这里也可以利用随机生成方向,这里就固定左边三个右边三个 canvas.drawCircle(beans1.getCx(), beans1.getCy(), beans1.getRadius(), paint); canvas.drawCircle(beans2.getCx(), beans2.getCy(), beans2.getRadius(), paint); canvas.drawCircle(beans3.getCx(), beans3.getCy(), beans3.getRadius(), paint); //让球往左边掉落 canvas.drawCircle(-beans4.getCx()+mWidth, beans4.getCy(), beans4.getRadius(), paint); canvas.drawCircle(-beans5.getCx()+mWidth, beans5.getCy(), beans5.getRadius(), paint); canvas.drawCircle(-beans6.getCx()+mWidth, beans6.getCy(), beans6.getRadius(), paint); } public void startAnim() { if (mWidth == 0) return; beans1.setState(0); beans1.setOff(0); beans1.setRand(RandomUtil.random(20));//随机生成抛出的速度值 beans2.setState(0); beans2.setOff(0); beans2.setRand(RandomUtil.random(20)); beans3.setState(0); beans3.setOff(0); beans3.setRand(RandomUtil.random(20)); beans4.setState(0); beans4.setOff(0); beans4.setRand(RandomUtil.random(20)); beans5.setState(0); beans5.setOff(0); beans5.setRand(RandomUtil.random(20)); beans6.setState(0); beans6.setOff(0); beans6.setRand(RandomUtil.random(20)); va = ValueAnimator.ofFloat(top, mHeight - top); va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float val = (float)animation.getAnimatedValue(); beans1.setCy(val); beans1.move(mWidth);//先移动坐标,实际上是改变了off偏移量的值 beans1.setCx(mWidth / 2 + beans1.getOff());//刷新X轴坐标 beans2.setCy(val); beans2.move(mWidth); beans2.setCx(mWidth / 2 + beans2.getOff()); beans3.setCy(val); beans3.move(mWidth); beans3.setCx(mWidth / 2 + beans3.getOff()); beans4.setCy(val); beans4.move(mWidth); beans4.setCx(mWidth / 2 + beans4.getOff()); beans5.setCy(val); beans5.move(mWidth); beans5.setCx(mWidth / 2 + beans5.getOff()); beans6.setCy(val); beans6.move(mWidth); beans6.setCx(mWidth / 2 + beans6.getOff()); invalidate(); } }); va.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { //防止停止后球体因为半径的不一样而降落到地面的水平不一样,统一水平线 beans1.setCy(mHeight - beans1.getRadius()); beans2.setCy(mHeight - beans2.getRadius()); beans3.setCy(mHeight - beans3.getRadius()); beans4.setCy(mHeight - beans4.getRadius()); beans5.setCy(mHeight - beans5.getRadius()); beans6.setCy(mHeight - beans6.getRadius()); invalidate(); } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }); va.setInterpolator(new BounceInterpolator());//重力差值器 va.setDuration(3000); va.setRepeatMode(ValueAnimator.RESTART); va.start(); } public void stopAnim() { va.cancel(); va = null; } }
这里主要把逻辑封装到单独的对象里面去了,所以view类看起来很清爽
下面是豆子类
package com.fragmentapp.view.beans; import android.util.Log; /** * Created by liuzhen on 2018/1/18. */ public class Beans { public Beans(){ } public Beans(int radius){ this.radius = radius; } /**X坐标*/ private float cx; /**Y坐标*/ private float cy; /**偏移量*/ private float off; /**随机生成的速度值*/ private float rand; /**是否碰到边缘*/ private int state; /**圆球的大小*/ private float radius; /**移动 X 坐标,并且碰到边界后回弹*/ public void move(int width){ if (cx < 0 || state == 1) {//碰到左边的边缘 state = 1; off += rand; } else if (cx >= width || state == 2) {//碰到右边的边缘 state = 2; off -= rand; }else if(state == 0) { state = 0; off += rand; } // Log.e("tag","-- cx "+(int)cx + " width "+width + " state "+state); } public float getCx() { return cx; } public void setCx(float cx) { this.cx = cx; } public float getOff() { return off; } public void setOff(float off) { this.off = off; } public float getRand() { return rand; } public void setRand(float rand) { this.rand = rand; } public int getState() { return state; } public void setState(int state) { this.state = state; } public float getCy() { return cy; } public void setCy(float cy) { this.cy = cy; } public float getRadius() { return radius; } public void setRadius(float radius) { this.radius = radius; } }
主要逻辑集中在move方法中
默认是正常抛出,然后碰到边缘后改变状态往回弹
使用上只关注两个方法就行了
这里是把控件放在了一个dialog里面,这个看个人喜欢,显然dialog不是很适合,或者可以加到下拉库的头部上去,效果应该不错
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/shape_dialog_bg" android:padding="@dimen/d20.0" android:orientation="vertical" android:id="@+id/root"> <com.fragmentapp.view.beans.BeansView android:id="@+id/beans" android:layout_width="@dimen/d350.0" android:layout_height="@dimen/d300.0" android:layout_gravity="center_horizontal" /> <!--<View--> <!--android:layout_width="match_parent"--> <!--android:layout_height="@dimen/d1.0"--> <!--android:background="@color/white"/>--> <TextView android:id="@+id/tv_val" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="@dimen/d20.0" android:text="加载中..." android:textColor="@color/color_cccccc" android:textSize="@dimen/d43.0" /> </LinearLayout>
到这里基本完成了,不过这样的效果绘制显然不是很好看,而且很low,所以要优化一下绘制的图形,让它看起来更高大上一些,最优先需要改的肯定是圆球了,各种3D形状,很好看,不过没法绘制,只能网上找个图片直接drawbitmap了,大多的华丽都是跟图片搭配的
然而就是代码了,代码看起来也有点low,也需要优化一下
1:代码优化
以前的是固定对象,然后绘制,肯定需要稍微动态一点了
2:圆球绘制
以前的是直接绘制圆,不好看,从网上下载一个圆形 icon,代替圆,看起来更立体一点
这里的做法是把圆形对象也放进实体类里面去,方便统一获取,然后创建一个统一管理实体类的集合
先获取到我们的icon,随机产生圆球的大小,添加进集合
接下来所有的固定的地方都换成for循环来代替
是不是方便多了,看起来简洁多了,可以对比两边的代码,你会发现,哎呦,不错哦
package com.fragmentapp.view.beans; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.drawable.BitmapDrawable; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.BounceInterpolator; import com.fragmentapp.R; import com.fragmentapp.helper.RandomUtil; import java.util.ArrayList; import java.util.List; /** * Created by liuzhen on 2017/1/17. */ public class BeansView extends View { private Paint paint; private int mWidth; private int mHeight; private int top; private ValueAnimator va; private List<Beans> beans = null; public BeansView(Context context) { this(context, null); } public BeansView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public BeansView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { Log.e("tag","init"); setWillNotDraw(false); paint = new Paint(); paint.setAntiAlias(true); paint.setStyle(Paint.Style.FILL); paint.setColor(getResources().getColor(R.color.color_ff9c19)); beans = new ArrayList<>(); Bitmap bitmap = ((BitmapDrawable)(getResources().getDrawable(R.mipmap.ball))).getBitmap(); //随机生成球体的大小、 for(int i = 0;i < 6; i ++){ int radius = RandomUtil.random(15,30); final Beans b = new Beans(radius); b.setDirection(i % 2 == 0 ? Beans.Left : Beans.Right); b.bitmap = Bitmap.createScaledBitmap(bitmap,radius,radius,true); beans.add(b); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { mWidth = getWidth(); mHeight = getHeight(); this.top = top; startAnim(); } } @Override protected void onDraw(Canvas canvas) { for (Beans b : beans) { if (b.bitmap != null) { if (b.getDirection() == Beans.Left) { canvas.drawBitmap(b.bitmap, b.getCx(), b.getCy(), null); } else { canvas.drawBitmap(b.bitmap, -b.getCx() + mWidth, b.getCy(), null); } } } } public void startAnim() { if (mWidth == 0 || beans.size() == 0) return; for (Beans b : beans) { b.setState(0); b.setOff(0); b.setRand(RandomUtil.random(20));//随机生成抛出的速度值 } va = ValueAnimator.ofFloat(top, mHeight); va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float val = (float)animation.getAnimatedValue(); for (Beans b : beans) { b.setCy(val - b.getRadius()); b.move(mWidth);//先移动坐标,实际上是改变了off 偏移量的值 b.setCx(mWidth / 2 + b.getOff());//刷新X轴坐标 } postInvalidate(); } }); va.setInterpolator(new BounceInterpolator());//重力差值器 va.setDuration(3500); va.setRepeatMode(ValueAnimator.RESTART); va.start(); } public void stopAnim() { va.cancel(); va = null; } }
package com.fragmentapp.view.beans; import android.graphics.Bitmap; import android.util.Log; /** * Created by liuzhen on 2018/1/18. */ public class Beans { public Beans(){ } public Beans(int radius){ this.radius = radius; } public static final int Left = 0; public static final int Right = 1; private int direction; /**X坐标*/ private float cx; /**Y坐标*/ private float cy; /**偏移量*/ private float off; /**随机生成的速度值*/ private float rand; /**是否碰到边缘*/ private int state; /**圆球的大小*/ private float radius; public Bitmap bitmap; /**移动 X 坐标,并且碰到边界后回弹*/ public void move(int width){ if (cx < 0 || state == 1) {//碰到左边的边缘 state = 1; off += rand; } else if (cx >= width || state == 2) {//碰到右边的边缘 state = 2; off -= rand; }else if(state == 0) { state = 0; off += rand; } // Log.e("tag","-- cx "+(int)cx + " width "+width + " state "+state); } public float getCx() { return cx; } public void setCx(float cx) { this.cx = cx; } public float getOff() { return off; } public void setOff(float off) { this.off = off; } public float getRand() { return rand; } public void setRand(float rand) { this.rand = rand; } public int getState() { return state; } public void setState(int state) { this.state = state; } public float getCy() { return cy; } public void setCy(float cy) { this.cy = cy; } public float getRadius() { return radius; } public void setRadius(float radius) { this.radius = radius; } public int getDirection() { return direction; } public void setDirection(int direction) { this.direction = direction; } }
接下来继续美化,可以在下面添加一个回弹的跳板样式,看起来效果更好,也同样是用二级贝塞尔来实现,先绘制出接触面
一条直线,然后物体挑落到底下的时候在触发接触面的控制点,达到回弹效果,但是怎么知道物体落到了地面吗,差值器我没找到什么好的办法去解决,所以自己先想了一个方法去判断到底了,但是感觉不太好,不过目前本人还没有更好的方法去实现
就是通过自己自定义差值器,然后在差值器里面添加回调判断,代码如下
package com.fragmentapp.view; import android.util.Log; import android.view.animation.BounceInterpolator; import android.view.animation.Interpolator; /** * Created by liuzhen on 2018/2/2. */ public class MyBounceInterpolator implements Interpolator { private CallBack callBack; private boolean t_3 = false,t_7 = false,t_9 = false,t_1 = false; public MyBounceInterpolator(CallBack callBack){ this.callBack = callBack; } private float bounce(float t) { return t * t * 8.0f; } @Override public float getInterpolation(float t) { // Log.e("tag",""+t); t *= 1.1226f; if (t < 0.3535f) { // Log.e("tag","----1");0.1 0.2 0.3 if (t_3 == false){ t_3 = true; // callBack.toLast(); } return bounce(t); } else if (t < 0.7408f) { // Log.e("tag","----2");4=0.6 5=0.8 6=0.8 if (t_7 == false){ t_7 = true; callBack.toLast(); } return bounce(t - 0.54719f) + 0.7f; } else if (t < 0.9644f) { // Log.e("tag","----3");7=0.8 8=0.9 9=1 if (t_9 == false){ t_9 = true; callBack.toLast(); } return bounce(t - 0.8526f) + 0.9f; } else { // Log.e("tag","----4"); if (t_1 == false){ t_1 = true; callBack.toLast(); } return bounce(t - 1.0435f) + 0.95f; } } public interface CallBack{ void toLast(); } }
这样就是说在物体开始回弹的时候回调,并且只有一次,不过看效果后发现有点误差,就是前面的几次回弹物体并没有在最底部就回弹了,这个想想后发现没有什么合适的方法解决,看看是不是可以改变它原本的算法去控制,后来在判断的地方把数值提高了一点,
发现果然有效果,好了,因为每个回弹的数值都是经过那里的
到这里告一段落了,不过不知道有没有发现有点问题,因为我的背景设置的是白色的,而且吃食的那段动画其实也是重新绘制了一层白色,所以看不出来,但是如果背景没有设置或者不是白色,那么就会出现问题了,这显然也不是我们想要的
于是还是得在次去优化,这里想来想去也只能在创建一个画板去绘制了,分两块,就类似于橡皮擦一样的效果,所以得小小的修改一下
然后把吃食物的那段的绘制移到新的画板中,这样就可以达到吃的效果了
protected void onDraw(Canvas canvas) { if (!isDraw) return; //绘制大球 path.reset(); path.moveTo(startPoint.x + faceRadius/2,startPoint.y); path.cubicTo(movePoint1.x,movePoint1.y + faceRadius/2,movePoint2.x,movePoint2.y + faceRadius/2,endPoint.x - faceRadius/2,endPoint.y); canvas.drawPath(path, facePaint); //绘制小球,需要在最后面绘制 canvas.drawArc(rectF, angle, 360 - angle * 2, true, facePaint); if (mBitmap != null) { canvas.drawBitmap(mBitmap, 0, 0, defPaint); } } private void draw(){ //绘制“食物” foodPath.reset(); foodPath.moveTo(startPoint.x,startPoint.y); foodPath.cubicTo(movePoint1.x,movePoint1.y,movePoint2.x,movePoint2.y,endPoint.x,endPoint.y); mCanvas.drawPath(foodPath, effectPaint); //吃掉“食物” for (PointF f : clears) { RectF rectF = new RectF(f.x-foodRadius*2,f.y-foodRadius*2,f.x+foodRadius*2,f.y+foodRadius*2); mCanvas.drawOval(rectF,clearPaint); } postInvalidate(); }
在次运行,可以看到,背景已经都移除,在无背景的状态下正常显示动画
下面是下载地址,谢谢收藏 ^_^