和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);
}
}
在代码中,可以通过设置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();
}
}