Animate loading spinner arc Xamarin.iOS - ios

In my Xamarin.iOS application, I am trying to animate a loading spinner. I can draw the circle and the arc, but I do not know how to animate it. This is my class for the loading spinner:
public class LoadingSpinnerView : UIView
{
private CAShapeLayer _thinCirlce;
private CAShapeLayer _arc;
public LoadingSpinnerView()
{
_arc = new CAShapeLayer();
_arc.LineWidth = 3;
_arc.StrokeColor = UIColor.Blue.CGColor;
_arc.FillColor = UIColor.Clear.CGColor;
_thinCirlce = new CAShapeLayer();
_thinCirlce.LineWidth = 1;
_thinCirlce.StrokeColor = UIColor.Red.CGColor;
_thinCirlce.FillColor = UIColor.Clear.CGColor;
Layer.AddSublayer(_thinCirlce);
Layer.AddSublayer(_arc);
}
private nfloat _angle;
public nfloat Angle
{
get
{
return _angle;
}
set
{
_angle = value;
}
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
_thinCirlce.Path = UIBezierPath.FromOval(new CoreGraphics.CGRect(0, 0, Frame.Width, Frame.Height)).CGPath;
nfloat radius = Frame.Width / 2;
_arc.Path = UIBezierPath.FromArc(new CoreGraphics.CGPoint(radius, radius), radius, 0, Angle, true).CGPath;
}
}
I want to be able to animate it, something like this:
UIView.Animate(5, () => { _loadingSpinnerView.Angle = 3.14f; }); // This doesn't actually work...

Ready to use control. Just set control to UIView in your Storyboard.
[Register(nameof(CircleLoadingView))]
public class CircleLoadingView : UIView
{
public CircleLoadingView(IntPtr handle) : base(handle)
{
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
SetUpAnimation(this.Layer, new CGSize(Frame.Width, Frame.Height), UIColor.Red);
}
public void SetUpAnimation(CALayer layer, CGSize size, UIColor color)
{
var beginTime = 0.5;
var strokeStartDuration = 1.2;
var strokeEndDuration = 0.7;
var strokeEndAnimation = CABasicAnimation.FromKeyPath("strokeEnd");
strokeEndAnimation.Duration = strokeEndDuration;
strokeEndAnimation.TimingFunction = CAMediaTimingFunction.FromControlPoints(0.4f, 0.0f, 0.2f, 1.0f);
strokeEndAnimation.From = NSNumber.FromFloat(0);
strokeEndAnimation.To = NSNumber.FromFloat(1);
var strokeStartAnimation = CABasicAnimation.FromKeyPath("strokeStart");
strokeStartAnimation.Duration = strokeStartDuration;
strokeStartAnimation.TimingFunction = CAMediaTimingFunction.FromControlPoints(0.4f, 0.0f, 0.2f, 1.0f);
strokeStartAnimation.From = NSNumber.FromFloat(0);
strokeStartAnimation.To = NSNumber.FromFloat(1);
strokeStartAnimation.BeginTime = beginTime;
var groupAnimation = new CAAnimationGroup
{
Animations = new CAAnimation[] {/*rotationAnimation,*/ strokeEndAnimation, strokeStartAnimation },
Duration = strokeStartDuration + beginTime,
RepeatCount = float.PositiveInfinity,
RemovedOnCompletion = false,
FillMode = CAFillMode.Forwards
};
var circle = CreateCircle(size, color);
var frame = CGRect.FromLTRB(
(layer.Bounds.Width - size.Width) / 2,
(layer.Bounds.Height - size.Height) / 2,
size.Width,
size.Height
);
circle.Frame = frame;
circle.AddAnimation(groupAnimation, "animation");
layer.AddSublayer(circle);
}
private CAShapeLayer CreateCircle(CGSize size, UIColor color)
{
var layer = new CAShapeLayer();
var path = new UIBezierPath();
path.AddArc(new CGPoint(size.Width / 2, size.Height / 2),
size.Width / 2,
-(float)(Math.PI / 2),
(float)(Math.PI + Math.PI / 2),
true);
layer.FillColor = null;
layer.StrokeColor = color.CGColor;
layer.LineWidth = 2;
layer.BackgroundColor = null;
layer.Path = path.CGPath;
layer.Frame = CGRect.FromLTRB(0, 0, size.Width, size.Height);
return layer;
}
}

I figured it out. Here is my loading spinner class for other Xamarin developers to use. You can adjust it to your own requirements.
public class LoadingSpinnerView : UIView
{
private CAShapeLayer _thinCirlce;
private CAShapeLayer _arcLayer;
public LoadingSpinnerView(nfloat radius)
{
UIColor.Red.SetColor();
_thinCirlce = new CAShapeLayer();
_thinCirlce.LineWidth = 1;
_thinCirlce.Path = UIBezierPath.FromOval(new CoreGraphics.CGRect(0, 0, radius * 2, radius * 2)).CGPath;
_thinCirlce.StrokeColor = UIColor.Red.CGColor;
_thinCirlce.FillColor = UIColor.Clear.CGColor;
Layer.AddSublayer(_thinCirlce);
UIColor.Blue.SetColor();
UIBezierPath arcPath = new UIBezierPath();
arcPath.LineWidth = 4;
arcPath.LineCapStyle = CGLineCap.Round;
arcPath.LineJoinStyle = CGLineJoin.Round;
arcPath.AddArc(new CoreGraphics.CGPoint(radius, radius), radius, 0, 2 * 3.14f, true);
_arcLayer = new CAShapeLayer();
_arcLayer.Path = arcPath.CGPath;
_arcLayer.StrokeColor = UIColor.Blue.CGColor;
_arcLayer.FillColor = UIColor.Clear.CGColor;
_arcLayer.LineWidth = 4;
_arcLayer.StrokeStart = 0;
_arcLayer.StrokeEnd = 1;
if (_arcLayer.SuperLayer != null)
{
_arcLayer.RemoveAllAnimations();
_arcLayer.RemoveFromSuperLayer();
}
}
public void StartAnimation()
{
Layer.AddSublayer(_arcLayer);
CABasicAnimation animation = new CABasicAnimation();
animation.KeyPath = "strokeEnd";
animation.Duration = 3;
animation.From = NSNumber.FromFloat(0);
animation.To = NSNumber.FromFloat(1);
_arcLayer.AddAnimation(animation, null);
}
public void StopAnimation()
{
_arcLayer.RemoveAllAnimations();
_arcLayer.RemoveFromSuperLayer();
}
}

Related

What methods are called after DidSelectAnnotationView/DidDeSelectAnnotationView in MapView in Xamarin iOS?

