Xamarin Pinch To Zoom and Pan Containers leaving their bounds - ios

I have a label with a lot of text that I want to enable pinch-to-zoom and panning gesture recognizers in. I used the recipes from here and then nested them within each other.
https://developer.xamarin.com/guides/xamarin-forms/user-interface/gestures/pinch/
https://developer.xamarin.com/guides/xamarin-forms/user-interface/gestures/pan/
Problem is, both container objects allow you to move the label completely outside of it's normal bounds anywhere within the top level page view (demonstrated in the pictures below).
Any thoughts on how to implement some limits on these? I'm sure it's just placing some limits on the math in the container code, but I haven't found the right thing to change yet.
As you can see in these images, both the pinch-to-zoom container (without panning) and the pan container (without zooming) allow you to alter the control so it goes outside it's bounds.
Initial Layout:
Pinch-To-Zoom only
Panning only
Pinch and Pan
The links above have the container code, but here it is:
PinchToZoomContainer.cs
public class PinchToZoomContainer : ContentView
{
// Pinch Gesture variables
double currentScale = 1;
double startScale = 1;
double xOffset = 0;
double yOffset = 0;
public PinchToZoomContainer ()
{
var pinchGesture = new PinchGestureRecognizer ();
pinchGesture.PinchUpdated += OnPinchUpdated;
GestureRecognizers.Add (pinchGesture);
}
void OnPinchUpdated (object sender, PinchGestureUpdatedEventArgs e)
{
if (e.Status == GestureStatus.Started) {
// Store the current scale factor applied to the wrapped user interface element,
// and zero the components for the center point of the translate transform.
startScale = Content.Scale;
Content.AnchorX = 0;
Content.AnchorY = 0;
}
if (e.Status == GestureStatus.Running) {
// Calculate the scale factor to be applied.
currentScale += (e.Scale - 1) * startScale;
currentScale = Math.Max (1, currentScale);
// The ScaleOrigin is in relative coordinates to the wrapped user interface element,
// so get the X pixel coordinate.
double renderedX = Content.X + xOffset;
double deltaX = renderedX / Width;
double deltaWidth = Width / (Content.Width * startScale);
double originX = (e.ScaleOrigin.X - deltaX) * deltaWidth;
// The ScaleOrigin is in relative coordinates to the wrapped user interface element,
// so get the Y pixel coordinate.
double renderedY = Content.Y + yOffset;
double deltaY = renderedY / Height;
double deltaHeight = Height / (Content.Height * startScale);
double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight;
// Calculate the transformed element pixel coordinates.
double targetX = xOffset - (originX * Content.Width) * (currentScale - startScale);
double targetY = yOffset - (originY * Content.Height) * (currentScale - startScale);
// Apply translation based on the change in origin.
Content.TranslationX = targetX.Clamp (-Content.Width * (currentScale - 1), 0);
Content.TranslationY = targetY.Clamp (-Content.Height * (currentScale - 1), 0);
// Apply scale factor
Content.Scale = currentScale;
}
if (e.Status == GestureStatus.Completed) {
// Store the translation delta's of the wrapped user interface element.
xOffset = Content.TranslationX;
yOffset = Content.TranslationY;
}
}
PanContainer.cs
public class PanContainer : ContentView
{
double startX, startY;
double x, y;
public PanContainer ()
{
// Set PanGestureRecognizer.TouchPoints to control the
// number of touch points needed to pan
var panGesture = new PanGestureRecognizer ();
panGesture.PanUpdated += OnPanUpdated;
GestureRecognizers.Add (panGesture);
}
void OnPanUpdated (object sender, PanUpdatedEventArgs e)
{
switch (e.StatusType) {
case GestureStatus.Started:
startX = Content.TranslationX;
startY = Content.TranslationY;
break;
case GestureStatus.Running:
// Translate and ensure we don't pan beyond the wrapped user interface element bounds.
//Content.TranslationX = Math.Max (Math.Min (0, x + e.TotalX), -Math.Abs (Content.Width - App.ScreenWidth));// App.ScreenWidth));
//Content.TranslationY = Math.Max (Math.Min (0, y + e.TotalY), -Math.Abs (Content.Height - App.ScreenHeight)); //App.ScreenHeight));
Content.TranslationX = startX + e.TotalX;
Content.TranslationY = startY + e.TotalY;
break;
case GestureStatus.Completed:
// Store the translation applied during the pan
x = Content.TranslationX;
y = Content.TranslationY;
break;
}
}
}
I imagine, on the PanContainer, my issue is in these lines that I had to comment out:
//Content.TranslationX = Math.Max (Math.Min (0, x + e.TotalX), -Math.Abs (Content.Width - App.ScreenWidth));// App.ScreenWidth));
//Content.TranslationY = Math.Max (Math.Min (0, y + e.TotalY), -Math.Abs (Content.Height - App.ScreenHeight)); //App.ScreenHeight));
I changed these to a more simple version because I can't find App.ScreenWidth or .ScreenHeight properties.
The pinch container, however, is just as it was originally in the recipe and still goes outside the bounds.

There is an IsClippedToBounds property that helped me with this issue.
For example:
<PanContainer IsClippedToBounds="true">
<PanContainer.Content>
<Image x:Name="SomeImage" />
</PanContainer.Content>
</PanContainer>
To get pinch and pan, you can either wrap a pinch element in a pan element or vice versa, or you can create a single class with the functions from both the pinch and pan classes. The latter is probably better.
That alone will probably not work exactly as you expect though because the calculations in the pinch and pan functionality are not aware of each other, so if for example you pinch to zoom in then the pan functionality doesn't know that it can now pan further.

This answer is mostly likely very late for your needs, Chet... but, you can simply wrap the whole thing in a ScrollView (which you will appropriately locate and/or size to your needs). That should work as expected.
<ScrollView Grid.Column="2" VerticalOptions="Start">
<PanContainer>
<PanContainer.Content>
<Image x:Name="SomeImage" Aspect="AspectFit" />
</PanContainer.Content>
</PanContainer>
</ScrollView>
Cheers!
Mike

Related

IOS Appium can not make scroll with Touch Action method

I have tried with following method to make scroll down from top to the bottom on the screen but it seems not responding at all. Do you have any idea what is the reason ?
public void scrolTest(){
TouchAction tc=new TouchAction(driver);
Dimension dimension=driver.manage().window().getSize();
Double screenHeightStart = dimension.getHeight() * 0.5;
int scrollStartY = screenHeightStart.intValue();
Double screenHeightEnd = dimension.getHeight() * 0.2;
int scrollEndY = screenHeightEnd.intValue();
int scrollX=dimension.getWidth()/2;
tc.longPress(scrollX,scrollStartY).moveTo(scrollX,scrollEndY).release(); //(0,scrollStartY,0,scrollEndY,2000);
}
You are not using perform() method.
tc.longPress(scrollX,scrollStartY).moveTo(scrollX,scrollEndY).release().perform();
Try this code
public void scrollTest() {
Dimension dimension = driver.manage().window().getSize();
int scrollStartY = (int) (dimension.getHeight() * 0.5);
int scrollEndY = (int) (dimension.getHeight() * 0.2);
int scrollX = dimension.getWidth() / 2;
int heightOffset = scrollStartY - scrollEndY;
//Also note that in moveTo function, you have to provide the offSet values, not the (x,y) coordinates
//of the point where you want to move. Hence, i have written 0 in x coordinate (as we don't want to
//change x coordinate) and calculated the heightOffset separately.
new TouchAction(driver).press(scrollX, scrollStartY).moveTo(0, heightOffset).release().perform();
}

Rotation Error on Object at 90° and -90°

I'm working on an app which allows the user to rotate an object with iOS touch controls.
I have the following script working fine, with 1 issue that I can't seem to crack.
GameObject mainCamera;
public Camera camMain;
// One Touch Rotation
public float rotateSpeed = 0.5f;
static float pitch = 0.0f, yaw = 0.0f, zed = 0.0f, pitchBravo = 0.0f, yawBravo = 0.0f;
// Two Touch Zoom
public float perspectiveZoomSpeed = 0.1f;
// Three Touch Pan
public float panSpeed = 0.5f;
private float xAxis = 0.0f, yAxis = 0.0f;
private float xMain, yMain, zMain;
// Game Objects, Public or Private
private GameObject bravo;
void Update()
{
// Grabs Bravo
bravo = GameObject.Find ("bravo");
pitch = bravo.transform.eulerAngles.x;
yaw = bravo.transform.eulerAngles.y;
// One Touch controls rotation of Bravo
if (Input.touchCount == 1)
{
// Retrieves a single touch and names it TouchZero
Touch touchZero = Input.GetTouch (0);
// The start of the rotation will be aligned with Bravo's current rotation
//pitch = bravo.transform.eulerAngles.x;
//yaw = bravo.transform.eulerAngles.y;
// Times the difference in position of touch between frames by the rotation speed. deltaTime to keep movement consistent on all devices
pitch += touchZero.deltaPosition.y * rotateSpeed * Time.deltaTime;
yaw -= touchZero.deltaPosition.x * rotateSpeed * Time.deltaTime;
// Assigns the new eulerAngles to Bravo
bravo.transform.eulerAngles = new Vector3 (pitch, yaw, 0.0f);
}
// Two Touch contols the Field of View of the Camera aka. Zoom
if (Input.touchCount == 2)
{
mainCamera = GameObject.Find("main");
// Store both touches.
Touch touchZero = Input.GetTouch(0);
Touch touchOne = Input.GetTouch(1);
// Find the position in the previous frame of each touch.
Vector2 touchZeroPrevPos = touchZero.position - touchZero.deltaPosition;
Vector2 touchOnePrevPos = touchOne.position - touchOne.deltaPosition;
// Find the magnitude of the vector (the distance) between the touches in each frame.
float prevTouchDeltaMag = (touchZeroPrevPos - touchOnePrevPos).magnitude;
float touchDeltaMag = (touchZero.position - touchOne.position).magnitude;
// Find the difference in the distances between each frame.
float deltaMagnitudeDiff = prevTouchDeltaMag - touchDeltaMag;
// Otherwise change the field of view based on the change in distance between the touches.
camMain.fieldOfView += deltaMagnitudeDiff * perspectiveZoomSpeed;
// Clamp the field of view to make sure it's between 0 and 180.
camMain.fieldOfView = Mathf.Clamp(camMain.fieldOfView, 2.0f, 30.0f);
xAxis -= touchOne.deltaPosition.x * panSpeed * Time.deltaTime;
yAxis -= touchOne.deltaPosition.y * panSpeed * Time.deltaTime;
zMain = mainCamera.transform.position.z;
mainCamera.transform.position = new Vector3 (xAxis, yAxis, zMain);
}
}
With this script the object rotates left and right perfectly, 360°. But when the model is rotated up and down it gets to the 90° or -90° mark and bugs out, not allowing the user to carry on rotating.
I've done a bit of research and I believe it may have something to do with gimbal lock, my lack of knowledge on the subject means I haven't been able to come up with a fix.
Any help is appreciated.
GIF of bugged rotation

Update the rotation of a CALayer

I am trying to update the current rotation (and sometimes the position) of a CALayer.
What I am trying to in a couple of simple steps:
Store a couple of CALayers in an array, so I can reuse them
Set the anchor point of all CALayers to 0,0.
Draw CALayer objects where the object starts at a position on a circle
The layers are rotated by the same angle as the circle at that position
Update the position and rotation of the CALayer to match new values
Here is a piece of code I have:
lineWidth is the width of a line
self.items is an array containing the CALayer objects
func updateLines() {
var space = 2 * M_PI * Double(circleRadius);
var spaceAvailable = space / (lineWidth)
var visibleItems = [Int]();
var startIndex = items.count - Int(spaceAvailable);
if (startIndex < 0) {
startIndex = 0;
}
for (var i = startIndex; i < self.items.count; i++) {
visibleItems.append(self.items[i]);
}
var circleCenter = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
/* Each line should move up and rotate accordin to this value */
var anglePerLine: CGFloat = (360 / CGFloat(visibleItems.count)).toRadians()
/* Starting position, 270 degrees is on top */
var startAngle: CGFloat = CGFloat(270).toRadians();
/* Lines default rotation, we rotate it to get the right side up */
var lineAngle: CGFloat = CGFloat(180).toRadians();
for (var itemIndex = 0; itemIndex < visibleItems.count; itemIndex++) {
var itemLayer = self.itemLayers[itemIndex];
itemLayer.opacity = 1 - ((0.9 / visibleItems.count) * itemIndex);
/* Calculate start position of layer */
var x = CGFloat(circleRadius) * cos(startAngle) + CGFloat(circleCenter.x);
var y = CGFloat(circleRadius) * sin(startAngle) + CGFloat(circleCenter.y);
var height = CGFloat((arc4random() % 80) + 10);
/* Set position and frame of layer */
itemLayer.frame = CGRectMake(CGFloat(x), CGFloat(y), CGFloat(lineWidth), height);
itemLayer.position = CGPointMake(CGFloat(x), CGFloat(y));
var currentRotation = CGFloat((itemLayer.valueForKeyPath("transform.rotation.z") as NSNumber).floatValue);
var newRotation = lineAngle - currentRotation;
var rotationTransform = CATransform3DRotate(itemLayer.transform, CGFloat(newRotation), 0, 0, 1);
itemLayer.transform = rotationTransform;
lineAngle += anglePerLine;
startAngle += anglePerLine;
}
}
The result of the first run is exactly as I want it to be:
The second run through this code just doesn't update the CALayers correctly and it starts to look like this:
I think it has to do with my code to update the location and transform properties of the CALayer, but whatever I do, it always results in the last picture.
Answered via Twitter: setting frames and transform is mutually exclusive. Happy to help. Finding my login credentials for SO is harder. :D
Found the answer thanks to #iosengineer on Twitter. When setting a position on the CALayer, you do not want to update the frame of the layer, but you want to update the bounds.
Smooth animation FTW

Moving and rotating a sprite with Accelerometer in SpriteKit

I'm trying to make my first game using Spritekit, so i have a sprite that i need to move around using my accelerometer. Well, no problem doing that; movement are really smooth and responsive, the problem is that when i try to rotate my sprite in order to get it facing its own movement often i got it "shaking" like he has parkinson. (:D)
i did realize that this happens when accelerometer data are too close to 0 on one of x, y axes.
So the question: Is there a fix for my pet parkinson?? :D
Here is some code:
-(void) update:(NSTimeInterval)currentTime{
static CGPoint oldVelocity;
//static CGFloat oldAngle;
if(_lastUpdatedTime) {
_dt = currentTime - _lastUpdatedTime;
} else {
_dt = 0;
}
_lastUpdatedTime = currentTime;
CGFloat updatedAccelX = self.motionManager.accelerometerData.acceleration.y;
CGFloat updatedAccelY = -self.motionManager.accelerometerData.acceleration.x+sinf(M_PI/4.0);
CGFloat angle = vectorAngle(CGPointMake(updatedAccelX, updatedAccelY));
_velocity = cartesianFromPolarCoordinate(MAX_MOVE_PER_SEC, angle);
if(oldVelocity.x != _velocity.x || oldVelocity.y != _velocity.y){
_sprite.physicsBody.velocity = CGVectorMake(0, 0);
[_sprite.physicsBody applyImpulse:CGVectorMake(_velocity.x*_sprite.physicsBody.mass, _velocity.y*_sprite.physicsBody.mass)];
_sprite.zRotation = vectorAngle(_velocity);
oldVelocity = _velocity;
}
}
static inline CGFloat vectorAngle(CGPoint v){
return atan2f(v.y, v.x);
}
i did try to launch the update of the _velocity vector only when updatedAccelX or updatedAccelY are, in absolute value >= of some values, but the result was that i got the movement not smooth, when changing direction if the value is between 0.1 and 0.2, and the problem wasn't disappearing when the value was under 0.1.
i would like to maintain direction responsive, but i also would like to fix this "shake" of the sprite rotation.
I'm sorry for my bad english, and thanks in advance for any advice.
You can try a low pass filter (cf. to isolate effect of gravity) or high pass filter (to isolate effects of user acceleration).
#define filteringFactor 0.1
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
//low pass
accelerX = (acceleration.x * filteringFactor) + (accelerX * (1.0 - filteringFactor));
//idem … accelerY
//idem … accelerZ
//or high pass
accelerX = acceleration.x - ( (acceleration.x * filteringFactor) + (accelerX * (1.0 - filteringFactor)) );
//idem … accelerY
//idem … accelerZ
}

