Xamarin.Forms Pinch Gesture inside ScrollView Issue - xamarin

I'm new to Xamarin.Forms and I've been trying to make a simple program. I wanted to make a program where I could pan and zoom an image. I found code on this site to handle the pinch gesture and view scaling (https://developer.xamarin.com/guides/xamarin-forms/user-interface/gestures/pinch/). I then tried to use this code to extend a 'ScrollView' class.
When I run my code on iOS, it works fine in the simulator. (The scaling is a little clunky, but I don't know if its the simulator or the technique)
When I run my code on Android (both the simulator OR an actual device), the page only scrolls. On the simulator, I'm using the command key to invoke the pinch gesture. I see the appropriate interface for the pinch gesture come up, but it looks like the scroll view is intercepting the gesture as a pan gesture.
When I take the ScrollView class out the mix (and simply make it a ContentView class), the pinch gesture works fine on both platforms.
Any ideas?
Here's the code for my container (which is almost an exact copy of the code at the link above)
using System;
using Xamarin.Forms;
namespace ImageTest
{
public class PinchToZoomContainer : ScrollView
{
double currentScale = 1;
double startScale = 1;
double xOffset = 0;
double yOffset = 0;
public PinchToZoomContainer()
{
var pinchGesture = new PinchGestureRecognizer();
pinchGesture.PinchUpdated += OnPinchUpdated;
GestureRecognizers.Add(pinchGesture);
}
public 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(0.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;
}
}
}
}

The answer lays within working on the Android, IOS project.
Check out this guide: http://www.xamboy.com/2017/08/02/creating-a-zoomable-scrollview-in-xamarin-forms/
What is suggested is to create a render that android and ios platformsupport to beable to zoom in on the application on ScrollView.

Related

Skiasharp how to get coordinates from touch location after scaling and transform

I have an Xamarin project where I am using Skiasharp. I am relatively new to the drawing utility. Ive spent a few days trying to figure out this issue with no luck. After scaling and transforming the canvas, when I touch the skcanvas view on the phone screen and look at the 'location' point in the touch event, its not the same location that the canvas drew something. I need the exact location I drew the rectangle.
Its a lot of code below and granted its not all the code, but its the important parts. I am absolutely baffled why I draw in one (X,Y) location yet when I touch the screen the touch event for the canvas gives me a completely different location than what than the (X,Y) the widget was drawn at.
'''
public static void DrawLayout(SKImageInfo info, SKCanvas canvas, SKSvg svg,
SetupViewModel vm)
var layout = vm.SelectedReticleLayout;
float yRatio;
float xRatio;
float widgetHeight = 75;
float widgetWidth = 170;
float availableWidth = 720;
float availableHeight = 1280;
var currentZoomScale = getScale();
canvas.Translate(info.Width / 2f, info.Height / 2f);
SKRect bounds = svg.ViewBox;
xRatio = (info.Width / bounds.Width) + ((info.Width / bounds.Width) * currentZoomScale);
yRatio = (info.Height / bounds.Height) + ((info.Height / bounds.Height) *
currentZoomScale);
float ratio = Math.Min(xRatio, yRatio);
canvas.Scale(ratio);
canvas.Translate(-bounds.MidX, -bounds.MidY);
canvas.DrawPicture(svg.Picture, new SKPaint { Color = SKColors.White, Style =
SKPaintStyle.Fill });
// now set the X,Y and Width and Height of the large Red Rectangle
float imageCenter = canvas.LocalClipBounds.Width / 2;
layout.RedBorderXOffSet = imageCenter - (imageCenter / 2.0f) +
canvas.LocalClipBounds.Left;
float redBorderYOffSet = (float)(svg.Picture.CullRect.Top +
Math.Ceiling(.0654450261780105f * svg.Picture.CullRect.Bottom));
layout.RedBorderYOffSet = (float)(canvas.LocalClipBounds.Top +
Math.Ceiling(.0654450261780105f * canvas.LocalClipBounds.Bottom));
layout.RedBorderWidth = canvas.LocalClipBounds.Width / 2.0f;
layout.RedBorderWidthXOffSet = layout.RedBorderWidth + layout.RedBorderXOffSet;
layout.RedBorderHeight = (float)(canvas.LocalClipBounds.Bottom -
Math.Ceiling(.0654450261780105f * canvas.LocalClipBounds.Bottom * 2)) -
canvas.LocalClipBounds.Top;
layout.RedBorderHeightYOffSet = layout.RedBorderYOffSet + layout.RedBorderHeight;
// draw the large red rectangle
canvas.DrawRect(layout.RedBorderXOffSet, layout.RedBorderYOffSet, layout.RedBorderWidth,
layout.RedBorderHeight, RedBorderPaint);
// clear the tracked widgets, tracked widgets are updated every time we draw the widgets
// base widgets contain the default size and location relative to the scope. base line
widgets
// will need to be multiplied by the node scale height and width
layout.TrackedWidgets.Clear();
var widget = new widget
{
X = layout.RedBorderXOffSet + 5;
Y = layout.RedBorderYOffSet + layout.TrackedReticleWidgets[0].Height + 15;
Height = layout.RedBorderHeight * (widgetHeight / availableHeight);
Width = layout.RedBorderWdith * (widgetWidth / availableWidth);
}
// define colors for text and border colors for small rectangles (widgets)
public static SKPaint SelectedWidgetColor => new SKPaint { Color = SKColors.LightPink,
Style = SKPaintStyle.StrokeAndFill, StrokeWidth = 3 };
public static SKPaint EmptyWidgetBorder => new SKPaint { Color = SKColors.DarkGray,
Style = SKPaintStyle.Stroke, StrokeWidth = 3 };
public static SKPaint EmptyWidgetText => new SKPaint { Color = SKColors.Black, TextSize
= 10, FakeBoldText = false, Style = SKPaintStyle.Stroke, Typeface =
SKTypeface.FromFamilyName("Arial") };
public static SKPaint DefinedWidgetText => new SKPaint { Color = SKColors.DarkRed,
FakeBoldText = false, Style = SKPaintStyle.Stroke };
// create small rectangle (widget) and draw the widget
var widgetRectangle = SKRect.Create(widget.X, widget.Y, widget.Width, widget.Height);
canvas.DrawRect(widgetRectangle, widget.IsSelected ? SelectedWidgetColor :
EmptyWidgetBorder);
// now lets create the text to draw in the widget
string text = EnumUtility.GetDescription(widget.WidgetDataType);
float textWidth = EmptyWidgetText.MeasureText(text);
EmptyWidgetText.TextSize = widget.Width * GetUnscaledWidgetWith(widget) *
EmptyWidgetText.TextSize / textWidth;
SKRect textBounds = new SKRect();
EmptyWidgetText.MeasureText(text, ref textBounds);
float xText = widgetRectangle.MidX - textBounds.MidX;
float yText = widgetRectangle.MidY - textBounds.MidY;
canvas.DrawText(text, xText, yText, EmptyWidgetText);
'''

xamarin forms image zoom and scroll

I am using this pinchGestureRecognizer in order to zoom on an image, but the problem is that after zooming I am not able to scroll the image vertically, even though I've wrapped my image in a scrollView with Orientation="Both"
Here is the PinchToZoomContainer class:
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.
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;
}
}
}
This the Clamp method:
public static class DoubleExtensions
{
public static double Clamp(this double self, double min, double max)
{
return Math.Min(max, Math.Max(self, min));
}
}
And this is the image in my XAML file:
<StackLayout Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Grid.RowSpan="3">
<ScrollView x:Name="imageScroll" Orientation="Both" VerticalOptions="FillAndExpand">
<entry:PinchToZoomContainer>
<entry:PinchToZoomContainer.Content>
<Image x:Name="zoomImage" Source="{images:EmbeddedImage Vebko.Images.CB_Edite_Vb1_2.jpg}" Margin="0,0,0,10" HorizontalOptions="Center" VerticalOptions="Center"/>
</entry:PinchToZoomContainer.Content>
</entry:PinchToZoomContainer>
</ScrollView>
</StackLayout>
Can anybody please help?
What i guess is that while you scale the Content, the PinchToZoomContainer : ContentView size remains the same that is why scrollview doesn't react = change control's size with height/width requests in code when its content gets scaled.

