CALayer gets redrawn frequently despite shouldRasterize --> why? - ios

I have some fairly complex layers in a part of my app and I want them to rasterize as their content changes close to never. The frame of the view these layers are in can be changed by dragging a "spacerbar" around with your finger.
I made a simple test-app to visualise my problem and show you some code:
#interface ViewController () {
UIView* m_MainView;
UIView* m_TopView;
UIView* m_BottomView;
UIView* m_MidView;
int m_Position;
}
#end
#implementation ViewController
- (void)loadView {
m_MainView = [[UIView alloc] init];
m_TopView = [[UIView alloc] init];
m_TopView.backgroundColor = UIColor.blueColor;
m_TopView.translatesAutoresizingMaskIntoConstraints = false;
m_BottomView = [[UIView alloc] init];
m_BottomView.backgroundColor = UIColor.blueColor;
m_BottomView.translatesAutoresizingMaskIntoConstraints = false;
m_MidView = [[UIView alloc] init];
m_MidView.backgroundColor = UIColor.blackColor;
m_MidView.translatesAutoresizingMaskIntoConstraints = false;
[m_MainView addSubview:m_TopView];
[m_MainView addSubview:m_BottomView];
[m_MainView addSubview:m_MidView];
CALayer* targetLayer = [[CALayer alloc] init];
targetLayer.frame = CGRectMake (100, 200, 500, 50);
targetLayer.backgroundColor = UIColor.yellowColor.CGColor;
targetLayer.shouldRasterize = true;
[m_BottomView.layer addSublayer:targetLayer];
UIPanGestureRecognizer* recognizer = [[UIPanGestureRecognizer alloc]
initWithTarget:self action:#selector(handlePan:)];
[m_MainView addGestureRecognizer:recognizer];
m_Position = 400;
super.view = m_MainView;
}
- (void)handlePan:(UIPanGestureRecognizer*)sender {
float touchY = [sender locationInView:m_MainView].y;
m_Position = (int)touchY;
[m_MainView setNeedsLayout];
}
- (void)viewWillLayoutSubviews {
CGRect bounds = super.view.bounds;
m_TopView.frame = CGRectMake (0, 0, bounds.size.width, m_Position - 10);
m_MidView.frame = CGRectMake (0, m_Position - 10, bounds.size.width, 20);
m_BottomView.frame = CGRectMake (0, m_Position + 10, bounds.size.width,
bounds.size.height - m_Position - 10);
}
#end
.
Now ... By using instruments and activating "Color Hits Green and Misses Red" one can see when a layer is redrawn (is then highlighted in red).
In this example the bar (targetLayer) is green (cached) most of the time, but if I start dragging the spacer (which in fact changes the frame of the bottom-view) the layer turns red some times ... especially if I reverse the direction of the drag or if I drag slowly.
In my app this causes flickering as redrawing these layers is expensive.
Why does this happen??
I never change any property of the layer but it gets redrawn anyways?
I am sure I am missing something :-)
As a workaround I could make a snapshot of the layers and use the image ... that would work I think and if there is no solution I will have to use this approach but I would like to understand what is the problem here.
Anyone?

Related

How to create view based animation in objective c?

