Как устранить мерцание
Синхронизация потоков
Спрайтовая анимация
Листинг 1
Листинг 2
Листинг 3
Листинг 4

Как устранить мерцание

Приглядевшись внимательно к бегущей строке, которая рисуется в приложении SimpleScroll, вы можете заметить, что изображение строки мерцает. Мерцание может быть весьма неприятным, поэтому постараемся от него избавиться.

В чем причина мерцания изображения в окне аплета? Каждый раз метод paint, ответственный за рисование, стирает содержимое окна, заполняя его фон заданным цветом. Процесс перерисовывания окна аплета, включающий в себя процедуру стирания фона окна аплета, занимает больше времени, чем период развертки изображения по вертикали на экране монитора. Поэтому изображение появляется в окне в несколько приемов, что и приводит к мерцанию.

В ряде случаев можно избавиться от мерцания, изменив технику анимации. Вместо того чтобы периодически вызывать метод repaint, вы можете рисовать в потоке, т. е. в методе run. Единственное, что вы должны при этом сделать, - это получить перед запуском контекст отображения окна аплета и передать его потоку. Так как в этом случае метод paint не вызывается, окно аплета не стирается и мерцание полностью отсутствует. Однако у данного метода есть один недостаток: каждый раз при перерисовке окна, скажем при изменении его размеров или в результате перекрытия окна аплета, изображение в окне будет стираться. Чтобы этого избежать, следует предусмотреть возможность восстановления внешнего вида окна внутри метода paint в том виде, в котором оно находилось до перерисовки, а это не всегда просто.

Более интересен и практичен метод двойной буферизации. Его идея заключается в том, что изображение рисуется не в окне аплета, а готовится в оперативной памяти и затем, уже готовое, выводится в окно. Так как процесс подготовки не виден пользователю, то не видно и мерцания. Для исключения стирания содержимого окна аплета в процессе перерисовки нужно переопределить метод update. Дело в том, что реализация update, используемая по умолчанию, стирает окно перед вызовом paint. Для исключения мерцания в процессе анимации мы должны подготовить свой вариант этого метода, не стирающий окно. В аплете ScrNoFlick, который функционально полностью аналогичен аплету SimpleScroll, мы применили описанный выше способ устранения мерцания окна, предусмотрев буферизацию вывода и переопределение метода update (см. листинг 2).

Исходный текст Web-страницы, включающей в себя аплет, приведен в листинге 1.

В аплете ScrNoFlick, как и в предыдущем аплете SimpleScrol, мы пользуемся многопоточностью. Основной класс аплета реализует интерфейс Runnable, а определенный в этом классе метод run периодически перерисовывает окно аплета, вызывая в бесконечном цикле с задержкой метод repaint. Но основной интерес вызывают методы update и paint, выполняющие всю основную работу по изображению бегущей текстовой строки.

Как мы уже говорили, метод update вызывается при обновлении окна аплета. В задачу нашей реализации метода update входит подготовка изображения сдвинутой текстовой строки, которое создается в оперативной памяти как объект класса Image. Затем метод update вызывает метод paint, рисующий это изображение в окне аплета. Предварительное стирание окна при этом выполнять не нужно. Процедуры подготовки изображения выполняются в несколько этапов. Прежде всего необходимо определить размеры окна аплета, так как создаваемое изображение класса Image должно иметь те же размеры, что и окно аплета:

Dimension d = size();
int nWidth = d.width;
int nHeight = d.height;

Ширина и высота окна сохраняются в переменных nWidth и nHeight соответственно. В основном классе нашего аплета предусмотрено поле m_MemImageDim класса Dimension, в котором будут храниться размеры изображения. Так как изображение нужно создавать только один раз, на следующем шаге метод update проверяет, была ли выполнена эта операция и соответствуют ли размеры изображения размерам окна аплета:

if((m_MemImageDim == null) ||
 (m_MemImageDim.width != nWidth) ||
 (m_MemImageDim.height != nHeight))
{
 . . .
}

Если изображение создано не было или его размеры не равны размерам окна аплета, выполняются следующие действия. Сначала метод update определяет необходимые размеры изображения, исходя из размеров окна аплета. Затем он создает изображение и получает для него контекст отображения:

m_MemImageDim = new Dimension(nWidth, nHeight);
m_MemImage = createImage(nWidth, nHeight);
m_MemImage_Graphics = m_MemImage.getGraphics();

Изображение создается в оперативной памяти методом createImage. Что же касается контекста отображения, который будет применен для рисования, то он сохраняется в поле m_MemImage_Graphics.

