Image flickering while zooming while using PinchGestureRecognizer - c#

I use the below code to place an <Image> and <Image.GestureRecognizers> to make the zoom functionality.
The code behind for the event PinchUpdated="PinchGestureRecognizer_PinchUpdated" is also provided.
The zoom function works, but too much flickering while pinching.
XAML :
<Image
Grid.Column="0"
Grid.Row="0"
x:Name="pageIterator"
Grid.ColumnSpan="3">
<Image.GestureRecognizers>
<PinchGestureRecognizer PinchUpdated="PinchGestureRecognizer_PinchUpdated"></PinchGestureRecognizer>
</Image.GestureRecognizers>
</Image>
Code behind :
private void PinchGestureRecognizer_PinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
{
double currentScale = 1;
double startScale = 1;
double xOffset = 0;
double yOffset = 0;
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;
}
}

You should move the below lines out of PinchGestureRecognizer_PinchUpdated method,otherwise you're essentially initializing these values every time you fire.
double currentScale = 1;
double startScale = 1;
double xOffset = 0;
double yOffset = 0;
private void PinchGestureRecognizer_PinchUpdated(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;
}
}

Related

Determining if a Line intersects with a rotated rectangle on a canvas

I have managed to find code that determines if a Line intersects with a rectangle. My problem is when the Rectangle is rotated. I have looked hi and low to find code that will give me the coordinates of the corners of the rectangle after the transform is performed with no luck. My rectangle when it is rotated is rotated around CenterX and CenterY of 0,0.
I'd appreciate any code that you may have that does this.
Thanks!
More information:
I am working on a program that I want to be able to select one or more rectangles on a canvas by drawing a line. The rectangles can be rotated.
I have tried the following code. The second function works properly for non rotated rectangles but not for rotated rectangles.:
public bool AdjustedIntersects(FrameworkElement elem, Line line)
{
double x = Canvas.GetLeft(elem);
double y = Canvas.GetTop(elem);
double X = x * Math.Cos(((RotateTransform)elem.RenderTransform).Angle)
- y * Math.Sin(((RotateTransform)elem.RenderTransform).Angle);
double Y = x * Math.Sin(((RotateTransform)elem.RenderTransform).Angle)
+ y * Math.Cos(((RotateTransform)elem.RenderTransform).Angle);
x = Canvas.GetLeft(elem) + elem.Width;
y = Canvas.GetTop(elem) + elem.Height;
double X2 = x * Math.Cos(((RotateTransform)elem.RenderTransform).Angle)
- y * Math.Sin(((RotateTransform)elem.RenderTransform).Angle);
double Y2 = x * Math.Sin(((RotateTransform)elem.RenderTransform).Angle)
+ y * Math.Cos(((RotateTransform)elem.RenderTransform).Angle);
return SegmentIntersectRectangle(X, Y,X2,Y2,
line.X1, line.Y1, line.X2, line.Y2);
}
public bool SegmentIntersectRectangle(
double rectangleMinX,
double rectangleMinY,
double rectangleMaxX,
double rectangleMaxY,
double p1X,
double p1Y,
double p2X,
double p2Y)
{
// Find min and max X for the segment
double minX = p1X;
double maxX = p2X;
if (p1X > p2X)
{
minX = p2X;
maxX = p1X;
}
// Find the intersection of the segment's and rectangle's x-projections
if (maxX > rectangleMaxX)
{
maxX = rectangleMaxX;
}
if (minX < rectangleMinX)
{
minX = rectangleMinX;
}
if (minX > maxX) // If their projections do not intersect return false
{
return false;
}
// Find corresponding min and max Y for min and max X we found before
double minY = p1Y;
double maxY = p2Y;
double dx = p2X - p1X;
if (Math.Abs(dx) > 0.0000001)
{
double a = (p2Y - p1Y) / dx;
double b = p1Y - a * p1X;
minY = a * minX + b;
maxY = a * maxX + b;
}
if (minY > maxY)
{
double tmp = maxY;
maxY = minY;
minY = tmp;
}
// Find the intersection of the segment's and rectangle's y-projections
if (maxY > rectangleMaxY)
{
maxY = rectangleMaxY;
}
if (minY < rectangleMinY)
{
minY = rectangleMinY;
}
if (minY > maxY) // If Y-projections do not intersect return false
{
return false;
}
return true;
}
A polygon intersects with a line if one of lines of the poligon overlap with the line. Then try this:
Find four conners of the rectangle by the above way.
Check if one of lines make from sequence of conners overlap with the line. I have a ideal for it:
private bool IsStraightLineOverlap(System.Windows.Shapes.Line line1, Line line2)
{
var line1Min_X = Math.Min(line1.X1, line1.X2);
var line1Max_X = Math.Max(line1.X1, line1.X2);
var line1Min_Y = Math.Min(line1.Y1, line1.Y2);
var line1Max_Y = Math.Max(line1.Y1, line1.Y2);
var line2Min_X = Math.Min(line2.X1, line2.X2);
var line2Max_X = Math.Max(line2.X1, line2.X2);
var line2Min_Y = Math.Min(line2.Y1, line2.Y2);
var line2Max_Y = Math.Max(line2.Y1, line2.Y2);
var isOverlap_X = (line1Min_X <= line2Max_X && line1Max_X >= line2Min_X);
var isOverlap_Y = (line1Min_Y <= line2Max_Y && line1Max_Y >= line2Min_Y);
return isOverlap_X && isOverlap_Y;
}
In our game suite I have to find rotated points for various purposes.
Here's an extension method:
public static class PointExtension
{
public static Point GetRotatedPoint(this Point point, Point centerPoint, double angleInDegrees)
{
double angleInRadians = angleInDegrees * (Math.PI / 180.0);
double cosTheta = Math.Cos(angleInRadians);
double sinTheta = Math.Sin(angleInRadians);
return new Point
{
X =
(int)
(cosTheta * (point.X - centerPoint.X) -
sinTheta * (point.Y - centerPoint.Y) + centerPoint.X),
Y =
(int)
(sinTheta * (point.X - centerPoint.X) +
cosTheta * (point.Y - centerPoint.Y) + centerPoint.Y)
};
}
}
I also have to work out the cells on a grid that a line passes through.
To do this I use a bresenham line algorithm.
You can google this and find various implementations.
Here's mine:
public static IEnumerable<Point> GetOrderedPointsOnLine(int x0, int y0, int x1, int y1)
{
bool steep = Math.Abs(y1 - y0) > Math.Abs(x1 - x0);
if (steep)
{
Swap<int>(ref x0, ref y0);
Swap<int>(ref x1, ref y1);
}
int dx = Math.Abs(x1 - x0);
int dy = Math.Abs(y1 - y0);
int error = (dx / 2);
int ystep = (y0 < y1 ? 1 : -1);
int xstep = (x0 < x1 ? 1 : -1);
int y = y0;
for (int x = x0; x != (x1 + xstep); x += xstep)
{
yield return new Point((steep ? y : x), (steep ? x : y));
error = error - dy;
if (error < 0)
{
y += ystep;
error += dx;
}
}
yield break;
}
The outline of a rectangle is of course 4 lines.
There is an edge case where both an edge and the line can be a variation of 4 degrees and intersection exactly picks cells don't match.
To obviate that you could use a bresenham variation which picks both cells the line partially passes through.
Or you can build two geometries and see if you get anything overlaps when you use the wpf library to detect intersection.
I'm not sure what happens if you apply a transform to a geometry and then use geometry.fillcontainswithdetail
https://learn.microsoft.com/en-us/dotnet/api/system.windows.media.geometry.fillcontainswithdetail?redirectedfrom=MSDN&view=net-5.0#overloads
There's also combinedgeometry to consider.
Pick the right options and it'll only give you what overlaps between 2 geometries.
I use neither because in game has to be very fast.
For pre game calculations I render offscreen and examine the bytes of the image I get. This is the fastest simplest way I've found to see which cells (px) an irregular shape occupies.
And another way you could approach this.
Solution 1: break the problem down into something that is easier to solve:
Pick any edge on the rectangle, represented by points P1 and P2.
Translate both the points in your rectangle and the points that define your line by -P1, so that the P1 point is now at the origin.
Calculate the angle of your edge i.e. Math.Atan2(deltaY, deltaX).
Rotate the points for both the rectangle and line in the opposite direction, so that you now have an axis-aligned rectangle.
Do your line/rectangle hit test between these new primitives, using the algorithm you already have for axis-aligned rectangles.
If you need the actual point of intersection in proper world space coordinates then rotate it forward by the angle and translate by +P1.
Solution 2: test the line against each line that forms the rectangle, your problem is now 4 line-to-line intersection tests.
Disclaimer: this is not c#. I only know javascript but the math should be the same.
The way I attack this is by creating an array for my shape that I use to store each vertex. I then use those points to determine where the objects boundaries are. I am assuming that translate and rotate work the same in c# and the objects coordinate are always axis-aligned.
When I draw my rectangle I draw it with the x (left) as -width/2 and y (top) as -height/2. This is because I am going to use translate to position it where I want it and also allow it to rotate from the center.
ctx.save();
ctx.translate(this.x, this.y)
ctx.rotate(this.rotation);
ctx.fillStyle = this.color;
ctx.fillRect(-this.width/2, -this.height/2, this.width, this.height)
ctx.restore();
I don't know if c# uses save and restore but it would be the same as just translating and rotating it back after i.e.
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
ctx.fillStyle = this.color;
ctx.fillRect(-this.width/2, -this.height/2, this.width, this.height);
ctx.rotate(-this.rotation);
ctx.translate(-this.x, -this.y)
Also you'll notice in the snippet code that I created an array to store my x and y coordinates of each vertex
this.vertices = [];
for (let i = 0; i < 4; i++) {
//initially set to (0, 0) and updated in updateVertices()
this.vertices.push({ x: 0, y: 0 });
}
This is part of my rectangle object by the array could be global also.
The part that really matters is the math associated to updating the position of the vertices. In this snippet I use a function called updateVertices(). In this function I need to first calculate the sin and cos based on the current rotation.
let cos = Math.cos(this.rotation); //passing in radians in JS
let sin = Math.sin(this.rotation); //passing in radians in JS
Once I have that I just update the array of vertices that I created
//update Top Left Corner
this.vertices[0].x =
(this.x - this.centerX) * cos -
(this.y - this.centerY) * sin +
(this.centerX - this.width / 2);
this.vertices[0].y =
(this.x - this.centerX) * sin +
(this.y - this.centerY) * cos +
(this.centerY - this.height / 2);
Do that with all four corners. The math is slightly different for each vertex.
That's it. You have an array with all 4 vertices and can use them how you want. In this example I iterate over them passing two (adjacent) at time to my intersectLines() function to see if my vector lines intersects. Since I am testing 4 edges I use a loop to test all four sides against my vector, I do this in my passToIntersectFunction() function.
In this snippet you can use the mouse to move the vector around and see how the intersect points move.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
canvas.width = 400;
canvas.height = 400;
let ptX, ptY;
let intersectPoints = [];
let mouse = {
x: null,
y: null
};
canvas.addEventListener("mousemove", (e) => {
mouse.x = e.x - canvas.getBoundingClientRect().x;
mouse.y = e.y - canvas.getBoundingClientRect().y;
});
class Square {
constructor() {
this.x = 100;
this.y = 100;
this.width = 50;
this.height = 50;
this.centerX = this.x + this.width / 2;
this.centerY = this.y + this.height / 2;
this.color = "red";
this.angle = 0;
this.rotation = (this.angle * Math.PI) / 180; //convert to rads
//used to store all four vertices
this.vertices = [];
for (let i = 0; i < 4; i++) {
//initially set to (0, 0) and updated in updateVertices()
this.vertices.push({ x: 0, y: 0 });
}
}
draw() {
this.angle += 0.5;
this.rotation = (this.angle * Math.PI) / 180;
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
ctx.fillStyle = this.color;
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
ctx.rotate(-this.rotation);
ctx.translate(-this.x, -this.y);
}
drawIntersectPoint() {
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(ptX, ptY, 3, 0, Math.PI * 2);
ctx.fill();
}
drawVertices() {
ctx.fillStyle = "blue";
ctx.beginPath();
for (let i = 0; i < this.vertices.length; i++) {
ctx.arc(this.vertices[i].x, this.vertices[i].y, 3, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
}
updateVertices() {
let cos = Math.cos(this.rotation);
let sin = Math.sin(this.rotation);
//update Top Left Corner
this.vertices[0].x =
(this.x - this.centerX) * cos -
(this.y - this.centerY) * sin +
(this.centerX - this.width / 2);
this.vertices[0].y =
(this.x - this.centerX) * sin +
(this.y - this.centerY) * cos +
(this.centerY - this.height / 2);
//updates Top Right Corner
this.vertices[1].x =
(this.x + this.width - this.centerX) * cos -
(this.y - this.centerY) * sin +
(this.centerX - this.width / 2);
this.vertices[1].y =
(this.x + this.width - this.centerX) * sin +
(this.y - this.centerY) * cos +
(this.centerY - this.height / 2);
//updates Bottom Right Corner
this.vertices[2].x =
(this.x + this.width - this.centerX) * cos -
(this.y + this.height - this.centerY) * sin +
(this.centerX - this.width / 2);
this.vertices[2].y =
(this.x + this.width - this.centerX) * sin +
(this.y + this.height - this.centerY) * cos +
(this.centerY - this.height / 2);
//updates Bottom Left Corner
this.vertices[3].x =
(this.x - this.centerX) * cos -
(this.y + this.height - this.centerY) * sin +
(this.centerX - this.width / 2);
this.vertices[3].y =
(this.x - this.centerX) * sin +
(this.y + this.height - this.centerY) * cos +
(this.centerY - this.height / 2);
}
}
let square = new Square();
class Vector {
constructor() {
this.x1 = 200;
this.y1 = 100;
this.x2 = mouse.x;
this.y2 = mouse.y;
}
draw() {
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
ctx.stroke();
}
updateVector() {
this.x2 = mouse.x;
this.y2 = mouse.y;
this.draw();
}
}
let vector = new Vector();
function intersectLines(coord1, coord2, vector) {
//this if statement just keeps the array from constantly growing
if (intersectPoints.length > 2) {
intersectPoints.shift();
}
let x1 = coord1.x;
let x2 = coord2.x;
let y1 = coord1.y;
let y2 = coord2.y;
let x3 = vector.x1;
let x4 = vector.x2;
let y3 = vector.y1;
let y4 = vector.y2;
let d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if (d == 0) {
return;
}
let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / d;
let u = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / d;
if (t > 0 && t < 1 && u > 0) {
intersectPoints.push({ x: x1 + t * (x2 - x1), y: y1 + t * (y2 - y1) });
}
return;
}
function passToIntersectFunction() {
for (let i = 0; i < square.vertices.length; i++) {
intersectLines(
square.vertices[i],
square.vertices[i + 1] ?? square.vertices[0],
vector
);
}
}
function drawIntersectPoints() {
for (let i = 0; i < intersectPoints.length; i++) {
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(intersectPoints[i].x, intersectPoints[i].y, 3, 0, Math.PI * 2);
ctx.fill();
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
square.draw();
square.updateVertices();
square.drawIntersectPoint();
square.drawVertices();
vector.updateVector();
drawIntersectPoints();
passToIntersectFunction();
requestAnimationFrame(animate);
}
animate();
<canvas id="canvas"></canvas>
Sorry its not c# but maybe it can help.

Xamarin add Scale/Rotate to ContentView

I have a question.
I created a CustomView where I can drag/drop the view inside, but now I also want to add a scale/rotate function to it. Now here is the OnTouchEvent that created to move the view inside:
public override bool OnTouchEvent(MotionEvent e)
{
float x = e.RawX;
float y = e.RawY;
var dragView = Element as DraggableView.DraggableView;
switch (e.Action)
{
case MotionEventActions.Down:
if (dragView.DragMode == DragMode.Touch)
{
if (!touchedDown)
{
if (firstTime)
{
originalX = GetX();
originalY = GetY();
firstTime = false;
}
dragView.DragStarted();
}
TextMoved = false;
touchedDown = true;
stopwatch.Start();
}
dX = x - this.GetX();
dY = y - this.GetY();
break;
case MotionEventActions.Move:
if (touchedDown)
{
if (dragView.DragDirection == DragDirectionType.All || dragView.DragDirection == DragDirectionType.Horizontal)
{
SetX(x - dX);
}
if (dragView.DragDirection == DragDirectionType.All || dragView.DragDirection == DragDirectionType.Vertical)
{
SetY(y - dY);
}
TextMoved = true;
}
break;
case MotionEventActions.Up:
touchedDown = false;
if(TextMoved == true)
{
dragView.DragEnded();
}
else
{
MessagingCenter.Send<object, DraggableView.DraggableView>(this, "EditSelectedText", dragView);
}
break;
case MotionEventActions.Cancel:
touchedDown = false;
break;
}
return base.OnTouchEvent(e);
}
But now I also need the Scale/Rotate function.
The problem is that I already created it for my skiasharp bitmaps, but this isn't skiasharp, so I can't use that.
How can I implement this function in a OnTouchEvent without skiasharp?
There is a xamarin-forms-samples about ScaleAndRotate , however it is not related to TouchEvent .I found a way by using PanGestureRecognizer and PinchGestureRecognizer to implement it .
Create a ScaleAndRotateContainer ContentView which contains PanGestureRecognizer and PinchGestureRecognizer :
public class ScaleAndRotateContainer : ContentView
{
double currentScale = 1;
double startScale = 1;
double xOffset = 0;
double yOffset = 0;
double rotateNum = 1;
public ScaleAndRotateContainer()
{
var pinchGesture = new PinchGestureRecognizer ();
pinchGesture.PinchUpdated += OnPinchUpdated;
var panGesture = new PanGestureRecognizer();
panGesture.PanUpdated += PanGesture_PanUpdated;
GestureRecognizers.Add (pinchGesture);
GestureRecognizers.Add(panGesture);
}
private void PanGesture_PanUpdated(object sender, PanUpdatedEventArgs e)
{
rotateNum++;
this.RotateTo(rotateNum);
this.AnchorX = 0.5;
this.AnchorY = 0.5;
}
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;
}
}
}
Then use it in Xaml for ContentPage :
xmlns:local="clr-namespace:PinchGesture;assembly=YourNameSpace"
<local:ScaleAndRotateContainer>
<local:ScaleAndRotateContainer.Content>
<StackLayout HorizontalOptions="Center" VerticalOptions="Center" >
<Label Text="Hello Xamarin.Forms!" />
<Image Source="waterfront.jpg" />
</StackLayout>
</local:ScaleAndRotateContainer.Content>
</local:ScaleAndRotateContainer>
The effect :

how to enable scrolling when zoom in and zoom out on a content page in xamarin forms

i am trying to zoom in and zoom out on a content page using xamarin.forms.
I am able zoom in and zoom out but the problem is scrolling is not working.
i want zoom an image. with this code zooming is working perfectly. But while zooming i am not able to see full image. i must scroll to view the rest of the image. for that i need to scroll. but scrolling is not working.
XAML
xmlns:helper="clr-namespace:KPGTC.Deals.Mobile.Helpers"
<helper:PinchToZoomContainer>
<helper:PinchToZoomContainer.Content>
<Image x:Name="img_Popup"/>
</helper:PinchToZoomContainer.Content>
</helper:PinchToZoomContainer>
Code:
public class PinchToZoomContainer : ContentView
{
double MIN_SCALE = 1;
double MAX_SCALE = 4;
double startScale = 1;
double currentScale = 1;
double xOffset = 0;
double yOffset = 0;
bool _isActive = false;
public PinchToZoomContainer()
{
DependencyService.Get<IHelpers>().ShowAlert("Double-tap to zoom");
//var _pinchGesture = new PinchGestureRecognizer();
//_pinchGesture.PinchUpdated += OnPinchUpdated;
//GestureRecognizers.Add(_pinchGesture);
var _tapGesture = new TapGestureRecognizer { NumberOfTapsRequired = 2 };
_tapGesture.Tapped += On_Tapped;
GestureRecognizers.Add(_tapGesture);
var _panGesture = new PanGestureRecognizer();
_panGesture.PanUpdated += OnPanUpdated;
GestureRecognizers.Add(_panGesture);
TranslationX = 0;
TranslationY = 0;
_isActive = false;
}
private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
if (_isActive)
{
if (e.TotalX > 0)
{
if (e.TotalX > 2)
{
TranslationX += 15;
}
}
else
{
if (e.TotalX < -2)
{
TranslationX -= 15;
}
}
}
}
private void On_Tapped(object sender, EventArgs e)
{
if (Scale > MIN_SCALE)
{
_isActive = false;
this.ScaleTo(MIN_SCALE, 250, Easing.CubicInOut);
this.TranslateTo(0, 0, 250, Easing.CubicInOut);
}
else
{
_isActive = true;
AnchorX = AnchorY = 0.5;
this.ScaleTo(MAX_SCALE, 250, Easing.CubicInOut);
}
}
void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
{
if (e.Status == GestureStatus.Started)
{
startScale = Content.Scale;
Content.AnchorX = 0;
Content.AnchorY = 0;
}
if (e.Status == GestureStatus.Running)
{
currentScale += (e.Scale - 1) * startScale;
currentScale = Math.Max(1, currentScale);
double renderedX = Content.X + xOffset;
double deltaX = renderedX / Width;
double deltaWidth = Width / (Content.Width * startScale);
double originX = (e.ScaleOrigin.X - deltaX) * deltaWidth;
double renderedY = Content.Y + yOffset;
double deltaY = renderedY / Height;
double deltaHeight = Height / (Content.Height * startScale);
double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight;
double targetX = xOffset - (originX * Content.Width) * (currentScale - startScale);
double targetY = yOffset - (originY * Content.Height) * (currentScale - startScale);
Content.TranslationX = targetX.Clamp(-Content.Width * (currentScale - 1), 0);
Content.TranslationY = targetY.Clamp(-Content.Height * (currentScale - 1), 0);
Content.Scale = currentScale;
}
if (e.Status == GestureStatus.Completed)
{
xOffset = Content.TranslationX;
yOffset = Content.TranslationY;
}
}
}
Alright, this was quite a tough one and admittedly I don't understand fully how I made it, but I made it.
Some thoughts:
You mixed the translation of the container and the content, which is quite tricky to handle - if this is possible at all
When panning, you added 15 every time the pan event was raised, but there is a better way: Just store the initial offset of the content and then add the TotalX and TotalY respectively to the TranslationX and the TranslationY of the content (this was the easy part)
Panning while zooming was quite hard to get right and I had to find it out by trial and error
Basically you have to store the origin of the pinch gesture when the gesture starts and calculate the diff between the original origin and the current origin
Then you have to add the diff (multiplied by the with and height respectively of the control) to the target translation
Here is the code for the panning:
private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
if (e.StatusType == GestureStatus.Started)
{
this.xOffset = this.Content.TranslationX;
this.yOffset = this.Content.TranslationY;
}
if (e.StatusType != GestureStatus.Completed
&& e.StatusType != GestureStatus.Canceled)
{
this.Content.TranslationX = this.xOffset + e.TotalX;
this.Content.TranslationY = this.yOffset + e.TotalY;
}
if (e.StatusType == GestureStatus.Completed)
{
this.xOffset = this.Content.TranslationX;
this.yOffset = this.Content.TranslationY;
}
}
And here for the pinching
void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
{
if (e.Status == GestureStatus.Started)
{
this.startScale = this.Content.Scale;
this.Content.AnchorX = 0;
this.Content.AnchorY = 0;
this.startScaleOrigin = e.ScaleOrigin;
}
if (e.Status == GestureStatus.Running)
{
var originDiff = PinchToZoomContainer.CalculateDiff(e.ScaleOrigin, this.startScaleOrigin);
this.currentScale += (e.Scale - 1) * this.startScale;
this.currentScale = Math.Max(1, this.currentScale);
double renderedX = this.Content.X + this.xOffset;
double deltaX = renderedX / this.Width;
double deltaWidth = this.Width / (this.Content.Width * this.startScale);
double originX = (this.startScaleOrigin.X - deltaX) * deltaWidth;
double renderedY = this.Content.Y + this.yOffset;
double deltaY = renderedY / this.Height;
double deltaHeight = this.Height / (this.Content.Height * this.startScale);
double originY = (startScaleOrigin.Y - deltaY) * deltaHeight;
double targetX = this.xOffset - ((originX) * this.Content.Width) * (this.currentScale - this.startScale) - originDiff.X * this.Content.Width;
double targetY = this.yOffset - ((originY) * this.Content.Height) * (this.currentScale - this.startScale) - originDiff.Y * this.Content.Height;
this.Content.TranslationX = targetX.Clamp(-this.Content.Width * (this.currentScale - 1), 0);
this.Content.TranslationY = targetY.Clamp(-this.Content.Height * (this.currentScale - 1), 0);
this.Content.Scale = this.currentScale;
}
if (e.Status == GestureStatus.Completed)
{
this.xOffset = this.Content.TranslationX;
this.yOffset = this.Content.TranslationY;
}
}
(Of course you have to add Point startScaleOrigin to your class).
Finally you need the method to calculate the distance between two points
private static Point CalculateDiff(Point first, Point second)
{
return second.Offset(-first.X, -first.Y);
}
Unfortunately I did not manage to get the tapping right, but I think you should be able to figure it out from here.