I have to add the following animation in my iOS app, I have used a scroll bar along with UITableView and achieved the top and bottom animation, but I'm still stuck at the middle animation part where the 4 UIViews come in a single horizontal line. Any suggestions?
http://www.image-maps.com/m/private/0/af8u4ulika9siddnf6k6hhrtg2_untitled-2.gif
Code:-
#implementation AnimatedView {
UIScrollView *mainScroll;
UIScrollView *backgroundScrollView;
UILabel *_textLabel;
UITableView *_commentsTableView;
UIView *menuView;
UIView *_commentsViewContainer;
UIView *fadeView;
UIImageView *imageView;
NSMutableArray *comments;
}
- (id)init {
self = [super init];
if (self) {
_mainScrollView = [[UIScrollView alloc] initWithFrame:[UIApplication sharedApplication].keyWindow.frame];
self.view = _mainScrollView;
_backgroundScrollView = [[UIScrollView alloc] initWithFrame:HEADER_INIT_FRAME];
imageView = [[UIImageView alloc] initWithFrame:HEADER_INIT_FRAME];
fadeView = [[UIView alloc] initWithFrame:imageView.frame];
_textLabel = [[UILabel alloc] initWithFrame:CGRectMake(10.0f, 100.0f, 150.0f, 25.0f)];
menuView = [[UIView alloc] initWithFrame:CGRectMake(0,_textLabel.frame.size.height+150, self.view.frame.size.width+30, 180)];
[_backgroundScrollView addSubview:imageView];
[_backgroundScrollView addSubview:fadeView];
[_backgroundScrollView addSubview:menuView];
[_backgroundScrollView addSubview:_textLabel];
_commentsViewContainer = [[UIView alloc] init];
_commentsTableView = [[UITableView alloc] init];
_commentsTableView.scrollEnabled = NO;
_commentsTableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
[self.view addSubview:_backgroundScrollView];
[_commentsViewContainer addSubview:_commentsTableView];
[self.view addSubview:_commentsViewContainer];
// fake data!
comments = [#[#"Array for tableview"] mutableCopy];
}
return self;
}
#pragma mark Scroll
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat delta = 0.0f;
CGRect rect = HEADER_INIT_FRAME;
// Here is where I do the "Zooming" image and the quick fade out the text and toolbar
if (scrollView.contentOffset.y < 0.0f) {
delta = fabs(MIN(0.0f, _mainScrollView.contentOffset.y));
_backgroundScrollView.frame = CGRectMake(CGRectGetMinX(rect) - delta / 2.0f, CGRectGetMinY(rect) - delta, CGRectGetWidth(rect) + delta, CGRectGetHeight(rect) + delta);
[_commentsTableView setContentOffset:(CGPoint){0,0} animated:NO];
} else {
delta = _mainScrollView.contentOffset.y;
_textLabel.alpha = 1.0f;
CGFloat backgroundScrollViewLimit = _backgroundScrollView.frame.size.height - kBarHeight;
// Here I check whether or not the user has scrolled passed the limit where I want to stick the header, if they have then I move the frame with the scroll view
// to give it the sticky header look
if (delta > backgroundScrollViewLimit) {
_backgroundScrollView.frame = (CGRect) {.origin = {0, delta - _backgroundScrollView.frame.size.height + kBarHeight}, .size = {self.view.frame.size.width, HEADER_HEIGHT}};
_commentsViewContainer.frame = (CGRect){.origin = {0, CGRectGetMinY(_backgroundScrollView.frame) + CGRectGetHeight(_backgroundScrollView.frame)}, .size = _commentsViewContainer.frame.size };
_commentsTableView.contentOffset = CGPointMake (0, delta - backgroundScrollViewLimit);
CGFloat contentOffsetY = -backgroundScrollViewLimit * kBackgroundParallexFactor;
[_backgroundScrollView setContentOffset:(CGPoint){0,contentOffsetY} animated:NO];
}
else {
_backgroundScrollView.frame = rect;
_commentsViewContainer.frame = (CGRect){.origin = {0, CGRectGetMinY(rect) + CGRectGetHeight(rect)}, .size = _commentsViewContainer.frame.size };
[_commentsTableView setContentOffset:(CGPoint){0,0} animated:NO];
[_backgroundScrollView setContentOffset:CGPointMake(0, -delta * kBackgroundParallexFactor)animated:NO];
}
}
}
- (void)viewDidAppear:(BOOL)animated {
_mainScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.frame), _commentsTableView.contentSize.height + CGRectGetHeight(_backgroundScrollView.frame));
}
You don't need any scrollview to implement this really. All you need is 1 UITableView with 2 sections. First section has a single empty element (but set the row height to 0), and enable the header for both sections. You can use UIViews for the headerViews. Then, you only need to change the header height (with icon positioning) based on tableview Delegate scrollViewDidScroll. scrollViewDidScroll is also a delegate of UITableView since one of TableView's element inherits from UIScrollView.

Creating a mask with edge detection by selecting part of an image