На следующем этапе метод update определяет цвет рисования и фона окна аплета и устанавливает в контексте отображения m_MemImage_Graphics цвет фона, соответствующий цвету фона окна аплета:

Color fg = getForeground();
Color bg = getBackground();
m_MemImage_Graphics.setColor(bg);

После установки цвета метод update закрашивает этим цветом изображение, пользуясь контекстом m_MemImage_Graphics:

m_MemImage_Graphics.fillRect(0, 0, m_MemImageDim.width, 
 m_MemImageDim.height);

Далее в этом же контексте устанавливается такой же цвет фона, что и в окне аплета:

m_MemImage_Graphics.setColor(fg);

Теперь изображение в памяти готово для рисования в нем текстовой строки. Мы выполняем эту операцию в текущей позиции m_CurrentXPosition, а затем уменьшаем позицию на единицу:

m_MemImage_Graphics.drawString(m_Text, m_CurrentXPosition, 20);
m_CurrentXPosition-;

В результате каждый раз при вызове метода update строка будет сдвигаться в изображении m_MemImage на один пиксел влево. Когда же текущая позиция становится меньше длины строки, эта позиция вновь устанавливается в начальное значение:

if(m_CurrentXPosition < -m_StringSize)
 m_CurrentXPosition = size().width;

Последнее, что делает метод update перед тем как вернуть управление, - это вызов метода paint:

paint(g);

Метод paint рисует подготовленное методом update изображение m_MemImage в окне аплета, пользуясь для этого методом drawImage:

if(m_MemImage != null)
 g.drawImage(m_MemImage, 0, 0, null);

В результате текстовая строка появится на экране в том состоянии, в котором она находилось на момент вызова метода paint.

Синхронизация потоков

В многопоточных приложениях зачастую требуется выполнять синхронизацию одновременно работающих потоков. Обращение потоков одновременно к одним и тем же ресурсам может привести к неправильной работе приложений. В критической ситуации может быть нарушена последовательность выполнения операций, что вызывает возникновение трудно обнаруживаемых ошибок.

В языке Java предусмотрены средства, предназначенные для синхронизации потоков.

Синхронизация методов и объектов

Достаточно простой способ синхронизации потоков, обращающихся к общим критическим ресурсам, например к банковским счетам, - это использование синхронизированных методов. Сделать метод синхронизированным можно, объявив его synchronized:

public synchronized void addValue()
{
 . . .
}

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

Можно выполнить синхронизацию не всего метода, а только критичного фрагмента кода:

. . .
synchronized(Account)
{
 if(Account.check(3000000))
   Account.decrement(3000000);
}
. . .

В этом случае от одновременного доступа защищается только объект Account.

Ожидание извещения

Каждый поток может управлять работой других потоков, для чего в Java имеются методы wait, notify и notifyAll, определенные в классе Object. Метод wait может использоваться либо с параметром, либо без параметра. Он переводит поток в состояние ожидания. В этом состоянии поток будет находиться до тех пор, пока не будет вызван извещающий метод notify, или notifyAll, или же пока не истечет время, указанное в параметре метода wait. Метод, который будет переводиться в состояние ожидания, должен быть синхронизированным, т. е. его следует описать как synchronized:

public synchronized void run()
{
 try
 {
  Thread.wait();
 }
 catch (InterruptedException e)
 {
 }
 while (true)
 {
  . . .
 }
}

В этом примере внутри метода run определен цикл. Перед его запуском метод run переводится в состояние ожидания до тех пор, пока другой поток не пошлет ему извещение с помощью метода notify. А вот пример вызова метода notify:

synchronized(SleepingThread)
{
 SleepingThread.notify();
}

Обратите внимание, что вызов метода notify выполняется в синхронизированном режиме. В качестве объекта синхронизации выступает поток, для которого вызывается метод notify.

Ожидание завершения потока

Еще одно средство синхронизации - это метод join, о котором мы уже рассказывали. С помощью join можно выполнять ожидание завершения потока, для которого этот метод вызван.

Спрайтовая анимация

Ничто так не оживляет страницы сервера Web, как анимация. Простейшее анимационное изображение можно создать в виде многосекционного графического файла формата GIF. Такой файл состоит из отдельных кадров (спрайтов), которые отображаются в окне браузера как кадры мультфильма. Вы можете подготовить кадры для многосекционного файла GIF с помощью подходящего графического редактора или извлечь из файла формата AVI, а затем объединить с помощью такой программы, как, например, Microsoft GIF Animator.

Однако более интересные видеоэффекты получаются при использовании для отображений спрайтов аплетов Java. Аплет имеет полный контроль над своим окном, поэтому он может не просто выполнять последовательный показ кадров, но и изменять эту последовательность, дорисовывать изображения в кадрах, смешивать кадры из разных последовательностей и так далее - дело лишь за вашей фантазией!