Numeric Extensions.Clamp(double, double, double)' is inaccessible due to its protection level in xamarin forms class

I am working on adding pitch to zoom into my xamarin forms content page and I'm using this example and i'm getting this error upon running the app on my phone:
Severity Code Description Project File Line Suppression State
Error CS0122 'NumericExtensions.Clamp(double, double, double)' is inaccessible due to its protection level AppName.Forms C:\TFSMOBILE\AppName.NETStandard\AppName.Forms\Pages\PinchToZoomContainer.cs 55 Active
here's the code for the PinchToZoomContainer:
using System;
using Xamarin.Forms;
namespace AppName.Forms.Pages
{
public class PinchToZoomContainer : ContentView
{
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.
//error 1:
Content.TranslationX = targetX.Clamp(-Content.Width * (currentScale - 1), 0);
//error 2:
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;
}
}
}
}
any help would be amazing!!
Thanks in advance! :)
That project you referenced on GitHub is using its own Clamp function and not the internal Forms-based one and due to the error I would assume you are missing this class:
using System;
namespace PinchGesture
{
public static class DoubleExtensions
{
public static double Clamp (this double self, double min, double max)
{
return Math.Min (max, Math.Max (self, min));
}
}
}
Re: https://github.com/xamarin/xamarin-forms-samples/blob/290d8947432fef55852bf381dcaec393d638f150/WorkingWithGestures/PinchGesture/PinchGesture/Extensions/DoubleExtensions.cs
in the current release version of Xamarin Forms, NumericExtensions is internal. It will be changed to public in a future release.
In the meantime, you can easily implement Clamp yourself
return Math.Min(max, Math.Max(value, min));