The story is that, the user selects a part of an image by drawing lines or curves. Based on the selection, a mask will be created using an edge detector.
How can i achieve this?
Are CoreImage and GPUImage could help to make this possible?
Is edge detection enough to make a mask?
I don't have any advanced knowledge on image processing and manipulation. I am just a starter trying to understand and learn it. By the way, this should be done in iOS.
Just like this one (the masking part).
I wrote a sample for you to show you how to use mask to select part of image, if you want to implement more complex effect, you just need to change the path, then you can get any shape you want.
#interface MainViewController ()
#property UIImageView* imageV;
#property UIView* selectView;
#property UIView *blockView;
#property CGPoint startPoint;
#property CGPoint endPoint;
#end
#implementation MainViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
UIBarButtonItem *recoverItem = [[UIBarButtonItem alloc] initWithTitle:#"Recover" style:UIBarButtonItemStyleDone target:self action:#selector(recover)];
[self.navigationItem setLeftBarButtonItem:recoverItem];
_imageV = [[UIImageView alloc] initWithFrame:[UIScreen mainScreen].bounds];
_imageV.image = [UIImage imageNamed:#"test"];
[self.view addSubview:_imageV];
_blockView = [[UIView alloc] init];
_blockView.frame = _imageV.bounds;
_blockView.backgroundColor = [UIColor whiteColor];
[_imageV addSubview:_blockView];
_blockView.hidden = true;
_selectView = [[UIView alloc] init];
_selectView.layer.borderColor = [UIColor greenColor].CGColor;
_selectView.layer.borderWidth = 2;
[_imageV addSubview:_selectView];
_selectView.hidden = true;
UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[self.view addGestureRecognizer:panGR];
}
- (void) handlePan: (UIPanGestureRecognizer *)pan{
if (UIGestureRecognizerStateBegan == pan.state) {
_startPoint = [pan locationInView:_imageV];
}
else if (UIGestureRecognizerStateChanged == pan.state) {
_selectView.hidden = false;
CGPoint currentP = [pan locationInView:_imageV];
float rectWidth = currentP.x - _startPoint.x;
float rectHeight = currentP.y - _startPoint.y;
_selectView.frame = CGRectMake(_startPoint.x, _startPoint.y, rectWidth,rectHeight);
}
else if (UIGestureRecognizerStateEnded == pan.state) {
_endPoint = [pan locationInView:_imageV];
float rectWidth = _endPoint.x - _startPoint.x;
float rectHeight = _endPoint.y - _startPoint.y;
//create path
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRect:[UIScreen mainScreen].bounds];
UIBezierPath *otherPath = [[UIBezierPath bezierPathWithRect:CGRectMake(_startPoint.x, _startPoint.y, rectWidth,rectHeight)] bezierPathByReversingPath];
[maskPath appendPath:otherPath];
//set mask
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = maskPath.CGPath;
[_blockView.layer setMask:maskLayer];
_blockView.hidden = false;
_selectView.hidden = true;
}
}
-(void) recover{
_blockView.hidden = true;
}
#end
Hope it can help you.

multiple punch-out style mask?

I've done simple CALayer masks before but I think I'm getting confused on what they do. I'm trying to have a punch out effect with several (2) views.
Here's what I have so far. I'm looking to have a white square with punched out label and image (so you can see the brown background through it. Where am I going wrong?
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor brownColor];
self.viewToPunch = [[UIView alloc] initWithFrame:CGRectZero];
[self.view addSubview:self.viewToPunch];
self.viewToPunch.backgroundColor = [UIColor whiteColor];
self.punchLabel = [[UILabel alloc] initWithFrame:CGRectZero];
self.punchLabel.textColor = [UIColor blackColor];
self.punchLabel.font = [UIFont boldSystemFontOfSize:20.0];
self.punchLabel.text = #"punch";
self.punchLabel.textAlignment = NSTextAlignmentCenter;
self.punchImage = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:#"plus"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
[self.punchImage setContentMode:UIViewContentModeCenter];
self.viewsToPunch = #[self.punchLabel,self.punchImage];
[self punch:self.viewToPunch withUIViews:self.viewsToPunch];
}
- (void)punch:(UIView *) viewToPunch withUIViews:(NSArray *)viewsToPunch
{
CALayer *punchMask = [CALayer layer];
punchMask.frame = viewToPunch.frame;
NSMutableArray *sublayers = [[NSMutableArray alloc] init];
for (UIView *views in viewsToPunch){
[sublayers addObject:views.layer];
}
punchMask.sublayers = sublayers;
punchMask.masksToBounds = YES;
viewToPunch.layer.mask = punchMask;
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
self.viewToPunch.frame = CGRectMake(50, 50, 100, 100);
self.punchLabel.frame = CGRectMake(0, 0, 100, 100);
self.punchImage.frame = CGRectMake(0, 0, self.viewToPunch.frame.size.width, 40.);
[self punch:self.viewToPunch withUIViews:self.viewsToPunch];
}
So not only do the frames seem to be off, it seems to be the opposite of a punch out. How do I invert the mask and fix up the frames?
Thanks a lot for any help! I put it in a method punch:withUIViews: so I can hopefully reuse it in other areas.
When you apply a mask to a CALayer, it only gets drawn in the parts where the mask is not transparent. But you're simply applying an empty (transparent) mask, with the wrong coordinates (which is why your view isn't completely transparent: the mask isn't covering the view completely; it should be punchMask.frame = viewToPunch.bounds;)
You might want to look into CAShapeLayer and assign it a path. Use that as mask layer.
For example, see CAShapeLayer mask view or Getting Creative with CALayer Masks (cached blog post).
I tried to combine mask of CAGradationLayer and CAShapeLayer, and It is possible.
https://stackoverflow.com/a/32220792/3276863

