Предотвращение мерцания при вызове Drawable.draw()

У меня есть небольшое экспериментальное приложение (по сути, очень урезанная версия LunarLander в Android SDK) с одним SurfaceView. У меня есть "спрайт" Drawable, который я периодически рисую в объекте Canvas SurfaceView в разных местах, не пытаясь стереть предыдущее изображение. Таким образом:

private class MyThread extends Thread {
    SurfaceHolder holder;  // Initialised in ctor (acquired via getHolder())
    Drawable      sprite;  // Initialised in ctor
    Rect          bounds;  // Initialised in ctor

    ...

    @Override
    public void run() {
        while (true) {
            Canvas c = holder.lockCanvas();
            synchronized (bounds) {
                sprite.setBounds(bounds);
            }
            sprite.draw(c);
            holder.unlockCanvasAndPost(c);
        }
    }

    /**
     * Periodically called from activity thread
     */
    public void updatePos(int dx, int dy) {
        synchronized (bounds) {
            bounds.offset(dx, dy);
        }
    }
}

Запустив эмулятор, я наблюдаю, что после того, как произошло несколько обновлений, несколько старых "копий" изображения начинают мерцать, т.е. то появляются, то исчезают. Сначала я предположил, что, возможно, я неправильно понял семантику Canvas, и что он каким-то образом поддерживает «слои», и что я забивал его до смерти. Однако затем я обнаружил, что получаю этот эффект, только если пытаюсь обновлять быстрее, чем примерно каждые 200 мс. Итак, моя следующая лучшая теория заключается в том, что это, возможно, артефакт эмулятора, который не может идти в ногу и разрывает дисплей. (У меня пока нет физического устройства для тестирования.)

Верна ли какая-либо из этих теорий?

Примечание: на самом деле я не хочу делать это на практике (т. е. рисовать сотни наложенных друг на друга копий одного и того же объекта). Однако я хотел бы понять, почему это происходит.

Окружающая среда:

  • Eclipse 3.6.1 (Гелиос) в Windows 7
  • JDK 6
  • Инструменты Android SDK r9
  • Приложение предназначено для Android 2.3.1.

Касательный вопрос:

Мой метод run() по сути представляет собой урезанную версию того, как работает пример LunarLander (с удаленной лишней логикой). Я не совсем понимаю, почему это не будет насыщать ЦП, поскольку, похоже, ничто не мешает ему работать на полную мощность. Кто-нибудь может это прояснить?


person Oliver Charlesworth    schedule 13.02.2011    source источник
comment
Вы рисуете на холсте в обоих потоках? Что стирает то, что вы нарисовали в предыдущем кадре? Разве в этом примере не рисуется фон?   -  person Reuben Scratton    schedule 13.02.2011
comment
@Reuben: Нет, только ветку выше. Больше ничего не рисуется (я максимально урезал LunarLander в качестве учебного упражнения). Я намеренно не очищаю вид, чтобы посмотреть, что произойдет.   -  person Oliver Charlesworth    schedule 13.02.2011
comment
Попробуйте вставить c.drawColor(Color.BLACK); перед вашим sprite.draw()?   -  person Reuben Scratton    schedule 13.02.2011
comment
@Rebuen: я знаю, что это даст мне одну копию спрайта на дисплее в любой момент (и действительно, это то, что происходит, когда я пытаюсь это сделать). Но это не то, что мне нужно; Я хочу отображать постоянно увеличивающееся количество копий спрайта одновременно, причем каждая копия находится в случайном месте.   -  person Oliver Charlesworth    schedule 13.02.2011


Ответы (2)


Хорошо, я разделал Лунный посадочный модуль таким же образом, как и вы, и, увидев мерцание, я могу сказать вам, что то, что вы видите, является простым артефактом механизма двойной буферизации, который есть у каждого Surface.

Когда вы рисуете что-либо на Canvas, прикрепленном к Surface, вы рисуете в «задний» буфер (невидимый). И когда вы unlockCanvasAndPost() меняете местами буферы... то, что вы нарисовали, внезапно становится видимым, когда "задний" буфер становится "передним", и наоборот. Итак, ваш следующий кадр рисования выполняется в старом «переднем» буфере...

Дело в том, что вы всегда рисуете отдельные буферы на чередующихся кадрах. Я предполагаю, что в графической архитектуре существует неявное предположение, что вы всегда будете писать каждый пиксель.

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

person Reuben Scratton    schedule 17.02.2011
comment
Спасибо за расследование; это объяснение действительно имеет большой смысл! Однако откуда берется зависимость от периода обновления? Почему частота обновления координат предстоящего спрайта влияет на мерцание старых копий? - person Oliver Charlesworth; 17.02.2011
comment
Вы увидите мерцание только тогда, когда последовательность через ЦП будет обновление-›отрисовка-›обновление. Если между любыми двумя «обновлениями» есть более одного «отрисовки», то эта отрисовка была отрисована в обоих буферах, и, следовательно, вы никогда не увидите мерцание этого конкретного отрисовки. - person Reuben Scratton; 17.02.2011
comment
Поскольку ваш поток рендеринга работает настолько быстро, насколько это возможно, ваша частота обновления в потоке пользовательского интерфейса должна быть достаточно высокой, чтобы бросить вызов процессорному времени и, таким образом, войти в цикл обновления->отрисовка->обновление, вызывающий мерцание (потому что только один буфер получает именно эту запись). - person Reuben Scratton; 17.02.2011
comment
Это звучит довольно убедительно. Я собираюсь попробовать некоторые эксперименты позже; если они подтвердят ваше объяснение, то вы можете получить награду! - person Oliver Charlesworth; 18.02.2011
comment
@ Оли, я полагаю, мне не следует ворчать на половину награды, но ... ба. :-\ - person Reuben Scratton; 24.02.2011

Вам нужно очистить предыдущую позицию спрайта, а также новую позицию. Это то, что система View делает автоматически. Однако, если вы используете поверхность напрямую и не перерисовываете каждый пиксель (будь то с непрозрачным цветом или с использованием режима наложения SRC), вы должны очистить содержимое буфера самостоятельно. Обратите внимание, что вы можете передать грязный прямоугольник в lockCanvas(), и он сделает за вас объединение предыдущего грязного прямоугольника и того, который вы передаете (это механизм, используемый набором инструментов пользовательского интерфейса). Он также установит прямоугольник клипа. Холста быть объединением этих двух прямоугольников.

Что касается вашего второго вопроса, unlockAndPost() будет выполнять вертикальную синхронизацию, поэтому вы никогда не будете рисовать со скоростью более ~ 60 кадров в секунду (большинство устройств, которые я видел, имеют частоту обновления экрана около 55 Гц).

person Romain Guy    schedule 18.02.2011
comment
Да, я знаю, что в любой нормальной ситуации я хотел бы очистить поверхность перед рисованием следующего кадра! Моя ситуация нереалистична; Я просто пытаюсь понять, почему я получаю тот эффект, который вижу. - person Oliver Charlesworth; 18.02.2011
comment
Потому что, как сказал Рубен выше, поверхность имеет двойную буферизацию, поэтому вы видите, что предыдущий кадр чередуется с новыми, которые вы рисуете. - person Romain Guy; 18.02.2011