很久之前做过一个风水项目,需要自定义一个放生池,模拟鱼儿在水里游动,用户可以购买鱼然后显示在池子里面,点击还可以弹窗显示放生人的信息。趁着现在有空写篇博客记录下。
先说一下思路,分析下需求可以点击,那么这条鱼优先考虑继承 imageView 这样方便我们去处理点击事件,然后是水池,不多想就决定继承viewGroup重写它的onLayout方法不断的进行鱼儿的摆放来咱们上一道菜:清蒸罗非鱼
public class FishModel extends ImageView {
private static final double SPEED = 0.001;
private double progress = 0;
private int fishWidth ;
private int fishHeight ;
private float pos [] = new float[2] ;
private float tan [] = new float[2] ;
private Path path;
private AnimationDrawable anim; //鱼动画
private PathMeasure pMeasure;
public FishModel(Context context) {
this(context, null, 0);
init();
}
private void init() {
FrameLayout.LayoutParams param = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
setLayoutParams(param);
}
//获取鱼下一个移动的点坐标
public Point getNextMovePoi(){
progress = progress < 1 ? progress + SPEED : 1;
if(progress < 1){
pMeasure.getPosTan((int) (pMeasure.getLength() * progress), pos, tan);
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
setRotation(degrees);
return new Point((int)pos[0], (int)pos[1]);
}else{
return null;
}
}
//给鱼设置行走路线
public void setPath(Path path) {
this.path = path;
progress = 0;
pMeasure = new PathMeasure(path,false);
}
public void setAnim(AnimationDrawable anim) {
this.anim = anim;
setAdjustViewBounds(true);
setImageDrawable(anim);
fishWidth = anim.getMinimumWidth();
fishHeight = anim.getMinimumHeight();
}
}
每一条鱼都有自己的游走路径,因此声明一个path变量存放游动的路径,然后鱼儿的游动就使用帧动画一帧帧的播放,使用progress去记录鱼儿当前游动的位置(游动的路径在创建的时候就已经生成好了)。
当池子进行onLayout的时候,摆放鱼儿的位置需要一个点,这里就是使用progress去获取当前path的指定点,因此我们需要使用PathMeasure这个类来完成
pMeasure.getPosTan((int) (pMeasure.getLength() * progress), pos, tan);
这个方法可以获取到点的位置以及相应点的切线。
由于每条鱼的游动路线不可能是一条笔直笔直的路线,生成路径使用的是贝塞尔曲线(稍后会介绍)因此我们还需要计算相应位置的时候这条鱼的旋转角度(总不能鱼头只指向一个方向的游动吧)
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
setRotation(degrees);
具体的公式我就不介绍了都是些高中的数学知识(其实我数学不过关)接下来就是鱼儿的生成,新建一个FishManager类,代码比较多就贴些关键的代码
public class FishManager {
private final int FRAME_INTERVAL = 150;
private final int MAX_COUNT = 17; //最大数量
private int width;
private int height;
private int fishWidth;
private int fishHeight;
private Context context;
private Random random ;
private List<FishModel> fishs ;
public FishManager(Context context,int width,int height){
this.width = width;
this.height = height;
this.context = context;
random = new Random();
fishs = new ArrayList<FishModel>();
}
/**
* 创建一只鱼
*/
public FishModel createFish(FishBean fishBean){
FishModel fishModel = new FishModel(context);
fishModel.setAnim(getFishAnim());
fishModel.setPath(getRandomPath());
fishs.add(fishModel);
return fishModel;
}
/**
* 获取随机路线
* @return
*/
public Path getRandomPath(){
Path path = new Path();
List<Point> points = getRandomPoint();
path.moveTo(points.get(0).x,points.get(0).y);
path.cubicTo(points.get(1).x,points.get(1).y,points.get(2).x,points.get(2).y,points.get(3).x,points.get(3).y);
return path;
}
//获取贝塞尔4个随机点
private List<Point> getRandomPoint(){
int num = getRandomInt(200,false);
List<Point> points = null;
//随机鱼儿的游动方向
switch (num % 4){
case 0:
points = fromeLeft2Right();
break;
case 1:
points = fromeBottom2Top();
break;
case 2:
points = fromeRight2Left();
break;
default:
points = fromeTop2Bottom();
break;
}
return points;
}
省略了一大堆代码
}
这个类只需要关注生成鱼儿路径这一块就好了,也就是生成贝塞尔曲线的部分
生成贝塞尔曲线需要用到path类的cubicTo()方法去描述一条三阶贝塞尔曲线,只需要我们传入3个点坐标即可。
path.cubicTo(points.get(1).x,points.get(1).y,points.get(2).x,points.get(2).y,points.get(3).x,points.get(3).y);
相关的贝塞尔曲线知识可以参考这博客贝塞尔曲线
接下来就是池子的view。
public class FreePoolView extends ViewGroup implements View.OnClickListener{
private static final int HANDLE_REFRSH = 1; //刷新鱼的游动
private static final int HANDLE_ADD = 0; //添加新的鱼
private final int FISH_COME_TIME = 5 * 100 ; //鱼出场间隔
private final int INTERVAL = 10; //重绘间隔时间
private int width;
private int height;
private static int fishW;
private static int fishH;
private Paint mPaint;
private List<FishModel> fishs;
private FishManager fManager;
private MyHandler mHandler;
public FreePoolView(Context context) {
this(context, null);
}
public FreePoolView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FreePoolView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
setWillNotDraw(false);
mHandler = new MyHandler(this);
fishs = new ArrayList<FishModel>();
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
mPaint.setColor(getResources().getColor(R.color.colorAccent));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),MeasureSpec.getSize(heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if(fishs != null && fishs.size()!= 0){
for(int i = 0 ; i < fishs.size() ; i++){
FishModel fish = fishs.get(i);
Point poi = fish.getNextMovePoi(); //获取鱼儿下一个移动点
if(poi != null){
fish.layout(poi.x,poi.y-fishH,poi.x+fishW,poi.y);
}else{
fish.setPath(fManager.getRandomPath());
poi = fish.getNextMovePoi();
fish.layout(poi.x,poi.y-fishH,poi.x+fishW,poi.y);
}
}
mHandler.sendEmptyMessageDelayed(HANDLE_REFRSH,INTERVAL);
}
}
/**
* 添加一只鱼
*/
public void addFish(FishBean fishBean){
Message msg = Message.obtain(mHandler,HANDLE_ADD,fishBean);
mHandler.sendMessageDelayed(msg,FISH_COME_TIME);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w,h,oldw,oldh);
width = w;
height = h;
if(fManager == null){
fManager = new FishManager(getContext(),w,h);
}
}
@Override
public void onClick(View v) {
Toast.makeText(getContext(), "鱼: 点你妹啊", Toast.LENGTH_SHORT).show();
}
private static class MyHandler extends Handler{
WeakReference<FreePoolView> weakView;
public MyHandler(FreePoolView view){
weakView = new WeakReference<FreePoolView>(view);
}
@Override
public void handleMessage(Message msg) {
FreePoolView v = weakView.get();
if(v == null)
return;
switch (msg.what){
//刷新鱼游动
case HANDLE_REFRSH:
v.requestLayout();
break;
//往池子里面加鱼
case HANDLE_ADD:
FishBean bean = (FishBean) msg.obj;
FishModel model = v.getfManager().createFish(bean);
fishW = model.getFishWidth();
fishH = model.getFishHeight();
model.setOnClickListener(v);
v.getFishs().add(model);
v.addView(model);
model.start();
break;
}
}
}
@Override
protected void onDetachedFromWindow() {
if(mHandler != null){
mHandler.removeMessages(HANDLE_REFRSH);
mHandler.removeMessages(HANDLE_ADD);
mHandler = null;
}
if(fishs != null && fishs.size() != 0){
for(FishModel fish : fishs){
fish.stop();
}
}
super.onDetachedFromWindow();
}
public FishManager getfManager() {
return fManager;
}
public List<FishModel> getFishs() {
return fishs;
}
}
这里重点在于onLayout方法,在里面进行了鱼儿的摆放,然后通过handle每隔10ms进行一次鱼儿的layout,达到鱼儿游动的效果具体的代码差不多就这样了,完整的源码请点击这里
现在回过头来看2年前的代码,其实觉得还有很多不足的地方以及优化的空间,使用viewGroup+imageView 个人感觉这样做有点过重了,其次在帧动画的使用容易导致内存的泄漏,大家有什么好的想法可以留言一起探讨,共同进步!