和UIPlace类似,UIItem继承自UIObject,为虚拟实验室中物品(VItem)提供一个可渲染的对象。其继承关系如下:

配置文件

前文说过,在虚拟实验室的引擎中,我使用两类XML文件来传递数据,这两个XML文件分别对应“灵魂”(V开头)和“外表”(UI开头)。关于VItem的属性文件,为以下形式:

<person name='周慊' centerX='73' centerY='144' x='1100' y='780' file='engine1/michael.xml'
        id='80d76e6b-08d6-4225-bdcc-bf68ca19ca58' actionid='default' />

而UIItem所用的配置文件,为以下形式:

<?xml version="1.0" encoding="utf-8"?>
<item image="girl.xml.png" width="147" height="144">
  <action name="facerightdown" fps="10" next="facerightdown">
    <rectangle x="1176" y="0" />
    <rectangle x="1323" y="0" />
    <rectangle x="1470" y="0" />
    <rectangle x="1617" y="0" />
    <rectangle x="1764" y="0" />
    <rectangle x="1911" y="0" />
    <rectangle x="2058" y="0" />
    <rectangle x="2205" y="0" />
    </actions>
  <action name="faceright" fps="10" next="faceright">
    <rectangle x="0" y="0" />
    <rectangle x="147" y="0" />
    <rectangle x="294" y="0" />
    <rectangle x="441" y="0" />
    <rectangle x="588" y="0" />
    <rectangle x="735" y="0" />
    <rectangle x="882" y="0" />
    <rectangle x="1029" y="0" />
  </actions>
</item>

这两个配置文件和VObject、UIObject的关系如下图所示:

素材

和UIPlace所用的分块素材不同,我用一整张图片作为UIItem的素材,该图片的相对地址在UIItem的配置文件制定:

item image="girl.xml.png"。(注意,在设计中,该地址是相对于配置文件的)

 

上图中,红色实线框住的部分表示一个Action所需用到的图片,而绿色实线框住的部分表示一个Action中的一帧所用的图片。

动画

Silverlight和Flex不同,Flex可以使用SWFLoader控件调用Flash制作的逐帧动画,并和SWF中的ActionScript交互,而Silverlight不具备类似的能力,因此只能通过编程的方式来实线同样的效果,Blogcn上有一篇文章介绍如何在Silverlight中制作逐帧动画,这里借用其思想。

 

导演、剧本、演员、舞台

让我们把UIItem想象成一幕包括导演、剧本、演员和舞台的舞台剧,看看这幕系该如何演出:

导演在舞台剧上的作用为告诉演员在表演的时候该如何执行剧本。在UIItem里我使用一个Storyboard对象和两个DoubleAnimationUsingKeyFrames对象来模拟导演:

protected Storyboard ActionBoard { get; set; }
protected DoubleAnimationUsingKeyFrames ActionXAnimation { get; set; }
protected DoubleAnimationUsingKeyFrames ActionYAnimation { get; set; }

剧本是告诉演员在表演的时候究竟是执行哪种动作,UIItem中的剧本,就是VItem属性的Action集合(this.VItem.Actions):

public VItem VItem
{
    get
    {
        return VObject as VItem;
    }
    set
    {
        VObject = value;
    }
}

演员是动作的表演者,在UIItem中对应一个Image对象:

protected Image Image { get; set; }

舞台:演员演出的场所。在UIItem中,舞台相当于UIItem的Clip属性(该属性来源于Canvas,Canvas的Clip属性类似Flash中的遮罩层,只有在该属性区域下的内容,才会被显示出来):

public RectangleGeometry RectangleClip { get; set; }

 

初始化

在UIItem的构造函数里,初始化上面提及的对象:

public UIItem(VItem source)
{
    //添加演员
    Image = new Image();
    this.Children.Add(Image);
    Canvas.SetLeft(Image, 0);
    Canvas.SetTop(Image, 0);
    //添加舞台
    RectangleClip = new RectangleGeometry();
    this.Clip = RectangleClip;
    //添加导演
    ActionBoard = new Storyboard();
    ActionBoard.Completed += new EventHandler(OnActionCompleted);
    ActionXAnimation = new DoubleAnimationUsingKeyFrames();
    Storyboard.SetTarget(ActionXAnimation, this.Image);
    Storyboard.SetTargetProperty(ActionXAnimation, new PropertyPath("(Canvas.Left)"));
    ActionBoard.Children.Add(ActionXAnimation);
    ActionYAnimation = new DoubleAnimationUsingKeyFrames();
    Storyboard.SetTarget(ActionYAnimation, this.Image);
    Storyboard.SetTargetProperty(ActionYAnimation, new PropertyPath("(Canvas.Top)"));
    ActionBoard.Children.Add(ActionYAnimation);
    //添加剧本载体
    VItem = source;
}
读取剧本

剧本有UIItem的配置文件制定,通过覆盖OnConfigureFileDownload方法来读取剧本

