In order to see the Flickr control that goes here, please install Microsoft Silverlight!
Get Microsoft Silverlight

Writing a LensPanel (aka FishEyePanel) in Silverlight

by Kevin Doolan 27. August 2008 04:49

Writing a custom control is a pretty good exercise if you're new to Silverlight or WPF.  I originally wrote the LensPanel Flickr control for this blog in WPF and later ported it to Silverlight.  In the process I delved into Storyboards (when to use them and more importantly when not to), Dependency Properties, layout, transforms, resources and everything in between.  I recently refactored it with my brother in an international pairing exercise (he's in Ireland, I'm in Germany).  It was slightly different from the traditional XP pairing process, but extremely effective none the less - we covered a lot of ground in a very short time.  We both ended up with cosmetically different Controls (we weren't operating off the same source, just the same problem, with the same solutions applied).

Storyboards

Storyboards are used in two places, the fade up effect when an image loads (non-interactive), and for global animation when performing the manual calculations of size and position of the items based on the mouse position (also non-interactive, at least not in the traditional sense).  The latter is the 'chained' Storyboard technique recommended for Game-type applications.  Essentially it gives you a run loop.

Layout

Initially, I calculated the scale of each item and I let the base Panel class perform the layout pass, but this turns out to be a bad idea.  What happens is the items to the left and right of the lens wobble slightly as you move across the control because of width changes in the items that are scaling under the influence of the lens.  There's also an issue of centering the items - you don't always want to, like when the mouse is on the far left or right of the item group. It depends on how much you're willing to accept in terms of visible artifacts.  For me, the artifacts were too jarring so I went in and calculated layout manually.

ItemsControl, Templates and ItemsSource

The ItemsControl is the glue that holds everything together, and takes care of injecting the control with items harvested from a custom provider, FlickrPhotoProvider, which is derived from ObservableCollection.  It also allows specification of templates for the Panel itself and the Items that populate it.   Providers are ideal for a control that gets populated based on, in this case, a dialogue with Flickr's API.  Here's what the xaml looks like:

   1: <UserControl x:Class="LensPanel.Page"
   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
   4:     xmlns:local="clr-namespace:LensPanel;assembly=LensPanel"
   5:     Height="135">
   6:     <UserControl.Resources>
   7:         <local:FlickrPhotoProvider x:Key="FlickrPhotoProvider" NumberOfItems="12" />
   8:     </UserControl.Resources>
   9:     <Grid>
  10:         <ItemsControl x:Name="icLensPanel" ItemsSource="{StaticResource FlickrPhotoProvider}">
  11:             <ItemsControl.ItemsPanel>
  12:                 <ItemsPanelTemplate>
  13:                     <local:LensPanel ElementWidth="50" ElementHeight="100" ElementRenderTransformOriginX="0" ElementRenderTransformOriginY=".5" ElementSelectableHeightFactor=".5">
  14:                         <local:LensPanel.Background>
  15:                             <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
  16:                                 <GradientStop Color="#FF333333" Offset=".54"/>
  17:                                 <GradientStop Color="#FF383838" Offset=".68"/>
  18:                                 <GradientStop Color="#FF303030" Offset=".70"/>
  19:                                 <GradientStop Color="#FF333333" Offset=".90"/>
  20:                             </LinearGradientBrush>
  21:                         </local:LensPanel.Background>
  22:                     </local:LensPanel>
  23:                 </ItemsPanelTemplate>
  24:             </ItemsControl.ItemsPanel>
  25:             <ItemsControl.ItemTemplate>
  26:                 <DataTemplate>
  27:                     <local:LensPanelReflectedItem ImageUri="{Binding ImageFarmUrl}" NavigateUri="{Binding ImagePageUrl}" />
  28:                 </DataTemplate>
  29:             </ItemsControl.ItemTemplate>
  30:         </ItemsControl>
  31:         <TextBlock x:Name="uxError" Text="" Foreground="Silver" HorizontalAlignment="Center" VerticalAlignment="Bottom" />
  32:     </Grid>
  33: </UserControl>

This gives you a clean separation of interests and dependency injection.  The LensPanel and LensPanelRelfectedItem have no idea about any of the Flickr code, and vice versa.

Quick and Dirty Reflections

Silverlight doesn't have a VisualBrush.  This makes typical effects a little more challenging in Silverlight, but not impossible. 

There are two options.  You can either attempt to procedurally create the reflections in code by cloning and flipping the elements, which is a lot of relatively heavy lifting on your part, or you can take the quick and dirty option and flesh it out in pure xaml. 

I opted for the xaml approach as a quick way to prototype the method, but it worked so well, I left is as it is.

