Within a UISrollView, I have several programmatically-added subviews representing nodes in a tree:
Each node's frame includes the node itself plus the line connecting it to its parent. (I did this to facilitate animation of the line with the node.) In the picture below, the frame is drawn for one of the nodes:
When the user taps on one of the nodes, two child nodes are "born". I'd like to animate this by having the child nodes descend down from behind the parent node. My basic animation code is:
- (void)descendFromParent
{
// Do nothing if this is root node
if (!self.parent)
return;
// Move node to parent location
self.frame = CGRectMake(self.frame.origin.x + self.parent.nodeFrame.origin.x - self.nodeFrame.origin.x,
self.frame.origin.y + self.parent.nodeFrame.origin.y - self.nodeFrame.origin.y,
self.frame.size.width, self.frame.size.height);
// Animate the move back to original location
[UIView animateWithDuration:1.0
delay:0.0
options:0
animations:^{
self.frame = self.trueFrame;
}
completion:nil];
}
(nodeFrame is a frame containing just the circular part of the view.)
The problem of course is that as the child node is descending, it (especially the line) is visible on top of and above the parent node:
I've tried a lot of different ways to make this work -- using CGContextClip, etc. -- but none of them work, mainly because drawRect: isn't called during the animation.
How can I make this work?
I think your problem is not how to arrange the views in the correct view hierarchy, but the following:
After you have arranged the child disks behind the parent disk, you want them to slide down, and while they are sliding, the edge that connects their centers should have first zero length (when all 3 disks are at the same place), and should then be extended until it reaches its final length in the end.
If this is the case, one possible solution would be:
Lets call the initial x,y center coordinate of one disk (0,0), and the final coordinate (X,Y). Using the usual animation code, it is easy to move the child center to (X,Y) in time t.
Now assume you have an additional image view that shows the edge along the diagonal. In the end position, the center of the "edge view" is at (X/2,Y/2), and its size is (X,Y). If this view is placed behind all others, it will connect the two disks at their final position.
The animation consists now of 1) changing the center property from the initial position (0,0) to (X/2,Y/2), and 2) changing the scale of the view (using its transform property) from zero to the final size, also in time t.
This should do it.
You may want to look at the UIView methods
-insertSubview:belowSubview:
-insertSubview:atIndex:
-sendSubviewToBack:
In your case, you can either send the two subviews to the background with -sendSubviewToBack: after you have added them. Or you can just add them with -insertSubview:atIndex: and provide 0 as the index.
You can specific an index for a subview, try inserting the nodes you want to animate at index 0 and have the blue nodes at a higher index.
Related
I have a problem in my game and i am trying to find out what it is and it is about the position of a SKSpriteNode and i made a SKLabelNode to display the position so i could see what is going wrong. I did:
testlable.text = "\(leftObstacle.position)"
testlable.zPosition = 50
testlable.fontSize = 60
It displays the position of (00,00) when it is in the top left corner or outside of the screen not ever the right position and it isn't ever at (00,00) so what should i do to make it display the right position?
The position parameter is the position of a node relative to its parent node - so if leftObstacle is a child of another node in the scene that would explain why it always looks like it is at 0,0. In this case report the position of its parent (or its grand-parent etc.) to get what you need.
Given following view hierarchy:
root (e.g. view of a view controller)
|_superview: A view where we will draw a cross using core graphics
|_container: Clips subview
|_subview: A view where we will show a cross adding subviews, which has to align perfectly with the cross drawn in superview
|_horizontal line of cross
|_vertical line of cross
Task:
The crosses of superview and subview have to be always aligned, given a global transform. More details in "requirements" section.
Context:
The view hierarchy above belongs to a chart. In order to provide maximal flexibility, it allows to present chart points & related content in 3 different ways:
Drawing in the chart's base view (superview) draw method.
Adding subviews to subview. subview is transformed on zoom/pan and with this automatically its subviews.
Adding subviews to a sibling of subview. Not presented in view hierarchy for simplicity and because it's not related with the problem. Only mentioning it here to give an overview. The difference between this method and 2., is that here the view is not transformed, so it's left to the implementation of the content to update "manually" the transform of all the children.
Maximal flexibility! But with this comes the cost that it's a bit tricky to implement. Specifically point 2.
Currently I got zoom/pan working by basically processing the transforms for superview core graphics drawing and subview separately, but this leads to redundancy and error-proneness, e.g. repeated code for boundary checks, etc.
So now I'm trying to refactor it to use one global matrix to store all the transforms and derive everything from it. Applying the global matrix to the coordinates used by superview to draw is trivial, but deriving the matrix of subview, given requirements listed in next section, not so much.
I mention "crosses" in the view hierarchy section because this is what I'm using in my playgrounds as a simplified representation of one chart point (with x/y guidelines) (you can scroll down for images and gists).
Requirements:
The content can be zoomed and panned.
The crosses stay always perfectly aligned.
subview's subviews, i.e. the cross line views can't be touched (e.g. to apply transforms to them) - all that can be modified is subview's transform.
The zooming and panning transforms are stored only in a global matrix matrix.
matrix is then used to calculate the coordinates of the cross drawn in superview (trivial), as well as the transform matrix of subview (not trivial - reason of this question).
Since it doesn't seem to be possible to derive the matrix of subview uniquely from the global matrix, it's allowed to store additional data in variables, which are then used together with the global matrix to calculate subview's matrix.
The size/origin of container can change during zoom/pan. The reason of this is that the labels of the y-axis can have different lengths, and the chart is required to adapt the content size dynamically to the space occupied by the labels (during zooming and panning).
Of course when the size of container changes, the ratio of domain - screen coordinates has to change accordingly, such that the complete original visible domain continues to be contained in container. E.g if I'm displaying an x-axis with a domain [0, 10] in a container frame with a width of 500pt, i.e. the ratio to convert a domain point to screen coordinates is 500/10=50, and shrink the container width to 250, now my [0, 10] domain, which has to fit in this new width, has a ratio of 25.
It has to work also for multiple crosses (at the same time) and arbitrary domain locations for each. This should happen automatically by solving 1-7 but mentioning it for completeness.
What I have done:
Here are step-by-step playgrounds I did to try to understand the problem better:
Step 1 (works):
Build hierarchy as described at the beginning, displaying nothing but crosses that have to stay aligned during (programmatic) zoom & pan. Meets requirements 1, 2, 3, 4 and 5:
Gist with playground.
Particularities here:
I skipped container view, to keep it simple. subview is a direct subview of superview.
subview has the same size as superview (before zooming of course), also to keep it simple.
I set the anchor point of subview to the origin (0, 0), which seems to be necessary to be in sync with the global matrix.
The translation used for the anchor change has to be remembered, in order to apply it again together with the global matrix. Otherwise it gets overwritten. For this I use the variable subviewAnchorTranslation. This belongs to the additional data I had in mind in the bullet under requirement 5.
Ok, as you see everything works here. Time to try the next step.
Step 2 (works):
A copy of step 1 playground with modifications:
Added container view, resembling now the view hierarchy described at the beginning.
In order for subview, which is now a subview of container to continue being displayed at the same position, it has to be moved to top and left by -container.origin.
Now the zoom and pan calls are interleaved randomly with calls to change the frame position/size of container.
The crosses continue to be in sync. Requirements met: All from step 1 + requirement 6.
Gist with playground
Step 3 (doesn't work):
So far I have been working with a screen range that starts at 0 (left side of the visible playground result). Which means that container is not fulfilling it's function to contain the range, i.e. requirement 7. In order to meet this, container's origin has to be included in the ratio calculation.
Now also subview has to be scaled in order to fit in container / display the cross at the correct place. Which adds a second variable (first being subviewAnchorTranslation), which I called contentScalingFactor, containing this scaling, that has to be included in subview's matrix calculation.
Here I've done multiple experiments, all of them failed. In the current state, subview starts with the same frame as container and its frame is adjusted + scaled when the frame of container changes. Also, subview being now inside container, i.e. its origin being now container's origin and not superview's origin, I have to set update its anchor such that the origin is not at (0,0) but (-x,-y), being x and y the coordinates of container's origin, such that subview continues being located in relation to superview's origin. And it seems logical to update this anchor each time that container changes its origin, as this changes the relative position from content's origin to superview's origin.
I uploaded code for this - in this case a full iOS project instead of only a playground (I thought initially that it was working and wanted to test using actual gestures). In the actual project I'm working on the transform works better, but I couldn't find the difference. Anyway it doesn't work well, at some point there are always small offsets and the points/crosses get out of sync.
Github project
Ok, how do I solve this such that all the conditions are met. The crosses have to stay in sync, with continuous zoom/pan and changing the frame of container in between.
The present answer allows for any view in the Child hierarchy to be arbitrarily transformed. It does not track the transformation, merely converts a transformed point, thus answers the question:
What are the coordinates of a point located in a subview in the coordinate system of another view, no matter how much that subview has been transformed.
To decouple the Parent from the clipping Container and offer a generic answer, I propose place them at the same level conceptually, and in different order visually (†):
Use a common superview
To apply the scrolling, zooming or any other transformation from the Child to the Parent, go through common superview (named Coordinator in the present example).
The approach is very similar to this Stack Overflow answer where two UIScrollView scroll at different speed.
Notice how the red hairline and black hairline overlap, regardless of the position, scrolling, transform of any and all off the views in the Child hierarchy, including that of Container.
↻ replay animation
Code
Coordinate conversion
Simplified for clarity, using an arbitrary point (50,50) in the coordinate system of the Child view (where that point is effectively drawn), and convert it in the Parent view system looks like this:
func coordinate() {
let transfer = theChild.convert(CGPoint(x:50, y:50), to: coordinator)
let final = coordinator.convert(transfer, to: theParent)
theParent.transformed = final
theParent.setNeedsDisplay()
}
Zoom & Translate Container
func zoom(center: CGPoint, delta: CGPoint) {
theContainer.transform = theContainer.transform.scaledBy(x: delta.x, y: delta.y)
coordinate()
}
func translate(delta: CGPoint) {
theContainer.transform = theContainer.transform.translatedBy(x: delta.x, y: delta.y)
coordinate()
}
(†) I have renamed Superview and Subview to Parent and Child respectively.
I have a view to whose layer I applied transformations - altered the m34 field, rotated it on the x axis and scaled it on x and y. Then I added this view to a bigger superview. My issue is that every other view I add to the bigger superview gets hidden or overlapped by the transformed one (if the new view's frame intersects the transformed view's frame), even though the new views stand higher in the hierarchy than the transformed one and those new views get added to the end of the subviews array of the bigger superview. Any ideas what is the reason behind this behavior? :-) Thanks a lot!
3D transformations are used to make pseudo-3D. And in this 3D space your layer overlaps others. To change its z-pozition use CAlayer's property
#property CGFloat zPosition;
The layer’s position on the z axis. Animatable. The default value of
this property is 0. Changing the value of this property changes the
the front-to-back ordering of layers onscreen. This can affect the
visibility of layers whose frame rectangles overlap. The value of this
property is measured in points.
The view hierarchy is like a series of sheets of paper - they can be "on top" of each other, but there is no real depth. As soon as you put a 3D transform on a layer, this will have some depth and, if you've rotated it around the x axis, this will be sticking out in front of the other views.
You could try adjusting the z position of the layer to move it behind other views.
How do I change the transformIdentity. How do we set the transform "zero", or alter the transformIdentity of a view to the current state of the transform of said view.
In other words, I want to scale a view, and then set the current state (say scale of 2.5) to the default scale of the view (scale of 1).
example code:
view.transform = CGAffineTransformMakeScale(1, 2.5);
pseudo code for what I want to do:
view.transform = setTransformIdentityTo:view.currentState;
If I understand correctly transformIdentity is the state at which a scale would be 1, or a rotation would be zero, the default "zero" transform.
NOTE: The reason I want to do this is so that I can set a negative scale transform on only one axis of the view and alway get a flipped view relative to the last state of the view before the flip was invoked.
CGAffineTransformIdentity does reset a view or layer to its original, untransformed state, and thus cannot be redefined.
But if you want your "personal" reset transform, e.g. with a different scale, why don't you simply define it, e.g. by using CGAffineTransform myCGAffineTransformIdentity = CGAffineTransform CGAffineTransformMakeScale (sx,sy);, and apply it to your views?
selectedSticker.transform = CGAffineTransformIdentity;
Seems like this works for what I want to do:
CGAffineTransform trans = CGAffineTransformMakeScale(1, 2.5);
view.transform = CGAffineTransformConcat(selectedSticker.transform, trans);
What you can do, if you want to have your view still be transformed even when its own transform is the identity transform, is to put it inside another view that is transformed. Give the outer view the transform you want for your default; the inner view is the one where you do the actual work.
So, let's say you want your “identity” to be scaled by 2× horizontally. You set your outer view's transform to that transform, and leave it that way, and leave the inner view untransformed. When you want to add further transformation, you add it to the inner view, and when you want to reset it back to your default, you set the inner view's transform back to (true) identity. Your inner view will still scaled (or whatever) by the outer view's transform.
Note: I tried this briefly and found that (a) Auto Layout barfed on it, (b) scaling is outward from the anchor point, which means my outer view's horizontal scale pushed my inner view a little bit out-of-bounds, and (c) changing the outer view's anchor point to CGPointZero produced even further hilarity.
So, while this is theoretically a nice, simple, elegant solution, it may actually be more problematic than it's worth, in which case I recommend what Reinhard Männer suggested.
I am trying to achieve some effect using imageViews and I need to set the anchorpoint correctly. It is very hard without it being visible.
How would you go about showing it up on the imageview ?
The anchor point is a property specified on the layer (you will need QuartCore to access it). It is specified in the unit coordinate space of the device (both x and y goes from 0 to 1 within the bounds of the layer. You can know the coordinate of the actor point by multiplying the x value with the width and the y value with the height. To draw a point at that location you could just add a new, small layer (bounds 4x4 and corner radius 4 (to get a circle))
Be aware that changing the anchor point will change where the layer (and the view) appears on screen as the frame is calculated relative to the anchor point. To have the frame appear in the same place after the anchor point has been set you could re-set the frame (this will update the layers position so that the frame is what you expect it to be)