protected override void OnConfigureFileDownload(object sender,
    DownloadStringCompletedEventArgs e)
{
    if (String.IsNullOrEmpty(e.Result)) return;
    //开始读取剧本
    XElement data = XElement.Parse(e.Result);
    this.VItem.Actions.Clear();
    this.VItem.ActionNames.Clear();
    //VItem的大小
    this.VItem.Width = data.Attribute<Int32>("width", 0, XElementExtensions.Int32Parser);
    this.VItem.Height = data.Attribute<Int32>("height", 0, XElementExtensions.Int32Parser);
    if (data.Elements("action") != null)
    {
        //从配置文件中读取每一个Action
        foreach (var xAction in data.Elements("action"))
        {
            //读取一个Action中的每一个Rectangle帧
            var rects = xAction.Elements("rectangle").Select(x => new VRectangle()
            {
                X = x.Attribute<Int32>("x", 0, XElementExtensions.Int32Parser),
                Y = x.Attribute<Int32>("y", 0, XElementExtensions.Int32Parser),
                Width = this.VItem.Width,
                Height = this.VItem.Height
            }).ToArray();
            //读取该Action的名字,一个Action可以有多个名字,以逗号分割
            var names = xAction.Attribute("name").Value.Split(',');
            //设置Action,包括名字、下一动作、帧率,并添加到Actions集合
            foreach (var actionName in names)
            {
                VAction action = new VAction(
                    actionName,
                    data.Attribute("next", "default"),
                    data.Attribute<Int32>("fps", 10, XElementExtensions.Int32Parser),
                    rects
                );
                this.VItem.Actions.Add(action.Name, action);
                this.VItem.ActionNames.Add(action.Name);
            }
        }
    }
    //读取演员
    LoadImage(data.Attribute("image").Value);
}
读取演员

演员及素材,是一张PNG图片(也可以是JPG格式或其他Silverlight支持的图片格式)。素材的地址为配置文件的image属性,我在设计的时候,把该地址相对于配置文件(而不是XAP文件,这点和UIPlace不同)。因此,在地址转换的时候,先要通过XAP文件的地址得到配置文件的地址,然后再通过得到的配置文件的地址得到素材的地址(有点乱)。

重载以下方法:

public static Uri GetLocalURI(String path1, String path2)
{
    if (path2.ToLower().StartsWith("http://"))
    {
        return new Uri(path2);
    }
    else
    {
        return new Uri(GetLocalURI(path1), path2);
    }
}

添加UIItem的LoadImage方法:

protected virtual void LoadImage(String path)
{
    if (!String.IsNullOrEmpty(path))
    {
        BitmapImage bm = new BitmapImage();
        bm.DownloadProgress += new EventHandler<DownloadProgressEventArgs>(OnImageDownload);
        Image.Source = bm;
        bm.UriSource = URIToolkit.GetLocalURI(VItem.ConfigFile, path);
    }
}

最后,处理一下回调函数,当素材装载完毕后,开始有VItem的ActionID制定的动作,为了方便子类覆盖,我把这个函数设置成虚(Virtual)的:

protected virtual void OnImageDownload(object sender, DownloadProgressEventArgs e)
{
    if (e.Progress == 100)
    {
        DoAction(VItem.ActionID);
    }
}

设置舞台大小

为了在每个时刻只显示一帧图片,需要设置Clip属性的大小,把Clip设置成和VItem一样大,由于RectangleClip不是DependcyObject类型的,因此不能用绑定的形式,而需要通过覆盖UIItem的OnSourcePropertyChanged方法来达到和绑定类似的效果:

protected override void OnSourcePropertyChanged(object sender, PropertyChangedEventArgs e)
{
    base.OnSourcePropertyChanged(sender, e);

    if (e.PropertyName == "ActionID")
    {
        DoAction(VItem.ActionID);
    }
    else if (e.PropertyName == "Width" || e.PropertyName == "Height")
    {
        RectangleClip.Rect = new Rect(0, 0, VItem.Width, VItem.Height);
    }
}
执行指定Action

在代码中,可以通过设置VItem的ActionID属性来修改当前UIItem的Action,通过DoAction函数实现:

protected virtual void DoAction(string actionID)
{
    //如果VItem的Action列表中有名为actionID的Action
    if (VItem.Actions.ContainsKey(actionID))
    {
        VAction vAction = VItem.Actions[actionID];
        //导演说:停止上一个动作,各单位准备
        ActionBoard.Stop();
        ActionXAnimation.KeyFrames.Clear();
        ActionYAnimation.KeyFrames.Clear();
        double i = 0;
        //计算Action一帧所需的时间
        double t = 1.0 / vAction.FPS;
        //开始工作,在关键帧中添加每一个Rectangle
        foreach (var rect in vAction.Rectangle)
        {
            ActionXAnimation.KeyFrames.Add(new DiscreteDoubleKeyFrame()
            {
                KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(i)),
                Value = 0 - rect.X
            });
            ActionYAnimation.KeyFrames.Add(new DiscreteDoubleKeyFrame()
            {
                KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(i)),
                Value = 0 - rect.Y
            });

            i += t;
        }
        //用一个全局属性保存当前Action
        CurrentAction = vAction;
        //导演说,开始!
        ActionBoard.Begin();
    }
}