So far in this series there have been quite a few mentions of frame rates, and assertions that we want to keep our frame rates as high as possible. In this article we’ll look a little closer at how we can measure frame rates, and explore how changes in our layouts and views can affect our frame rates.
Let’s start at the very basics by defining precisely what the frame rate is. When performing any kind of animation, the frame rate is the number of different frames which get displayed each second, and the higher the frame rate, the smoother the animation will appear. It is believed that most humans well be able to perceive the individual frames up to around 10-12 fps, but we actually need to exceed that in order to create the impression of seamless movement. For movies, typically 24 frames per second (fps) are used; this compares to between 24-30 fps typically for television (but with standard variants going up to 300 fps); and many video games will attempt to achieve frame rates in excess of 30 fps.
While we don’t necessarily need to achieve broadcast or gaming standard for our animations, we should still be looking to keep our animations as smooth as possible in order to make things feel more polished, and so we should aim to achieve the best possible frame rate.
When animating things in Android, irrespective of which animation framework we use, the basic cycle is:
- Initial layout pass to measure and position all of the View objects
- call
onDraw()
to draw all of the Views - The object performing the animation will change values which will affect how
onDraw()
will render some object based on the elapsed time - Go to 2
This will continue until the animation completes, the View hierarchy is destroyed, or the layout changes. In the latter instance the above sequence will be repeated in full as we’ll need to perform the layout pass once again.
Hopefully it should be clear from this why we need to keep onDraw()
as efficient as possible because the more time we spend in onDraw()
the lower our frame rate will be.
So how can we actually measure frame rates? The simplest way is to simply count how often onDraw() is called and by dividing the number of frames drawn by the elapsed time we can easily obtain the frame rate. However, we’ll follow the example of TimingLogger make our measurement points as lightweight as possible, and perform any heavier operations outside of the code that we’re trying to measure.
Let’s start by creating a custom control which subclasses TextView named BlurredTextView and simply holds a count of how many time onDraw()
has been called:
public class BlurredTextView extends TextView{ private long mFrameCount = 0; public BlurredTextView(Context context) { this(context, null, -1); } public BlurredTextView(Context context, AttributeSet attrs) { super(context, attrs, -1); } public BlurredTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mFrameCount++; } public long getFrameCount() { return mFrameCount; } public void setFrameCount(long frameCount) { this.mFrameCount = frameCount; } }
We now change our layout to use this control instead of the standard TextView we used before, and add a button to start and stop our animations:
Finally we need to update our MainActivity class to include the button handling logic to start and stop animating our TextView, count the frames (yes, we’re going to count them both in the MainActivity and our custom TextView for reasons which will become clear),
public class MainActivity extends Activity { private static final String TAG = "Blurring"; private ImageView mImage; private BlurredTextView mText; private ObjectAnimator mAnimator = null; private long mStart = 0; private long mFrameCount = 0; private OnPreDrawListener mPreDrawListener = new OnPreDrawListener() { @Override public boolean onPreDraw() { if(mFrameCount == 0) { Drawable drawable = mImage.getDrawable(); if (drawable != null && drawable instanceof BitmapDrawable) { Bitmap bitmap = ((BitmapDrawable) drawable) .getBitmap(); if (bitmap != null) { blur(bitmap, mText, 25); //blurJava(bitmap, mText, 25); } } } mFrameCount++; return true; } }; . . . /** * Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); . . . mText.setText("Animated"); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mAnimator == null) { button.setText(R.string.stop); startAnimation(); } else { button.setText(R.string.start); stopAnimation(); } } }); } private void startAnimation() { mStart = System.currentTimeMillis(); mText.setFrameCount(0); mFrameCount = 0; mAnimator = ObjectAnimator.ofFloat(mText, "translationY", 0, -200); mAnimator.setDuration(1000); mAnimator.setRepeatMode(ValueAnimator.REVERSE); mAnimator.setRepeatCount(ValueAnimator.INFINITE); mAnimator.start(); } private void stopAnimation() { mAnimator.cancel(); float elapsed = (float) (System.currentTimeMillis() - mStart) / 1000.0f; long framecount = mText.getFrameCount(); float fps = framecount / elapsed; Log.d(TAG, getString(R.string.framerate, elapsed, mFrameCount, framecount, fps)); mAnimator = null; } . . . }
If we run this we can see that the animation is fairly smooth (although the video is not quite a smooth as seeing it on the device), but the blurred background moves with the custom TextView. This is to be expected because we haven’t yet made any changes to blur during the animation.
But what about the all important frame rate that gets dumped once we stop the animation:
Elapsed time 6.0 sec, local frames 1044, frames 1, 0.2 fps
Now that can’t be right, we can see from the video that we got more that 0.2fps. local frames
indicates how many times our onPreDraw()
method was called, and frames
indicates how many times the onDraw()
method of our custom class was called.
What I suspect is happening here is that there is an optimisation that is occurring because the state of our custom TextView isn’t changing, and a ViewOverlay is being used for the animation instead of the view itself, resulting in onDraw()
not being called unnecessarily, and therefore we’re not seeing our counter incrementing.
However, we can see how many times onPreDraw() is being called during the animation, and by performing the calculation manually based on this value, we get a rather impressive 174 fps. But this is on pretty good hardware, and there’s an obvious optimisation going on which will certainly improve the frame rate.
In the next article in this series we’ll look at getting the blur to happen dynamically and see what effect it has on the frame rate.
The source code for this article is available here.
© 2014, Mark Allison. All rights reserved.
Copyright © 2014 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.
What I suspect is happening here is that there is an optimisation that is occurring because the state of our custom TextView isn’t changing, and a ViewOverlay is being used for the animation instead of the view itself, resulting in onDraw() not being called unnecessarily, and therefore we’re not seeing our counter incrementing.
Here because your view isn’t changing,so the display lists do not have to be regenerated. You must running the demo above 4.1, if without display list properties, onDraw should be called.