Monday, June 8, 2015

Building the Swiper control - Part 2 (Android)

This is part two in a three-part series about the Swiper control. The first part can be read here, if you are new to custom control/renderers in Xamarin I suggest you read it first since I'll skip the parts that the renderers have in common.


Why didn't I just use the native controls?

I've been getting some comments about the iOS and Android implementation for this, stating that I could have done this a lot simpler by using the native controls (the ViewPager and the UICollectionView). This is perfectly true, but it wasn't the purpose why I created the control.

The reasons I choose to do it the way I did was

  • I originally planned to add custom graphic effects
  • I wanted to see if I could make it perfectly fluid on my own
Having that said, I might convert the control to use the ViewPager and the UICollectionView and create another experimental Swiper as separate control. I got lazy for the WP implementation of the renderer and used a Panorama. So I'm kind of in between the two ways to do this.

So what's the theory behind the Droid renderer?

I went even more back to basic this time. I decided to juggle pure bitmaps and override the Draw(...) method of the renderer. This means we are doing pure rendering on demand of the entire control. 

If we start from the top, you'll see that this time, the renderer inherits from ViewRenderer where View is the thin wrapper for an Android View which is the most basic building block for an Android GUI thingy. View itself inherits from java.lang.Object so it's pretty much bare metal from here on.

   public class SwiperRenderer : ViewRenderer<Swiper, View>

As with the iOS version we need to override a few methods to get going
  • OnElementChanged
  • OnElementPropertyChanged
  • Draw
  • OnTouchEvent

OnElementChanged

The first one being OnElementChanged which is called by Xamarin Forms when it's time to create a platform specific object. (I don't like to use the word Native since it implies that Xamarin isn't native). Anyhow, the method looks like this. It's way shorter than the iOS counter-part.

  protected override void OnElementChanged(ElementChangedEventArgs<Swiper> e)
  {
       base.OnElementChanged(e);
            
       UpdateSizes();

       _rootView = new View(Context);
       SetNativeControl(_rootView);
  }

The first thing we do is call UpdateSizes that simple copies size data into local variables for easier lookup later on.

  private void UpdateSizes()
   {
        if (this.Element == null)
        {
            return;
        }

        if (this.Width > 0 && this.Height > 0)
        {
            _width = this.Width;
            _halfWidth = _width / 2;

            _height = this.Height;
            _halfHeight = _height / 2;
        }
   }

Then we create the platform specific control and set it as the "Native" control. Since this is Android we need to pass the context to every corner of our code. I'm pretty sure the Android team use that as the solution to everything in life, as long as we have a context... We're fine!

At this point we have a control that will do the rendering for us.

OnElementPropertyChanged

This method is a lookalike to the iOS counterpart and I should really look into sharing some more code here by abstracting the events that go on in the renderers. All renderers have an InitializeImages method for example. I could define an interface for all common stuff and create a platform agnostic controller... Well, I didn't so we're stuck with code duplication for the time being. I didn't include the whole method in the sample below, simply the first two as an example of what it looks like.

        protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == Swiper.SourceProperty.PropertyName)
            {
                InitializeImages();
            }

            if (e.PropertyName == Swiper.WidthProperty.PropertyName || e.PropertyName == Swiper.HeightProperty.PropertyName)
            {
                UpdateSizes();
            }

            if (e.PropertyName == Swiper.SelectedIndexProperty.PropertyName)
            {
                // TODO Check for index overrun
                if (this.Element.SelectedIndex > 0 &&
                    _currentImageUrl != this.Element.Source[this.Element.SelectedIndex])
                {
                    _currentImageUrl = this.Element.Source[this.Element.SelectedIndex];
                    InitializeImages();
                }
            }


            // Code omitted (there's more in this method)
    }

As with the iOS version, we listen for changes in properties and call the appropriate methods to handle this change. For example, if we change the source we need to reinitialize the images. If we change the width or height we need to update sizes. Those sizes are needed to render later on.

Async image downloading

I played around with a couple of different approaches to async image downloading. I ended up with the basic WebClient since it would be cool to have download progress if the images are large. Looking at the code now, I realize that it's not fully implemented yet. I registered an issue (#13) for this and hopefully I'll get something done. The downloading works but the showing the progress is not completed yet. We just draw a loading text.

