Диспетчер визуальных состояний (Visual State Manager)

135

Разработка шаблонов и пользовательских элементов управления в Silverlight практически аналогична разработке в WPF. Ознакомтесь со статьями в следующей теме - «Шаблоны и пользовательские элементы управления WPF». Главным отличием является то, что Silverlight не поддерживает триггеры, предоставляя вместо них диспетчер визуальных состояний, который подробно мы и рассмотрим ниже.

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

Шаблон можно разбить на небольшие компоненты, однако для этого нужно понимать модель частей (parts) и состояний (states), используемую для организации шаблонов Silverlight. Часть — это именованный элемент шаблона, ожидаемый элементом управления. Состояние — это именованная анимация, применяемая в заданное время.

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

Как узнать, какие части и состояния должны быть предоставлены шаблоном элемента управления обязательно? Есть два способа решения этой проблемы. Во-первых, можно ознакомиться с документацией Silverlight. На каждой странице, посвященной конкретному элементу управления, все необходимые части и состояния перечислены отдельно в двух таблицах.

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

Именованные состояния класса Button

Во-вторых, для ознакомления с классом элемента управления можно применить рефлексию кода. Каждая часть представлена в объявлении класса отдельным атрибутом TemplatePart, а каждое состояние — атрибутом TemplateVisualState.

Состояния кнопки

Если вы посмотрите в объявление класса Button (или его документацию, показанную на рисунке), то увидите, что для создания кнопки нужно предоставить шесть состояний:

[TemplateVisualState(Name="Normal", GroupName="CommonStates")]
[TemplateVisualState(Name="MouseOver", GroupName="CommonStates")]
[TemplateVisualState(Name="Pressed", GroupName="Commonstates")]
[TemplateVisualState(Name="Disabled", GroupName="CommonStates")] 
[TemplateVisualState(Name="Unfocused", GroupName="FocusStates")] 
[TemplateVisualState(Name="Focused", GroupName="FocusStates")]
public class Button : ButtonBase
{ ... }

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

Когда кнопка получает фокус в результате нажатия клавиши <Tab>, она принимает состояние Normal (группы CommonStates) и Focused (группы FocusStates). Если после этого на кнопку навести указатель, она получит состояния MouseOver и Focused. Группирование состояний облегчает управление ими. Без группирования пришлось бы вводить приоритеты состояний (например, чтобы выход из состояния MouseOver не приводил к потере фокуса) или создавать дополнительные состояния (например, FocusedNormal, UnfocusedNormal, FocusedMouseOver, UnfocusedMouseOver и др.).

Для определения групп состояний нужно добавить группу VisualStateManager.VisualStates в корневой элемент шаблона элемента управления.

Для добавления элемента VisualStateManager в шаблон необходим контейнер. В нем будут находиться видимые компоненты элемента управления и невидимый объект VisualStateManager. Как и другие ресурсы, элемент Visual StateManager определяет объекты (например, раскадровки), используемые элементом управления в заданные моменты времени.

Обычно на корневой уровень шаблона добавляют контейнер Grid. В примере с кнопкой показанный ниже контейнер Grid содержит элемент VusualStateGroups, определяющий группы, и элемент Border, выводящий кнопку. В элементе VisualStateGroups группы создаются с помощью именованных элементов VisualStateGroup. Для кнопки нужно определить две группы - CommonStates и FocusStates.

После добавления элементов VisualStateManager и VisualStateGroup можно добавлять объекты VisualState для каждого состояния. Добавить можно либо все состояния, объявленные в документации шаблонов и атрибутах TemplateVisualState, либо только используемые. Например, при создании кнопки с эффектом наведения указателя можно добавить только состояния MouseOver (которое создает эффект) и Normal (приводящее кнопку к исходному внешнему виду).

Ниже показан пример шаблона кнопки с объявлением всех состояний:

<Grid x:Name="LayoutRoot" Background="#555">
        <Grid.Resources>
            <LinearGradientBrush x:Key="NormalBackgroundButton" EndPoint="0.5,1" StartPoint="0.5,0">
                <GradientStop Color="#FFF4CB80" Offset="0"/>
                <GradientStop Color="#FFFFA705" Offset="1"/>
            </LinearGradientBrush>
            <LinearGradientBrush x:Key="HighlightBackgroundButton" EndPoint="0.5,1" StartPoint="0.5,0">
                <GradientStop Color="#FF80F49B" Offset="0"/>
                <GradientStop Color="#FF05FF62" Offset="1"/>
            </LinearGradientBrush>
            <Style TargetType="Button">
                <Setter Property="Foreground" Value="White"/>
                <Setter Property="FontSize" Value="14"/>
                <Setter Property="FontWeight" Value="Bold"/>
                <Setter Property="VerticalContentAlignment" Value="Center"/>
                <Setter Property="Width" Value="200"/>
                <Setter Property="Height" Value="40"/>
                <Setter Property="Padding" Value="10"/>
                <Setter Property="Margin" Value="5"/>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="Button">
                            <Grid x:Name="rootGrid">
                                <!-- Диспетчер состояний -->
                                <VisualStateManager.VisualStateGroups>
                                    <VisualStateGroup x:Name="CommonStates">
                                        <VisualState x:Name="MouseOver">
                                            <Storyboard>
                                                <DoubleAnimation Duration="0:0:0" To="1"
                                                                 Storyboard.TargetName="bg_highlight"
                                                                 Storyboard.TargetProperty="Opacity" />
                                            </Storyboard>
                                        </VisualState>
                                        <VisualState x:Name="Normal">
                                            <Storyboard>
                                                <DoubleAnimation Duration="0:0:0" To="0"
                                                                 Storyboard.TargetName="bg_highlight"
                                                                 Storyboard.TargetProperty="Opacity" />
                                            </Storyboard>
                                        </VisualState>
                                        <VisualState x:Name="Disabled">
                                            <Storyboard>
                                                <DoubleAnimation Duration="0:0:0" To="1"
                                                                 Storyboard.TargetName="bg_disabled"
                                                                 Storyboard.TargetProperty="Opacity" />
                                            </Storyboard>
                                        </VisualState>
                                    </VisualStateGroup>
                                    <VisualStateGroup x:Name="FocusStates">
                                        
                                    </VisualStateGroup>
                                </VisualStateManager.VisualStateGroups>
                                
                                <!-- Шаблон -->
                                <Border x:Name="bg" Background="{StaticResource NormalBackgroundButton}"
                                        BorderThickness="3" BorderBrush="Orange"/>
                                <Border x:Name="bg_highlight" Opacity="0" Background="{StaticResource HighlightBackgroundButton}"
                                        BorderThickness="3" BorderBrush="LimeGreen"/>
                                <Border x:Name="bg_disabled" Opacity="0" Background="#666"
                                        BorderThickness="3" BorderBrush="#888"/>
                                <ContentPresenter VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
                                                  HorizontalAlignment="Center"/>
                            </Grid>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </Grid.Resources>
        <StackPanel>
            <Button Content="MOUSEOVER"/>
            <Button Content="NORMAL"/>
            <Button Content="DISABLED" IsEnabled="False"/>
        </StackPanel>
</Grid>

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

В приведенной выше разметке мы использовали первый вариант:

Использование диспетчера VSM в шаблоне кнопки

Индикатор фокуса

В предыдущем примере состояния Normal и MouseOver группы CommonStates использовались для изменения внешнего вида кнопки при наведении указателя.

Кнопка имеет две группы состояний. Кроме четырех состояний группы CommonStates, доступны два состояния группы FocusStates, позволяющие кнопке иметь или не иметь фокус. Группы CommonStates и FocusStates независимы. Это означает, что кнопка может иметь или не иметь фокус независимо от того, где находится указатель. Конечно, могут быть исключения, обусловленные внутренней логикой элемента управления. Например, отключенная кнопка не может получить фокус ввода, поэтому состояние Focused не может быть установлено, когда активно состояние Disabled.

Состояния группы FocusStates используются для выяснения, имеет ли элемент управления фокус. В приведенном ниже шаблоне кнопки индикатор фокуса отображается на экране с помощью объекта Rectangle с пунктирной рамкой. Индикатор фокуса располагается поверх кнопки с помощью контейнера Grid, который содержит в одной ячейке как индикатор фокуса, так и рамку кнопки. Анимация группы FocusStates выводит или скрывает индикатор фокуса (пунктирный прямоугольник) путем настройки свойства Opacity:

...
   <VisualStateGroup x:Name="FocusStates">
        <VisualState x:Name="Focused">
            <Storyboard>
                <DoubleAnimation Duration="0:0:0" To="1"
                                 Storyboard.TargetName="focusrect"
                                 Storyboard.TargetProperty="Opacity" />
            </Storyboard>
        </VisualState>
        <VisualState x:Name="Unfocused">
            <!-- Раскадровка не нужна, потому что данное 
                 состояние всего лишь преобразуется к исходной прозрачности-->
        </VisualState>
   </VisualStateGroup>
   
   ...
   
   <Rectangle x:Name="focusrect" Stroke="Black" StrokeThickness="1" StrokeDashArray="1 2" 
                 Opacity="0" Margin="3"/>

На кнопке отображается пунктирный прямоугольник, когда она имеет фокус клавиатуры:

Создание индикатора фокуса с помощью шаблона

Объекты переходов типа VisualTransition

В предыдущем примере установлена нулевая длительность анимации. В результате этого при наведении указателя на кнопку ее цвет изменяется мгновенно. Для создания более плавного эффекта нужно увеличить длительность анимации. Приведенная ниже разметка задает изменение цвета за 0,2 секунды:

<VisualState x:Name="MouseOver">
       <Storyboard>
              <DoubleAnimation Duration="0:0:0.2" To="1"
                      Storyboard.TargetName="bg_highlight"
                      Storyboard.TargetProperty="Opacity" />
       </Storyboard>
</VisualState>

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

Чтобы анимационный эффект сигнализировал о переключении элемента управления из одного состояния в другое, рекомендуется использовать специальный объект перехода VisualTransition, который представляет собой анимацию, начинающуюся в текущем состоянии элемента и заканчивающуюся в новом стоянии. Одно из преимуществ модели переходов состоит в том, что при ее использовании для анимации не нужно создавать раскадровку. Надстройка Silverlight создает необходимую анимацию автоматически.

Элементы управления достаточно "интеллигентны": они пропускают анимацию перехода, когда она начинается с определенного состояния.

Рассмотрим, например, элемент управления CheckBox, имеющий состояния Unchecked и Checked. Предположим, вы решили применить анимацию для плавного выведения галочки при установке флажка. Если добавить эффект плавного выведения в анимацию состояния Checked, анимация будет применена только при первом выводе установленного флажка (например, если страница содержит три установленных флажка, все три галочки будут плавно выведены при первом появлении страницы).

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

Переход, установленный по умолчанию

Переходы применяются к группам состояний. При определении объекта перехода его нужно добавить в коллекцию VisualStateGroup.Transitions. Простейший тип перехода — это переход, установленный по умолчанию. Он применяется ко всем изменениям состояний группы. Для создания перехода, установленного по умолчанию, нужно всего лишь добавить элемент VisualTransition и присвоить свойству GeneratedDuration длительность перехода:

<VisualStateGroup x:Name="CommonStates">
     <VisualStateGroup.Transitions>
         <VisualTransition GeneratedDuration="0:0:0.2" />
     </VisualStateGroup.Transitions>
     
     <VisualState x:Name="MouseOver">
         <Storyboard>
             <DoubleAnimation Duration="0" To="1"
                              Storyboard.TargetName="bg_highlight"
                              Storyboard.TargetProperty="Opacity" />
         </Storyboard>
     </VisualState>
     <VisualState x:Name="Normal">
         <Storyboard>
             <DoubleAnimation Duration="0" To="0"
                              Storyboard.TargetName="bg_highlight"
                              Storyboard.TargetProperty="Opacity" />
         </Storyboard>
     </VisualState>
     <VisualState x:Name="Disabled">
         <Storyboard>
             <DoubleAnimation Duration="0" To="1"
                              Storyboard.TargetName="bg_disabled"
                              Storyboard.TargetProperty="Opacity" />
         </Storyboard>
     </VisualState>
</VisualStateGroup>

Теперь при каждом изменении состояния кнопки (состояние должно принадлежать группе CommonStates) будет запущена анимация перехода длительностью 0,2 секунды. Это означает, что при наведении указателя на кнопку и переходе в состояние MouseOver новый цвет будет полностью выведен только через 0,2 секунды, даже несмотря на то, что анимация состояния MouseOver имеет нулевую длительность. Аналогично при удалении указателя из области кнопки исходный цвет будет полностью отображен только через 0,2 секунды.

Важно учитывать, что переход является анимацией, запускаемой при изменении состояния. Элемент VisualStateManager может создавать анимации переходов для таких типов:

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

Иногда в состоянии применяется несколько анимаций. В этом случае все анимации поддерживаемых типов будут выполняться посредством объектов перехода. Анимации всех неподдерживаемых типов будут мгновенно установлены в конце перехода.

Переходы From и То

Переходы, установленные по умолчанию, удобны, но они все же оптимальны не во всех случаях. Предположим, нужно выполнить переход в состояние MouseOver за 0,5 секунды и вернуться в исходное состояние Normal за 0,1 секунды, когда указатель выходит за пределы кнопки. Для создания такого эффекта необходимо определить два перехода и установить свойства From и То, задающие, когда выполняется переход. Рассмотрим следующую коллекцию переходов:

<VisualStateGroup.Transitions>
       <VisualTransition To="MouseOver" GeneratedDuration="0:0:0.5" />
       <VisualTransition To="Normal" GeneratedDuration="0:0:0.1" />
</VisualStateGroup.Transitions>

Кнопка отображает состояние MouseOver через 0,5 секунды, а перестает отображать его — через 0,1 секунды после удаления указателя с области кнопки. Переходов, установленных по умолчанию, нет, поэтому отображение других состояний выполняется мгновенно.

В примере продемонстрированы переходы, выполняемые при входе в определенное состояние и при выходе из него. Это два отдельных перехода. Однако свойства То и From можно также использовать совместно для создания более специфичных переходов, выполняемых при переключении между двумя заданными состояниями. При выполнении переходов надстройка Silverlight просматривает их коллекцию и применяет наиболее специфичный переход.

Например, когда пользователь наводит указатель на кнопку, надстройка Silverlight просматривает коллекцию в приведенной ниже последовательности и останавливается, обнаружив совпадение:

  1. Переход с From="Normal" и То="MouseOver".

  2. Переход с То="MouseOver".

  3. Переход с From= "Normal".

  4. Переход, установленный по умолчанию.

Если перехода, установленного по умолчанию, нет, переключение между двумя состояниями выполняется мгновенно.

Переходы с повторениями

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

Предположим, нужно создать кнопку, которая пульсирует при наведении на нее указателя. Для этого необходимо присвоить свойству RepeatBehavior либо количество повторений, либо значение Forever, задающее бесконечное повторение анимации.

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

В качестве примера ниже приведена разметка повторяющейся анимации пульсирующей кнопки:

<VisualStateGroup.Transitions>
        <VisualTransition From="Normal" To="MouseOver" GeneratedDuration="0:0:0.4" />
</VisualStateGroup.Transitions>
    
<VisualState x:Name="MouseOver">
        <Storyboard>
            <DoubleAnimation Duration="0:0:0.4" From="0" To="1"
                             Storyboard.TargetName="bg_highlight"
                             Storyboard.TargetProperty="Opacity"
                             RepeatBehavior="Forever"/>
        </Storyboard>
</VisualState>

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

Пользовательский переход

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

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

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

<VisualStateGroup.Transitions>
      <VisualTransition From="Normal" To="MouseOver" GeneratedDuration="0:0:0.6">
          <Storyboard>
              <DoubleAnimationUsingKeyFrames Storyboard.TargetName="scaleTransform"
       Storyboard.TargetProperty="ScaleX">
                  <LinearDoubleKeyFrame KeyTime="0:0:0.4" Value="0"/>
                  <LinearDoubleKeyFrame KeyTime="0:0:0.6" Value="1"/>
              </DoubleAnimationUsingKeyFrames>
          </Storyboard>
      </VisualTransition>
</VisualStateGroup.Transitions>

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

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

<Grid x:Name="rootGrid" RenderTransformOrigin="0.5,0.5">
      <Grid.RenderTransform>
              <ScaleTransform x:Name="scaleTransform" ScaleX="1"/>
      </Grid.RenderTransform>
      
      ...

Когда переход завершается, анимация перехода останавливается и анимированным свойствам возвращаются их исходные значения (или значения, установленные анимацией текущего состояния). В данном примере анимация возвращает объекту ScaleTransform его исходное значение ScaleX, равное 1, поэтому по завершении анимации перехода изменения не заметны:

Пользовательская анимация перехода
Пройди тесты
Лучший чат для C# программистов