Let’s blend some Native Animation goodness to our Flippin’ Flipity Flippable View in Xamarin.Forms…
So I hop yol’ remember my previous post, It’s a Flippin’ Flipity Flippable View in Xamarin.Forms! where I showcased my awesome control built right from Xamarin.Forms without any native code implementation. 😉
But you may have noticed a slight issue in the Flip Animation, specially on Android and iOS as well (slightly though), where Flip animation moves the View out of it’s bounds.
^As you can see above, in the animation screenshots… 😮
Some improvement needed…
If you look closely, during the flip rotation, the View sort of scales up itself and moves out of the bounds of itself and scales back and revert back to the normal bounds.
This was kind of annoying me from a personal perspective, so that’s why I thought of finding a solution by trying to render the whole animation natively for Android and iOS separately. 😀
Behold ze Native Animation…
So basically the whole logic of the FlipViewControl is going to be the same, only the animation part would be executed natively. Let’s discuss how we could implement a native animation for each Android and iOS below. 😀
As of Android…
As of Android, the reason why the View scales out of bounds during the flip animation is because that is the default behavior of Flip Animation in Android. Since Xamarin.Forms Aniamtions binds to the native default behavior you could definitely expect it to behave that way. There’s an aspect called Camera View distance perspective for any given view, by default during any animation the Camera View aspect doesn’t change, thus causing the overblown effect of the Flip Animation.
So by implementing a native animation what we could achieve is to control the Camera View Distance value for each animation frame manually, also something to keep in mind this needs to be done according to the Screen density. I found this solution thanks to this forum post: https://forums.xamarin.com/discussion/49978/changing-default-perspective-after-rotation
As of iOS…
Here for the iOS its not much of an issue, but you do see a bit of the View scaling out of the boundary. So let’s dive into the iOS native flip animation.
We’ll be using a CATransform3D to maintain the transformation of the View’s Layer and execute the animation using UIView.Animate(), we will be using two CATransform3D objects to make sure the View doesn’t scale beyond the boundaries during the animation. This whole awesome solution I found via a random snippet search https://gist.github.com/aloisdeniel/3c8b82ca4babb1d79b29
Time for some coding…
Let’s get started off with the subclassed custom control, naming it XNFlipView and the implementation is actually same as our previous XFFlipView control implementation, but the only difference is there’s no Xamarin.Forms Animation implementation, or handling of the IsFlipped property in the PCL code, since it will be handled in the Renderer level.
public class XNFlipView : ContentView { public XNFlipView() { ... } public static readonly BindableProperty FrontViewProperty ... public static readonly BindableProperty BackViewProperty ... // Everything else is same as XFFlipView implementation public static readonly BindableProperty IsFlippedProperty = BindableProperty.Create( nameof(IsFlipped), typeof(bool), typeof(XNFlipView), false, BindingMode.Default, null); /// <summary> /// Gets or Sets whether the view is already flipped /// ex : /// </summary> public bool IsFlipped { get { return(bool)this.GetValue(IsFlippedProperty);} set { this.SetValue(IsFlippedProperty, value); } } ... }
You can take a look at the full class implementation in the github repo file: XFFlipViewControl/XNFlipView.cs
Native Renderers implementation…
Since the animations are going to be handled natively, we need to create the Custom Renderers for our XNFlipView for Android and iOS separately, so let’s get started…
Android Custom Renderer
Alright then let’s go ahead and create the XNFlipViewRenderer extending from ViewRenderer, as of Xamarin.Forms 2.5 and later we have to pass the Context in the Custom Renderer’s constructor, so let’s begin with that.
public class XNFlipViewRenderer : ViewRenderer { private float _cameraDistance; private readonly ObjectAnimator _animateYAxis0To90; private readonly ObjectAnimator _animateYAxis90To180; public XNFlipViewRenderer(Context context) : base(context) { ... //Animation Initialization ... } protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.View> e) { base.OnElementChanged(e); if (((XNFlipView)e.NewElement) != null) { // Calculating Camera Distance //to be used at Animation Runtime // https://forums.xamarin.com/discussion/49978/changing-default-perspective-after-rotation var distance = 8000; _cameraDistance = Context.Resources.DisplayMetrics.Density * distance; } } protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); if (e.PropertyName == XNFlipView.IsFlippedProperty.PropertyName) { if (!((XNFlipView)sender).IsFlipped) { this.RotationY = 0; } AnimateFlipHorizontally(); } } private void AnimateFlipHorizontally() { SetCameraDistance(_cameraDistance); _animateYAxis0To90.Start(); } }
Now as you can see above in the constructor we’re initializing the ObjectAnimator objects _animateYAxis0To90 and _animateYAxis90To180 which will be executing the native Flip Animation.
Then in the Renderer’s OnElementChanged we’re calculating the Camera distance value to be used during the Animations execution as we explained before in the concept.
Also you can see how we’re listening to the XNFlipView.IsFlipped value change and executing Animations.
Next let’s take a look into the Animation execution implementation which goes inside the Constructor as you can see in the previous code snippet…
// Initiating the first half of the animation _animateYAxis0To90 = ObjectAnimator.OfFloat(this, "RotationY", 0.0f, -90f); _animateYAxis0To90.SetDuration(500); _animateYAxis0To90.Update += (sender, args) => { // On every animation Frame we have to update the Camera Distance since Xamarin overrides it somewhere SetCameraDistance(_cameraDistance); }; _animateYAxis0To90.AnimationEnd += (sender, args) => { if (((XNFlipView)Element).IsFlipped) { // Change the visible content ((XNFlipView)Element).FrontView.IsVisible = false; ((XNFlipView)Element).BackView.IsVisible = true; } else { // Change the visible content ((XNFlipView)Element).BackView.IsVisible = false; ((XNFlipView)Element).FrontView.IsVisible = true; } this.RotationY = -270; _animateYAxis90To180.Start(); }; // Initiating the second half of the animation _animateYAxis90To180 = ObjectAnimator.OfFloat(this, "RotationY", -270f, -360f); _animateYAxis90To180.SetDuration(500); _animateYAxis90To180.Update += (sender1, args1) => { // On every animation Frame we have to update the Camera Distance since Xamarin overrides it somewhere SetCameraDistance(_cameraDistance); };
As you can see we’re instantiating the animation objects accordingly to the degree angle of the Y Axis they’re suppose to animate the view. Also something very important is that in each animation frame we’re also updating the Camera View Distance, as we discussed earlier this to prevent the View from scaling beyond it’s boundaries. That SetCameraDistance() call takes of it with the previous calculated value. 😉
You can also change the speed of the animation by changing the SetDuration() parameters, which currently I’ve set to 1 second.
You could take a look at the full implementation of the android custom renderer in the github file: XFFlipViewControl.Android/XNFlipViewRenderer.cs
iOS Custom Renderer
Alright then let’s move to the iOS Custom Renderer…
public class XNFlipViewRenderer : ViewRenderer protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); if (e.PropertyName == XNFlipView.IsFlippedProperty.PropertyName) { if (((XNFlipView)sender).IsFlipped) { AnimateFlipHorizontally(NativeView, false, 0.5, () => { // Change the visible content ((XNFlipView)sender).FrontView.IsVisible = false; ((XNFlipView)sender).BackView.IsVisible = true; AnimateFlipHorizontally (NativeView, true, 0.5, null); }); } else { AnimateFlipHorizontally(NativeView, false, 0.5, () => { // Change the visible content ((XNFlipView)sender).FrontView.IsVisible = true; ((XNFlipView)sender).BackView.IsVisible = false; AnimateFlipHorizontally (NativeView, true, 0.5, null); }); } } } public void AnimateFlipHorizontally(...) { ... }
So here in iOS Renderer, it seems a bit straight forward as we’re simply listening to the IsFlipped property change and directly executing the animation.
Next let’s see the Animation implementation…
//https://gist.github.com/aloisdeniel/3c8b82ca4babb1d79b29 public void AnimateFlipHorizontally (UIView view, bool isIn, double duration = 0.3, Action onFinished = null) { var m34 = (nfloat)(-1 * 0.001); var minTransform = CATransform3D.Identity; minTransform.m34 = m34; minTransform = minTransform. Rotate((nfloat)((isIn ? 1 : -1) * Math.PI * 0.5), (nfloat)0.0f, (nfloat)1.0f, (nfloat)0.0f); var maxTransform = CATransform3D.Identity; maxTransform.m34 = m34; view.Layer.Transform = isIn ? minTransform : maxTransform; UIView.Animate(duration, 0, UIViewAnimationOptions.CurveEaseInOut, () => { view.Layer.AnchorPoint = new CGPoint((nfloat)0.5, (nfloat)0.5f); view.Layer.Transform = isIn ? maxTransform : minTransform; }, onFinished ); }
So that’s basically the animation implementation code, which I have extracted from the given gist link at the top, which I have explained in the concept description as well.
You can change the speed of the flip animation by changing the duration.
You could take a look at the full implementation of the android custom renderer in the github file: XFFlipViewControl.iOS/XNFlipViewRenderer.cs
Try it out eh! 😀
Well its use is exactly same as our previous XFFlipView Control. As of an example you could take a look here in my github file: XNFlipViewDemoPage.xaml
So now to execute the awesome Flip Animation, simply change the value of the IsFlipped as follows.
XNFlipViewControl1.IsFlipped = !XNFlipViewControl1.IsFlipped;
As you can see in code behind, we’re changing the value of the control’s IsFlipped property, Simples eh! 😀 This is fully bindable as well, so you can directly bind this to a ViewModel property as well.
... <xfFlipViewControl:XNFlipView x:Name="XNFlipViewControl1" IsFlipped="{Binding IsViewFlipped}"> ... </xfFlipViewControl:XNFlipView>
So you can directly use this in your beautifully crafted MVVM Xamarin.Forms app as well. 😀
Some Live Action…
Here we go baby! iOS and Android running side by side…
Woot!
Look at that the Flip Animation maintains the Bounds of the View nicely during the animation in both Android and iOS! 😉
This whole awesome project i hosted up in my Github repo : https://github.com/UdaraAlwis/XFFlipViewControl
Cheers! 😀 Keep on going my fellow devs!
Spread the love…