Почему аппаратное ускорение не работает в моем представлении?

Я использую библиотеку Facebook Rebound, чтобы воспроизвести прыгучие анимации, которые можно увидеть в их реализации чатов. Проблема в том, что в большинстве случаев анимация заикается. Несколько фотографий пояснят это лучше. Вот маслянисто-гладкая анимация чатов:

Facebook Messenger

А вот моя попытка (обратите внимание, как анимация белого View пропускает почти все кадры):

Заикание анимации

Время от времени он работает гладко:

Плавная анимация

Ниже приведен код, который я сейчас использую (весь проект на Github, если вы хотите настроить его быстро). Я предполагаю, что это как-то связано с некорректным включением аппаратного ускорения на моем View. В моем SpringSystem есть 2 Spring, один для «пузыря» (значок Android), а другой для содержимого (белый View, который отображается при нажатии на пузырь). Любая помощь в решении этой проблемы будет принята с благодарностью. Спасибо.

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

        // ...
    });

person Vicky Chijwani    schedule 07.09.2014    source источник
comment
прежде всего удалите Log.d из onSpringUpdate, так как это замедляет анимацию   -  person pskink    schedule 07.09.2014
comment
@pskink Я знаю об этом. Я вставил этот журнал только потом, чтобы проверить, правильно ли включено аппаратное ускорение для представления.   -  person Vicky Chijwani    schedule 07.09.2014
comment
вместо этого вы можете добавить Log.d, чтобы увидеть, действительно ли вторая анимация запускается один раз   -  person pskink    schedule 07.09.2014
comment
@pskink да, анимация каждый раз запускается правильно. Я проверил это с помощью журналов и отладчика.   -  person Vicky Chijwani    schedule 07.09.2014


Ответы (1)


Я понял это, просмотрев исходный код фреймворка.

TL;DR: добавьте WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED к флагам макета, когда вы вручную присоединяете View к Window / WindowManager; установка android:hardwareAccelerated=true в манифесте не будет работать.


Я вручную прикрепляю View в WindowManager (потому что мне нужно создать свой пользовательский интерфейс в Service для эмуляции головок чата) следующим образом:

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

Пошли копать...

Добро пожаловать в платформу Android

Я начал отладку со страницы View#draw(...), затем поднялся по стеку вызовов к ViewRootImpl#draw(boolean). Здесь я наткнулся на этот кусок кода:

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

В моем случае ViewRootImpl#drawSoftware(), который использует программный рендерер. Хм... значит HardwareRenderer это null. Поэтому я отправился на поиски точки построения HardwareRenderer, которая находится в 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);
        // ...
    }

Ага! Вот наш преступник!

Вернемся к проблеме

В этом случае Android не устанавливает автоматически FLAG_HARDWARE_ACCELERATED для этого Window, хотя я установил android:hardwareAccerelated=true в манифесте. Итак, исправление просто:

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

Хотя анимация все же не такая плавная, как у Facebook. Интересно, почему... (прежде, чем кто-то спросит: нет, во время анимации нет обильных логов; и да, я пробовал с релизной сборкой)

person Vicky Chijwani    schedule 09.09.2014
comment
Я не добавляю флаг FLAG_HARDWARE_ACCELERATED в WindowManager.LayoutParams, но когда я проверяю Layout.isHardwareAccelerated(), он все равно возвращает true? - person Wayne; 16.01.2015
comment
У вас есть android:hardwareAccelerated=true в вашем манифесте? Я думаю, что это автоматически включает аппаратное ускорение во всем приложении. - person Vicky Chijwani; 16.01.2015
comment
Конечно, у меня есть. Это означает, что нам не нужно добавлять FLAG_HARDWARE_ACCELERATED, как вы упомянули. - person Wayne; 19.01.2015
comment
Ну, либо вы не подключаете вид к своему окну вручную, либо используете другую версию Android, которая не требует явного добавления FLAG_HARDWARE_ACCELERATED. Я не могу сказать, не видя соответствующий код. - person Vicky Chijwani; 19.01.2015
comment
Точно такая же проблема у меня сейчас, но я не уверен, замечаю ли я что-то другое. - person Denny; 01.02.2019