Image only zoom in but not zoom out in Xamarin forms

I have content page which contain photo. I wanted to add the zoom in and zoom out options however when i tried to add the pinch gesture it is only zoom in but not zoom out? how can I add the zoom out options?
here is my XAML:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ComplexInstructionApp.ShowProgram"
Title="dsa">
<ContentPage.Content>
</ContentPage.Content>
<Grid>
<Image x:Name="IMI" Aspect="Fill"
RelativeLayout.WidthConstraint=
"{ConstraintExpression Type=RelativeToParent, Property=Width}"
RelativeLayout.HeightConstraint=
"{ConstraintExpression Type=RelativeToParent, Property=Height}"
/>
</Grid>
</ContentPage>
and here is my code:
public partial class ShowProgram : ContentPage
{
private double currentScale = 1;
private double startScale = 1;
private double xOffset = 0;
private double yOffset = 0;
public ShowProgram(string txt)
{
InitializeComponent();
switch (txt)
{
case "קובני":
IMI.Source = "Cuben_Expend.png";
break;
case "קובני מצומצם":
IMI.Source = "Cuben_short.png";
break;
case "אל איי":
IMI.Source = "LA_Expend.png";
break;
case "אל איי מצומצם":
IMI.Source = "LA_short.png";
break;
case "בצ'אטה":
IMI.Source = "bachata.png";
break;
case "זוק":
IMI.Source = "zouk.png";
break;
}
var pinchGesture = new PinchGestureRecognizer();
pinchGesture.PinchUpdated += (s, 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 = Clamp1(targetX ,- Content.Width * (currentScale - 1), 0);
Content.TranslationY = Clamp1(targetY, -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;
}
};
IMI.GestureRecognizers.Add(pinchGesture);
}
public double Clamp1(double self, double min, double max)
{
return Math.Min(max, Math.Max(self, min));
}
}

Categories

Resources