Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

09 Aug 2020

XAML控件的VisualState管理

XAML的Control支持VisualStateManager进行视觉呈现状态管理。

VisualStateManager是直接从相依对象派生出来的,它声明有一个添附的相依部属,就是VisualStateManager.VisualStateGroups,可以用在其他元素之上,用来添加视觉呈现状态,下面是一个例子:

<ControlTemplate TargetType="Button">
  <Grid >
    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup x:Name="CommonStates">

        <VisualStateGroup.Transitions>

          <!--Take one half second to transition to the PointerOver state.-->
          <VisualTransition To="PointerOver" 
                              GeneratedDuration="0:0:0.5"/>
        </VisualStateGroup.Transitions>
        
        <VisualState x:Name="Normal" />

        <!--Change the SolidColorBrush, ButtonBrush, to red when the
            Pointer is over the button.-->
        <VisualState x:Name="PointerOver">
          <Storyboard>
            <ColorAnimation Storyboard.TargetName="ButtonBrush" 
                            Storyboard.TargetProperty="Color" To="Red" />
          </Storyboard>
        </VisualState>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <Grid.Background>
      <SolidColorBrush x:Name="ButtonBrush" Color="Green"/>
    </Grid.Background>
  </Grid>
</ControlTemplate>

视觉呈现状态是按照VisualStateGroup来组织的,同一个组里面的状态彼此是互斥的,只有不同组之内的状态可以同时应用在一个元素之上。但是不同组内的状态不可同名。

XAML背后代码可以调用VisualStateManager.GoToState来切换状态。这个方法的第一个参数为被添附VisualStateManager.VisualStateGroups的元素对象(必须是Control派生的),第二个参数是状态的名字,第三个参数用来控制是否需要进行动画效果。动画效果可以参考Storyboarded animations for visual states。VisualStateManager也提供一定的自定义支持。

VisualStateGroup必须有一个用x:Name定义的名字。VisualStateGroup中的某个VisualState可以采用连画板(Storyboard),当跳转到自身的时候触发这个连画板效果。但是同一个VisualStateGroup下一定要定义一个没有连画板效果的VisualState,这样才能取消同组内其他VisualState触发的连画板效果。

除了默认的VisualState列表之外,还可以通过VisualStateGroup的Transitions部属添加VisualTransition元素。VisualTransition的子内容是一个Storyboard对象,VisualTransition的部属可以用来定义这个Storyboard的起始状态和终止状态。如果有多个VisualTransition可以用的话,那么优先级顺序如下:

  1. 起始和终止都定义,并且匹配
  2. 只定义终止,并且匹配
  3. 只定义起始,并且匹配

调用VisualStateManager.GoToState的时候会执行以下动作:

  • 之前的VisualState如果应用了Storyboard,那么该Storyboard效果会被撤销
  • 两个状态之间存在的的VisualTransition会被运行
  • 如果终止状态自带有Storyboard,那么那个Storyboard会被运行

对于只定义GeneratedDuration的VisualTransition,会变为隐式过渡。隐式过度只能应用在Double、Color或者Point之上。参考Implicit transitions

另一相关概念叫做过渡动画,跟VisualStateGroup没有太大关系,可以参考Storyboarded animations for visual states

VisualStateGroup里面包含的是VisualState,其默认内容也是一个Storyboard(但是可以不用指定)。但是VisualState具有Setters和StateTriggers部属,前者用来设置目标元素的一些部属,后者用来定义状态触发契机。要让StateTriggers生效,其VisualStateGroup必须是在XAML根元素的第一个子元素。

前面提到,使用StateTriggers可以用到定义状态触发的契机。StateTriggers所触发的Storyboard状态会在StateTriggers的条件失效时自动清除。所以并不需要使用GoToState 来跳到一个不包含动画效果的状态。另外使用StateTriggers来触发的状态,可以没有x:Name所定义的名字。

替换一个Control的模板的时候,记得要重新定义原先模板内VisualStateManager.VisualStateGroups 定义的所以命名的状态。因为Control的代码有可能会调用VisualStateManager.GoToState来跳转到之前定义的状态。如果新模板中没有定义跳转的目标状态,虽然不会抛出异常,但是这是一种设计错误。所以定义新模板时,最好从老模板的代码上开始修改。

对于自定义的控件,可以通过TemplateVisualStateAttribute从属来暴露涉及的VisualState的名字列表。

StateTrigger是从StateTriggerBase派生出来的。StateTriggerBase只有一个方法,就是SetActive。StateTrigger添加了IsActive部属。另一个从StateTriggerBase派生出来的时AdaptiveTrigger,其定义了MinWindowHeight和MinWindowWidth这两个部属,可以用来根据窗口大小变化而决定是否触发状态。

(本篇完)

2020-09-05更新

VisualStateManager.GoToState的第一个参数必须是一个XAML的Control。

直接在Page底下定义<VisualStateManager.VisualStateGroups>貌似VisualStateManager.GoToState会失败。如果在Page下加一层Panel,然后在Panel上定义<VisualStateManager.VisualStateGroups>VisualStateManager.GoToState就会成功:

<Page>
<Grid> <-- 或者是其他 panel -->
<VisualStateManager.VisualStateGroups> ... </VisualStateManager.VisualStateGroups>>
</Grid>
</Page>

参考

VisualStateManager.GoToState allways returns false.

(更新完)

comments powered by Disqus