We directly convert the downloaded bits into a Android Bitmap object.

      private async Task LoadWithProgress()
        {
            try
            {
                var webClient = new WebClient();
                webClient.DownloadProgressChanged += webClient_DownloadProgressChanged;

                var bytes = await webClient.DownloadDataTaskAsync(new Uri(_url));
                _bitmap = await BitmapFactory.DecodeByteArrayAsync(bytes, 0, bytes.Length);

                if (Completed != null && _bitmap != null)
                {
                    Completed(this);
                }
            }
            catch (Exception ex)
            {
                Log.Debug("SwipeRenderer", "Exception loading image '{0}' using WebClient", _url);
            }
        }

        void webClient_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
        {
            int i = 42;
        }

Caching

The caching in the Android version is just as basic. We simply store a dictionary of string and AsyncImageLoader for now. It's the same with all three platforms and a common caching strategy is required here.

Tracking your fingers

Handling touch events is easy. Simply override the OnTouchEvent method and check what type of event is being raised. We can break down this method in three parts categorized by the action type.


  1. The user starts a touch and we get a MotionEventActions.Down. We store the start location of the X-axis of the touch position to keep for later reference.
  2. If the action is Move we calculate the offset value of the current finger position on the X-axis. Then we call Invalidate() to force a redraw of the control.
  3. When the user lets go of the image we need to check if the image has moved far enough to count as a image switch motion and in that case, what direction. If it's not a switch we still need to animate the images back into the original place.


 public override bool OnTouchEvent(MotionEvent e)
        {
            switch(e.Action)
            {
                case MotionEventActions.Down:
                    _swipeStartX = e.GetX();
                    return true;


                case MotionEventActions.Move:
                    _swipeCurrectXOffset = e.GetX() - _swipeStartX;
                    Invalidate();
                    return true;

                case MotionEventActions.Up:
                    var index = this.Element.Source.IndexOf(_currentImageUrl);
                    
                    if(Math.Abs(_swipeCurrectXOffset)>30) // TODO Add a variable for the trigger offset?
                    {
                        if(_swipeCurrectXOffset > 0 && index > 0)
                        {
                            // Left swipe
                            AnimateLeft(index);
                        }
                        else if (_swipeCurrectXOffset < 0 && index < this.Element.Source.Count() -1)
                        {
                            // Right swipe
                            AnimateRight(index);
                        }
                        else
                        {
                            AnimateBackToStart();
                        }
                    }
                    else
                    {
                        AnimateBackToStart();
                    }
                    
                    return true;

                
            }

            return base.OnTouchEvent(e);
        }

Animation of images

Each platform offers different kind of animation APIs. In this sample I've chosen to use the static ValueAnimator to animate a float value. We can take the AnimateLeft(...) method as an example. It's dead simple to use. Simply state the initial value and the end value in the ValueAnimator.OfFloat(...). Hook up events for Update (that fires every frame) and for AnimationEnd(that fires when the end result has been achieved).

For this specific function we continue to animate the current x-offset to the left and when we hit the end we set a new _currentImageUrl (that represents the center image) and reinitialize all images so the new images are displayed.

       private void AnimateLeft(int index)
        {
            var animator = ValueAnimator.OfFloat(_swipeCurrectXOffset, this.Width);
            animator.Start();

            animator.Update += (object sender, ValueAnimator.AnimatorUpdateEventArgs args) =>
            {
                _swipeCurrectXOffset = (float)args.Animation.AnimatedValue;
                Invalidate();
            };
            animator.AnimationEnd += (object sender, EventArgs args) =>
            {
                _swipeCurrectXOffset = 0f;
                _currentImageUrl = this.Element.Source[index - 1];
                InitializeImages();
            };
        }

Drawing

The method signature of Draw(...) looks like this.

    public override void Draw(Android.Graphics.Canvas canvas)

It passes in a single argument in the form of a Canvas object. This Canvas represents the drawable surface that we have access to. The Draw(...) method is called everytime Invalidate() is called else where in the code or when the operating system wants you to update.

