Why is hardware acceleration not working on my View?

Cross-posted from a self-answered question on StackOverflow.

Recently I was intrigued about how Facebook Messenger implements chat heads on Android. Turned out a lot of people were wondering the same thing. So I decided to try to replicate the effect, replete with the springy animations...

The Question

I'm using Facebook's Rebound library to replicate the bouncy animations seen in their chat heads implementation. The problem is, most of the time the animation stutters. A few GIFs will explain this better. Here's the buttery-smooth chat heads animation:

Facebook Messenger

And here's my attempt (notice how the animation for the white View skips nearly all frames):

Stuttering animation

Once in a while it works smoothly:

Smooth animation

Below is the code I'm using currently (the entire project is up on Github if you want to set it up quickly). I'm guessing this has something to do with hardware acceleration not being enabled correctly on my View. There are 2 Springs in my SpringSystem, one for the "bubble" (the Android icon) and another for the content (the white View that is displayed on tapping the bubble). Any help on how to solve this issue would be greatly appreciated. Thanks.

AndroidManifest.xml:

<application android:hardwareAccelerated="true" ...>  
    ...
</application>  

AppService.java:

// the following code is in AppService#onCreate()
// AppService extends android.app.Service
// full code at https://github.com/vickychijwani/BubbleNote

mContent.setLayerType(View.LAYER_TYPE_HARDWARE, null);

final Spring bubbleSpring = system.createSpring();  
bubbleSpring.setCurrentValue(1.0);  
bubbleSpring.addListener(new SpringListener() {  
    @Override
    public void onSpringUpdate(Spring spring) {
        float value = (float) spring.getCurrentValue();
        params.x = (int) (mPos[0] * value);
        params.y = (int) (mPos[1] * value);
        mWindowManager.updateViewLayout(mBubble, params);
        // fire the second animation when this one is about to end
        if (spring.isOvershooting() && contentSpring.isAtRest()) {
            contentSpring.setEndValue(1.0);
        }
    }

    // ...
});

final Spring contentSpring = system.createSpring();  
contentSpring.setCurrentValue(0.0);  
contentSpring.addListener(new SpringListener() {  
    @Override
    public void onSpringUpdate(Spring spring) {
        // always prints false?!
        Log.d(TAG, "hardware acc = " + mContent.isHardwareAccelerated());
        float value = (float) spring.getCurrentValue();
        // clamping is required to prevent flicker
        float clampedValue = Math.min(Math.max(value, 0.0f), 1.0f);
        mContent.setScaleX(value);
        mContent.setScaleY(value);
        mContent.setAlpha(clampedValue);
    }

    // ...
});

The Solution

I've figured it out by going through the framework source code.

TL;DR: add WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED to the layout flags when you manually attach a View to a Window / WindowManager; setting android:hardwareAccelerated=true in the manifest won't work.

I'm manually attaching my View to the WindowManager (because I need to create my UI in a Service to emulate chat heads) like so:

// code at https://github.com/vickychijwani/BubbleNote/blob/eb708e3910a7279c5490f614a7150009b59bad0b/app/src/main/java/io/github/vickychijwani/bubblenote/BubbleNoteService.java#L54
mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);  
LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);  
mBubble = (LinearLayout) inflater.inflate(R.layout.bubble, null, false);  
// ...
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(  
        WindowManager.LayoutParams.WRAP_CONTENT,
        WindowManager.LayoutParams.WRAP_CONTENT,
        WindowManager.LayoutParams.TYPE_PHONE,
        WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
            | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
        PixelFormat.TRANSLUCENT);
// ...
mWindowManager.addView(mBubble, params);  

Let's go digging...

Welcome to the Android framework

I started debugging at View#draw(...), then went up the call stack to ViewRootImpl#draw(boolean). Here I came across this piece of code:

if (!dirty.isEmpty() || mIsAnimating) {  
    if (attachInfo.mHardwareRenderer != null && attachInfo.mHardwareRenderer.isEnabled()) {
        // Draw with hardware renderer.
        mIsAnimating = false;
        mHardwareYOffset = yoff;
        mResizeAlpha = resizeAlpha;

        mCurrentDirty.set(dirty);
        dirty.setEmpty();

        attachInfo.mHardwareRenderer.draw(mView, attachInfo, this,
                animating ? null : mCurrentDirty);
    } else {
        // If we get here with a disabled & requested hardware renderer, something went
        // wrong (an invalidate posted right before we destroyed the hardware surface
        // for instance) so we should just bail out. Locking the surface with software
        // rendering at this point would lock it forever and prevent hardware renderer
        // from doing its job when it comes back.
        // Before we request a new frame we must however attempt to reinitiliaze the
        // hardware renderer if it's in requested state. This would happen after an
        // eglTerminate() for instance.
        if (attachInfo.mHardwareRenderer != null &&
                !attachInfo.mHardwareRenderer.isEnabled() &&
                attachInfo.mHardwareRenderer.isRequested()) {

            try {
                attachInfo.mHardwareRenderer.initializeIfNeeded(mWidth, mHeight,
                        mHolder.getSurface());
            } catch (OutOfResourcesException e) {
                handleOutOfResourcesException(e);
                return;
            }

            mFullRedrawNeeded = true;
            scheduleTraversals();
            return;
        }

        if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) {
            return;
        }
    }
}

In my case ViewRootImpl#drawSoftware() was being called, which uses the software renderer. Hmm... that means the HardwareRenderer is null. So I went searching for the point of construction of the HardwareRenderer, which is in ViewRootImpl#enableHardwareAcceleration(...):

// Try to enable hardware acceleration if requested
final boolean hardwareAccelerated =  
        (attrs.flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0;
if (hardwareAccelerated) {  
    // ...
    mAttachInfo.mHardwareRenderer = HardwareRenderer.createGlRenderer(2, translucent);
    // ...
}

Aha! There's our culprit!

Back to the problem at hand

In this case Android does not automatically set FLAG_HARDWARE_ACCELERATED for this Window, even though I've set android:hardwareAccerelated=true in the manifest. So the fix is simply:

mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);  
LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);  
mBubble = (LinearLayout) inflater.inflate(R.layout.bubble, null, false);  
// ...
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(  
        WindowManager.LayoutParams.WRAP_CONTENT,
        WindowManager.LayoutParams.WRAP_CONTENT,
        WindowManager.LayoutParams.TYPE_PHONE,
        // NOTE
        WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
            | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
            | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
        PixelFormat.TRANSLUCENT);
// ...
mWindowManager.addView(mBubble, params);  
comments powered by Disqus