sgmunn

Mostly MonoTouch with a chance of Other Stuff.

UIPanoramaView Part 1

One of the things I like about Windows Phone is the panorama view, and I’ve started to see a few examples of it appearing on iOS. A particularly nice example I’ve seen is Track 8. I wondered what would be involved in making something similar.

What I came up with is essentially a UIScrollView with a variable number of ContentItems. A ContentItem is an arbitrary UIView with a title that is placed inside the scrollview. As the scrollview pans its content (our ContentItems), we pan the main title a little bit to give the effect of depth.

Content is added to the panorama by adding a ContentItem to the panorama’s ContentItems collection. A ContentItem takes a title and a function that will create the view required for that item. You can place any arbitrary view into this view, tableviews work well here. Optionally, you can specify the width that the content item should be, otherwise the panorama will choose a width that leaves a little of the next content item visible.

1
2
3
4
5
panorama.Title = "my minions";

panorama.Add(new ContentItem("item 1", createView, 0));
panorama.Add(new ContentItem("item 2", createView, 600));
panorama.Add(new ContentItem("item 3", createView, 0));

The view returned by the ‘createView’ function is placed inside the scrollview so that it can be scrolled when the user pans across. The main title is placed outside the scrollview so that we can move it in relation to the scrollview scrolling, but at a slower pace.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Export("scrollViewDidScroll:")]
private void DidScroll(UIScrollView sView)
{
    var offset = this.scrollView.ContentOffset.X * this.titleRate;
    var f = this.Title.Frame;
    f.X = this.leftMargin - offset;
    this.Title.Frame = f;

    if (this.AnimateBackground)
    {
        offset = this.scrollView.ContentOffset.X * this.backgroundRate;
        f = this.Title.Frame;
        f.X = 0 - offset;
        this.BackgroundView.Frame = f;
    }
}

Here you can see that all we do is change the Frame of the title based on the scrollviews ContentOffset by multiplying it by some rate that we calculated earlier. We are also panning a background view by a different, slower, rate.

The rate at which the title scrolls is calculated so that the title disappears just as the last content item comes into full view. If it scrolled at the same rate as the scrollview, it would disappear long before the last item comes into view.

Track 8 uses the paging mode of the scrollview. This makes the scrollview stop and bounce on page boundaries. There are 2 drawbacks to this approach. The first is that is can be a pain to scroll to the last content item - each page requires a specific scroll by the user, it’s not possible to just swipe and get to the last item in one motion. The second is that it kind of forces you to have content items width be just one screen width, or at the least a multiple. What if I’d like to have a content item 1.5 times the screen width?

The solution is, don’t use the paging mode. This solves both issues in one easy step, trouble is that it still kinda has an issue. The content items will almost never align on the left like you would expect.

Having the content items stop and align like this would not be nice at all.

The way to handle this, is to respond to scrollViewWillEndDragging: withVelocity: targetContentOffset:. This is called when the scrollview is about to stop scrolling and targetContentOffset can be updated to specify where the scrollview should actually stop. This allows us to have the scrollview to snap to the left edge of content items.

1
2
3
4
5
6
7
8
9
10
11
12
[Export("scrollViewWillEndDragging:withVelocity:targetContentOffset:")]
private void WillEndDragging(UIScrollView sView, PointF velocity, ref PointF target)
{
    // BUG: setting the target.X doesn't always have an effect.
    // setting X = 0 doesn't work

    // if we set to zero, just bump it to 1 to make it actually scroll to there - zero won't do
    if (target.X > 0)
    {
        target.X = Math.Max(this.NearestItemLeftFromOffset(target.X), 1);
    }
}

Now when we pan, the scrollview will just slide to the edge of the nearest item. It gives the the panorama a nice fluid motion when flicking from side to side and stops where it should. If an item’s width is wider than the screen, we get it to stop mid-way thru the item.

That’s about it for now. I have to follow up this with a part two to fine tune a view controller. Presently it holds a reference to the view of each content item and won’t unload them. I’d also like to have a nicer way of integrating other view controllers and to transition to them.

Code is on github.

Cheers.

Comments