Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Google Image Search Using Conveyor-Belt Animatable Panel

0.00/5 (No votes)
11 Dec 2011 3  
Performing four different Google Image searches in a single web-page, each search result is displayed and managed in a 'conveyor-belt' form.

Attention

This code sample requires the following Installations in order to Run and Compile:

As it is based upon features from both sources !

Introduction

For some time, I've wanted to write a conveyor-belt display.
I have 'played' with numerous kinds of 'multi-elements' displays and find many of them to have too many UI-related faults.

The conveyor-belt display has some 'built-in' advantages:

  • Fits acceptably well to a rectangular defined area
  • Elements have different sizes - close, more relevant elements are larger hence, information-relevancy oriented
  • Users have an absolute conception about the quantity of items
  • Users have a continues orientation regarding the elements
  • Items movement along the 'belt' is relatively elegant (during my experiments in different multi-items UI displays, I've noticed that if one or more items moves faster than the rest, it actually 'irritates the eye')

What finally gave me the incentive to sit down and write this display were two things:

So I sat down to write a 'conveyor-belt' display Animated-PANEL...

Background

Why A Panel (Even More - Why Animated)?

Actually, there are some good reasons why all WPF/Silverlight out-of-the-box panels are 'static-content-arrangers'. Mainly, it keeps them basic, thus generic.

What made me override this convention were the following reasons:

  • Why not? Nobody said that panel shouldn't have animation quality.
  • In this special case - spreading the Panels children along the conveyor-belt's path has no meaning without the belt actually turning.
  • In this special case - the conveyer-belt turn implies a close interaction with the panel's children order (zorder), which is managed inside the panel.
  • Encapsulation - a standalone panel with built-in functionality without outside code manipulations.
  • My own desire to write an animatable-custom-conveyor-belt panel.

Controllable Bindable Panel

The first hurdle I met was the 'Binded-panel' control issue, explained and solved in Dr. WPF's great article, Conceptual Children: A powerful new concept in WPF.
In a nutshell: when you wish a panel to display items from a binded source, you usually set the panel as the ItemsPanel property of some kind of ItemsControl (ListBox* or ItemsControl), then bind the ItemsControl's ItemsSource property to your logical list of items (usually in the ViewModel when using the MVVM pattern).
Then the ItemsControl takes (rightfully) an Exclusive control over the panel's children, so manipulating the children through the panel (or even inside it) is banned!
While Dr. WPF used some clever 'tricks' to overcome this hurdle, and 'control the uncontrollable', I took another approach altogether...

Looking at things from a different angle, what we basically want, is to bind panel's children to a logical items-source.
We are forced to use an ItemsControl as a mediator between the logical items-source and the panel's children (thus, losing control over the panel's children).
Would it be great if we could have a less 'control-freak' mediator or even, a panel to have an 'ItemsSource' property?
Well, we can, using AttachedProperty!

<ConvbeltPanel:AnimConvbeltPanel x:Name="AnimConvbeltPanel" 
    ConvbeltPanel:PanelChildrenBinder.ItemsSource ="{Binding Path=Images}" 
    ConvbeltPanel:PanelChildrenBinder.ItemTemplate="{StaticResource ImageDataTemplate}"
    ...    

As you can see, I also added 'ItemTemplate' support...

* Naming Issue thought #1:

I have a problem with the name 'ListBox' as the term 'Box' implies VISUAL quality, which contradicts one of WPF's core conceptions, regarding the 'Lookless' of controls. Would a better name for this control be 'SelectableItemsControl' ?

Cross-Domain Issue

As the name of this article implies, the application's core functionality is to utilize Google image search API in order to display Images from different sources (domains) in a conveyor-belt form.
Cross-Domain data is forbidden in Silverlight unless:

  • Data is fetched from approved WCF service (service has a valid User-Access-policy file)
  • Silverlight application runs as Out-Of-Browser(OOB) app. So, it is 'free' from Silverlight's Cross-Domain restriction.

