主要原因可能是每个人似乎都复制并使用此代码(来自 dev.xamarin 站点)及其非常复杂且非常不必要的坐标计算:-)。没有必要,因为我们可以简单地要求视图为我们做繁重的工作,使用
AnchorX
和 AnchorY
属性,这正是这个目的。
我们可以通过双击操作来放大并恢复到原始比例。请注意,由于 Xamarin 无法为其
Tap
事件提供坐标值(实际上是一个 非常 不明智的决定),所以我们现在只能从中心进行缩放:
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);
}
}
捏合处理程序同样简单,根本不需要计算任何平移。我们所要做的就是将锚点设置为捏合起点,框架将完成其余的工作,缩放将围绕该点进行。请注意,我们甚至在这里还有一个额外的功能,即变焦范围两端过冲时的弹性反弹。
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;
}
}
还有平移处理程序,甚至更简单。开始时,我们从锚点计算起点,在平移过程中,我们不断更改锚点。该锚点相对于视图区域,我们可以轻松地将其限制在 0 和 1 之间,这会在极端情况下停止平移,而根本不需要任何平移计算。
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;
}
}
使用的常量和变量就是这些:
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;
采用完全不同的方法来处理这个问题。对于任何遇到问题的人来说,这都是 100% 有效的。
OnPan 已更新
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;
}
}
捏合更新
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);
}
}
OnSizeAlulated(其中大部分您可能不需要,但有些您需要。考虑
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;
}
}
布局(C# 中的 XAML)
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;
我一直在开发具有平移和缩放功能的图像查看器...
我达到了另一种变化。
我来分享给你。
首先,我们需要一个平移/缩放类控制器:
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;
}
}
}
}
在内容页面:
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);
}
}
}
我有正在生产中运行的工作代码。您在平移函数中使用的 x 和 y 变量与捏合函数中的 xOffset 和 yOffset 相同。因此,删除 x 和 y 变量,只使用 xOffset 和 yOffset 代替它们。
希望有帮助。
对我来说,它的工作原理如下,只是对问题中给出的代码做了一些更改,
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;
}
}
泛码
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;
}
}
}