Johan Karlsson: Building the Swiper control – Part 2 (Android)
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
So what’s the theory behind the Droid renderer?
- OnElementChanged
- OnElementPropertyChanged
- Draw
- OnTouchEvent
OnElementChanged
{
base.OnElementChanged(e);
UpdateSizes();
_rootView = new View(Context);
SetNativeControl(_rootView);
}
{
if (this.Element == null)
{
return;
}
if (this.Width > 0 && this.Height > 0)
{
_width = this.Width;
_halfWidth = _width / 2;
_height = this.Height;
_halfHeight = _height / 2;
}
}
OnElementPropertyChanged
{
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)
}
Async image downloading
We directly convert the downloaded bits into a Android Bitmap object.
{
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
Tracking your fingers
- 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.
- 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.
- 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
{
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
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.