In this sample, I tried to combine both options :
On startup, app (normal Silverlight in a web-browser) checks if local WCF is present. This self-hosted console-based service acts as a proxy between Silverlight and the actual data source. Just like any other EXE, it has Cross-Domain Data-access permissions, and it is a valid data-source for Silverlight as it publishes a runtime generated User-Access-policy file.
I must admit that I love this kind of WCF (self hosted console based), I regard it as the 'missing-link' between the old-fashioned hand-written, self-contained, socket-based TCP server, and the conventional IIS dependent WCF.
It was great to find out that someone already wrote such a service - Enabling cross-domain calls for SL apps on self-hosted TCP services, so all I needed to do is to add GetImageBytes/s methods.
One note - this service actually creates a 'clientaccesspolicy.xml' file 'on the fly', and return it for calls on port 80 (where WCF clients check for this file), this means that IIS should not run!!! as it uses the same port (default HTTP port).

Back to this sample, as I said earlier, on startup, the app checks if local WCF is available (by 'Timeouted' call to the service).
If it does - the app uses this service as a cross-domain proxy - the app calls GetImageBytes/s on service with url/s parameter which downloads and returns Byte array/s back to the app.
If it doesn't (echo call on service timed out) - the app prompts the user with his two options:

  • Install this Silverlight app onto a local machine & run it as OOB with cross-domain permissions.
  • Download & run local WCF, then, refresh the current Silverlight's web page.

Using the Code

The Panel

Construction

Using High-School geometry, I defined the conveyor-belt path to fit the panel's area.
I did that by defining two circles with connected outer arcs, then tilt the hole graph in a desired angle, like so:

