Related Articles:
Google I/O 2017: Official Android Application Development Architecture Guide, with Demo and Official Documentation
Awesome! Complete Source Code for 74 Apps!
Kotlin for Android Development Official Documentation and Chinese eBook Resources, with Learning Videos
Author: Exploding Girl
Original URL: http://www.apkbus.com/myspaceblog-901466.html
Introduction
I have always been interested in the stunning scenes of Moji Weather, so I took the time to mimic one of the scenes. The other scenes are similar; the main focus is on writing the AI logic of the objects.
Presentation Effect
Let’s first take a look at the effect. Due to heavy compression, the dynamic effect appears slightly blurry; just focus on the code itself.
Code Analysis
Let’s take a look at the code structure. Here, we used SurfaceView instead of View; in fact, drawing the weather scene is more like game development, and using SurfaceView is more flexible.
public SceneSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
surfaceHolder = getHolder();
surfaceHolder.addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
this.setKeepScreenOn(true);
}
This is the constructor, implementing SurfaceHolder.Callback to listen for events.
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.d("weather", "surfaceCreated");
if (renderThread == null) {
renderThread = new RenderThread(surfaceHolder, getContext());
renderThread.start();
}
}
In the surface creation callback, we generated a RenderThread to handle logic and drawing.
@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);
}
}
Recording the measured width and height.
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d("weather", "surfaceDestroyed");
renderThread.getRenderHandler().sendEmptyMessage(1);
}
When destroyed, a message must be sent; what to do specifically will be discussed below. The following is the RenderThread source code.
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");
//Use message queue in non-main thread
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);
}
}
In the constructor, a scene background was added, with two birds on top and one cloud and sunshine on each side.
Here, a message queue for the thread was generated in the run method; note that it is not the main thread. It could also be handled with a while loop like typical game processing, but using a message queue is lighter and more efficient.
Let’s take another look at the RenderHandler; there are two situations: one for drawing and one for exiting, basically just these two.Remember the exit called in surfaceDestroyed? It’s right here, haha.Then comes the most important draw method; drawing is done in Scene, let’s take a look at the code.
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);
}
}
}
You can draw a background image and an Actor list in the scene.What is an Actor? It’s an object, like a bird, cloud, rain, etc.
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);
}
This is an abstract class; Context can load resource files, and Matrix describes the transformation of the object. The abstract method draw is our logic and drawing method.
Let’s take a look at the code for the bird above.
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) {
//Logic processing
//Initialization
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;
}
//Move
matrix.postTranslate(2, 0);
//Boundary processing
matrix.mapRect(targetBox, box);
if (targetBox.left > width) {
matrix.postTranslate(-targetBox.right, 0);
}
//Get frame animation image
long curTime = System.currentTimeMillis();
curFrameIndex = (int) ((curTime - lastTime) / 500 % 8);
Bitmap curBitmap = frames.get(curFrameIndex);
//Draw
canvas.save();
canvas.drawBitmap(curBitmap, matrix, paint);
canvas.restore();
}
}
The main logic is in the draw method; the comments are quite clear. First, initialization operations load resources and set the starting position, then comes the logic for moving each frame and handling boundary logic, which means when it reaches the far right, it pulls back to the far left, haha. Below is the handling of the bird animation; here I change the image every 500 milliseconds, which means the bird animation effect is produced by changing the image every 500 milliseconds.
Here, I want to emphasize a method: matrix.mapRect(targetBox, box); this method is quite important, and you will definitely use it often in the future. What does it mean? The box parameter is the original image size data, while targetBox is the data produced after the Matrix transformation.
Now, the logic for the other birds is basically the same, just with different starting positions. The cloud logic is similar to the bird, but one thing to note is that I scaled the cloud by 2 times.
matrix.reset();
matrix.setScale(2f, 2f);
matrix.mapRect(targetBox, box);
matrix.postTranslate(initPositionX - targetBox.width() / 2, initPositionY - targetBox.height() / 2);
Here, the initial position is also adjusted based on the scaled width and height. Please note that the order of scaling and setting the position will produce different effects. You can try it out yourself.
Now, let’s take a look at our sunshine code; here are some key codes, and you can download the full code from my GitHub.
//Rotation
matrix.mapRect(targetBox, box);
matrix.postRotate(0.5F, targetBox.centerX(), targetBox.centerY());
//Opacity change
if (alphaUp) {
alpha++;
} else {
alpha--;
}
if (alpha >= 255) {
alphaUp = false;
}
if (alpha <= 0) {
alphaUp = true;
}
paint.setAlpha(alpha);
//Draw
canvas.drawBitmap(frame, matrix, paint);
This mainly introduces how to use matrices for rotation and opacity operations. One thing to note is that when rotating, you need to set the center point.
Conclusion
Alright, we’ve covered most of the code and the knowledge points. Looking back, achieving such an effect is not that difficult, right? Haha. Of course, the implementation is still relatively rushed; it’s just a simple frame and a scene. If you’re interested, you can add more object AI logic and more scenes. In fact, if optimized well in the end, it should be like this: each scene could have an XML or other script language, and then parse this XML to dynamically generate a scene. Of course, it’s not very difficult. If you really want to do weather, you could do it this way. For detailed source code, please click “Read the Original” to communicate with the author. Everyone is welcome to like and comment.
For more learning materials, click on the “Read the Original” below to get them.