Mobile Zone
Mobile Zone is brought to you in partnership with:
spacer
spacer spacer Den D.
  • Website

Den is a DZone Zone Leader and has posted 460 posts at DZone. You can read more from them at their website. View Full User Profile

Building an Imgur Client for Windows Phone - Part 2 - Infinite Image Scroll

01.16.2013
| 3056 views |
  • Tweet
  • spacer

Related MicroZone Resources

NoSQL Evaluators Guide

Free Intel Software Tech Webinars: C/C++/Fortran. See abstracts and register

Couchbase Mobile

The 2014 NoSQL Benchmark Showdown

NoSQL Performance When Scaling by RAM

Like this piece? Share it with your friends:

| More

In the previous article in the series I talked about how it is possible to load the main gallery images, as well as how to display a small subset of those on the main page. Today I am going to talk about a way to add more images in the ListBox, loading them as the user needs them, as well as about a way to view image details if the user taps on one of the items.

So let's begin with the ListBox. Currently, when the main page is loaded, I am checking whether there is anything in the HomeImages container - that's where all active images are stored. There is also an additional container - DeserializedHomeImages, that contains the image references, but not the images themselves.

Looking at the code we have, you can see that currently I am loading only 10 images:

ImageDownloadHelper.DownloadImages(MainPageViewModel.Instance.DeserializedHomeImages, 0, 10, (image) =>
    {
        MainPageViewModel.Instance.HomeImages.Add(image);
        Debug.WriteLine(string.Format("[{0}] Image added to HomeImages in MainPageViewModel.",
            image.Type));
    });

Which are then displayed in the core view:

spacer

Perfect, but 10 images are not exactly what we're looking for, since there are many more out there in the reference container (DeserializedHomeImages). So how do we keep adding more to the bound collection?

We need to make sure that we can detect when the user scrolled to the bottom of the list. There is no default event handler, but we can surely implement a custom control that notifies the application when the user reached the end of the current set. The inspiration behind my implementation for this was the control documented by Eric here. I ported it from VB to C#.

I create a new Controls folder in the project and added an InfiniteListBox new class. This class inherits from a standard ListBox, but also provides the proper hooks to let the developer known when a compression, therefore list finalization, event occurs.

Here are the contents of the file:

using System;
using System.Collections;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Imagine.Controls
{
    public class InfiniteListBox : ListBox
    {
        public delegate void OnCompression(object sender, CompressionEventArgs args);
        public event OnCompression CompressionOccured;

        private bool scrollEventsHooked = false;

        public InfiniteListBox()
        {
            this.Loaded += InfiniteListBox_Loaded;
        }

        void InfiniteListBox_Loaded(object sender, RoutedEventArgs e)
        {
            PrepareCompressionTracking();
        }
        
        private void PrepareCompressionTracking()
        {
            ScrollViewer scrollViewer = null;

            if (scrollEventsHooked)
                return;

            scrollViewer = FindFirstElement(this, typeof(ScrollViewer)) as ScrollViewer;

            if (scrollViewer != null)
            {
                FrameworkElement element = VisualTreeHelper.GetChild(scrollViewer, 0) as FrameworkElement;

                if (element != null)
                {
                    var verticalStateGroup = FindVisualStateGroup(element, "VerticalCompression");
                    var horizontalStateGroup = FindVisualStateGroup(element, "HorizontalCompression");

                    if (verticalStateGroup != null)
                    {
                        verticalStateGroup.CurrentStateChanging += verticalStateGroup_CurrentStateChanging;
                    }

                    if (horizontalStateGroup != null)
                    {
                        horizontalStateGroup.CurrentStateChanging += horizontalStateGroup_CurrentStateChanging;
                    }
                }
            }

            scrollEventsHooked = true;
        }
  
        void horizontalStateGroup_CurrentStateChanging(object sender, VisualStateChangedEventArgs e)
        {
            if (e.NewState.Name == "CompressionLeft")
            {
                CompressionOccured(this, new CompressionEventArgs(CompressionType.Left));
            }
            else if (e.NewState.Name == "CompressionRight")
            {
                CompressionOccured(this, new CompressionEventArgs(CompressionType.Right));
            }
        }

        void verticalStateGroup_CurrentStateChanging(object sender, VisualStateChangedEventArgs e)
        {
            if (e.NewState.Name == "CompressionTop")
            {
                CompressionOccured(this, new CompressionEventArgs(CompressionType.Top));
            }
            else if (e.NewState.Name == "CompressionBottom")
            {
                CompressionOccured(this, new CompressionEventArgs(CompressionType.Bottom));
            }
        }

        private VisualStateGroup FindVisualStateGroup(FrameworkElement parent, string name)
        {
            if (parent == null)
                return null;

            IList groups = VisualStateManager.GetVisualStateGroups(parent);
            foreach (VisualStateGroup group in groups)
            {
                if (group.Name == name)
                    return group;
            }

            return null;
        }

        private UIElement FindFirstElement(FrameworkElement parent, Type targetType)
        {
            int childCount = VisualTreeHelper.GetChildrenCount(parent);
            UIElement returnedElement = null;

            if (childCount > 0)
            {
                for (int i = 0; i < childCount; i++)
                {
                    var element = VisualTreeHelper.GetChild(parent, i);
                    if (element.GetType().Equals(targetType))
                    {
                        returnedElement = (UIElement)element;
                        break;
                    }
                }
            }

            return returnedElement;
        }
    }

    public class CompressionEventArgs : EventArgs
    {
        CompressionType _type;

        public CompressionType Type
        {
            get
            {
                return _type;
            }
            set
            {
                _type = value;
            }
        }

        public CompressionEventArgs(CompressionType type)
        {
            _type = type;
        }
    }

    public enum CompressionType
    {
        Top,
        Bottom,
        Left,
        Right
    }
}
There is not much going on in the backend other than the visual tree being traversed to find the ScrollViewer and the associated VisualStateGroup instances for horizontal and vertical compression.