The method is quite repetitive so I'll just take a sample out of it.

     // Clear the canvas
            canvas.DrawARGB(255, 255, 255, 255);

            if(_centerBitmap != null && _centerBitmap.Bitmap != null)
            {
                var dest = CalculateCentrationRect(_centerBitmap.Bitmap);
                 canvas.DrawBitmap(_centerBitmap.Bitmap, dest.Left + _swipeCurrectXOffset, dest.Top, null);
            }
            else if (_centerBitmap != null)
            {
                DrawLoadingText(canvas, 0);
            }



This is pretty much what's going on, but times three. One for each image. First we need to clear the frame from the previous stuff drawn onto it. We do that with the canvas.DrawARBG(...) call. This could easily be extended to take a background color or image instead.

Then for each image we either draw the image or draw a loading text if the image isn't downloaded yet.

Summary

You could have made this a lot simpler and perhaps I'll revisit this control and redo it. But as a learning experience it was quite fun. Feel free to steal any code! My code is your code!

Tuesday, June 2, 2015

Building the Swiper control (the iOS part)

This is going to be a rather long post about building the Swiper control for iOS. There is also a WP and an Android-renderer available but in this post I'll focus on iOS.

The Swiper control is a image flipping control that displays images in a long line and allows for lazy-loading of new images.

All source code is available at GitHub (https://github.com/johankson/flipper) and the bits can also be downloaded through nuget (http://www.nuget.org/packages/Flipper.Forms/).

What's the purpose with this blog post?

The purpose is to show you how the swiper control is built from an iOS perspective.


Introduction

This control is a standard Xamarin Forms control. That means that there are two major parts;
  • The shared code in the PCL
  • One renderer for each platform
In the shared code we declare our control in a platform agnostic way. I've chosen to name it Swiper.cs (upper blue arrow). Each platform then has a corresponding renderer if they do custom rendering that is. In this case the lower blue arror pointing at the file named SwiperRenderer.cs.


We're going to look into details for each of these files in a little while. But first I think we should start by looking at how the control is used and what it looks like in action.

How is it used?

We're going to start at the end where it gets used. In a XAML-page, the example is from the sample app included in the project.

The first thing you need to do is to declare a namespace so that we can reference the control.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:flipper="clr-namespace:Flipper.Controls;assembly=Flipper"
             x:Class="Flipper.Sample.Views.SwiperView">

After the namespace is declared, you are free to use the control.

        <flipper:Swiper Source="{Binding Items}" 
                        IsNearEnd="{Binding EndIsNearCommand}" 
                        SelectedIndex="{Binding Index, Mode=TwoWay}"
                        SelectedUrl="{Binding Url, Mode=TwoWay}"
                        NearEndTreshold="4" />

The only mandatory property that you need to set is the Source property that is an ObservableCollection that contains a list of image URLs. The reason for it being observable is that the control needs to react to you adding (or removing) images to the list and update stuff based on that.

To help you with the lazy loading you can specify a command to be executed when the user is closing in to the end of the list. The name of this command is IsNearEnd. You can control the timing of this by setting the NearEndTreshold property to fire the command when the desired number of images are left.

The SelectedUrl and SelectedIndex can be set and read. Please note that the URL must exist in the already supplied list. I was thinking about adding a behavior that simply adds a new URL to the end of the list if the URL isn't in the list.

So what does it look like?

This is 29 amazing seconds of random image flipping...


Ok, show me some code!

Let's start with the platform agnostic part. I've chosen to inherit from the Xamarin Forms View class since it best fits my needs of behavior. I'm not going to show all the code here since it would just take up a lot of space. For the full sample, check out the source at Github.

  public class Swiper : View
  {
        public static readonly BindableProperty SourceProperty =
            BindableProperty.Create<Swiper, ObservableCollection<string>>(
            (p) => p.Source, null);

       public ObservableCollection<string> Source
        {
            get { return (ObservableCollection<string>)GetValue(SourceProperty); }
            set { SetValue(SourceProperty, value); }
        }

The second thing you need to do is to define static BindableProperty objects for each property that you want to bindable and then map these to the actual member property of the class. In a nutshell, this is what my platform agnostic piece of the Swiper control do. It could do more. One example is to move the code that determines if we are getting close to the end of our image list. This code is now in each of the three renderers... Bad practice... Well, no ones perfect.

The renderer

Most of the code is within the renderer class in the iOS project. To allow for the Xamarin Forms framework to be aware of what renderer that goes with what control, you need to register them. In our case it looks like this:

[assembly: ExportRenderer(typeof(Swiper), typeof(SwiperRenderer))]

It declares the fact that if the framework needs to render a control of the type of Swiper, please use the SwiperRenderer. In fact, you can create a renderer for any Xamarin forms control. For example overriding a Label by inheriting form a LabelRenderer and add custom logic to it. This line of code must be outside any namespace declaration.

The SwiperRenderer class inherits from ViewRenderer and here's where the platform specific magic kicks in. Notice the UIView at the end of the generic declaration. This is a "native" (everything is native, but this is more native) iOS UIView that we are going to render our stuff on.

Some image swapping theory first

The way I created this control is not the best way, the first way or the worst way. It's just one way of doing it. I did it to learn more about custom renderers.

What I did is that I took three UIImageView object called them left, middle and right. If we just look at the control, the middle image is the one you see. I then listen to touch events and track drag along the x axis, repositioning the images as the user moves his/hers finger. If the delta value of the drag is above a certain threshold when the user ends the draw the images are animated either left or right. As soon as the images are animated the position of the UIImageView objects are reset and the current index is updated and images are reset. You never see the "jump". 

The normal flow

Right after the renderer is created, the OnElementChanged(...) method is called. This is called once in the renderers lifetime (normally) and lets you create the native control and pass it back to Xamarin forms. It looks like this:
      protected async override void OnElementChanged(ElementChangedEventArgs<Swiper> e)
        {
            base.OnElementChanged(e);

            if (this.Element == null)
            {
                return;
            }

            _leftImageView = CreateImageView();
            _rightImageView = CreateImageView();

            _centerImageView = CreateImageView();
            _centerImageView.UserInteractionEnabled = true;
            _centerImageView.AddGestureRecognizer(new UIPanGestureRecognizer(OnPan));

            UpdateSizes();

            _rootView = new UIView();
            _rootView.ContentMode = UIViewContentMode.ScaleAspectFit;
            _rootView.AddSubview(_centerImageView);
            _rootView.AddSubview(_leftImageView);
            _rootView.AddSubview(_rightImageView);

            this.SetNativeControl(_rootView);

            if (this.Element.Width > 0 && this.Element.Height > 0)
            {
                await InitializeImagesAsync();
            }
        }

The important parts here are:
  • We add a gesture recognizer to the center image. That's the only one that needs one.
  • We update sizes, setting the frame of the image and position them in a fine row.
  • We create a UIView and add three subviews to it.
  • We then call InitializeImagesAsync() that loads the correct images onto the three images displayed.
Then each time a property is updated on the control the framework calls OnElementPropertyChanged(...) and that one looks like this:

 protected async override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == Swiper.SourceProperty.PropertyName)
            {
                await InitializeImagesAsync();
            }
            ... more code omitted - it looks the same
       }