Спрайтовая анимация в аплетах Java предполагает загрузку отдельных изображений GIF или JPEG из каталога сервера Web с последующим поочередным их отображением в окне аплета с помощью метода drawImage. Если вы умеете рисовать отдельные файлы с применением указанного метода и владеете основными средствами многопоточности, для вас не составит особого труда отобразить в окне аплета небольшой мультфильм. Тем не менее есть один тонкий момент, на котором нам хотелось бы остановиться.

Не секрет, что скорость передачи данных в сети Internet весьма невелика и "при хорошей погоде и попутном ветре" составляет в среднем 1 Кбайт/с. А раз анимационный ролик состоит из многих файлов, его загрузка может отнять чересчур большое время. Поэтому перед началом отображения анимации вы обязательно должны предусмотреть ожидание завершения процесса загрузки кадров. Проще всего это сделать с помощью класса MediaTracker.

Аплет Noise

На примере аплета Noise мы покажем, как можно создать спрайтовую анимацию в окне аплета Java. Кроме того, мы продемонстрируем применение извещений для синхронизации одновременно работающих потоков.

Сразу после запуска аплет Noise приступает к загрузке графических изображений отдельных кадров анимационного ролика. Во время загрузки в окне аплета отображается сообщение.

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

Отдельные кадры анимационного ролика появляются на экране по очереди: в прямом, а затем в обратном порядке. В результате изображение слова Noise (шум) вначале как бы растворяется в шуме (показан точками), а затем появляется снова. Этот процесс будет повторяться бесконечно. Поток, закрашивающий окно аплета точками случайного цвета, сразу после запуска переводится в состояние ожидания. В этом состоянии он находится до момента завершения загрузки всех кадров ролика. Когда окно аплета перерисовывается, оно закрашивается белым цветом, а затем на его поверхности вновь появляются точки случайного цвета.

Заготовку исходного текста аплета можно сделать с помощью системы разработки приложений Microsoft Visual J++. В диалоговой панели Java Applet Wizard/Step 3 of 5 нужно включить переключатели Yes в полях Would you like your applet to be multi-threaded и Would you like support animation. Далее автоматически создается исходный текст, куда нужно внести все необходимые изменения, чтобы получился файл с текстом аплета (см. листинг 3).

Все та же система Visual J++ создаст для нашего аплета обрамляющую Web-страницу (см. листинг 4).

Основной класс аплета Noise реализует интерфейс Runnable. Кроме того, в нашем аплете на базе класса Thread мы создали класс DrawPoints. Метод run этого класса рисует точки случайного цвета в окне аплета. Таким образом, помимо главного потока аплета, мы запускаем еще два потока.

После вычисления размеров окна аплета, установки белого цвета фона и рисования рамки окна аплета метод paint проверяет содержимое флага m_fAllLoaded. Изначально он установлен в false. Когда же все изображения анимационного ролика будут загружены, флаг переключается в состояние true. Если загрузка изображений еще не закончена, метод paint отображает в окне аплета соответствующее сообщение. После окончания загрузки изображений вызывается метод displayImage, рисующий в окне аплета текущий кадр анимационного ролика.

После установки номера текущего кадра m_nCurrImage в нулевое значение метод run проверяет, закончена ли загрузка всех изображений. При первом запуске этого метода она еще не начиналась, поэтому метод run должен инициировать этот процесс. Загрузка всех изображений с ожиданием выполняется следующим образом. Сначала создается объект класса MediaTracker:

MediaTracker tracker = new MediaTracker(this);

Затем все изображения загружаются в цикле методом getImage и добавляются к объекту MediaTracker методом addImage:

m_Images[i-1] = getImage(getDocumentBase(), strImage);
tracker.addImage(m_Images[i-1], 0);

После завершения цикла все изображения загружаются асинхронно по отношению к главному потоку аплета, а также по отношению к потоку, в рамках которого работает метод run. Ожидание завершения процесса выполняется методом waitForAll, определенным в классе MediaTracker:

tracker.waitForAll();
m_fAllLoaded = !tracker.isErrorAny();

Если после завершения ожидания не возникло никаких ошибок, в поле m_fAllLoaded записывается значение true.

На следующем этапе начинается подготовка к отображению кадров анимации. В поля m_nImgWidth и m_nImgHeight записываются размеры кадров, после чего вызывается метод repaint. Это приводит к отображению первого кадра анимационной последовательности. Далее метод run разблокирует поток, рисующий точки случайного цвета в окне аплета:

synchronized(m_DrawPointsThread)
{
 m_DrawPointsThread.notify();
}

И наконец, последнее, что делает метод run перед запуском цикла анимации, - устанавливает начальное значение переменной reverse, определяющей прямой или обратный порядок отображения кадров анимационного ролика.

Отображение кадров в цикле вызывается методом displayImage, рисующим в окне аплета текущий кадр. Затем номер текущего кадра m_nCurrImage увеличивается или уменьшается в зависимости от состояния переменной reverse.

Конструктор класса DrawPoints получает в качестве параметра ссылку на аплет. Эта ссылка затем используется для получения контекста отображения, необходимого для рисования точек случайного цвета. Кроме того, конструктор определяет и сохраняет размеры окна аплета.

Обратите внимание на начальный фрагмент метода run, переводящий поток, в рамках которого работает этот метод, в состояние ожидания:

try
{
 Thread.wait();
}
catch (InterruptedException e)
{
}

В этом состоянии поток будет находиться до тех пор, пока метод run главного класса аплета не завершит загрузку всех изображений и не вызовет метод notify.

После разблокирования метод run класса DrawPoints получает случайные координаты и цвет для рисования точки, вызывая метод random из класса Math. Цвет устанавливается в контексте отображения окна аплета, а точка рисуется методом fillRect.

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


Александр Вячеславович Фролов, Григорий Вячеславович Фролов - авторы серий книг "Библиотека системного программиста" и "Персональный компьютер. Шаг за шагом". E-mail: frolov@glas.apc.org Web: http://www.glasnet.ru/~frolov, http://www.dials.ccas.ru/frolov

Листинг 1



ScrNoFlick




The source.

Листинг 2

import java.applet.*;
import java.awt.*;

public class ScrNoFlick extends Applet implements Runnable
{
 private Thread  m_ScrNoFlick = null;
 private String m_Text = "Scrolling String";
 private final String PARAM_Text = "Text";
 int m_StringSize;
 int m_CurrentXPosition;

 private Image m_MemImage;
 private Graphics m_MemImage_Graphics;
 Dimension m_MemImageDim = null;

 public String getAppletInfo()
 {
  return "Name: ScrNoFlick
" +
   "Author: Alexandr Frolov
" +
   "E-mail: frolov@glas.apc.org
" +
   "Web: http://www.glasnet.ru/~frolov," + 
   " http://www.dials.ccas.ru/frolov";
 }

 public String[][] getParameterInfo()
 {
  String[][] info =
  {
   { PARAM_Text, "String", "Scrolling String" },
  };
  return info;  
 }

 public void init()
 {
  String param;

  param = getParameter(PARAM_Text);
  if (param != null)
   m_Text = param;

  FontMetrics fm = getFontMetrics(getFont());
  m_StringSize = fm.stringWidth(m_Text);
  m_CurrentXPosition = size().width;
  setBackground(Color.yellow);
 }

 public void update(Graphics g)
 {
  Dimension d = size();
  int nWidth = d.width;
  int nHeight = d.height;

  if((m_MemImageDim == null) ||
    (m_MemImageDim.width != nWidth) ||
    (m_MemImageDim.height != nHeight))
  {
   m_MemImageDim = new Dimension(nWidth, nHeight);
   m_MemImage = createImage(nWidth, nHeight);
   m_MemImage_Graphics = m_MemImage.getGraphics();
  }

  Color fg = getForeground();
  Color bg = getBackground();
  m_MemImage_Graphics.setColor(bg);

  m_MemImage_Graphics.fillRect(0, 0, 
   m_MemImageDim.width, m_MemImageDim.height);

  m_MemImage_Graphics.setColor(fg);

  m_MemImage_Graphics.drawString(m_Text, m_CurrentXPosition, 20);
  m_CurrentXPosition-;

  if(m_CurrentXPosition < -m_StringSize)
    m_CurrentXPosition = size().width;

  paint(g);
 }

 public void paint(Graphics g)
 {
  if(m_MemImage != null)
   g.drawImage(m_MemImage, 0, 0, null);
 }

 public void start()
 {
  if (m_ScrNoFlick == null)
  {
   m_ScrNoFlick = new Thread(this);
   m_ScrNoFlick.start();
  }
 }
 
 public void stop()
 {
  if (m_ScrNoFlick != null)
  {
   m_ScrNoFlick.stop();
   m_ScrNoFlick = null;
  }
 }

 public void run()
 {
  while (true)
  {
   try
   {
    repaint();
    Thread.sleep(50);
   }
   catch (InterruptedException e)
   {
    stop();
   }
  }
 }
}


Листинг 3

import java.applet.*;
import java.awt.*;