For this to work, you need to define a custom style for the ScrollViewer control. Once again, I've used the original from Eric's article:
<Style TargetType="ScrollViewer">
    <Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
    <Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="Padding" Value="0"/>
    <Setter Property="BorderThickness" Value="0"/>
    <Setter Property="BorderBrush" Value="Transparent"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ScrollViewer">
                <Border BorderBrush="{TemplateBinding BorderBrush}" 
                        BorderThickness="{TemplateBinding BorderThickness}" 
                       >
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="ScrollStates">
                            <VisualStateGroup.Transitions>
                                <VisualTransition GeneratedDuration="00:00:00.5"/>
                            </VisualStateGroup.Transitions>
                            <VisualState x:Name="Scrolling">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="VerticalScrollBar" 
                                                     Storyboard.TargetProperty="Opacity" 
                                                     To="1" Duration="0"/>
                                    <DoubleAnimation Storyboard.TargetName="HorizontalScrollBar" 
                                                     Storyboard.TargetProperty="Opacity" To="1" 
                                                     Duration="0"/>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="NotScrolling"/>
                        </VisualStateGroup>
                        <VisualStateGroup x:Name="VerticalCompression">
                            <VisualState x:Name="NoVerticalCompression"/>
                            <VisualState x:Name="CompressionTop"/>
                            <VisualState x:Name="CompressionBottom"/>
                        </VisualStateGroup>
                        <VisualStateGroup x:Name="HorizontalCompression">
                            <VisualState x:Name="NoHorizontalCompression"/>
                            <VisualState x:Name="CompressionLeft"/>
                            <VisualState x:Name="CompressionRight"/>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    
                    <Grid Margin="{TemplateBinding Padding}">
                        <ScrollContentPresenter x:Name="ScrollContentPresenter" 
                                                Content="{TemplateBinding Content}"
                                                ContentTemplate="{TemplateBinding ContentTemplate}"/>
                        
                        <ScrollBar x:Name="VerticalScrollBar" 
                                   IsHitTestVisible="False" 
                                    
                                   HorizontalAlignment="Right"
                                   VerticalAlignment="Stretch" 
                                   Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"
                                   IsTabStop="False" Maximum="{TemplateBinding ScrollableHeight}" 
                                   Minimum="0" Value="{TemplateBinding VerticalOffset}"
                                   Orientation="Vertical" ViewportSize="{TemplateBinding ViewportHeight}" />
                        
                        <ScrollBar x:Name="HorizontalScrollBar"
                                   IsHitTestVisible="False" 
                                    
                                   HorizontalAlignment="Stretch"
                                   VerticalAlignment="Bottom" 
                                   Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"
                                   IsTabStop="False" Maximum="{TemplateBinding ScrollableWidth}" Minimum="0" 
                                   Value="{TemplateBinding HorizontalOffset}" 
                                   Orientation="Horizontal" ViewportSize="{TemplateBinding ViewportWidth}" />
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
I would recommend including it in App.xaml, in order for it to be an app-wide accessible resource. Going back to MainPage.xaml, where the contents are being rendered, I am replacing the ListBox with InfiniteListBox:
<controls:InfiniteListBox x:Name="mainList"
    CompressionOccured="mainList_CompressionOccured_1"
    ItemsSource="{Binding Path=Instance.HomeImages,
    Source={StaticResource MainPageViewModel}}">
    <controls:InfiniteListBox.ItemTemplate>
        <DataTemplate>
            <Image Stretch="UniformToFill"   Source="{Binding Image}"></Image>
        </DataTemplate>
    </controls:InfiniteListBox.ItemTemplate>

    <controls:InfiniteListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <toolkit:WrapPanel Item Item/>
        </ItemsPanelTemplate>
    </controls:InfiniteListBox.ItemsPanel>
</controls:InfiniteListBox>
The controls namespace was declared in the XAML file header as:
xmlns:controls="clr-namespace:Imagine.Controls"
Notice that the item and item panel template declarations do not change. But there is now a hooked CompressionOccured event:
private void mainList_CompressionOccured_1(object sender, Controls.CompressionEventArgs args)
{
    Debug.WriteLine(args.Type.ToString());
}
For testing purposes, I am simply writing in the output console that the list has reached a specific compression state. And it sure works:
spacer
Now let's get back to the interesting part. As you already know, the main image reference container has images of all formats. However, Windows Phone applications are not able to render GIF images in Image controls. This problem is handled when I am calling DownloadImages, which filters out the entire stack of GIF candidates. But this throws off the total image counter, and so that we won't have to download unnecessary content, I thought that for now I will eliminate GIFs completely from the main gallery showcase.

NOTE: I will introduce those back in the part of the series where I cover animation-based galleries.

I need to eliminate all GIF images from MainPageViewModel.Instance.DeserializedHomeImages. In Silverlight 5 (and Windows Phone 8) I could call
gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.