I'm working on a Xamarin iOS project and have a view with a MapView that contains modified MKAnnotations and implements IMKMapViewDelegate.
The view shows a map with a few annotations on the map. Tapping an annotation displays a callout bubble with extra info.
The view also has an overlay view at the bottom of the screen which can be swiped up to display detailed info. The problem is that when an annotation is selected or deselected, the overlay view moves half way up the screen. The annotation selection event calls the methods DidSelectAnnotationView/DidDeselectAnnotationView but the movement of the overlay is triggered after those calls and I cannot hit any break points in any of my code when this event occurs.
So the question is, which iOS methods get called after DidSelectAnnotationView and DidDeselectAnnotationView which could be causing my issue?
The code below reverts the overlay back to its original position but with an obvious UI glitch.
[Export("mapView:didSelectAnnotationView:")]
public void DidSelectAnnotationView(MKMapView mapView, MKAnnotationView annotationView)
{
DelayedOverlayReset();
}
[Export("mapView:didDeselectAnnotationView:")]
public void DidDeselectAnnotationView(MKMapView mapView, MKAnnotationView annotationView)
{
DelayedOverlayReset();
}
private async Task DelayedOverlayReset()
{
await Task.Run(async () =>
{
Thread.Sleep(150);
});
_overlayView?.SetUpOverlayMinPosition();
}
I can share more code but its work related so I need to anonymise it. Happy to provide further detail or answer questions as necessary.
EDIT overlay view:
namespace Touch.Views.Detail
{
[Register("DetailsOverlayView")]
public sealed class DetailsOverlayView : UIView
{
private const float OverlayDragBarWidth = 56.0f;
private const float OverlayDragBarTopPadding = 20.0f;
private const float OverlayDragBarHeight = 6.0f;
private const float OverlayDragBarBottomPadding = 18.0f;
private const float CornerRadius = 18.0f;
private const float ScoreImageWidth = 60.0f;
private const float IconImageWidth = 16.0f;
private const float ShadowOpacity = 0.15f;
private const float ShadowRadius = 10.0f;
private const float ShadowOffsetWidth = 0f;
private const float ShadowOffsetHeight = 10.0f;
private const float ViewAnchorMidPoint = 0.5f;
private const float ImageDurationToLabelGap = 30;
private const float AddressIconWidthAndPadding = 18.0f;
private const double StartingOverlayAnimationSpeed = 0.3;
private const double AnimationSpeedThreshold = 1.3;
private const string DistanceImageName = "icon_map";
private const string DurationImageName = "icon_clock";
private static readonly TextStyle Heading3SemiBold = TextStyle.Heading3.SemiBold.BrandSecondary;
private static readonly TextStyle BodyRegularCaption = TextStyle.Body.Regular.TextCaption;
private readonly DetailOverlayDisplayData? _displayData;
private readonly double _maxPosition;
private readonly double _minPosition;
private readonly UIView? _parentView;
private readonly ElementLinksViewModel? _ActionViewModel;
private NSLayoutConstraint? _dragBarBottomPadding;
private UIView? _overlayDragBarView;
private UIView? _overlayHeaderAndContentView;
private UIPanGestureRecognizer? _panGestureRecognizer;
private UIScrollView? _scrollView;
private UIStackView? _stackView;
private double _startingPosition;
public NSLayoutConstraint? OverlayHeight;
public DetailsOverlayView(DetailOverlayDisplayData displayData, ElementLinksViewModel action,
UIView parentView)
{
_parentView = parentView;
_displayData = displayData;
_ActionViewModel = action;
TranslatesAutoresizingMaskIntoConstraints = false;
BackgroundColor = UIColor.White;
ClipsToBounds = false;
Layer.CornerRadius = CornerRadius;
Hidden = true;
Layer.ShadowOpacity = ShadowOpacity;
Layer.ShadowOffset = new CGSize(ShadowOffsetWidth, -ShadowOffsetHeight);
Layer.ShadowRadius = ShadowRadius;
SetUp();
var headerView = new DetailHeaderView();
headerView.SetUp(displayData, _scrollView!, _stackView!, parentView);
CreateContentPanel();
SetUpOverlayConstraints();
_minPosition = Math.Round(parentView.Frame.Height - GetHeaderHeight(displayData, parentView.Bounds.Width));
_maxPosition = 0;
}
public void SetUpOverlayStartingPosition()
{
if (_parentView == null)
{
return;
}
Center = new CGPoint(Center.X, _parentView.Frame.Height);
Layer.AnchorPoint = new CGPoint(ViewAnchorMidPoint, 0f);
Hidden = false;
SetOverlayPosition(StartingOverlayAnimationSpeed, _minPosition, CornerRadius, true);
_overlayDragBarView!.Hidden = false;
}
public void SetUpOverlayMinPosition()
{
SetOverlayPosition(0.01, _minPosition, CornerRadius, true);
_overlayDragBarView!.Hidden = false;
}
private void SetUp()
{
_overlayDragBarView = new UIView
{
TranslatesAutoresizingMaskIntoConstraints = false,
BackgroundColor = UIColor.LightGray,
ClipsToBounds = true
};
_overlayDragBarView.RoundCorners(ViewCornerStyle.All, OverlayDragBarHeight / 2);
_overlayHeaderAndContentView = new UIView
{
TranslatesAutoresizingMaskIntoConstraints = false,
ClipsToBounds = true
};
AddSubview(_overlayDragBarView);
AddSubview(_overlayHeaderAndContentView);
_panGestureRecognizer = new UIPanGestureRecognizer(PanOverlayView);
_panGestureRecognizer.CancelsTouchesInView = true;
AddGestureRecognizer(_panGestureRecognizer);
var (outerScrollView, _, innerStackView) =
_overlayHeaderAndContentView!.CreateScrollViewWithFooterAndStackView(UIColor.White);
_stackView = innerStackView;
_stackView.LayoutMarginsRelativeArrangement = true;
_scrollView = outerScrollView;
_scrollView.UserInteractionEnabled = false;
_scrollView.Bounces = true;
_scrollView.SetContentOffset(CGPoint.Empty, true);
_scrollView.Scrolled += ScrollViewOnScrolled;
}
private void CreateContentPanel()
{
var view = new UIView
{
TranslatesAutoresizingMaskIntoConstraints = false,
BackgroundColor = UIColor.Clear,
ClipsToBounds = true
};
var contentStackView = new UIStackView
{
TranslatesAutoresizingMaskIntoConstraints = false,
Axis = UILayoutConstraintAxis.Vertical,
LayoutMarginsRelativeArrangement = true,
Spacing = StyleGuideDimens.PaddingSmall,
DirectionalLayoutMargins =
new NSDirectionalEdgeInsets(0, StyleGuideDimens.PaddingSmall, StyleGuideDimens.PaddingSmall,
StyleGuideDimens.PaddingSmall)
};
_stackView!.AddArrangedSubview(view);
view.AddSubview(contentStackView);
NSLayoutConstraint.ActivateConstraints(new[]
{
view.LeadingAnchor.ConstraintEqualTo(view.Superview.LeadingAnchor),
view.TrailingAnchor.ConstraintEqualTo(view.Superview.TrailingAnchor),
contentStackView.TopAnchor.ConstraintEqualTo(view.TopAnchor),
contentStackView.BottomAnchor.ConstraintEqualTo(view.BottomAnchor),
contentStackView.LeadingAnchor.ConstraintEqualTo(view.LeadingAnchor),
contentStackView.TrailingAnchor.ConstraintEqualTo(view.TrailingAnchor)
});
var milesTimePanel = SetUpDistanceAndDurationLayout();
var ActionView = ElementLinksView.Create();
_ActionViewModel!.Alignment = Alignment.Center;
ActionView.SetUp(_ActionViewModel);
var scoreSummaryView = new ScoreSummaryView();
scoreSummaryView.SetUp(_displayData!.ScoreSummary);
contentStackView.AddArrangedSubview(milesTimePanel);
contentStackView.AddArrangedSubview(ActionView);
contentStackView.AddArrangedSubview(scoreSummaryView);
contentStackView.SetCustomSpacing(StyleGuideDimens.PaddingMedium, milesTimePanel);
milesTimePanel.LayoutIfNeeded();
view.LayoutIfNeeded();
_stackView.LayoutIfNeeded();
}
private void SetUpOverlayConstraints()
{
NSLayoutConstraint.ActivateConstraints(new[]
{
_overlayDragBarView!.CenterXAnchor.ConstraintEqualTo(CenterXAnchor),
_overlayDragBarView.TopAnchor.ConstraintEqualTo(TopAnchor, OverlayDragBarTopPadding),
_overlayDragBarView.WidthAnchor.ConstraintEqualTo(OverlayDragBarWidth),
_overlayDragBarView.HeightAnchor.ConstraintEqualTo(OverlayDragBarHeight),
_dragBarBottomPadding = _overlayHeaderAndContentView!.TopAnchor.ConstraintEqualTo(
_overlayDragBarView!.BottomAnchor, OverlayDragBarBottomPadding),
_overlayHeaderAndContentView.BottomAnchor.ConstraintEqualTo(BottomAnchor),
_overlayHeaderAndContentView.LeadingAnchor.ConstraintEqualTo(LeadingAnchor),
_overlayHeaderAndContentView.TrailingAnchor.ConstraintEqualTo(TrailingAnchor)
});
_overlayDragBarView.LayoutIfNeeded();
_overlayHeaderAndContentView.LayoutIfNeeded();
}
private void PanOverlayView(UIPanGestureRecognizer gesture)
{
if (_parentView == null || OverlayHeight == null)
{
return;
}
if (gesture.State == UIGestureRecognizerState.Began)
{
_startingPosition = Frame.Y;
}
var scrollOffset = _scrollView!.ContentOffset.Y;
var velocity = gesture.VelocityInView(_parentView);
var y = _parentView.Frame.GetMinY();
var newPos = _startingPosition + gesture.TranslationInView(_parentView).Y;
if (newPos >= _minPosition)
{
SetOverlayPosition(0, _minPosition);
}
else if (newPos <= _maxPosition)
{
SetOverlayPosition(0, _maxPosition, 0, true, 0);
}
else
{
//Scroll the scrollView content to the top
_scrollView!.SetContentOffset(CGPoint.Empty, true);
if (scrollOffset <= 0)
{
Frame = new CGRect(0, newPos, Frame.Width, Frame.Height);
}
}
if (gesture.State == UIGestureRecognizerState.Ended)
{
var duration = velocity.Y < 0 ? (y - _maxPosition) / -velocity.Y : _minPosition - (y / velocity.Y);
duration = duration > AnimationSpeedThreshold ? 1 : duration;
if (velocity.Y > 0)
{
if (scrollOffset <= 0)
{
//Scroll the scrollView content to the top
_scrollView!.SetContentOffset(CGPoint.Empty, true);
_scrollView!.UserInteractionEnabled = false;
SetOverlayPosition(duration, _minPosition);
}
}
else
{
_scrollView!.UserInteractionEnabled = true;
SetOverlayPosition(duration, _maxPosition, 0, true, 0);
}
}
}
private void SetOverlayPosition(double animationDuration, double position, float cornerRadius = CornerRadius,
bool dragBarVisibility = false, float dragBarBottomPadding = 18.0f)
{
Animate(animationDuration, () =>
{
Frame = new CGRect(0, position, Frame.Width, Frame.Height);
_overlayDragBarView!.Hidden = dragBarVisibility;
_dragBarBottomPadding!.Constant = dragBarBottomPadding;
});
AnimateCornerRadius(cornerRadius);
}
private void ScrollViewOnScrolled(object sender, EventArgs e)
{
var scrollOffset = _scrollView!.ContentOffset.Y;
if (scrollOffset >= -1 && _panGestureRecognizer != null)
{
_scrollView.UserInteractionEnabled = false;
}
}
#region Helper Methods
private void AnimateCornerRadius(float cornerRadius) =>
Animate(0.1, () => { Layer.CornerRadius = cornerRadius; });
private UIView SetUpDistanceAndDurationLayout()
{
var containerView = new UIView
{
TranslatesAutoresizingMaskIntoConstraints = false,
ClipsToBounds = false
};
if (_displayData == null)
{
return containerView;
}
var imageDistance = new UIImageView
{
TranslatesAutoresizingMaskIntoConstraints = false,
Image = UIImage.FromBundle(DistanceImageName),
ContentMode = UIViewContentMode.ScaleAspectFit
};
var imageDuration = new UIImageView
{
TranslatesAutoresizingMaskIntoConstraints = false,
Image = UIImage.FromBundle(DurationImageName),
ContentMode = UIViewContentMode.ScaleAspectFit
};
var distanceDurationWrapperView = new UIView
{
TranslatesAutoresizingMaskIntoConstraints = false,
ClipsToBounds = true
};
var stackView = new UIStackView
{
TranslatesAutoresizingMaskIntoConstraints = false,
Axis = UILayoutConstraintAxis.Horizontal,
Alignment = UIStackViewAlignment.Fill,
Spacing = 0
};
containerView.AddSubview(stackView);
stackView.TopAnchor.ConstraintEqualTo(containerView.TopAnchor).Active = true;
stackView.BottomAnchor.ConstraintEqualTo(containerView.BottomAnchor).Active = true;
stackView.LeadingAnchor.ConstraintEqualTo(containerView.LeadingAnchor).Active = true;
stackView.TrailingAnchor.ConstraintEqualTo(containerView.TrailingAnchor, 0).Active = true;
var distanceLabel = _displayData.TotalMiles.CreateLabel(BodyRegularCaption, ElementMargins.None(),
UITextAlignment.Left);
var durationLabel = _displayData.TotalJourneyTime.CreateLabel(BodyRegularCaption, ElementMargins.None(),
UITextAlignment.Left);
stackView.AddArrangedSubview(distanceDurationWrapperView);
distanceDurationWrapperView.AddSubview(imageDistance);
distanceDurationWrapperView.AddSubview(distanceLabel);
distanceDurationWrapperView.AddSubview(imageDuration);
distanceDurationWrapperView.AddSubview(durationLabel);
imageDistance.WidthAnchor.ConstraintEqualTo(IconImageWidth).Active = true;
imageDistance.HeightAnchor.ConstraintEqualTo(IconImageWidth).Active = true;
imageDuration.WidthAnchor.ConstraintEqualTo(IconImageWidth).Active = true;
imageDuration.HeightAnchor.ConstraintEqualTo(IconImageWidth).Active = true;
imageDistance.CenterYAnchor.ConstraintEqualTo(distanceDurationWrapperView.CenterYAnchor).Active = true;
imageDistance.LeadingAnchor.ConstraintEqualTo(distanceDurationWrapperView.LeadingAnchor).Active = true;
distanceLabel.TopAnchor.ConstraintEqualTo(distanceDurationWrapperView.TopAnchor).Active = true;
distanceLabel.BottomAnchor.ConstraintEqualTo(distanceDurationWrapperView.BottomAnchor, 0).Active = true;
distanceLabel.LeadingAnchor.ConstraintEqualTo(imageDistance.TrailingAnchor,
StyleGuideDimens.PaddingExtraSmall).Active = true;
imageDuration.CenterYAnchor.ConstraintEqualTo(distanceDurationWrapperView.CenterYAnchor).Active = true;
imageDuration.LeadingAnchor.ConstraintEqualTo(distanceLabel.TrailingAnchor, ImageDurationToLabelGap)
.Active = true;
durationLabel.TopAnchor.ConstraintEqualTo(distanceDurationWrapperView.TopAnchor).Active = true;
durationLabel.BottomAnchor.ConstraintEqualTo(distanceDurationWrapperView.BottomAnchor, 0).Active = true;
durationLabel.LeadingAnchor.ConstraintEqualTo(imageDuration.TrailingAnchor,
StyleGuideDimens.PaddingExtraSmall).Active = true;
durationLabel.TrailingAnchor.ConstraintEqualTo(distanceDurationWrapperView.TrailingAnchor).Active = true;
distanceDurationWrapperView.LayoutIfNeeded();
containerView.LayoutIfNeeded();
stackView.LayoutIfNeeded();
return containerView;
}
public static nfloat GetHeaderHeight(JourneyDetailOverlayDisplayData displayData, nfloat availableWidth)
{
//Left padding, left score panel padding, score panel width, right padding
var totalAvailableWidth = availableWidth - (StyleGuideDimens.PaddingSmall +
StyleGuideDimens.PaddingSmall + ScoreImageWidth + StyleGuideDimens.PaddingSmall);
var headerString = new NSString(displayData.JourneyDate);
var distance = new NSString(displayData.TotalMiles);
var duration = new NSString(displayData.TotalJourneyTime);
var sizeOfHeader = headerString.GetCalculatedSizeOfString(FontStyle.SemiBold.GetFont(Heading3SemiBold.Size),
totalAvailableWidth);
var (sizeOfStartAddress, sizeOfEndAddress) = GetAddressPanelHeights(displayData, availableWidth);
var sizeOfDistance = distance.GetCalculatedSizeOfString(FontStyle.Regular.GetFont(BodyRegularCaption.Size),
totalAvailableWidth);
var sizeOfDuration = duration.GetCalculatedSizeOfString(FontStyle.Regular.GetFont(BodyRegularCaption.Size),
totalAvailableWidth);
var distanceHeight = sizeOfDistance.Height > IconImageWidth
? sizeOfDistance.Height
: IconImageWidth;
var durationHeight = sizeOfDuration.Height > IconImageWidth
? sizeOfDuration.Height
: IconImageWidth;
var distanceDurationPanelHeight = distanceHeight > durationHeight ? distanceHeight : durationHeight;
return OverlayDragBarTopPadding
+ OverlayDragBarHeight
+ OverlayDragBarBottomPadding
+ sizeOfHeader.Height
+ StyleGuideDimens.PaddingSmall
+ sizeOfStartAddress.Height
+ StyleGuideDimens.PaddingExtraSmall
+ sizeOfEndAddress.Height
+ StyleGuideDimens.PaddingMedium
+ distanceDurationPanelHeight
+ StyleGuideDimens.PaddingSmall;
}
public static (CGSize startAddressHeight, CGSize endAddressHeight) GetAddressPanelHeights(
JourneyDetailOverlayDisplayData displayData, nfloat availableWidth)
{
//Left padding, Address icon width and padding, left score panel padding, score panel width, right padding
var totalAvailableWidth = availableWidth - (StyleGuideDimens.PaddingSmall + AddressIconWidthAndPadding +
StyleGuideDimens.PaddingSmall + ScoreImageWidth + StyleGuideDimens.PaddingSmall);
var startAddress = new NSString(displayData.JourneyStartAddress);
var endAddress = new NSString(displayData.JourneyEndAddress);
var sizeOfStartAddress = startAddress.GetCalculatedSizeOfString(
FontStyle.Regular.GetFont(BodyRegularCaption.Size),
totalAvailableWidth);
var sizeOfEndAddress = endAddress.GetCalculatedSizeOfString(
FontStyle.Regular.GetFont(BodyRegularCaption.Size),
totalAvailableWidth);
return (sizeOfStartAddress, sizeOfEndAddress);
}
#endregion
}
}

