android 仿鱼儿游动

很久之前做过一个风水项目,需要自定义一个放生池,模拟鱼儿在水里游动,用户可以购买鱼然后显示在池子里面,点击还可以弹窗显示放生人的信息。趁着现在有空写篇博客记录下。

先说一下思路,分析下需求可以点击,那么这条鱼优先考虑继承 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 个人感觉这样做有点过重了,其次在帧动画的使用容易导致内存的泄漏,大家有什么好的想法可以留言一起探讨,共同进步!
 

个人博客地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值