Each item in the LensPanel is a Grid with two rows.  The Grid houses the Image and its' reflection underneath.  In fact the Grid houses two Grid container objects, each of which contains an image.  The reason for the Grid container is to provide an initial background, that the images can be faded onto when they load.

Here's a snapshot of the xaml for the LensPanelReflectedItem.

   1: <UserControl x:Class="LensPanel.LensPanelReflectedItem"
   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
   4:     xmlns:local="clr-namespace:LensPanel;assembly=LensPanel">
   5:     <UserControl.Resources>
   6:         <Style x:Key="SubjectImageStyle" TargetType="Image">
   7:             <Setter Property="VerticalAlignment" Value="Center" />
   8:             <Setter Property="HorizontalAlignment" Value="Center" />
   9:             <Setter Property="Stretch" Value="UniformToFill" />
  10:             <Setter Property="Opacity" Value="0" />
  11:         </Style>
  12:         <Style x:Key="SubjectImageGridStyle" TargetType="Grid">
  13:             <Setter Property="Background" Value="#515151" />
  14:         </Style>
  15:         <Style x:Key="ReflectionImageGridStyle" TargetType="Grid">
  16:             <Setter Property="Background" Value="#515151" />
  17:             <Setter Property="RenderTransformOrigin" Value="0.5,0.5" />
  18:             <Setter Property="RenderTransform">
  19:                 <Setter.Value>
  20:                     <TransformGroup>
  21:                         <ScaleTransform ScaleY="-1"/>
  22:                     </TransformGroup>
  23:                 </Setter.Value>
  24:             </Setter>
  25:             <Setter Property="OpacityMask">
  26:                 <Setter.Value>
  27:                     <LinearGradientBrush EndPoint="0,0" StartPoint="0,1">
  28:                         <LinearGradientBrush.GradientStops>
  29:                             <GradientStop Color="#55FFFFFF" Offset="0"/>
  30:                             <GradientStop Color="#00FFFFFF" Offset=".5"/>
  31:                         </LinearGradientBrush.GradientStops>
  32:                     </LinearGradientBrush>
  33:                 </Setter.Value>
  34:             </Setter>
  35:         </Style>
  36:         <Storyboard x:Name="fadeUp" BeginTime="00:00:00.00" Duration="00:00:01.00" >
  37:             <DoubleAnimation Storyboard.TargetName="imgSubjectItem" Storyboard.TargetProperty="Opacity" To="1"/>
  38:             <DoubleAnimation Storyboard.TargetName="imgReflectionItem" Storyboard.TargetProperty="Opacity" To="1"/>
  39:         </Storyboard>
  40:     </UserControl.Resources>
  41:     <Grid Margin="1" >
  42:         <Grid.RowDefinitions>
  43:             <RowDefinition Height="*"/>
  44:             <RowDefinition Height="*"/>
  45:         </Grid.RowDefinitions>
  46:         <Grid Style="{StaticResource SubjectImageGridStyle}" MouseLeftButtonUp="LensPanelReflectedItem_MouseLeftButtonUp">
  47:             <Image x:Name="imgSubjectItem" Style="{StaticResource SubjectImageStyle}"/>
  48:         </Grid>
  49:         <Grid Grid.Row="1" Style="{StaticResource ReflectionImageGridStyle}">
  50:             <Image x:Name="imgReflectionItem" Style="{StaticResource SubjectImageStyle}"/>
  51:         </Grid>
  52:     </Grid>
  53: </UserControl>

The upper Grid is pretty self explanatory, and holds the main image.  The reflection Grid holds the same image, and has a render transform applied to flip it.  The reflection Grid also has an Opacity Map which effectively fades the reflection out.  At no point are the dimensions of the Item specified - it's up to the LensPanel Control to set the dimensions.

There's a little duplication in there.  Right now, Silverlight doesn't support BasedOn styles, so there's not a huge amount you can do about it.

You can see the finished product on the top of this page, provided you're viewing on a system that Silverlight can run on.  If the page detects it's on a non-Silverlight platform, it collapses the Silverlight HTML object, so you don't see the "Install Silverlight" button.

Add comment


(Will show your Gravatar icon)  

biuquote
  • Comment
  • Preview
Loading



About

I'm a software engineer with a background in game engine development as well as art and animation.  More detail...

Calendar

<<  July 2009  >>
MoTuWeThFrSaSu
293012345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar

Details

Powered by BlogEngine.NET 1.4.5.0
Theme and LensPanel/Flickr Control by Kevin Doolan



Hosted at DiscountAsp.net

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

This website is not responsible for externally hosted material, including linked articles, photos or any other media.

© Copyright 2009, Kevin Doolan.