Xamarin Forms pinch and pan together

I have implemented both pan and pinch individually, and it works fine. I'm now trying to use pinch and pan together and I'm seeing some issues. Here's my code:
XAML:
<AbsoluteLayout x:Name="PinchZoomContainer">
<controls:NavBar x:Name="NavBar" ShowPrevNext="true" ShowMenu="false" IsModal="true" />
<controls:PanContainer x:Name="PinchToZoomContainer">
<Image x:Name="ImageMain" />
</controls:PanContainer>
</AbsoluteLayout>
Pinch/Pan Gesture Add's:
var panGesture = new PanGestureRecognizer();
panGesture.PanUpdated += OnPanUpdated;
GestureRecognizers.Add(panGesture);
var pinchGesture = new PinchGestureRecognizer();
pinchGesture.PinchUpdated += OnPinchUpdated;
GestureRecognizers.Add(pinchGesture);
Pan Method:
void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
switch (e.StatusType)
{
case GestureStatus.Started:
startX = e.TotalX;
startY = e.TotalY;
Content.AnchorX = 0;
Content.AnchorY = 0;
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));
Content.TranslationY = Math.Max(Math.Min(0, y + e.TotalY), -Math.Abs(Content.Height - App.ScreenHeight));
break;
case GestureStatus.Completed:
// Store the translation applied during the pan
x = Content.TranslationX;
y = Content.TranslationY;
break;
}
}
Pinch Method:
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;
//ImageMain.AnchorX = 0;
//ImageMain.AnchorY = 0;
}
if (e.Status == GestureStatus.Running)
{
// Calculate the scale factor to be applied.
currentScale += (e.Scale - 1) * startScale;
currentScale = Math.Max(1, currentScale);
currentScale = Math.Min(currentScale, 2.5);
// 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;
}
}
If I turn off either gesture and only use the other then the functionality works perfectly. The issue arises when I add the pan AND pinch gestures. What seems to be happening is this:
1) The pan actually seems to be working as expected
2) When you pan on the image initially, let's say, move the image to Y-center and X-center, and then you try to zoom, the image gets set back to it's initial state. Then, when you pan, it moves you back to where you were before you tried to zoom (which is why I say the pan is working fine).
From what I'm understanding from my debugging is that when you zoom it's not taking into consideration the position you are currently at. So when you pan first, and then zoom, it doesn't zoom on the position you're at but the beginning point of the image. Then when you try to pan from there, the pan method still remembers where you were, and it moves you back to where you were before you tried to zoom.
Hoping some insight on this. Obviously, there's an issue with my pinch method. I just think (obviously can't figure out) I need to add logic into it that takes into consideration where you're currently at.
The main reason might be that everybody seems to copy and use this code (coming from the dev.xamarin site) with its very convoluted and very unnecessary co-ordinate calculations :-). Unnecessary because we could simply ask the view to do the heavy lifting for us, using the AnchorX and AnchorY properties which serve exactly this purpose.
We can have a double tap operation to zoom in and to revert to the original scale. Note that because Xamarin fails to provide coordinate values with its Tap events (a very unwise decision, actually), we can only zoom from the center now:
private void OnTapped(object sender, EventArgs e)
{
if (Scale > MIN_SCALE)
{
this.ScaleTo(MIN_SCALE, 250, Easing.CubicInOut);
this.TranslateTo(0, 0, 250, Easing.CubicInOut);
}
else
{
AnchorX = AnchorY = 0.5;
this.ScaleTo(MAX_SCALE, 250, Easing.CubicInOut);
}
}
The pinch handler is similarly simple, no need to calculate any translations at all. All we have to do is to set the anchors to the pinch starting point and the framework will do the rest, the scaling will occur around this point. Note that we even have an extra feature here, springy bounce-back on overshoot at both ends of the zoom scale.
private void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
{
switch (e.Status)
{
case GestureStatus.Started:
StartScale = Scale;
AnchorX = e.ScaleOrigin.X;
AnchorY = e.ScaleOrigin.Y;
break;
case GestureStatus.Running:
double current = Scale + (e.Scale - 1) * StartScale;
Scale = Clamp(current, MIN_SCALE * (1 - OVERSHOOT), MAX_SCALE * (1 + OVERSHOOT));
break;
case GestureStatus.Completed:
if (Scale > MAX_SCALE)
this.ScaleTo(MAX_SCALE, 250, Easing.SpringOut);
else if (Scale < MIN_SCALE)
this.ScaleTo(MIN_SCALE, 250, Easing.SpringOut);
break;
}
}
And the panning handler, even simpler. On start, we calculate the starting point from the anchor and during panning, we keep changing the anchor. This anchor being relative to the view area, we can easily clamp it between 0 and 1 and this stops the panning at the extremes without any translation calculation at all.
private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
switch (e.StatusType)
{
case GestureStatus.Started:
StartX = (1 - AnchorX) * Width;
StartY = (1 - AnchorY) * Height;
break;
case GestureStatus.Running:
AnchorX = Clamp(1 - (StartX + e.TotalX) / Width, 0, 1);
AnchorY = Clamp(1 - (StartY + e.TotalY) / Height, 0, 1);
break;
}
}
The constants and variables used are just these:
private const double MIN_SCALE = 1;
private const double MAX_SCALE = 8;
private const double OVERSHOOT = 0.15;
private double StartX, StartY;
private double StartScale;
Went with a completely different method of handling this. For anyone who is having issues, this works 100%.
OnPanUpdated
void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
var s = (ContentView)sender;
// do not allow pan if the image is in its intial size
if (currentScale == 1)
return;
switch (e.StatusType)
{
case GestureStatus.Running:
double xTrans = xOffset + e.TotalX, yTrans = yOffset + e.TotalY;
// do not allow verical scorlling unless the image size is bigger than the screen
s.Content.TranslateTo(xTrans, yTrans, 0, Easing.Linear);
break;
case GestureStatus.Completed:
// Store the translation applied during the pan
xOffset = s.Content.TranslationX;
yOffset = s.Content.TranslationY;
// center the image if the width of the image is smaller than the screen width
if (originalWidth * currentScale < ScreenWidth && ScreenWidth > ScreenHeight)
xOffset = (ScreenWidth - originalWidth * currentScale) / 2 - s.Content.X;
else
xOffset = System.Math.Max(System.Math.Min(0, xOffset), -System.Math.Abs(originalWidth * currentScale - ScreenWidth));
// center the image if the height of the image is smaller than the screen height
if (originalHeight * currentScale < ScreenHeight && ScreenHeight > ScreenWidth)
yOffset = (ScreenHeight - originalHeight * currentScale) / 2 - s.Content.Y;
else
//yOffset = System.Math.Max(System.Math.Min((originalHeight - (ScreenHeight)) / 2, yOffset), -System.Math.Abs((originalHeight * currentScale - ScreenHeight - (originalHeight - ScreenHeight) / 2)) + (NavBar.Height + App.StatusBarHeight));
yOffset = System.Math.Max(System.Math.Min((originalHeight - (ScreenHeight)) / 2, yOffset), -System.Math.Abs((originalHeight * currentScale - ScreenHeight - (originalHeight - ScreenHeight) / 2)));
// bounce the image back to inside the bounds
s.Content.TranslateTo(xOffset, yOffset, 500, Easing.BounceOut);
break;
}
}
OnPinchUpdated
void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
{
var s = (ContentView)sender;
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 = s.Content.Scale;
s.Content.AnchorX = 0;
s.Content.AnchorY = 0;
}
if (e.Status == GestureStatus.Running)
{
// Calculate the scale factor to be applied.
currentScale += (e.Scale - 1) * startScale;
currentScale = System.Math.Max(1, currentScale);
currentScale = System.Math.Min(currentScale, 5);
//scaleLabel.Text = "Scale: " + currentScale.ToString ();
// The ScaleOrigin is in relative coordinates to the wrapped user interface element,
// so get the X pixel coordinate.
double renderedX = s.Content.X + xOffset;
double deltaX = renderedX / App.ScreenWidth;
double deltaWidth = App.ScreenWidth / (s.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 = s.Content.Y + yOffset;
double deltaY = renderedY / App.ScreenHeight;
double deltaHeight = App.ScreenHeight / (s.Content.Height * startScale);
double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight;
// Calculate the transformed element pixel coordinates.
double targetX = xOffset - (originX * s.Content.Width) * (currentScale - startScale);
double targetY = yOffset - (originY * s.Content.Height) * (currentScale - startScale);
// Apply translation based on the change in origin.
var transX = targetX.Clamp(-s.Content.Width * (currentScale - 1), 0);
var transY = targetY.Clamp(-s.Content.Height * (currentScale - 1), 0);
s.Content.TranslateTo(transX, transY, 0, Easing.Linear);
// Apply scale factor.
s.Content.Scale = currentScale;
}
if (e.Status == GestureStatus.Completed)
{
// Store the translation applied during the pan
xOffset = s.Content.TranslationX;
yOffset = s.Content.TranslationY;
// center the image if the width of the image is smaller than the screen width
if (originalWidth * currentScale < ScreenWidth && ScreenWidth > ScreenHeight)
xOffset = (ScreenWidth - originalWidth * currentScale) / 2 - s.Content.X;
else
xOffset = System.Math.Max(System.Math.Min(0, xOffset), -System.Math.Abs(originalWidth * currentScale - ScreenWidth));
// center the image if the height of the image is smaller than the screen height
if (originalHeight * currentScale < ScreenHeight && ScreenHeight > ScreenWidth)
yOffset = (ScreenHeight - originalHeight * currentScale) / 2 - s.Content.Y;
else
yOffset = System.Math.Max(System.Math.Min((originalHeight - ScreenHeight) / 2, yOffset), -System.Math.Abs(originalHeight * currentScale - ScreenHeight - (originalHeight - ScreenHeight) / 2));
// bounce the image back to inside the bounds
s.Content.TranslateTo(xOffset, yOffset, 500, Easing.BounceOut);
}
}
OnSizeAllocated (most of this you probably dont need, but some you do. consider ScreenWidth, ScreenHeight, yOffset, xOffset, currentScale)
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height); //must be called
if (width != -1 && (ScreenWidth != width || ScreenHeight != height))
{
ResetLayout(width, height);
originalWidth = initialLoad ?
ImageWidth >= 960 ?
App.ScreenWidth > 320
? 768
: 320
: ImageWidth / 3
: imageContainer.Content.Width / imageContainer.Content.Scale;
var normalizedHeight = ImageWidth >= 960 ?
App.ScreenWidth > 320 ? ImageHeight / (ImageWidth / 768)
: ImageHeight / (ImageWidth / 320)
: ImageHeight / 3;
originalHeight = initialLoad ?
normalizedHeight : (imageContainer.Content.Height / imageContainer.Content.Scale);
ScreenWidth = width;
ScreenHeight = height;
xOffset = imageContainer.TranslationX;
yOffset = imageContainer.TranslationY;
currentScale = imageContainer.Scale;
if (initialLoad)
initialLoad = false;
}
}
Layout (XAML in C#)
ImageMain = new Image
{
HorizontalOptions = LayoutOptions.CenterAndExpand,
VerticalOptions = LayoutOptions.CenterAndExpand,
Aspect = Aspect.AspectFill,
Source = ImageMainSource
};
imageContainer = new ContentView
{
Content = ImageMain,
BackgroundColor = Xamarin.Forms.Color.Black,
WidthRequest = App.ScreenWidth - 250
};
var panGesture = new PanGestureRecognizer();
panGesture.PanUpdated += OnPanUpdated;
imageContainer.GestureRecognizers.Add(panGesture);
var pinchGesture = new PinchGestureRecognizer();
pinchGesture.PinchUpdated += OnPinchUpdated;
imageContainer.GestureRecognizers.Add(pinchGesture);
double smallImageHeight = ImageHeight / (ImageWidth / 320);
absoluteLayout = new AbsoluteLayout
{
HeightRequest = App.ScreenHeight,
BackgroundColor = Xamarin.Forms.Color.Black,
};
AbsoluteLayout.SetLayoutFlags(imageContainer, AbsoluteLayoutFlags.All);
AbsoluteLayout.SetLayoutBounds(imageContainer, new Rectangle(0f, 0f, AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
absoluteLayout.Children.Add(imageContainer, new Rectangle(0, 0, 1, 1), AbsoluteLayoutFlags.All);
Content = absoluteLayout;
I've been working on a Image viewer with pan&zoom...
I reached another variation.
I'll share with you.
First, we need a Pan/Zoom class controller:
using System;
using Xamarin.Forms;
namespace Project.Util
{
public class PanZoom
{
bool pitching = false;
bool panning = false;
bool collectFirst = false;
double xOffset = 0;
double yOffset = 0;
//scale processing...
double scaleMin;
double scaleMax;
double scale;
double _xScaleOrigin;
double _yScaleOrigin;
double panTotalX;
double panTotalY;
ContentPage contentPage;
View Content;
public void Setup(ContentPage cp, View content)
{
contentPage = cp;
Content = content;
PinchGestureRecognizer pinchGesture = new PinchGestureRecognizer();
pinchGesture.PinchUpdated += PinchUpdated;
contentPage.Content.GestureRecognizers.Add(pinchGesture);
var panGesture = new PanGestureRecognizer();
panGesture.PanUpdated += OnPanUpdated;
contentPage.Content.GestureRecognizers.Add(panGesture);
contentPage.SizeChanged += (sender, e) => { layoutElements(); };
}
public void layoutElements()
{
if (contentPage.Width <= 0 || contentPage.Height <= 0 || Content.WidthRequest <= 0 || Content.HeightRequest <= 0)
return;
xOffset = 0;
yOffset = 0;
double pageW = contentPage.Width;
double pageH = contentPage.Height;
double w_s = pageW / Content.WidthRequest;
double h_s = pageH / Content.HeightRequest;
if (w_s < h_s)
scaleMin = w_s;
else
scaleMin = h_s;
scaleMax = scaleMin * 3.0;
scale = scaleMin;
double w = Content.WidthRequest * scale;
double h = Content.HeightRequest * scale;
double x = pageW / 2.0 - w / 2.0 + xOffset;
double y = pageH / 2.0 - h / 2.0 + yOffset;
AbsoluteLayout.SetLayoutBounds(Content, new Rectangle(x, y, w, h));
}
void fixPosition(
ref double x, ref double y, ref double w, ref double h,
bool setoffset
)
{
double pageW = contentPage.Width;
double pageH = contentPage.Height;
if (w <= pageW)
{
double new_x = pageW / 2.0 - w / 2.0;
if (setoffset)
xOffset = new_x - (pageW / 2.0 - w / 2.0);
x = new_x;
} else
{
if (x > 0)
{
double new_x = 0;
if (setoffset)
xOffset = new_x - (pageW / 2.0 - w / 2.0);
x = new_x;
}
if (x < (pageW - w))
{
double new_x = (pageW - w);
if (setoffset)
xOffset = new_x - (pageW / 2.0 - w / 2.0);
x = new_x;
}
}
if (h <= pageH)
{
double new_y = pageH / 2.0 - h / 2.0;
if (setoffset)
yOffset = new_y - (pageH / 2.0 - h / 2.0);
y = new_y;
}
else
{
if (y > 0)
{
double new_y = 0;
if (setoffset)
yOffset = new_y - (pageH / 2.0 - h / 2.0);
y = new_y;
}
if (y < (pageH - h))
{
double new_y = (pageH - h);
if (setoffset)
yOffset = new_y - (pageH / 2.0 - h / 2.0);
y = new_y;
}
}
}
private void PinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
{
if (sender != contentPage.Content)
return;
switch (e.Status)
{
case GestureStatus.Started:
{
pitching = true;
collectFirst = true;
double pageW = contentPage.Width;
double pageH = contentPage.Height;
_xScaleOrigin = e.ScaleOrigin.X * pageW;
_yScaleOrigin = e.ScaleOrigin.Y * pageH;
}
break;
case GestureStatus.Running:
if (pitching)
{
double targetScale = scale * e.Scale;
targetScale = Math.Min(Math.Max(scaleMin, targetScale), scaleMax);
double scaleDelta = targetScale / scale;
double pageW = contentPage.Width;
double pageH = contentPage.Height;
double w_old = Content.WidthRequest * scale;
double h_old = Content.HeightRequest * scale;
double x_old = pageW / 2.0 - w_old / 2.0 + xOffset;
double y_old = pageH / 2.0 - h_old / 2.0 + yOffset;
scale = targetScale;
//new w and h
double w = Content.WidthRequest * scale;
double h = Content.HeightRequest * scale;
//transform x old and y old
// to get new scaled position over a pivot
double _x = (x_old - _xScaleOrigin) * scaleDelta + _xScaleOrigin;
double _y = (y_old - _yScaleOrigin) * scaleDelta + _yScaleOrigin;
//fix offset to be equal to _x and _y
double x = pageW / 2.0 - w / 2.0 + xOffset;
double y = pageH / 2.0 - h / 2.0 + yOffset;
xOffset += _x - x;
yOffset += _y - y;
x = pageW / 2.0 - w / 2.0 + xOffset;
y = pageH / 2.0 - h / 2.0 + yOffset;
fixPosition(ref x, ref y, ref w, ref h, true);
AbsoluteLayout.SetLayoutBounds(Content, new Rectangle(x, y, w, h));
}
break;
case GestureStatus.Completed:
pitching = false;
break;
}
}
public void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
if (sender != contentPage.Content)
return;
switch (e.StatusType)
{
case GestureStatus.Started:
{
panning = true;
panTotalX = e.TotalX;
panTotalY = e.TotalY;
collectFirst = true;
}
break;
case GestureStatus.Running:
if (panning)
{
if (collectFirst)
{
collectFirst = false;
panTotalX = e.TotalX;
panTotalY = e.TotalY;
}
double pageW = contentPage.Width;
double pageH = contentPage.Height;
double deltaX = e.TotalX - panTotalX;
double deltaY = e.TotalY - panTotalY;
panTotalX = e.TotalX;
panTotalY = e.TotalY;
xOffset += deltaX;
yOffset += deltaY;
double w = Content.WidthRequest * scale;
double h = Content.HeightRequest * scale;
double x = pageW / 2.0 - w / 2.0 + xOffset;
double y = pageH / 2.0 - h / 2.0 + yOffset;
fixPosition(ref x, ref y, ref w, ref h, true);
AbsoluteLayout.SetLayoutBounds(Content, new Rectangle(x, y, w, h));
}
break;
case GestureStatus.Completed:
panning = false;
break;
}
}
}
}
In the content page:
using System;
using FFImageLoading.Forms;
using Xamarin.Forms;
using Project.Util;
namespace Project.ContentPages
{
public class ContentPage_ImageViewer : ContentPage
{
AbsoluteLayout al = null;
CachedImage image = null;
PanZoom panZoom;
public ContentPage_ImageViewer(string imageURL)
{
MasterDetailPage mdp = Application.Current.MainPage as MasterDetailPage;
mdp.IsGestureEnabled = false;
NavigationPage.SetHasBackButton(this, true);
Title = "";
image = new CachedImage()
{
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand,
Aspect = Aspect.Fill,
LoadingPlaceholder = "placeholder_320x322.png",
ErrorPlaceholder = "placeholder_320x322.png",
Source = imageURL,
RetryCount = 3,
DownsampleToViewSize = false,
IsVisible = false,
FadeAnimationEnabled = false
};
image.Success += delegate (object sender, CachedImageEvents.SuccessEventArgs e)
{
Device.BeginInvokeOnMainThread(() =>
{
image.WidthRequest = e.ImageInformation.OriginalWidth;
image.HeightRequest = e.ImageInformation.OriginalHeight;
image.IsVisible = true;
for(int i = al.Children.Count-1; i >= 0; i--)
{
if (al.Children[i] is ActivityIndicator)
al.Children.RemoveAt(i);
}
panZoom.layoutElements();
});
};
ActivityIndicator ai = new ActivityIndicator()
{
IsRunning = true,
Scale = (Device.RuntimePlatform == Device.Android) ? 0.25 : 1.0,
VerticalOptions = LayoutOptions.Fill,
HorizontalOptions = LayoutOptions.Fill,
Color = Color.White
};
Content = (al = new AbsoluteLayout()
{
VerticalOptions = LayoutOptions.Fill,
HorizontalOptions = LayoutOptions.Fill,
BackgroundColor = Color.Black,
Children =
{
image,
ai
}
});
AbsoluteLayout.SetLayoutFlags(image, AbsoluteLayoutFlags.None);
AbsoluteLayout.SetLayoutBounds(ai, new Rectangle(0, 0, 1, 1));
AbsoluteLayout.SetLayoutFlags(ai, AbsoluteLayoutFlags.All);
panZoom = new PanZoom();
panZoom.Setup(this, image);
}
}
}
For me it worked like below, just did some changes in the code given in the question,
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;
width = Content.Width * currentScale;
height = Content.Height * currentScale;
}
if (e.Status == GestureStatus.Completed)
{
// Store the translation delta's of the wrapped user interface element.
xOffset = Content.TranslationX;
yOffset = Content.TranslationY;
x = Content.TranslationX;
y = Content.TranslationY;
}
}
Pan Code
void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
if (!width.Equals(Content.Width) && !height.Equals(Content.Height))
{
switch (e.StatusType)
{
case GestureStatus.Started:
startX = Content.TranslationX;
startY = Content.TranslationY;
break;
case GestureStatus.Running:
if (!width.Equals(0))
{
Content.TranslationX = Math.Max(Math.Min(0, x + e.TotalX), -Math.Abs(Content.Width - width));// App.ScreenWidth));
}
if (!height.Equals(0))
{
Content.TranslationY = Math.Max(Math.Min(0, y + e.TotalY), -Math.Abs(Content.Height - height)); //App.ScreenHeight));
}
break;
case GestureStatus.Completed:
// Store the translation applied during the pan
x = Content.TranslationX;
y = Content.TranslationY;
xOffset = Content.TranslationX;
yOffset = Content.TranslationY;
break;
}
}
}

