19 июля 2011

GUI: Стилизованное анимированное меню

Давно не делал демок для журнала. Сегодня решил исправить этот недостаток и сел за Delphi, чтобы продолжить цикл сообщений о GUI. В поисках интересных интерфейсных решений, набрел сюда, но сделать такое за короткое время не удалось. Видимо подзабыл я математику и нужно садиться за листочек бумажки, чтобы накидать основные формулы для такой карусельки. Надеюсь, как-нибудь и такой PageSlider добавится в мою коллекцию GUI-элементов. А сегодня я остановился на анимированной подсветке элементов горизонтального списка. В итоге я скачал несколько иконок и соорудил небольшую демку. В принципе, такой подход может использоваться как для элементов меню, так и для подсветки пунктов в списке апгредов юнита, выбора магии или чего-то подобного.

Результат у нас будет такой:


Так как вся ставка идет на анимации, поэтому записал видео:



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

Кнопкой-иконкой у нас является такая конструкция:

  TItem = class
    fMainHud: TGlHudSprite;
    fTextHud: TGlHudText;
    fBottomText: String;
    fPosition: TVector;
    fY: Single;
    fSelected: Boolean;
    fWidthArea: Single;
  end;

где fMainHud - основное изображение элемента, fTextHud - текст, прячущийся под иконкой, fBottomText - текст, который выезжает слева, fPosition - базовая позиция элемента, fY - single-значение для реализации tween'инга, fSelected - наведена ли мышь?, fWidthArea - можно использовать, если визуальная часть иконки не совпадает с областью реакции на мышь.
Сама структура такого элемента заполняется здесь:

Function TfrmMain.AddItem(aMaterial: String; aText: String; aPosX, aPosY, aWidthArea: Single; aBottomText: String): TItem;
begin
  result := TItem.Create;
  with result do
  begin
    fPosition := VectorMake(80 + aPosX * 70, aPosY, 0);
    
    fTextHud := TGlHudText.CreateAsChild(fMainDummy);
    with fTextHud do
    begin
      BitmapFont := fWinFont;
      ModulateColor.SetColor(0, 0, 0.3, 0.7);
      Alignment := taCenter;
      Text := aText;
      Position.SetPoint(fPosition[0], fPosition[1] - 10, 0);
    end;

    fMainHud := TGlHudSprite.CreateAsChild(fMainDummy);
    with fMainHud do
    begin
      Material.MaterialLibrary := fMatLib;
      Material.LibMaterialName := aMaterial;
      with Material.GetActualPrimaryTexture.Image do
        SetSize(Width, Height);
      Position.SetPoint(fPosition);
    end;

    fBottomText := aBottomText;
    fWidthArea := aWidthArea;
    fY := aPosY;
  end;
  fMenuItems.Add(result);
end;

"Магические числа" нужны для более опрятного вида списка на экране. Поэкспериментируйте, изменив те или иные значения при заполнении элемента списка!
Остальные вспомогательные методы вроде InitTweener(), InitMaterials(), InitFontAndSprites() мы уже разбирали ранее и теперь я просто перейду к описанию основного динамического метода UpdateSelection():

Function TfrmMain.UpdateSelection(x, y: Integer): integer;
var
  i: integer;
begin
  result := -1;
  for i := 0 to fMenuItems.Count - 1 do
    with GetItem(i), GetItem(i).fMainHud do
      if (x > Position.X - Width/2 * fWidthArea)  and (x < Position.X + Width/2 * fWidthArea)
      and(y > Position.Y - fPosition[1] - Height) and (y < fPosition[1] + Height)then
      begin
        result := i;
        if not fSelected then
        begin
          fTweener.DeletePSingle(@fY);
          fTweener.AddTweenPSingle(@fY, ts_ExpoEaseIn, fY, fPosition[1] + 45, 1, 0);
        end;
        fSelected := true;
      end
      else
      begin
        if fSelected then
        begin
          fTweener.DeletePSingle(@fY);
          fTweener.AddTweenPSingle(@fY, ts_ElasticEaseOut, fY, fPosition[1], 2.5, 0);
        end;
        fSelected := false;  
      end;  
end;

Очевидно, что здесь мы проходим по списку элементов и проверяем, наведена ли мышь на один из них. Здесь стоит пояснить новый вызов fTweener.DeletePSingle(@fY), который удаляет все Tween'ы, связанные с адресом переменной. Это необходимо, чтобы прежние анимации не накладывались на новые, что вызывает порой жуткие скачки при движениях элементов. А далее, как обычно:
  • если объект выделен, а на предыдущем кадре нет, тогда запускаем ts_ExpoEaseIn
  • если объект был выделен, а теперь мышь ушла, тогда запускаем ts_ElasticEaseOut в обратном направлении
При клике левой кнопки мыши, запускаются два tween'а на движение панельки и текста внизу:

   if IsKeyDown(vk_LButton) and not MousePressed and (Selected <> -1)then
  begin
    fMenuText.Text := GetItem(Selected).fTextHud.Text;
    fBottomText.Text := GetItem(Selected).fBottomText;

    fTweener.DeletePSingle(@fTextBarX);
    fTweener.AddTweenPSingle(@fTextBarX, ts_ExpoEaseIn, -500, 250, 1.5, 0);

    fTweener.DeletePSingle(@fBottomTextX);
    fTweener.AddTweenPSingle(@fBottomTextX, ts_ExpoEaseIn, -400, 80, 1.5, 0.4);
  end;

А сами позиции применяются простым присвоением:

  fTextBar.Position.X := fTextBarX;
  fMenuText.Position.X := 530 - fTextBarX;
  fBottomText.Position.X := fBottomTextX;

Можно заметить, что панелька fTextBar и сам текст fMenuText движутся благодаря одному и тому же tween-элементу, но выползают они с разных сторон окна, что придает их движению разнообразия.
Наверно это все, что можно пояснить в тексте... какие-то отдельные моменты наверно все же придется смотреть в коде, чтобы понять более детально устройство самой демы...
Само демонстрационное приложение можно скачать здесь, либо воспользуйтесь svn для выкачивания сразу всех примеров, написанных мною. Данная демка добавилась в 9ой ревизии.

p.s. Последний месяц для fun'а я занимался только конкурсом, поэтому журнал не наполнялся gui-вкусностями и различными демками. Через месяц-другой, как станет посвободнее, обязательно вернусь к славной традиции добавления новых интерфейсных элементов на страницы журнала. Ну а пока в следующих записях буду рассказывать о текущих проектах, сложностях в познании flash, результатах конкурса, создании своего сайта и прочего... Следите за новостями!

Сообщения, схожие по тематике: