Главная » Разработка для Windows Phone 7 » Диспетчер визуальных состояний

0

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

На самом деле, чтобы сделать этот шаблон функционально и визуально завершенным, необходимо добавить в него всего две возможности:

•         Button должен обеспечивать визуальную обратную связь при нажатии кнопки пользователем.

•           Находясь в неактивном состоянии, Button должен визуально отображать это.

Эти две возможности взаимосвязаны, потому что обе касаются изменения визуальных элементов элемента управления в определенных условиях. Также их связывает используемый для их реализации инструмент Silverlight, называемый Visual State Manager ().

Visual State Manager помогает разработчику работать с визуальными состояниями. Визуальные состояния – это изменения визуальных элементов элемента управления, являющиеся результатом изменений свойств (или состояний) элемента управления. Все важные визуальные состояния класса Button в Windows Phone 7 связаны со свойствами IsPressed (Нажат) и IsEnabled (Включен).

Все визуальные состояния, поддерживаемые конкретным элементом управления, описаны в его документации. На первой странице документации класса Button можно увидеть класс, определенный шестью атрибутами типа TemplateVisualStateAttribute (Атрибут визуального состояния шаблона):

[TemplateVisualStateAttribute(Name = "Disabled", GroupName = "CommonStates")] [TemplateVisualStateAttribute(Name = "Normal", GroupName = "CommonStates")] [TemplateVisualStateAttribute(Name = "MouseOver", GroupName = "CommonStates")] [TemplateVisualStateAttribute(Name = "Pressed", GroupName = "CommonStates")] [TemplateVisualStateAttribute(Name = "Unfocused", GroupName = "FocusStates")] [TemplateVisualStateAttribute(Name = "Focused", GroupName = "FocusStates")] public class Button : ButtonBase

Класс Button имеет шесть визуальных состояний. У каждого из этих состояний есть имя, но также обратите внимание, что для каждого из них указано и имя группы: CommonStates (Общие состояния) или FocusStates (Состояния фокуса).

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

Реализация перехода кнопки в определенное состояние осуществляется в коде класса Button посредством вызовов статического метода VisualStateManager.GoToState (Перейти в состояние). Шаблон отвечает за визуальные изменения на основании этих состояний.

В шаблонах для Windows Phone 7 все намного проще, чем в Silverlight для Веб, потому что здесь нам не надо беспокоиться о двух состояниях группы FocusStates или о состоянии MouseOver (Наведение указателя мыши). Таким образом, остаются только состояния Normal, Disabled и Pressed.

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

неактивное состояние может быть реализовано путем помещения полупрозрачного Rectangle поверх всего элемента управления.

Итак, давайте поместим все дерево визуальных элементов в Grid с одной ячейкой и добавим Rectangle в конце, чтобы он располагался поверх всех элементов:

<ControlTemplate TargetType="Button"> <Grid>

<Border BorderBrush="{TemplateBinding BorderBrush}"

BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="2 4"> <ContentPresenter

Content="{TemplateBinding Content}"

ContentTemplate="{TemplateBinding ContentTemplate}" Margin="{TemplateBinding Padding}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />

</Border>

<Rectangle Name="disableRect"

Fill="{StaticResource PhoneBackgroundBrush}" 0pacity="0" />

</Grid> </ControlTemplate>

К счастью, свойство Opacity нового Rectangle имеет значение 0. В противном случае этот Rectangle загораживал бы весь элемент управления! Но если задать Opacity значение 0,6, например, мы получим необходимый эффект затемнения, не зависящий от содержимого элемента управления.

Обратите внимание, при задании цвета Rectangle используется ресурс PhoneBackgroundBrush. Углы нашего Button скруглены, и мы совсем не хотим, чтобы Rectangle искажал цвет элементов, находящихся за Button и видимых из-за этих скруглений. Также можно задать для Rectangle такое же скругление углов, как и для Border, что обеспечит большую гибкость при выборе цвета для Rectangle.

Теперь, когда Rectangle на месте, нам осталось лишь найти способ изменять значение Opacity с 0 на 0,6 при переходе кнопки в состояние Disabled.

Разметка для Visual State Manager всегда располагается после открывающего тега элемента верхнего уровня шаблона, в данном случае это Grid. Эта разметка начинается с тега VisualStateManager.VisualStateGroups (Группы визуальных состояний), в рамках которого может быть множество разделов VisualStateGroups. Я не буду включать группу FocusStates:

<ControlTemplate TargetType="Button"> <Grid>

<VisualStateManager.VisualStateGroups>

<VisualStateGroup x:Name="CommonStates">

</VisualStateGroup> </VisualStateManager.VisualStateGroups>

</Grid> </ControlTemplate>

Теги VisualStateGroup включают наборы тегов VisualState (Визуальное состояние) для каждого визуального состояния этой группы:

<ControlTemplate TargetType="Button"> <Grid>

<VisualStateManager.VisualStateGroups>

<VisualStateGroup x:Name="CommonStates">

<VisualState x:Name="Normal" /> <VisualState x:Name="MouseOver" />

<VisualState x:Name="Pressed">

</VisualState>

<VisualState x:Name="Disabled">

</VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>

</Grid> </ControlTemplate>

Тег VisualState для состояния Normal пуст, потому что шаблон изначально создается для кнопки в обычном состоянии. Однако этот тег нельзя опустить, потому что в этом случае элемент управления не сможет вернуться в состояние Normal после пребывания в другом состоянии. Состояние MouseOver не используется, поэтому тоже остается пустым.

В тегах VisualState мы указываем, что должно происходить, когда элемент управления находится в данном состоянии. Как это делается? Можно предположить, что для этого используется тег Setting, как в Style, и такой подход прекрасно работал бы. Но Visual State Manager обеспечивает намного большую гибкость и позволяет использовать анимации. А поскольку синтаксис анимаций не намного сложнее синтаксиса Setting, Visual State Manager буквально требует применения анимаций. В теги VisualState мы помещаем Storyboard, включающий одну или более анимаций, целевыми свойствами которых являются свойства именованных элементов шаблона. В большинстве случаем для этих анимаций будет задана продолжительность (Duration), равная 0, что обеспечивает мгновенное изменение визуального состояния. Но по желанию можно сделать анимации смены состояний более плавными. Рассмотрим анимацию свойства Opacity объекта Rectangle под именем disableRect:

<ControlTemplate TargetType="Button"> <Grid>

<VisualStateManager.VisualStateGroups>

<VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="Normal" /> <VisualState x:Name="MouseOver" />

<VisualState x:Name="Pressed">

</VisualState>

<VisualState x:Name="Disabled"> <Storyboard>

<DoubleAnimation Storyboard.TargetName="disableRect" Storyboard.TargetProperty="Opacity" To="0.6" Duration="0:0:0" />

</Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>

</Grid> </ControlTemplate>

Как правило, анимациям в Visual State Manager не задано значение From, поэтому они просто стартуют от существующего значения. Пустой тег VisualState для состояния Normal при переходе элемента управления в это состояние эффективно восстанавливает для Opacity значение, которое это свойство имело до анимации.

Реализация состояния Pressed представляет некоторые сложности. Как правило, состояние Pressed визуализируется в форме обратного видео. В Веб-версии Silverlight прямо в коде шаблона Button в качестве фона задан LinearGradientBrush, и для состояния Pressed изменяются значения свойств этой кисти. Поскольку шаблон управляет кистью для состояния Normal, он без труда может изменить эту кисть для отображения состояния Pressed.

В создаваемом здесь шаблоне Button цвет Foreground по умолчанию задан в стиле темы для Button, а цвет Background по умолчанию определен в Style, частью которого является наш шаблон. Если эти свойства не меняются, этими цветами будут белый на черном (для «темной» темы) или черным на белом. Но свойства могут переопределяться локальными параметрами Button.

Было бы замечательно, если бы имелся некоторый графический эффект для инвертирования цветов, но такого эффекта нет. Для состояния Pressed нам приходится задавать новые цвета фона и переднего плана анимаций, чтобы создать видимость инверсии цветов. То есть если в качестве переднего плана задан ресурс PhoneForegroundBrush и в качестве фона – ресурс PhoneBackgroundBrush, для состояния Pressed в качестве Foreground можно задать PhoneBackgroundBrush и в качестве Background – PhoneForegroundBrush.

А можем ли мы использовать ColorAnimation для этого? Это было бы возможным, если бы нам точно было известно, что кисти для Foreground и Background являются объектами SolidColorBrush. Но мы этого не знаем. Поэтому приходится использовать объекты ObjectAnimationUsingKeyFrames (Анимация свойств типа Object с использованием ключевых кадров) для применения анимаций непосредственно к свойствам Foreground и Background. Дочерними элементами ObjectAnimationUsingKeyFrames могут быть только объекты типа DiscreteObjectKeyFrame (Дискретный ключевой кадр типа Object).

Начнем со свойства Background и зададим имя объекту Border:

<Border Name="border"

BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="12">

Посредством этого имени анимация может применяться к свойству Background этого Border:

<VisualState x:Name="Pressed"> <Storyboard>

<0bjectAnimationUsingKeyFrames Storyboard.TargetName="border"

Storyboard.TargetProperty="Background"> <Discrete0bjectKeyFrame KeyTime="0:0:0"

Value="{StaticResource PhoneForegroundBrush}" /> </0bjectAnimationUsingKeyFrames> </Storyboard> </VisualState>

Для состояния Pressed анимация меняет свойство Background элемента Border и задает для него кисть, описанную как ресурс PhoneForegroundBrush. Превосходно!

Теперь добавим такую же анимацию для свойства Foreground элемента … какого элемента? В дереве визуальных элементов этого шаблона нет элемента со свойством Foreground!

Было бы просто идеально, если бы ContentPresenter имел свойство Foreground, но этого свойства у него нет.

Но, минутку. А что же ContentControl? ContentControl – это по сути ContentPresenter, но у ContentControl есть свойство Foreground. Поэтому заменим ContentPresenter на ControlControl и зададим для него имя:

<ContentControl Name="contentControl"

Content="{TemplateBinding Content}"

ContentTemplate="{TemplateBinding ContentTemplate}" Margin="{TemplateBinding Padding}"

HorizontalAlignment="{TemplateBinding HorizontalContentAlignment} VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />

Теперь мы можем определить вторую анимацию для состояния Pressed:

<VisualState x:Name="Pressed"> <Storyboard>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="contentControl"

Storyboard.TargetProperty="Foreground"> <DiscreteObjectKeyFrame KeyTime="0:0:0"

Value="{StaticResource PhoneBackgroundBrush}" </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState>

И так наша кнопка выглядит при нажатии:

Теперь я объявляю, что этот шаблон полностью завершен! (И теперь абсолютно ясно, почему шаблон Button по умолчанию включает ContentControl.)

Рассмотрим полный текст Style и ControlTemplate в контексте страницы. В приложении CustomButtonTemplate (Пользовательский шаблон кнопки) Style описан в коллекции Resources страницы. Главным образом чтобы сократить длину строк и вместить их в ширину страницы без переносов, ControlTemplate определен как отдельный ресурс, на который затем ссылается Style. Привожу ControlTemplate, сразу за которым следует Style, использующий этот шаблон:

Проект Silverlight: CustomButtonTemplate Файл: MainPage.xaml (фрагмент)

<phone:PhoneApplicationPage.Resources>

<ControlTemplate x:Key="buttonTemplate" TargetType="Button"> <Grid>

<VisualStateManager.VisualStateGroups>

<VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="Normal" />

<VisualState x:Name="Mouse0ver" />

<VisualState x:Name="Pressed"> <Storyboard>

<0bjectAnimationUsingKeyFrames

Storyboard.TargetName="border" Storyboard.TargetProperty="Background"> <Discrete0bjectKeyFrame KeyTime="0:0:0"

Value="{StaticResource PhoneForegroundBrush}" /> </0bjectAnimationUsingKeyFrames>

<0bjectAnimationUsingKeyFrames

Storyboard.TargetName="contentControl" Storyboard.TargetProperty="Foreground"> <Discrete0bjectKeyFrame KeyTime="0:0:0"

Value="{StaticResource PhoneBackgroundBrush}" /> </0bjectAnimationUsingKeyFrames> </Storyboard> </VisualState>