Screen positions of GUI elements in Unity3D

I am using the standard GUI of Unity3D.
How can I get the screen position of a GUI element?
Basically you can't. Using better words, as you already noticed GUI.Button simply returns a bool that indicated if the button has been pressed.
Since you are actually re-creating the button every frame (your GUI.Button code is inside a callback such as Update,FixedUpdate, OnGUI,...), and when you are calling GUI.Button you are passing it the Rect bounds by your self, there isn't actually any need of querying any object to retrieve the actual coordinates. Simply store them somewhere.
Rect buttonBounds = new Rect (50,60,100,20);
bool buttonPressed = GUI.Button (buttonBounds, "get postion");
if (buttonPressed)
{
//you know the bounds, because buttonBounds button has been pressed
}
Try this:
var positionGui : Vector2;
positionGui = Vector2 (guiElement.transform.position.x * Screen.width, guiElement.transform.position.y * Screen.height);
you can do something like this
public static Rect screenRect
(float tx,
float ty,
float tw,
float th)
{
float x1 = tx * Screen.width;
float y1 = ty * Screen.height;
float sw = tw * Screen.width;
float sh = th * Screen.height;
return new Rect(x1,y1,sw,sh);
}
public void OnGUI()
{
if (GUI.Button(screenRect(0.4f, 0.6f, 0.2f, 0.1f), "TRY AGAIN"))
{
Application.LoadLevel(0);
}
print ("button position + framesize"+screenRect(0.4f, 0.6f, 0.2f, 0.1f));
}