Working out the orientation of a target to a rotating sprite

I've got a player sprite that moves by rotation and its rotation is constantly changing but I also need to work out if a target is to the left or right of it and not within 45 degrees of the front or rear rotation.
I've written this code which I think should work but it seems only occasional pick up one side and slightly more on the other.
public void GrappleCheck(AsteroidSprite target)
{
float targetTragectory = (float)Math.Atan2(Position.Y - target.Position.Y, Position.X - target.Position.X);
if (targetTragectory < 0)
targetTragectory += (float)(Math.PI * 2);
if (Rotation < 0)
Rotation += (float)(Math.PI * 2);
if ((targetTragectory > Rotation + (float)(MathHelper.PiOver4 / 2)) && (targetTragectory < Rotation + (float)(Math.PI - (MathHelper.PiOver4 / 2))))
{
target.Distance = Vector2.Distance(Position, target.Position);
if (RightTarget != null)
{
if (RightTarget.Distance > target.Distance)
{
RightTarget.isTarget = false;
RightTarget = target;
RightTarget.ColorTint = Color.Blue;
RightTarget.isTarget = true;
}
}
else
{
RightTarget = target;
RightTarget.ColorTint = Color.Blue;
RightTarget.isTarget = true;
}
}
else if ((targetTragectory < Rotation - (float)(MathHelper.PiOver4 / 2)) && (targetTragectory > Rotation - (float)(Math.PI - (MathHelper.PiOver4 / 2))))
{
target.Distance = Vector2.Distance(Position, target.Position);
if (LeftTarget != null)
{
if (LeftTarget.Distance > target.Distance)
{
LeftTarget.isTarget = false;
LeftTarget = target;
LeftTarget.ColorTint = Color.Red;
LeftTarget.isTarget = true;
}
}
else
{
LeftTarget = target;
LeftTarget.ColorTint = Color.Red;
LeftTarget.isTarget = true;
}
}
else
{
target.isTarget = false;
}
if (controlInput.IsHeld(Keys.X))
{
Speed = Speed;
}
Working with angles can be quite annoying. Here are some ways to solve your problems without using angles:
First, we need the direction to the target and the movement direction:
var targetDirection = target.Positon - Position;
// Update this to match the actual direction. The following line assumes that
// a rotation of 0 results in the right direction.
var movementDirection = new Vector2((float)Math.Cos(Rotation), (float)Math.Sin(Rotation));
The first problem you want to solve is determining, if the target is within a 45° cone. You can calculate the actual angle with the following formula:
var dot = Vector2.Dot(myDirection, targetDirection);
//if dot is negative, then target is behind me, so just use the absolute value
var cos = Math.Abs(dot) / myDirection.Length() / targetDirection.Length();
var angle = Math.Acos(cos);
if(angle < MathHelper.PiOver4 / 2) //45° opening angle
; // within cone
else
; // outside cone
Your second problem is determining, if the target is on the left or right side. We use a vector that is ortogonal to myDirection and points to the left for this:
//assuming that +x is the right axis and +y is the down axis
var normal = new Vector2(myDirection.Y, -myDirection.X);
dot = Vector2.Dot(normal, targetDirection);
if(dot > 0)
; // target is on the left side
else
; // target is on the right side
I hope that makes cleaning up your code a bit easier and more comprehensible. You should consider extracting some code in separate methods to make it more readable.
Okay I've solved it, the player rotation can be from 0 to 2 x PI + or -, to keep it + though I put in
if (Rotation < 0)
Rotation += (float)Math.PI * 2;
the rotation to the target can be 0-PI or 0 - Negative PI depending on the way you declare the atan2 and where the player is.
//This works out the difference from the targets rotation to the players rotation.
RotationDif = TargetRotation - PlayerRotation;
//If the difference is greater than PI then when we check to see if its is within
//the range 0-PI or 0-Negative PI it will be missed whilst still possibly being on
//either side of the player, adding PI * 2 to the targets rotation basically spins
//it past the player so the Difference will be the shortest angle.
if(Math.Abs(RotationDif) > Math.PI)
RotationDif = TargetRotation + (float)(Math.PI * 2) - PlayerRotation;
//Next we check to see if the target is left(negative) or
//the right(positive), the negative/positive assignment will depend
//on which way round you did the atan2 to get the target rotation.
if ((RotationDif > 0) && (RotationDif < (float)Math.PI))
//Insert right target code here
else if ((RotationDif < 0) && (RotationDif > -(float)Math.PI))
//Insert left target code here
else
//Insert no target code here to cover all basis
And that's it, I've made the If (RotationDif > 0) etc differently so the a 45 degree angle front and back is ignored by making it
If ((RotationDif > (float)(Math.PI / 8) &&
(RotationDif < (float)(Math.PI - (Math.PI / 8)))
and the opposite for the other side, hope this helps someone else as it took me nearly 2 sodding weeks to work out :/

Resources