Why is hardware acceleration not working on my View?
P

1

12

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 pictures 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);
        }

        // ...
    });
Parisparish answered 7/9, 2014 at 18:1 Comment(4)
first of all remove Log.d from onSpringUpdate as it slows down the animationGannes
@Gannes I'm aware of that. I only put that log in afterwards to check if hardware acceleration for the View was correctly enabled.Parisparish
instead you could add Log.d to see if the second animation is fired really onceGannes
@Gannes yes, the animation is fired correctly every time. I checked it with the logs and the debugger.Parisparish
P
11

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(WindowManager.LayoutParams):

    // 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);

Although the animation is still not as smooth as Facebook's. I wonder why... (before anyone asks: no, there are no copious logs during the animation; and yes, I've tried with a release build)

Parisparish answered 9/9, 2014 at 3:7 Comment(5)
I don't add FLAG_HARDWARE_ACCELERATED flag to WindowManager.LayoutParams but when I check Layout.isHardwareAccelerated() it still returns true?Harley
Do you have android:hardwareAccelerated=true in your manifest? That automatically enables hardware acceleration throughout the app I think.Parisparish
Of course I have. That means we don't need to add FLAG_HARDWARE_ACCELERATED as you mentioned.Harley
Well either you're not attaching the View to your Window manually, or you're using a different version of Android that doesn't require you to add FLAG_HARDWARE_ACCELERATED explicitly. I can't say without seeing the relevant code.Parisparish
Exactly the same problem I'm currently having but I'm not sure if I notice anything different.Tildy

© 2022 - 2024 — McMap. All rights reserved.