UISnapBehavior Causing Distortion

For whatever reason, whenever I use the snapping behavior, it causes the UIButtons to turn into rectangles of various sizes when "snapped". I'm probably just looking right over the issue. My deadline is tomorrow to get the last bugs worked out, so I would GREATLY appreciate ANY help! Thank you!
vvv - ViewController.m - vvv
//set points
CGPoint bullyboxPoint = {80, 188};
CGPoint weemPoint = {240, 188};
CGPoint yeaPoint = {80, 339};
CGPoint charityPoint = {240, 339};
CGPoint phhsPoint = {80, 488};
CGPoint experiencePoint = {240, 488};
//make the buttons closer to speed up the snap
bullybox.frame = CGRectMake(40, 788, 120, 120);
weem.frame = CGRectMake(200, 788, 120, 120);
yea.frame = CGRectMake(30, 939, 120, 120);
charity.frame = CGRectMake(190, 939, 120, 120);
phhs.frame = CGRectMake(20, 1328, 120, 120);
experience.frame = CGRectMake(180, 1328, 120, 120);
//create behaviors
UISnapBehavior *snapBehavior = [[UISnapBehavior alloc] initWithItem:self->bullybox snapToPoint:bullyboxPoint];
UISnapBehavior *snapBehavior1 = [[UISnapBehavior alloc] initWithItem:self->weem snapToPoint:weemPoint];
UISnapBehavior *snapBehavior2 = [[UISnapBehavior alloc] initWithItem:self->yea snapToPoint:yeaPoint];
UISnapBehavior *snapBehavior3 = [[UISnapBehavior alloc] initWithItem:self->charity snapToPoint:charityPoint];
UISnapBehavior *snapBehavior4 = [[UISnapBehavior alloc] initWithItem:self->phhs snapToPoint:phhsPoint];
UISnapBehavior *snapBehavior5 = [[UISnapBehavior alloc] initWithItem:self->experience snapToPoint:experiencePoint];
//dampen the snap
snapBehavior.damping = 1.2f;
snapBehavior1.damping = 1.2f;
snapBehavior2.damping = 1.2f;
snapBehavior3.damping = 1.2f;
snapBehavior4.damping = 1.2f;
snapBehavior5.damping = 1.2f;
//add behaviors
[self.animator addBehavior:snapBehavior];
[self.animator addBehavior:snapBehavior1];
[self.animator addBehavior:snapBehavior2];
[self.animator addBehavior:snapBehavior3];
[self.animator addBehavior:snapBehavior4];
[self.animator addBehavior:snapBehavior5];
The problem is probably that you're using Autolayout. Autolayout and UIKit Dynamics are enemies (if you look at Apple's example code, you'll see that they have circumvented this issue by turning Autolayout off).
The way I like to solve this problem is: Make snapshot images of the objects to be animated; hide the actual objects and put the snapshot images in their place; animate the snapshot images; when it's all over, take the snapshot images out of the interface and show the actual objects in their new location.
This works because the snapshot images are not subject to Autolayout. However, you will still need to grapple with the issue, because you cannot move buttons that are subject to Autolayout by setting their frame; you must set their constraints.

