Why is hardware acceleration not working on my Vie

2019-03-13 06:04发布

问题:

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:

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

Once in a while it works smoothly:

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

        // ...
    });

回答1:

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)