public class Noise extends Applet implements Runnable
{
 private Thread m_Noise = null;
 DrawPoints m_DrawPointsThread = null;

 private Graphics m_Graphics;
 private Image   m_Images[];
 private int    m_nCurrImage;
 private int    m_nImgWidth = 0;
 private int    m_nImgHeight = 0;
 private boolean  m_fAllLoaded = false;
 private final int NUM_IMAGES  = 18;

 public String getAppletInfo()
 {
  return "Name: Noise
" +
   "Author: Alexandr Frolov
" +
   "E-mail: frolov@glas.apc.org
" +
   "Web: http://www.glasnet.ru/~frolov, 
" +
   "     http://www.dials.ccas.ru/frolov";
 }

 private void displayImage(Graphics g)
 {
  if (!m_fAllLoaded)
   return;

  g.drawImage(m_Images[m_nCurrImage],
      (size().width - m_nImgWidth)  / 2,
      (size().height - m_nImgHeight) / 2, null);
 }

 public void paint(Graphics g)
 {
  Dimension dimAppWndDimension = size();
  setBackground(Color.white);

  g.drawRect(0, 0, 
   dimAppWndDimension.width - 1, 
   dimAppWndDimension.height - 1);

  if (m_fAllLoaded)
  {
   displayImage(g);
  }
  else
   g.drawString("Подождите, идет загрузка изображений...", 
    10, 20);
 }

 public void start()
 {
  if (m_Noise == null)
  {
   m_Noise = new Thread(this);
   m_Noise.start();
  }

  if (m_DrawPointsThread == null)
  {
   m_DrawPointsThread = new DrawPoints(this);
   m_DrawPointsThread.start();
  }
 }
 
 public void stop()
 {
  if (m_Noise != null)
  {
   m_Noise.stop();
   m_Noise = null;
  }
  if (m_DrawPointsThread != null)
  {
   m_DrawPointsThread.stop();
   m_DrawPointsThread = null;
  }
 }

 public void run()
 {
  m_nCurrImage = 0;
  
  if (!m_fAllLoaded)
  {
   repaint();
   m_Graphics = getGraphics();
   m_Images  = new Image[NUM_IMAGES];

   MediaTracker tracker = new MediaTracker(this);
   String strImage;

   for (int i = 1; i <= NUM_IMAGES; i++)
   {
    strImage = 
     "img/img0" + ((i < 10) ? "0" : "") + i + ".gif";
    m_Images[i-1] = getImage(getDocumentBase(), strImage);
    tracker.addImage(m_Images[i-1], 0);
   }

   try
   {
    tracker.waitForAll();
    m_fAllLoaded = !tracker.isErrorAny();
   }
   catch (InterruptedException e)
   {
   }
   
   if (!m_fAllLoaded)
   {
    stop();
    m_Graphics.drawString("Ошибка при загрузке изображений", 
     10, 40);
    return;
   }
   
   m_nImgWidth = m_Images[0].getWidth(this);
   m_nImgHeight = m_Images[0].getHeight(this);
  }  
  repaint();

  synchronized(m_DrawPointsThread)
  {
   m_DrawPointsThread.notify();
  }

  boolean reverse = false;
  while (true)
  {
   try
   {
    displayImage(m_Graphics);

    if(reverse)
    {
     m_nCurrImage-;
     if(m_nCurrImage == 0)
      reverse = false;
    }
    else
    {
     m_nCurrImage++;

     if(m_nCurrImage == NUM_IMAGES - 1)
      reverse = true;
    }

    Thread.sleep(150);
   }
   catch (InterruptedException e)
   {
    stop();
   }
  }
 }
}
class DrawPoints extends Thread
{
 Graphics g;
 Dimension dimAppWndDimension;

 public DrawPoints(Applet Appl)
 {
  g = Appl.getGraphics();
  dimAppWndDimension = Appl.size();
 }

 public synchronized void run()
 {
  try
  {
   Thread.wait();
  }

  catch (InterruptedException e)
  {
  }
  while (true)
  {
   int x, y, width, height;
   int rColor, gColor, bColor;
   
   x = (int)(dimAppWndDimension.width * Math.random());
   y = (int)(dimAppWndDimension.height * Math.random());
   
   rColor = (int)(255 * Math.random());
   gColor = (int)(255 * Math.random());
   bColor = (int)(255 * Math.random());
   g.setColor(new Color(rColor, gColor, bColor));
   g.fillRect(x, y, 1, 1);
   try
   {
    Thread.sleep(1);
   }
   catch (InterruptedException e)
   {
   }
  }
 }
}


Листинг 4



Noise




The source.