How to attach 2 views inorder for them to pan as one view

I want 2 views to act as if they were "one view" - meaning if view 1 moves on the 'x' n pixels I want view 2 to move on the x axis the same amount of pixels (in any arbitrary direction) - without having to calculate all sorts of offsets and so on.
I thought using the new UIDynamicAnimator would be a great candidate so I tried this
UIView *v1 = [[UIView alloc] initWithFrame:CGRectMake(320-150, 150, 150, 100)];
v1.backgroundColor = [UIColor blackColor];
[self.view addSubview:v1];
self.v1 = v1;
UIView *v2 = [[UIView alloc] initWithFrame:CGRectMake(self.view.bounds.size.width,
0,
self.view.bounds.size.width,
self.view.bounds.size.height)];
v2.backgroundColor = [UIColor redColor];
[self.view addSubview:v2];
self.v2 = v2;
UIPanGestureRecognizer *p =
[[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)];
[v1 addGestureRecognizer:p];
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
UIAttachmentBehavior *att =
[[UIAttachmentBehavior alloc] initWithItem:self.v1
offsetFromCenter:UIOffsetZero
attachedToItem:self.v2
offsetFromCenter:UIOffsetMake(0,
self.v1.center.y - self.v2.center.y)];
att.damping = 0;
[self.animator addBehavior:att];
self.att = att;
-(void)pan:(UIPanGestureRecognizer *)gesture {
if(gesture.state == UIGestureRecognizerStateChanged) {
CGPoint p = [gesture locationInView:self.view];
// move only on x axis but keep on same y point
self.v1.center = CGPointMake(p.x, self.v1.center.y);
[self.animator updateItemUsingCurrentState:self.v1];
}
}
But they are interacting with each other - the little view tilts and changes its rotation and y location.
I would have hoped - only on 1 axis and "hard" connected to each other - any way to do this?
Given that no further details about the context is provided, I assume that you want the two views' relative distance to each other to remain constant. If this is indeed the case, I am not sure why you consider UIDynamicAnimator. Instead, you could embed the two views in a containerview and then manipulate the origin of the container instead of the two views individually. By embracing this approach you don't have to worry about re-calculating the origin of one view when moving the other, and vice versa.
self.containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 100)];
UIView *v1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
UIView *v2 = [[UIView alloc] initWithFrame:CGRectMake(100 0, 100, 100)];
[self.containerView addSubview:v1];
[self.containerView addSubview:v2];
// now, changing the origin of containerView will move v1 and v2 simultaniously
- (void)pan:(UIPanGestureRecognizer *)gesture
{
if(gesture.state == UIGestureRecognizerStateChanged)
{
CGPoint p = [gesture locationInView:self.view];
// move only on x axis but keep on same y point
self.containerView.center = CGPointMake(p.x, self.containerView.center.y);
}
}
As an alternative, you could use autolayout and relate the two views in such way that your requirements are met. Then, when moving one of the views, the other will move accordingly. However, if you are actually using UIDynamics to manipulate views in that view hierarchy, autolayout is not the way to go since autolayout and UIDynamics tend to interfere with each other.
Set v1 origin 'X' to v2 origin 'X in panGesture.
-(void)pan:(UIPanGestureRecognizer *)gesture {
CGPoint point = [gesture locationInView:self.view];
CGRect boundsRect = CGRectMake(15, 40, 285, self.view.frame.size.height-60);
if (CGRectContainsPoint(boundsRect, point))
{
v1.center = point;
}
v2.frame = CGRectMake(v1.frame.origin.x, v2.frame.origin.y, v2.frame.size.width, v2.frame.size.height);
}
One easy way to solve this could be to add one of the views as a subview to the other or add both as subviews to a new view, which is only used to group the views. Depending on how you intend to use the views this might not be applicable to you though.
If you want to make one view a subview of the other but want the subview to be visible outside the bounds of the superview you can set clipToBounds to NO on the superview.

Resources