查看原文
其他

Android自定义View进阶之高仿墨迹天气蓝天白云场景效果

2017-06-14 达内Android培训

 Tips:

Android本月免费课程报名中,点击文末“阅读原文”快速抢!


前言

 一直对墨迹天气的绚丽的场景蛮感兴趣的,趁有时间,自己就高仿了其中的一个场景,其他场景呢,也是类似的,主要是写对象的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来动态生成一个场景。当然也不是很难。真正做天气的话,可以这样做。


后台回复「视频」,获取Kotlin视频学习资料。由于资源容易被删,发现删了请给我直接留言,我给你直发下载链接。


更多干货



学懂技术关键点,月薪3W,你也可以!


6月安卓免费课程报名中,4天学做一个APP!

回复「姓名+微信号+城市」报名或点页面底部「阅读原文」报名。

回复「领取」免费下载安卓学习资料&工具&组件


各类干货和励志鸡血,更有各种好玩好看的资讯!

点击阅读原文报名Android免费课程

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存