Which options do I have for longer option strings?

As we can see on the bottom of the screen, there are options to be chosen.
Which other options are there for this menu such that I can display longer strings fully?
For your requirement you have to make a customize one to do it:
Try to use this code:
using System;
using System.Collections.Generic;
using CoreGraphics;
using UIKit;
namespace CusActionSheet
{
public delegate void OneParameterEventHandler<T>(T data);
public class ALActionSheet : UIView
{
private nfloat maximumProportion = 0.8f;
public event OneParameterEventHandler<string> RowClicked;
//You also can use this if you don't know the generic(but remeber also change it for source):
//public delegate void OneParameterEventHandler(string data);
//public event OneParameterEventHandler SelectedPhoneNumber;
private UIView backgroundContainer;
private UIView topContainer;
private UILabel topContainerTitleView;
private UITableView topContainerTable;
private ALActionSheetSource topContainerTableSource;
private UIButton btnCancel;
private nfloat itemPadding = 10;
private nfloat itemHeight = 60;
private nfloat cornerRadius = 10;
private List<string> dataList;
private CGRect backContainerShownFrame;
private CGRect backContainerHiddenFrame;
private string selectedNumber;
public CGRect BackContainerShownFrame
{
get
{
return backContainerShownFrame;
}
set
{
backContainerShownFrame = value;
}
}
public ALActionSheet(List<string> _dataList)
{
this.BackgroundColor = UIColor.Clear;
this.dataList = _dataList;
backgroundContainer = new UIView();
backgroundContainer.Layer.CornerRadius = cornerRadius;
backgroundContainer.Layer.MasksToBounds = true;
topContainer = new UIView();
topContainer.BackgroundColor = UIColor.White;
topContainer.Layer.CornerRadius = cornerRadius;
topContainer.Layer.MasksToBounds = true;
topContainerTitleView = new UILabel();
topContainerTitleView.Text = "Select a number";
topContainerTitleView.TextAlignment = UITextAlignment.Center;
topContainerTitleView.BackgroundColor = UIColor.FromRGB(230, 230, 230);
topContainer.AddSubview(topContainerTitleView);
topContainerTableSource = new ALActionSheetSource(dataList);
topContainerTableSource.RowClicked += (data) =>
{
selectedNumber = data;
Close();
};
topContainerTable = new UITableView();
topContainerTable.RowHeight = itemHeight;
topContainerTable.Layer.CornerRadius = cornerRadius;
topContainerTable.Layer.MasksToBounds = true;
topContainerTable.BackgroundColor = UIColor.White;
topContainerTable.Source = topContainerTableSource;
topContainer.AddSubview(topContainerTable);
btnCancel = new UIButton(UIButtonType.System);
btnCancel.SetTitle("Cancel", UIControlState.Normal);
btnCancel.TitleLabel.Font = UIFont.SystemFontOfSize(18);
btnCancel.BackgroundColor = UIColor.White;
btnCancel.Layer.CornerRadius = cornerRadius;
btnCancel.Layer.MasksToBounds = true;
btnCancel.TouchUpInside += delegate
{
Close();
};
backgroundContainer.AddSubview(topContainer);
backgroundContainer.AddSubview(btnCancel);
Layout();
}
public void Layout()
{
this.Frame = UIScreen.MainScreen.Bounds;
nfloat tableMaxHeight = UIScreen.MainScreen.Bounds.Height * maximumProportion - 2 * itemPadding - 2 * itemHeight;
nfloat tableHeight = dataList.Count * itemHeight < tableMaxHeight ? dataList.Count * itemHeight : tableMaxHeight;
nfloat itemWidth = UIScreen.MainScreen.Bounds.Width - 2 * itemPadding;
nfloat backgroundContainerHeight = tableHeight + 2 * itemHeight + itemPadding;
backgroundContainer.Frame = new CGRect(0, 0, itemWidth, backgroundContainerHeight);
nfloat topContainerHeight = itemHeight + tableHeight;
topContainer.Frame = new CGRect(0, 0, itemWidth, topContainerHeight);
topContainerTitleView.Frame = new CGRect(0, 0, itemWidth, itemHeight);
topContainerTable.Frame = new CGRect(0, itemHeight, itemWidth, tableHeight);
btnCancel.Frame = new CGRect(0, topContainerHeight + itemPadding, itemWidth, itemHeight);
BackContainerShownFrame = new CGRect(new CGPoint(itemPadding, UIScreen.MainScreen.Bounds.Height - backgroundContainerHeight - itemPadding), backgroundContainer.Bounds.Size);
backContainerHiddenFrame = new CGRect(new CGPoint(itemPadding, UIScreen.MainScreen.Bounds.Height + itemPadding), backgroundContainer.Bounds.Size);
}
public void Show()
{
UIApplication.SharedApplication.KeyWindow.AddSubview(this);
backgroundContainer.Frame = backContainerHiddenFrame;
this.AddSubview(backgroundContainer);
UIView.Animate(0.3, delegate
{
this.BackgroundColor = UIColor.FromRGBA(0, 0, 0, 0.5f);
backgroundContainer.Frame = BackContainerShownFrame;
});
}
public void Close()
{
UIView.Animate(0.3, delegate
{
backgroundContainer.Frame = backContainerHiddenFrame;
this.BackgroundColor = UIColor.Clear;
}, delegate
{
this.RemoveFromSuperview();
if (null != RowClicked && null != selectedNumber)
RowClicked(selectedNumber);
});
}
}
class ALActionSheetSource : UITableViewSource
{
public event OneParameterEventHandler<string> RowClicked;
private string cellID = "ALActionSheetCell";
private List<string> dataList;
public ALActionSheetSource(List<string> _dataList)
{
dataList = _dataList;
}
public override nint RowsInSection(UITableView tableview, nint section)
{
return dataList.Count;
}
public override UITableViewCell GetCell(UITableView tableView, Foundation.NSIndexPath indexPath)
{
UITableViewCell cell = tableView.DequeueReusableCell(cellID);
if (null == cell)
{
cell = new UITableViewCell(UITableViewCellStyle.Default, cellID);
cell.TextLabel.TextAlignment = UITextAlignment.Center;
//cell.TextLabel.AdjustsFontSizeToFitWidth = true;//Auto resize by content
cell.TextLabel.Lines = int.MaxValue;//Multiple lines
}
cell.TextLabel.Text = dataList[indexPath.Row];
return cell;
}
public override void RowSelected(UITableView tableView, Foundation.NSIndexPath indexPath)
{
if (null != RowClicked)
RowClicked(dataList[indexPath.Row]);
}
}
}
And invoke it like this:
public override void ViewDidLoad()
{
base.ViewDidLoad();
// Perform any additional setup after loading the view, typically from a nib.
List<string> testList = new List<string>();
for (int i = 0; i < 10; i++) {
testList.Add("Item " + i.ToString());
}
testList.Add("I'm a very very very very very very very very long text.");
ALActionSheet actionSheet = new ALActionSheet(testList);
actionSheet.RowClicked += (data) => {
Console.WriteLine("data = "+data);
};
UIButton btnTest = new UIButton(UIButtonType.System);
btnTest.SetTitle("Test", UIControlState.Normal);
btnTest.Frame = new CoreGraphics.CGRect(50, 50, 80, 30);
btnTest.TouchUpInside += delegate {
actionSheet.Show();
};
this.Add(btnTest);
}
Hope it can help you.