<VisualState x:Name="Disabled"> <Storyboard>

<DoubleAnimation Storyboard.TargetName="disableRect" Storyboard.TargetProperty="0pacity" To="0.6" Duration="0:0:0" />

</Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>

<Border Name="border"

BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="12">

<ContentControl Name="contentControl"

Content="{TemplateBinding Content}"

ContentTemplate="{TemplateBinding ContentTemplate}" Margin="{TemplateBinding Padding}" HorizontalAlignment="{TemplateBinding

HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding

VerticalContentAlignment}" />

</Border>

<Rectangle Name="disableRect"

Fill="{StaticResource PhoneBackgroundBrush}" 0pacity="0" />

</Grid> </ControlTemplate>

<Style x:Key="buttonStyle" TargetType="Button">

<Setter Property="BorderBrush" Value="{StaticResource PhoneAccentBrush}" /> <Setter Property="BorderThickness" Value="6" />

<Setter Property="Background" Value="{StaticResource PhoneChromeBrush}" /> <Setter Property="Template" Value="{StaticResource buttonTemplate}" /> </Style>

</phone:PhoneApplicationPage.Resources>

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

Но кажется не вполне правильным включать (т.е. выделять цветом) ToggleButton, когда Button находится в своем обычном состоянии (т.е. активный). Я хотел, чтобы ToggleButton выводил «Button Enabled» (Кнопка активна), когда ToggleButton включен и Button активен, и «Button Disabled» (Кнопка неактивна), когда ToggleButton выключен и Button неактивен.

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

Проект Silverlight: CustomButtonTemplate Файл: MainPage.xaml (фрагмент)

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions>

<RowDefinition Height="*" /> <RowDefinition Height="*" /> </Grid.RowDefinitions>

<Button Grid.Row="0"

Content="Click me!"

Style="{StaticResource buttonStyle}"

IsEnabled="{Binding ElementName=toggleButton, Path=IsChecked}"

HorizontalAlignment="Center"

VerticalAlignment="Center" />

<ToggleButton Name="toggleButton" Grid.Row="1" IsChecked="true" HorizontalAlignment="Center" VerticalAlignment="Center"> <ToggleButton.Template>

<ControlTemplate TargetType="ToggleButton">

<Border BorderBrush="{StaticResource PhoneForegroundBrush}"

BorderThickness="{StaticResource PhoneBorderThickness}">

<VisualStateManager.VisualStateGroups>

<VisualStateGroup x:Name="CheckStates"> <VisualState x:Name="Checked"> <Storyboard>

<ObjectAnimationUsingKeyFrames

Storyboard.TargetName="txtblk" Storyboard.TargetProperty="Text"> <DiscreteObjectKeyFrame KeyTime="0:0:0"

Value="Button Enabled" /> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState>

<VisualState x:Name="Unchecked" />

</VisualStateGroup> </VisualStateManager.VisualStateGroups>

<TextBlock Name="txtblk"

Text="Button Disabled"/>

</Border> </ControlTemplate> </ToggleButton.Template> </ToggleButton> </Grid>

Данный ToggleButton имеет, что называется, специальный шаблон узкого назначения ControlTemplate, поэтому в нем нет никаких излишеств. Дерево визуальных элементов включает лишь Border и TextBlock. Свойство Content проигнорировано, и свойство Text объекта TextBlock инициализируется значением «Button Disabled». Все остальное делают

визуальные состояния. Кроме обычных визуальных состояний Button, ToggleButton также определяет группу CheckStates (Состояния переключателя), включающую состояния Checked (Установлен) и Unchecked (Снят). Данный шаблон обрабатывает только эти два состояния, и анимация состояния Checked задает свойству Text объекта TextBlock значение «Button Enabled». Вот как это выглядит при неактивном Button:

Я не описывал состояние Disabled для ToggleButton, потому что этот шаблон предполагается использовать только для данного приложения, и я знаю, что ToggleButton никогда не будет неактивным.

Источник: Чарльз Петзольд, Программируем Windows Phone 7, Microsoft Press, © 2011.

По теме:

  • Комментарии