Manas Shrestha
4 min readMay 27, 2016

--

Custom View Animation Using RxAndroid (Inside Weather Now)

Weather Now

Weather Now is an application that shows the implementation of Rx-Android and MVP pattern in custom views and custom view animations. The app is simple to use and designed in a way to give a quick overview of weekly weather forecast.

RxAndroid is a RxJava extensions for Android that will help you with Android threading and Loopers and RxBinding provides binding between RxJava and Android UI elements like Buttons and TextViews. This application uses RxAndroid to act as an observer for network calls and manipulate the response that we get using mapping functions.

If you are not familiar with the Rx or FRP terminologies, you can take a look at this post by Josh Skeen to get a good overview of FRP. You can also refer to this post by Dan Lew to get started with RxAndroid.

Android provide lots of value animations for the view. Android animations are generally divided into the following categories:

  1. Frame-by-frame animation
    A series of frames is drawn one after the other at regular intervals by loading a series of Drawable resources.
  2. Layout animation
    Animate views inside a container view such as lists and tables.
  3. View animation
    Animate any general-purpose view.

The animations that we used in Weather Now are handled by the custom views. For each weather type, there is a custom widget that is an integrated part of its view hierarchy and manually does its own animation drawing. There are separate threads for each different type of custom animation logic. Once the thread does some type of logical manipulation, the view is informed to get invalidated by the use of handler that has been passed to the thread during its creation. Lets take an example of a simple cross animation that we used to inform the users that there is no internet connection.

/**
* Animated no connection icon
*/
public class NoConnectionView extends View {

private final static int MSG_INVALIDATE = 0;
private final static int POST_DELAY_INTERVAL = 10;
private final static int STROKE_WIDTH = 3;
private static final int DEFAULT_WIDTH = 100;
private static final int DEFAULT_HEIGHT = 100;
private static final int CROSS_SIZE = 25;
private static final int PADDING = 10;
private static final int LENGTH_INCREMENT = 1;

private RectF rectBitmap = new RectF();
private RectF rectCross = new RectF();
private Bitmap networkBitmap;
private Paint paint = new Paint();
private int layout_width;
private int layout_height;

private float lineOneEndX;
private float lineOneEndY;
private double angle = 45 * Math.PI / 180;
private int lengthX = 1;

private float lineTwoEndX;
private float lineTwoEndY;
private double angleLineTwo = -(45 * Math.PI / 180);
private int lengthY = 1;
private boolean animateLineTwo = false;

Handler handler = new Handler((message -> {
invalidate();
return true;
}));

public NoConnectionView(Context context) {
this(context, null, 0);
}

public NoConnectionView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public NoConnectionView(Context context,
AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);

int[] attrsArray = new int[]{
android.R.attr.id, // 0
android.R.attr.background, // 1
android.R.attr.layout_width, // 2
android.R.attr.layout_height // 3
};

TypedArray ta = context.obtainStyledAttributes(attrs, attrsArray);
layout_width = ta.getDimensionPixelSize(2,
(int) GeneralUtils.convertDpToPixel(DEFAULT_WIDTH));
layout_height = ta.getDimensionPixelSize(3,
(int) GeneralUtils.convertDpToPixel(DEFAULT_HEIGHT));

setPadding((int) GeneralUtils.convertDpToPixel(PADDING),
(int) GeneralUtils.convertDpToPixel(PADDING),
(int) GeneralUtils.convertDpToPixel(PADDING),
(int) GeneralUtils.convertDpToPixel(PADDING));

networkBitmap = GeneralUtils.decodeSampledBitmapFromResource(getResources(),
R.drawable.wifi,
(int) GeneralUtils.convertDpToPixel(DEFAULT_WIDTH),
(int) GeneralUtils.convertDpToPixel(DEFAULT_HEIGHT));

rectBitmap.set(getPaddingLeft(), getPaddingTop(),
layout_width - getPaddingRight(),
layout_height - getPaddingBottom());

rectCross.set(rectBitmap.right -
GeneralUtils.convertDpToPixel(CROSS_SIZE),
rectBitmap.bottom -
GeneralUtils.convertDpToPixel(CROSS_SIZE),
rectBitmap.right,
rectBitmap.bottom);

paint.setColor(ContextCompat.
getColor(context, R.color.colorRed));
paint.setStrokeWidth(GeneralUtils.
convertDpToPixel(STROKE_WIDTH));
paint.setStrokeCap(Paint.Cap.ROUND);

lineTwoEndX = rectCross.right + LENGTH_INCREMENT;
lineTwoEndY = rectCross.top + LENGTH_INCREMENT;

new CrossAnimationThread().start();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

canvas.drawBitmap(networkBitmap, null,
rectBitmap, paint);
canvas.drawLine(rectCross.left,
rectCross.top,
lineOneEndX,
lineOneEndY,
paint);

if (animateLineTwo) {
canvas.drawLine(rectCross.right,
rectCross.top,
lineTwoEndX,
lineTwoEndY,
paint);
}

}

/**
* Logic for cross animation
* First draws the first diagonal cross then the second
*/
private class CrossAnimationThread extends Thread {

private boolean animate = true;

@Override
public void run() {
super.run();
while (animate) {
if (!animateLineTwo) {
float length = GeneralUtils.
convertDpToPixel(lengthX);
lineOneEndX = (float) (rectCross.left
+ length * Math.sin(angle));

lineOneEndY = (float)
(rectCross.top
+ length * Math.cos(angle));
lengthX = lengthX + LENGTH_INCREMENT;
} else {
lineTwoEndX = (float) (rectCross.right +
GeneralUtils.convertDpToPixel(lengthY) *
Math.sin(angleLineTwo));
lineTwoEndY = (float) (rectCross.top +
GeneralUtils.convertDpToPixel(lengthY) *
Math.cos(angleLineTwo));
lengthY = lengthY + LENGTH_INCREMENT;
}

if (lineOneEndX >= rectCross.right) {
animateLineTwo = true;
}

//stop once both lines are drawn
if (lineTwoEndX < rectCross.left) {
animate = false;
}

try {
Thread.sleep(POST_DELAY_INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace();
}

handler.sendEmptyMessage(MSG_INVALIDATE);
}
}
}

}

Here, you can see that all the initializations are done in the constructor. The onDraw is only responsible for drawing the bitmap and line. The manipulation part is done by the CrossAnimationThread and the view is notified via the handler. If you want to get started with custom android views, please refer to this training exercise by google developers.

--

--