Next, define positions along this path, in a non-linear manner (I would like Items to be more spread apart as they are 'closer' (larger), so as item's information become more relevant to the user, it would be less concealed by other items).

* As this part ('construction') wasn't 'high' on my priorities list, this code is far from perfect...

Animation

For performance reasons, all Positioning/Sizing is done using RenderTransform's Translate & Scale, thus omitting many time-consuming panels and its children, Measure/Arrange passes.
All children are initially placed at (0,0) and then Positioned/Moved (animated) by changing their RenderTransform's values.

I have tested various methods for the conveyor-belt's turn, and found that the 'Position-To-Position' animation method gave the best performance.

A thing to notice is the Zorder aspect:
Although, every panel has an Arrange override method, what actually determines the Panel's children zorder is the actual order in which they are set in the Panel's internal Children collection*.

* Naming Issue thought #2:

I find the term 'Arrange' confusing, for it has nothing to do with the children's Z-Ordering. Would a better name be 'Positioning2D' ?

Here's the problem: the order in which items are placed along the conveyer-belt is not the same order they are stored in the panel's children collection.
As the conveyer-belt turns, 'both' orders should be considered.

The following illustrations might clear it:

Blue='Natural Order'
Red=ZOrder

So, when animating children from one position to the next (or previous...):

  • Next position isn't always the neighbor position on the children collection order.
  • Some additional Place-Switching should take place.

To summarize, turning the belt (single step) requires two steps:

  • Rearranging the children collection, so the zorder will match new designated positions arrangement.
  • Animate elements to the designated position & size via render-transform values.

Animation Management

Using 'Position-To-Position' animation method means that for every turn of the conveyer-belt(single-step) multiple Animations (storyboards - one for each child) should take place.
There are several factors that should be dealt with:

  • Wait for all animations in current turn-step to end before starting the next single-step animations.
  • Acceleration - additional user 'request' to turn the conveyor-belt in the same direction while, still, turning.
    - This has an additional implication - Not only the next 'Single-Step-Animations' should be Faster, but also, the currently running Animations should 'update' their speed, so, user will receive an immediate feedback for his acceleration-request !
  • Halt - additional user 'request' while still turning, to turn the conveyor-belt in the opposite direction.
  • Deceleration - NO user 'request' to turn the conveyor-belt.
  • Turn to stand still - As Turn decelerates, when going under a pre-defined certain speed - turn should stop.

I will not get into each factor actual code-mechanism, rather, I will point the subroutines in which they are all being dealt:

public void DoSpinAnimation(int dir)
...
private void UpdateNewDuration(Storyboard sb, TimeSpan ts)
...
private async void DoChildrenSingleStepAnim(int dir)
...
private void AnimateChildrenToPos(int dir)
...
void SingleElementAnimation_Completed(object sender, EventArgs e)

Dirty Tricks

  • The who's on top issue...
    Illustrated in the Top-right image (blue numbers) - Element #8 is on top of element #9, but when the belt will turn clock-wise, element #9 should be on top of element #8. As the zorder is updated first (before animation), unwanted flip will take place (the same goes for Counter-Clock-Wise).
    To resolve this issue, these two 'problematic' elements are spread apart so they will not overlap.

        * Other place with the same issue is with element #1 & #16 -there, I didn't 'play' this trick, as it is hardly noticeable (and me being lazy...)

  • When animating elements from one position to another along a curved path, they are actually moving in a straight line, while positions are relatively close, it is hardly noticeable, but..,
    As a consequence to the previous 'dirty trick', the #8 & #9 elements should travel a long curved distance to the next position.
    One solution would be to break this special-case animation into a number of small sub-positions (key-frames) along the curve.
    Other solution is to give this special animation a curved path using a single easing key-frame and achieve the curved path by manipulating the easing function against the easing-mode property.
    ...
    SineEase seX = new SineEase(); seX.EasingMode = dir < 0 ? 
        EasingMode.EaseOut : EasingMode.EaseIn; edkfX.EasingFunction = seX;
    ...
    ...
    SineEase seY = new SineEase(); seY.EasingMode = dir < 0 ? 
        EasingMode.EaseIn : EasingMode.EaseOut; edkfY.EasingFunction = seY;
    ...

GoogleSearchConvBeltUserControl

As its name implies, this user control is oriented for search (using Google image search API) and display (using the conveyor-belt panel) images.

The actual work is done in its ViewModel:
Upon SearchCommand List of ImageResults is obtained from Google-Search-API.

GimageSearchClient client = new GimageSearchClient(); 
Task<IList<IImageResult>> t = Task<IList<IImageResult>>.Factory.FromAsync
(client.BeginSearch, client.EndSearch, _SearchText, MaxNumOfResultsSelected, null); 
await t;  

Then depending on the app's configuration (see 'Cross-Domain Issue' section), Image-Objects are generated.

if (IsImageBytesByUrlServiceAlive)
{
...
    // set Image-Source to the Bytes received from service    
...
}
else
{
    // when running as OOB : Image-Source can be URL string, so, 
    // no additional work is needed. 
    Images = new ObservableCollection<MyImageRes>
        (t.Result.Select(ir => new MyImageRes(this, (IImageResult)ir)));
}

The panel's Items-Source is binded to those images (see 'Controllable Bindable Panel' section) so they are displayed and animated as a conveyor-belt.
Also, the ViewModel updates its View on its Fetch/Search state, so the view could set the appropriate Visual-State.

This UserControl also bubbles information about 'Left-Clicked-Image' coming from the conveyor-belt panel UP to the application main window for a Window-Scoped response.

MainPage

Contains four tiled (with overlap) Conveyor-Belt-Image-Search UserControls.
As mentioned earlier, if running in browser - checks for proxy-service availability, and rearranges accordingly.
On receiving ShowLargeImage Event from any of the UserControls - animate image to show large, high-res version of it.

Testing

Build Solution(Release)
Run - ...\GoogleSearchConvBeltPanel\DemoProject\Bin\Release\DemoProjectTestPage.html.
You will be prompted with 'Cross-Domain' related message...
Run ImageBytesByUrl service (located in 'GoogleImageSearchService' folder)
Or, right-click and install app as OOB, and run it from start-menu or Desktop-Shortcut.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here