How to forward the pan gesture from UIScrollView to UIImageView?

I have a UIScrollView and inside a UIImageView so that I can pinche zoom the image view using:
extension CropperViewController : UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.imageView;
}
}
I now also want to be able to freely move the UIImageView so I tried adding a UIPanGestureRecognizer to myUIImageView`:
self.imageView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))));
func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: self.view);
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y);
gestureRecognizer.setTranslation(CGPoint.zero, in: self.view);
}
}
I now had the problem that no pan touch event was fired at all so I thought maybe the UIScrollView is catching all those events. So some research on Stackoverflow told me to add the following to my UIScrollView:
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)));
tapGestureRecognizer.numberOfTapsRequired = 1;
tapGestureRecognizer.cancelsTouchesInView = false;
self.scrollView.addGestureRecognizer(tapGestureRecognizer);
but actually that changed nothing. I can zoom and move the image after zooming but I can not move the image using UIPanGestureRecognizer. I want to use the UIScrollView to be able to zoom but I want to use the UIPanGestureRecognizer to move the UIImageView.
How can I do that?
EDIT
Maybe it is possible to disable or change the pan gesture recognizer of the UIScrollView and forward those events to the UIImageView?
You need to give the pan recognizer a delegate, then return true for shouldRecogniseSimultaneouslyWith....
You may also need to do the same with the scroll view's pan recognizer, which is available as a property.
Alternatively, add another target/action to the scroll view's pan recognizer (using addTarget(_, action:) instead of creating your own.
Try this:
scrollView.panGestureRecognizer.require(toFail: imagePanRecognizer)
If you still get problems (like scrolling feeling laggy) set scrollView's delaysContentTouches to false
Since it seems that there isnt a solution for that I came up with not using a UIScrollView and impelementing UIPinchGestureRecognizer and UIPanGestureRecognizer for my UIImageView by myself:
import Foundation
import UIKit
/**
*
*/
protocol CropperCallback {
/**
*
*/
func croppingDone(image: UIImage);
/**
*
*/
func croppingCancelled();
}
/**
*
*/
class CropperViewController : UIViewController {
/**
*
*/
#IBOutlet var imageView: UIImageView!;
/**
*
*/
var imageViewScaleCurrent: CGFloat! = 1.0;
var imageViewScaleMin: CGFloat! = 0.5;
var imageViewScaleMax: CGFloat! = 5.0;
/**
*
*/
#IBOutlet var cropAreaView: CropAreaView!;
/**
*
*/
#IBOutlet weak var cropAreaViewConstraintWidth: NSLayoutConstraint!
#IBOutlet weak var cropAreaViewConstraintHeight: NSLayoutConstraint!
/**
*
*/
#IBOutlet var btnCrop: UIButton!;
/**
*
*/
#IBOutlet var btnCancel: UIButton!;
/**
*
*/
var callback: CropperCallback! = nil;
/**
*
*/
var image: UIImage! = nil;
/**
*
*/
var imageOriginalWidth: CGFloat!;
var imageOriginalHeight: CGFloat!;
/**
*
*/
var cropWidth: CGFloat! = 287;
/**
*
*/
var cropHeight: CGFloat! = 292;
/**
*
*/
var cropHeightFix: CGFloat! = 1.0;
/**
*
*/
var cropArea: CGRect {
/**
*
*/
get {
let factor = self.imageView.image!.size.width / self.view.frame.width;
let scale = 1 / self.imageViewScaleCurrent;
let x = (self.cropAreaView.frame.origin.x - self.imageView.frame.origin.x) * scale * factor;
let y = (self.cropAreaView.frame.origin.y - self.imageView.frame.origin.y) * scale * factor;
let width = self.cropAreaView.frame.size.width * scale * factor;
let height = self.cropAreaView.frame.size.height * scale * factor;
return CGRect(x: x, y: y, width: width, height: height);
}
}
/**
*
*/
static func storyboardInstance() -> CropperViewController? {
let storyboard = UIStoryboard(name: String(describing: NSStringFromClass(CropperViewController.classForCoder()).components(separatedBy: ".").last!), bundle: nil);
return storyboard.instantiateInitialViewController() as? CropperViewController;
}
/**
*
*/
override func viewDidLoad() {
super.viewDidLoad();
self.imageView.image = self.image;
self.imageView.isUserInteractionEnabled = true;
self.imageView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))));
self.imageView.addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:))));
self.cropAreaViewConstraintWidth.constant = self.cropWidth;
self.cropAreaViewConstraintHeight.constant = self.cropHeight;
self.cropAreaView.layer.borderWidth = 1;
self.cropAreaView.layer.borderColor = UIColor(red: 173/255, green: 192/255, blue: 4/255, alpha: 1.0).cgColor;
self.btnCrop.addTarget(self, action: #selector(self.didTapCropButton), for: UIControlEvents.touchUpInside);
self.btnCancel.addTarget(self, action: #selector(self.didTapCancelButton), for: UIControlEvents.touchUpInside);
}
/**
*
*/
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews();
let imageOriginalRect = self.getRectOfImageInImageView(imageView: self.imageView);
self.imageOriginalWidth = imageOriginalRect.size.width;
self.imageOriginalHeight = imageOriginalRect.size.height;
}
/**
*
*/
func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let rect = self.getRectOfImageInImageView(imageView: self.imageView);
let xImage = rect.origin.x;
let yImage = rect.origin.y;
let widthImage = rect.size.width;
let heightImage = rect.size.height;
let xCropView = self.cropAreaView.frame.origin.x;
let yCropView = self.cropAreaView.frame.origin.y;
let widthCropView = self.cropAreaView.frame.size.width;
let heightCropView = self.cropAreaView.frame.size.height;
let translation = gestureRecognizer.translation(in: self.view);
var x: CGFloat;
var y: CGFloat;
if (translation.x > 0) {
if (!(xImage >= xCropView)) {
x = gestureRecognizer.view!.center.x + translation.x;
} else {
x = gestureRecognizer.view!.center.x;
}
} else if (translation.x < 0) {
if (!((xImage + widthImage) <= (xCropView + widthCropView))) {
x = gestureRecognizer.view!.center.x + translation.x;
} else {
x = gestureRecognizer.view!.center.x;
}
} else {
x = gestureRecognizer.view!.center.x;
}
if (translation.y > 0) {
if (!(yImage >= (yCropView - self.cropHeightFix))) {
y = gestureRecognizer.view!.center.y + translation.y;
} else {
y = gestureRecognizer.view!.center.y;
}
} else if (translation.y < 0) {
if (!((yImage + heightImage) <= (yCropView + heightCropView + self.cropHeightFix))) {
y = gestureRecognizer.view!.center.y + translation.y;
} else {
y = gestureRecognizer.view!.center.y;
}
} else {
y = gestureRecognizer.view!.center.y;
}
gestureRecognizer.view!.center = CGPoint(x: x, y: y);
gestureRecognizer.setTranslation(CGPoint.zero, in: self.view);
self.fixImageViewPosition();
}
}
/**
*
*/
func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
if let view = gestureRecognizer.view {
let widthCropView = self.cropAreaView.frame.size.width;
let heightCropView = self.cropAreaView.frame.size.height;
if (((self.imageViewScaleCurrent * gestureRecognizer.scale * self.imageOriginalWidth) > widthCropView)
&& ((self.imageViewScaleCurrent * gestureRecognizer.scale * self.imageOriginalHeight) > (heightCropView + (2 * self.cropHeightFix)))
&& ((self.imageViewScaleCurrent * gestureRecognizer.scale) < self.imageViewScaleMax)) {
self.imageViewScaleCurrent = self.imageViewScaleCurrent * gestureRecognizer.scale;
view.transform = CGAffineTransform(scaleX: self.imageViewScaleCurrent, y: self.imageViewScaleCurrent);
}
gestureRecognizer.scale = 1.0;
self.fixImageViewPosition();
}
}
/**
*
*/
func fixImageViewPosition() {
let rect = self.getRectOfImageInImageView(imageView: self.imageView);
let xImage = rect.origin.x;
let yImage = rect.origin.y;
let widthImage = rect.size.width;
let heightImage = rect.size.height;
let xCropView = self.cropAreaView.frame.origin.x;
let yCropView = self.cropAreaView.frame.origin.y;
let widthCropView = self.cropAreaView.frame.size.width;
let heightCropView = self.cropAreaView.frame.size.height;
if (xImage > xCropView) {
self.imageView.frame = CGRect(x: xCropView, y: self.imageView.frame.origin.y, width: widthImage, height: heightImage);
}
if ((xImage + widthImage) < (xCropView + widthCropView)) {
self.imageView.frame = CGRect(x: ((xCropView + widthCropView) - widthImage), y: self.imageView.frame.origin.y, width: widthImage, height: heightImage);
}
if (yImage > yCropView) {
self.imageView.frame = CGRect(x: self.imageView.frame.origin.x, y: (yCropView - self.cropHeightFix), width: widthImage, height: heightImage);
}
if ((yImage + heightImage) < (yCropView + heightCropView + self.cropHeightFix)) {
self.imageView.frame = CGRect(x: self.imageView.frame.origin.x, y: ((yCropView + heightCropView + self.cropHeightFix) - heightImage), width: widthImage, height: heightImage);
}
}
/**
*
*/
func getRectOfImageInImageView(imageView: UIImageView) -> CGRect {
let imageViewSize = imageView.frame.size;
let imageSize = imageView.image!.size;
let scaleW = imageViewSize.width / imageSize.width;
let scaleH = imageViewSize.height / imageSize.height;
let aspect = min(scaleW, scaleH);
var imageRect = CGRect(x: 0, y: 0, width: (imageSize.width * aspect), height: (imageSize.height * aspect));
imageRect.origin.x = (imageViewSize.width - imageRect.size.width) / 2;
imageRect.origin.y = (imageViewSize.height - imageRect.size.height) / 2;
imageRect.origin.x += imageView.frame.origin.x;
imageRect.origin.y += imageView.frame.origin.y;
return imageRect;
}
/**
*
*/
func didTapCropButton(sender: AnyObject) {
let croppedCGImage = self.imageView.image?.cgImage?.cropping(to: self.cropArea);
let croppedImage = UIImage(cgImage: croppedCGImage!);
if (self.callback != nil) {
self.callback.croppingDone(image: croppedImage);
}
self.dismiss(animated: true, completion: nil);
}
/**
*
*/
func didTapCancelButton(sender: AnyObject) {
if (self.callback != nil) {
self.callback.croppingCancelled();
}
self.dismiss(animated: true, completion: nil);
}
}
/**
*
*/
extension UIImageView {
/**
*
*/
func imageFrame() -> CGRect {
let imageViewSize = self.frame.size;
guard let imageSize = self.image?.size else {
return CGRect.zero;
}
let imageRatio = imageSize.width / imageSize.height;
let imageViewRatio = imageViewSize.width / imageViewSize.height;
if (imageRatio < imageViewRatio) {
let scaleFactor = imageViewSize.height / imageSize.height;
let width = imageSize.width * scaleFactor;
let topLeftX = (imageViewSize.width - width) * 0.5;
return CGRect(x: topLeftX, y: 0, width: width, height: imageViewSize.height);
} else {
let scaleFactor = imageViewSize.width / imageSize.width;
let height = imageSize.height * scaleFactor;
let topLeftY = (imageViewSize.height - height) * 0.5;
return CGRect(x: 0, y: topLeftY, width: imageViewSize.width, height: height);
}
}
}

Custom Rendering on IOS a CardView from Frame

I am trying to make a CardView on IOS Xamarin Forms by subclassing the Frame and making a custom renderer. I want to achieve something like this:
Upon looking into the API on setting a shadow, I've done this:
[assembly: ExportRenderer(typeof(CardView), typeof(CardViewRenderer))]
namespace TrabbleMobile.iOS.CustomRenderers
{
public class CardViewRenderer : FrameRenderer
{
public override void Draw(CGRect rect)
{
var cardView = (CardView)this.Element;
using (var context = UIGraphics.GetCurrentContext())
{
//nfloat cornerRadius = 2;
float shadowOffsetWidth = 2;
float shadowOffsetHeight = 4;
var shadowColor = new CGColor(0, 0, 0, (nfloat)0.5);
var boxColor = new CGColor(255, 255, 255);
var shadowBlur = (float)0.5;
var offset = new CGSize(shadowOffsetWidth, shadowOffsetHeight);
context.SetShadow(offset, shadowBlur, shadowColor);
However, it does not render as it should and no shadow at all.
I've done this, and the way that I did this is thru custom rendering on IOS and here is the custom renderer code:
public class CardViewRenderer : FrameRenderer
{
public override void Draw(CGRect rect)
{
SetupShadowLayer();
base.Draw(rect);
}
void SetupShadowLayer()
{
Layer.CornerRadius = 2; // 5 Default
if (Element.BackgroundColor == Xamarin.Forms.Color.Default)
{
Layer.BackgroundColor = UIColor.White.CGColor;
}
else
{
Layer.BackgroundColor = Element.BackgroundColor.ToCGColor();
}
Layer.ShadowRadius = 2; // 5 Default
Layer.ShadowColor = UIColor.Black.CGColor;
Layer.ShadowOpacity = 0.4f; // 0.8f Default
Layer.ShadowOffset = new CGSize(0f, 2.5f);
if (Element.OutlineColor == Xamarin.Forms.Color.Default)
{
Layer.BorderColor = UIColor.Clear.CGColor;
}
else
{
Layer.BorderColor = Element.OutlineColor.ToCGColor();
Layer.BorderWidth = 1;
}
Layer.RasterizationScale = UIScreen.MainScreen.Scale;
Layer.ShouldRasterize = true;
}
}

Sparkle effect iOS

I want to create sparkle effect over an image as shown in the video Sparkle Effect
the only way I could think of doing it animating each particle separately using core animation, but that would be inefficient as well as time consuming.
Is there any other way I can do the same?
Here is a solution from Erica Susan's cook book. See this works for you.
You can add visual interest to your interfaces by using emitters in tandem with user touches. The following class demonstrates how to follow a touch over its lifetime, adding a little sparkle to wherever the user touches on-screen.
The class begins as soon as the user touches the screen, creating an emitter layer and a single emitter cell. The cell defines the particles — their color, their birth rate, their lifetime, velocity, and so forth.
As the user's touch progresses, this class updates the emitter's location, removing the emitter once the touch is removed from the screen. Although this example is written for single touch interaction, you can easily update the code to add an array of emitters (rather than a single instance) for multi-touch interaction.
Emitters are easily added to your projects and efficient to run. While too much animation is never a good design idea, a little sparkle used judiciously can add life and movement.
#interface SparkleTouchView : UIView {
CAEmitterLayer *emitter;
}
#end
#implementation SparkleTouchView
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
float multiplier = 0.25f;
CGPoint pt = [[touches anyObject] locationInView:self];
//Create the emitter layer
emitter = [CAEmitterLayer layer];
emitter.emitterPosition = pt;
emitter.emitterMode = kCAEmitterLayerOutline;
emitter.emitterShape = kCAEmitterLayerCircle;
emitter.renderMode = kCAEmitterLayerAdditive;
emitter.emitterSize = CGSizeMake(100 * multiplier, 0);
//Create the emitter cell
CAEmitterCell* particle = [CAEmitterCell emitterCell];
particle.emissionLongitude = M_PI;
particle.birthRate = multiplier * 1000.0;
particle.lifetime = multiplier;
particle.lifetimeRange = multiplier * 0.35;
particle.velocity = 180;
particle.velocityRange = 130;
particle.emissionRange = 1.1;
particle.scaleSpeed = 1.0; // was 0.3
particle.color = [[COOKBOOK_PURPLE_COLOR colorWithAlphaComponent:0.5f] CGColor];
particle.contents = (__bridge id)([UIImage imageNamed:#"spark.png"].CGImage);
particle.name = #"particle";
emitter.emitterCells = [NSArray arrayWithObject:particle];
[self.layer addSublayer:emitter];
}
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint pt = [[touches anyObject] locationInView:self];
// Disable implicit animations
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
emitter.emitterPosition = pt;
[CATransaction commit];
}
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[emitter removeFromSuperlayer];
emitter = nil;
}
- (void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[self touchesEnded:touches withEvent:event];
}
#end
Don't forget to create a png file named spark.png in order to create the animation.
/*Tested in swift 5.1*/
Import UIKit
enum Images {
static let box = UIImage(named: "Box")!
static let triangle = UIImage(named: "Triangle")!
static let circle = UIImage(named: "Circle")!
static let swirl = UIImage(named: "Spiral")!
}
class ActivationRequiredViewController: UIViewController {
var emitter = CAEmitterLayer()
var colors:[UIColor] = [
Colors.red,
Colors.blue,
Colors.green,
Colors.yellow
]
var images:[UIImage] = [
Images.box,
Images.triangle,
Images.circle,
Images.swirl
]
var velocities:[Int] = [
100,
90,
150,
200
]
override func viewDidLoad() {
super.viewDidLoad()
startSparkle()
}
func startSparkle() {
emitter.emitterPosition = CGPoint(x: self.view.frame.size.width / 2, y: -10)
emitter.emitterShape = CAEmitterLayerEmitterShape.line
emitter.emitterSize = CGSize(width: self.view.frame.size.width, height: 2.0)
emitter.emitterCells = generateEmitterCells()
self.view.layer.addSublayer(emitter)
}
private func generateEmitterCells() -> [CAEmitterCell] {
var cells:[CAEmitterCell] = [CAEmitterCell]()
for index in 0..<16 {
let cell = CAEmitterCell()
cell.birthRate = 4.0
cell.lifetime = 14.0
cell.lifetimeRange = 0
cell.velocity = CGFloat(getRandomVelocity())
cell.velocityRange = 0
cell.emissionLongitude = CGFloat(Double.pi)
cell.emissionRange = 0.5
cell.spin = 3.5
cell.spinRange = 0
cell.color = getNextColor(i: index)
cell.contents = getNextImage(i: index)
cell.scaleRange = 0.25
cell.scale = 0.1
cells.append(cell)
}
return cells
}
private func getRandomVelocity() -> Int {
return velocities[getRandomNumber()]
}
private func getRandomNumber() -> Int {
return Int(arc4random_uniform(4))
}
private func getNextColor(i:Int) -> CGColor {
if i <= 4 {
return colors[0].cgColor
} else if i <= 8 {
return colors[1].cgColor
} else if i <= 12 {
return colors[2].cgColor
} else {
return colors[3].cgColor
}
}
private func getNextImage(i:Int) -> CGImage {
return images[i % 4].cgImage!
}
}
For anyone doing a Mac app, here's the same concept as #thevikasnayak , for an NSViewController
import Cocoa
enum Images {
static let box = NSImage(named: "Box")!
static let triangle = NSImage(named: "Triangle")!
static let circle = NSImage(named: "Circle")!
static let swirl = NSImage(named: "Spiral")!
}
class SparkleVC: NSViewController {
var emitter = CAEmitterLayer()
var colors:[NSColor] = [
NSColor.blue.withAlphaComponent(0.1),
NSColor.blue.withAlphaComponent(0.2),
NSColor.blue.withAlphaComponent(0.3),
NSColor.blue.withAlphaComponent(0.4)]
var images:[NSImage] = [Images.box, Images.triangle, Images.circle, Images.swirl]
override func viewDidAppear() {
super.viewDidAppear()
view.wantsLayer = true
startSparkle()
}
func startSparkle() {
emitter.emitterPosition = CGPoint(x: view.bounds.size.width / 2.0, y: view.bounds.size.height / 2.0)
emitter.emitterShape = CAEmitterLayerEmitterShape.circle
emitter.emitterSize = CGSize(width: view.bounds.size.width / 4.0, height: view.bounds.size.width / 4.0)
emitter.emitterCells = generateEmitterCells()
view.layer!.addSublayer(emitter)
}
private func generateEmitterCells() -> [CAEmitterCell] {
var cells:[CAEmitterCell] = [CAEmitterCell]()
for index in 0..<16 {
let cell = CAEmitterCell()
cell.birthRate = 200.0
cell.lifetime = 0.01
cell.lifetimeRange = 0.03
cell.velocity = 5
cell.velocityRange = 2
cell.emissionLatitude = Double.random(in: 0 ..< Double.pi)
cell.emissionLongitude = Double.random(in: 0 ..< Double.pi)
cell.emissionRange = Double.pi
cell.spin = 10
cell.spinRange = 10
cell.color = colors[index/4].cgColor
cell.contents = nextImage(i: index)
cell.scaleRange = 0.2
cell.scale = 1
cells.append(cell)
}
return cells
}
private func nextImage(i:Int) -> CGImage {
return (images[i % 4]).cgImage(forProposedRect: nil, context: nil, hints: nil)!
}
}
Please check this solution.
https://github.com/GabriellaQiong/CIS581-Project2-Image-Morphing
CIwarpKernel Kets you warp an image according to reference points. That is exactly what you want to do in order to have image morphing.

Resources