简介
一直对墨迹天气的绚丽的场景蛮感兴趣的,趁有时间,自己就高仿了其中的一个场景,其他场景呢,也是类似的,主要是写对象的AI也就是逻辑了。
代码分析
来看看代码结构吧
这里使用了SurfaceView而不是用的view,其实这个天气的场景绘制更像是游戏开发,使用SurfaceView会更灵活。
public SceneSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); surfaceHolder = getHolder(); surfaceHolder.addCallback(this); setFocusable(true); setFocusableInTouchMode(true); this.setKeepScreenOn(true); }
这就是构造方法了,实现SurfaceHolder.Callback来监听事件
@Override public void surfaceCreated(SurfaceHolder holder) { Log.d("weather", "surfaceCreated"); if (renderThread == null) { renderThread = new RenderThread(surfaceHolder, getContext()); renderThread.start(); } }
在surface创建回调中, 我们生成了一个RenderThread线程来专门做逻辑与绘制。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { width = getMeasuredWidth(); height = getMeasuredHeight(); super.onMeasure(widthMeasureSpec, heightMeasureSpec); Log.d("weather", "onMeasure width=" + width + ",height=" + height); if (renderThread != null) { renderThread.setWidth(width); renderThread.setHeight(height); } }
记录下测量的宽高
@Override public void surfaceDestroyed(SurfaceHolder holder) { Log.d("weather", "surfaceDestroyed"); renderThread.getRenderHandler().sendEmptyMessage(1); }
销毁的时候要发一个消息,具体做什么,下面在来说
下面是RenderThread源码
public class RenderThread extends Thread { private Context context; private SurfaceHolder surfaceHolder; private RenderHandler renderHandler; private Scene scene; public RenderThread(SurfaceHolder surfaceHolder, Context context) { this.context = context; this.surfaceHolder = surfaceHolder; scene = new Scene(context); //add scene/actor scene.setBg(BitmapFactory.decodeResource(context.getResources(), R.drawable.bg0_fine_day)); scene.add(new BirdUp(context)); scene.add(new CloudLeft(context)); scene.add(new CloudRight(context)); scene.add(new BirdDown(context)); scene.add(new SunShine(context)); } @Override public void run() { Log.d("weather", "run"); //在非主线程使用消息队列 Looper.prepare(); renderHandler = new RenderHandler(); renderHandler.sendEmptyMessage(0); Looper.loop(); } public RenderHandler getRenderHandler() { return renderHandler; } public class RenderHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case 0: if (scene.getWidth() != 0 && scene.getHeight() != 0) { draw(); } renderHandler.sendEmptyMessage(0); break; case 1: Looper.myLooper().quit(); break; } } } private void draw() { Canvas canvas = surfaceHolder.lockCanvas(); if (canvas != null) { scene.draw(canvas); surfaceHolder.unlockCanvasAndPost(canvas); } } public void setWidth(int width) { scene.setWidth(width); } public void setHeight(int height) { scene.setHeight(height); } }
在构造方法中添加了场景背景,上下两个鸟,左右各一个云彩和阳光。
这里在run方法里生成了一个线程的消息队列,注意额不是主线程的,其实也可以在里面搞个while循环,就像一般的游戏处理一样, 但是如果使用消息队列,会更轻巧有效。
大家再来看看RenderHandler
情况分2种,一个是绘制的一个是退出的,基本也就这2种了。
还记得在surfaceDestroyed中调用的退出吧,现在就在这了,呵呵
然后就是最重要的draw方法了
绘制是在Scene中操作的,来看看代码
public class Scene { private Context context; private int width; private int height; private Bitmap bg; private List<Actor> actors = new ArrayList<Actor>(); private Paint paint; public Scene(Context context) { this.context = context; paint = new Paint(); paint.setAntiAlias(true); } public void setBg(Bitmap bg) { this.bg = bg; } public void add(Actor actor) { actors.add(actor); } public void draw(Canvas canvas) { canvas.drawBitmap(bg, new Rect(0, 0, bg.getWidth(), bg.getHeight()), new Rect(0, 0, width, height), paint); for (Actor actor : actors) { actor.draw(canvas,width,height); } }
可以在场景中绘制一个背景图和Actor列表
Actor是啥呢,就是对象呗,像鸟啊,云啊,雨啊等等吧
public abstract class Actor { protected Context context; protected Matrix matrix = new Matrix(); protected Actor(Context context) { this.context = context; } public abstract void draw(Canvas canvas, int width, int height); }
这是一个抽象类,Context 可以加载资源文件,Matrix 来描述对象的变换,抽象方法draw就是咱们的逻辑和绘制方法喽
来看看上边的那个小鸟的代码吧
public class BirdUp extends Actor { private static final int[] imgs = new int[]{R.drawable.finedayup_1, R.drawable.finedayup_2, R.drawable.finedayup_3, R.drawable.finedayup_4, R.drawable.finedayup_5, R.drawable.finedayup_6, R.drawable.finedayup_7, R.drawable.finedayup_8}; float initPositionX; float initPositionY; boolean isInit; List<Bitmap> frames; RectF box; RectF targetBox; int curFrameIndex; long lastTime; Paint paint = new Paint(); protected BirdUp(Context context) { super(context); frames = new ArrayList<Bitmap>(); box = new RectF(); targetBox = new RectF(); paint.setAntiAlias(true); } @Override public void draw(Canvas canvas, int width, int height) { //逻辑处理 //初始化 if (!isInit) { initPositionX = width * 0.117F; initPositionY = height * 0.35F; matrix.reset(); matrix.postTranslate(initPositionX, initPositionY); for (int res : imgs) { frames.add(BitmapFactory.decodeResource(context.getResources(), res)); } box.set(0, 0, frames.get(0).getWidth(), frames.get(0).getHeight()); isInit = true; lastTime = System.currentTimeMillis(); return; } //移动 matrix.postTranslate(2, 0); //边界处理 matrix.mapRect(targetBox, box); if (targetBox.left > width) { matrix.postTranslate(-targetBox.right, 0); } //取得帧动画图片 long curTime = System.currentTimeMillis(); curFrameIndex = (int) ((curTime - lastTime) / 500 % 8); Bitmap curBitmap = frames.get(curFrameIndex); //绘制 canvas.save(); canvas.drawBitmap(curBitmap, matrix, paint); canvas.restore(); } }
主要逻辑就在draw了, 注释写的也比较清除了,先初始化操作,加载资源,设定起始位置,然后就是每帧的移动逻辑,和边界逻辑处理,就是跑到最右边,再把他拉到最左边,呵呵,下面是小鸟动画的处理,我这里是500毫秒更换一下图片,也就是说看到的小鸟的动画,其实是隔500毫秒更换了一次图片产生的效果,下面就只绘制了,好了so easy 吧!
这里要特别说明一个方法matrix.mapRect(targetBox, box);这个方法比较重要,大家以后肯定会经常用到,意思是啥呢,box这个参数是原始的图片大小数据,targetBox是经过Matrix矩阵变换后产生的数据。
好了,下面的鸟其实跟上面的逻辑一样的,只是起始位置不一样。
云彩呢,和小鸟逻辑也差不多,但是需要注意一个地方, 我把云彩给放大了2倍
matrix.reset(); matrix.setScale(2f, 2f); matrix.mapRect(targetBox, box); matrix.postTranslate(initPositionX - targetBox.width() / 2, initPositionY - targetBox.height() / 2);
这里初始位置呢, 我也根据放大后的宽和高进行了处理,大家注意啊,先放缩和先设置位置,出来的效果是不一样的。 大家可以自行试试效果。
现在来看看我们的阳光代码吧
这里就贴一些关键代码了, 全部代码可以在我的github上下载
//旋转 matrix.mapRect(targetBox, box); matrix.postRotate(0.5F, targetBox.centerX(), targetBox.centerY()); //透明度变化 if (alphaUp) { alpha++; } else { alpha--; } if (alpha >= 255) { alphaUp = false; } if (alpha <= 0) { alphaUp = true; } paint.setAlpha(alpha); //绘制 canvas.drawBitmap(frame, matrix, paint);
主要就是介绍一下,使用矩阵来进行旋转操作和透明度操作怎么来做。这里要注意一下的是旋转的时候,要设置中心点。
结束语
好了,代码说的也差不多啦,知识点也都过了一下。回过头来看看,要实现这么一个效果也不是那么难是吧。 呵呵。 当然了实现的还是比较仓促的,就是简单的一个架子和一个场景,如果感兴趣的话可以添加更多的对象AI逻辑,更多的场景。其实如果做到最后优化好的话应该是这样的一个情况,每个场景一个xml或者其他脚本语言吧,然后解析这个xml来动态生成一个场景。当然也不是很难。真正做天气的话,可以这样做。