flickering twin images in memory game

I have a problem of flickering twin images in https://github.com/dzenanr/educ_memory_game.
In the Board class of view/board.dart, the following code in the _imageBox method creates and loads images:
var imagePath = 'images/${cell.image}';
ImageElement image = new Element.tag('img');
image.src = imagePath;
image.onLoad.listen((event) {
context.drawImageToRect(image, new Rect(x, y, boxSize, boxSize));
});
A click on a board cell shows an image for a second. When the same 2 images are discovered, those twin images stay displayed , but they flicker every second (a timer refresh interval).
In order to solve the flickering problem, I create those images in a constructor of the Board class:
for (var cell in memory.cells) {
ImageElement image = new Element.tag('img');
image.src = 'images/${cell.image}';
imageMap[cell.image] = image;
}
Then, I get an image from the map. However neither of the following two works:
ImageElement image = imageMap[cell.image];
image.onLoad.listen((event) {
context.drawImageToRect(image, new Rect(x, y, boxSize, boxSize));
});
or
ImageElement image = imageMap[cell.image];
context.drawImageToRect(image, new Rect(x, y, boxSize, boxSize));
Changing the src attribute of the image imply a network access and is not good, you have to used the cached images.
To display the image correctly, just change the _imageBox function a little bit.
void _imageBox(Cell cell) {
var x = cell.column * boxSize;
var y = cell.row * boxSize;
context.beginPath();
// Moved at the beginning otherwise it is drawn above the image.
context.rect(x, y, boxSize, boxSize);
context.fill();
context.stroke();
context.closePath();
if (cell.hidden ) {
context.fillStyle = HIDDEN_CELL_COLOR_CODE;
var centerX = cell.column * boxSize + boxSize / 2;
var centerY = cell.row * boxSize + boxSize / 2;
var radius = 4;
context.arc(centerX, centerY, radius, 0, 2 * PI, false);
} else {
ImageElement image = imageMap[cell.image]; // if decomment, comment the above 3 lines
context.drawImageToRect(image, new Rect(x, y, boxSize, boxSize));
}
}

Resources