Here you can react to changes in properties. In the example above, if the Source property changes we need to reinitialize our images. In fact, we reinitalize them a lot.

And finally, the OnPan method we wired up to handle any drag events handle the interaction with the user. Check it out in the sample, it's long but self explanatory.

Also, worth noting, there is no draw call since we don't need to override it. iOS is very good at recognizing invalidation of the UI and redraws just fine without us bothering it. The Android version of this control however is mainly manual drawing. Mostly for fun...

What about caching

Well, there is still job to do here. At the moment, I use a simple Dictionary that has no concept of size management and storing to disk. It is destroyed as soon as the user navigates away from the swiper.
        // Primitive cache - no life time management or cleanup - also stores the image in full size.
        // Thinking about abstracting the cache away and inject it instead to make sure it can be
        // replaced during runtime.
        private Dictionary<string, byte[]> _cache = new Dictionary<string, byte[]>();

How does the loading overlay work

Oh you noticed that, nice! When I said that I had three UIImageView objects... I lied, I have three AsyncUIImageView objects that inherit from UIImageView that simply adds an overlay if an image hasn't loaded yet.

Any other special cases

I needed to set the alpha to 0 to make the "-1" picture invisible so you don't see it when scrolling left of the first picture. Clearing the Image source didn't work. Now that I think about it, that should work... I need to investigate that again.

Summary

This is just version 1 of the control. If you have suggestions, please add an issue to the Github project directly and feel free to steal any code you fancy.