I have a frame in absolutelayout as shown below. I would like that user to be able drag and relocate this frame on the screen. I tried to implement a pan gesture but unfortunately it doesnt work as expected. Can somebody show me the correct way? Is it possible without using any 3rd party library?
<AbsoluteLayout>
<Frame x:Name="frm" Padding="1" AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds="0.5,0,0.3,0.3" IsVisible="{Binding IsSmallTimerVisible}" >
<Frame.GestureRecognizers>
<PanGestureRecognizer PanUpdated="OnPanUpdated" />
</Frame.GestureRecognizers>
<shared:_customControl/>
</Frame>
Grid VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds="0,0,1,1">
<Grid.RowDefinitions>
<RowDefinition Height="0.1*" />
<RowDefinition Height="0.3*" />
<RowDefinition Height="2.5*" />
</Grid.RowDefinitions>
And in the code behind
double x, y;
private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
switch (e.StatusType)
{
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;
AbsoluteLayout.SetLayoutBounds(frm, new Rectangle(x, y, .3, .3));
break;
}
}
}
Seem like what you need is a Touch Tracking Effect
This example is very similar with your requirement.
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
BoxView boxView = sender as BoxView;
switch (args.Type)
{
case TouchActionType.Pressed:
// Don't allow a second touch on an already touched BoxView
if (!dragDictionary.ContainsKey(boxView))
{
dragDictionary.Add(boxView, new DragInfo(args.Id, args.Location));
// Set Capture property to true
TouchEffect touchEffect = (TouchEffect)boxView.Effects.FirstOrDefault(e => e is TouchEffect);
touchEffect.Capture = true;
}
break;
case TouchActionType.Moved:
if (dragDictionary.ContainsKey(boxView) && dragDictionary[boxView].Id == args.Id)
{
Rectangle rect = AbsoluteLayout.GetLayoutBounds(boxView);
Point initialLocation = dragDictionary[boxView].PressPoint;
rect.X += args.Location.X - initialLocation.X;
rect.Y += args.Location.Y - initialLocation.Y;
AbsoluteLayout.SetLayoutBounds(boxView, rect);
}
break;
case TouchActionType.Released:
if (dragDictionary.ContainsKey(boxView) && dragDictionary[boxView].Id == args.Id)
{
dragDictionary.Remove(boxView);
}
break;
}
}
The Pressed logic sets the Capture property of the TouchEffect object
to true. This has the effect of delivering all subsequent events for
that finger to the same event handler.
The Moved logic moves the BoxView by altering the LayoutBounds
attached property. The Location property of the event arguments is
always relative to the BoxView being dragged, and if the BoxView is
being dragged at a constant rate, the Location properties of the
consecutive events will be approximately the same. For example, if a
finger presses the BoxView in its center, the Pressed action stores a
PressPoint property of (50, 50), which remains the same for subsequent
events. If the BoxView is dragged diagonally at a constant rate, the
subsequent Location properties during the Moved action might be values
of (55, 55), in which case the Moved logic adds 5 to the horizontal
and vertical position of the BoxView. This moves the BoxView so that
its center is again directly under the finger.
You can move multiple BoxView elements simultaneously using different
fingers.
Related
I have created a button and added pressed event on it. The button height and width are 300x300 and the corner radius is 150. So it looks like circle.
<Button
WidthRequest="300"
HeightRequest="300"
BackgroundColor="Red"
VerticalOptions="Center"
Margin="20,0,20,0"
HorizontalOptions="CenterAndExpand"
CornerRadius="150">
<Button.Behaviors>
<behaviors:EventToCommandBehavior EventName="Pressed"
Command="{Binding ButtonPressedCommand}" />
</Button.Behaviors>
</Button>
The issue is when I click blue area it cahtches pressed event. I want blue area to not catch the event. Now it detects the button as square, not as circle.
Broadly speaking, you shouldn't do that. The touch input is imprecise and unless your button is particularly huge those blue areas on your pic are within the bounds of the imprecise measurement and as such should be treated in that way.
If you really need this, you may try with MR.Gestures.AbsoluteLayout. Here is an example where it provides the exact point that the user has tapped: https://devtut.github.io/xamarin/gestures.html#place-a-pin-where-the-user-touched-the-screen-with-mr-gestures
For iOS, you can create a custom class which inherites UIButton and override the method PointInside.
class CustomiOSButton : UIButton
{
public override bool PointInside(CoreGraphics.CGPoint point, UIKit.UIEvent uievent)
{
bool flag = base.PointInside(point, uievent);
if (flag)
{
UIBezierPath path = UIBezierPath.FromOval(this.Bounds);
if (path.ContainsPoint(point))
{
return true;
}
}
return false;
}
}
Give a try to add a label and add a long text against the property FormattedText - and set the LineBreakMode="TailTruncation". You will find that it's not adding the expected ellipses. But if you use the "Text" property it works fine. I still need to stick to this FormattedText property as we are achieving some formatting through this. Is there any alternative way to address this linebreak issue with FormattedText -
Example: "I need to show for long sample text with ellipses like in this example..."
<Grid Padding=" 0,50,0,0" HorizontalOptions="StartAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height ="*"/>
</Grid.RowDefinitions>
<StackLayout>
<Label x:Name="label" FormattedText ="testing this big text as a sample which needs to get truncated at any cost" LineBreakMode="TailTruncation"/>
</StackLayout>
</Grid>
The ellipses will appear when the content of the Label beyond it's border . I set the UnderLine in FormattedText and it works fine in my project .
NSMutableAttributedString str = new NSMutableAttributedString("I need to show for long sample text with ellipses like in this example...");
UIStringAttributes attributes = new UIStringAttributes() { UnderlineStyle = NSUnderlineStyle.Double,UnderlineColor=UIColor.Red, };
str.AddAttributes(attributes, new NSRange(0, str.Length - 1));
label.AttributedText = str;
label.LineBreakMode = UILineBreakMode.TailTruncation;
Update
in Forms you could use Custom Renderer .
Add the following class in your iOS project
using UIKit;
using Xamarin.Forms.Platform.iOS;
using Xamarin.Forms;
using xxx.iOS;
[assembly:ExportRenderer(typeof(Label),typeof(MyLabelRenderer))]
namespace xxx.iOS
{
public class MyLabelRenderer: LabelRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if(Control!=null)
{
Control.LineBreakMode = UILineBreakMode.TailTruncation;
}
}
}
}
I have this Xamain.Forms app where I hide page title when the device orientation changes. It all works fine but on Android a white space is showing above the content when I switch to horizontal view. You can see how it looks in the pictures below.
enter image description here
enter image description here
This is my xaml code:
<?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="Kanal10Live.VideoTest">
<ContentPage.Content>
<StackLayout x:Name="VideoStack">
<WebView x:Name="Browser" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"></WebView>
</StackLayout>
</ContentPage.Content>
And this is code behind:
private double width = 0;
private double height = 0;
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
switch (Device.RuntimePlatform)
{
case Device.Android:
if (width != this.width || height != this.height)
{
this.width = width;
this.height = height;
if (width > height)
{
NavigationPage.SetHasNavigationBar(this, false);
VideoStack.Orientation = StackOrientation.Horizontal;
}
else
{
NavigationPage.SetHasNavigationBar(this, true);
VideoStack.Orientation = StackOrientation.Vertical;
}
}
break;
}
}
The funny thing is that if I use the below code when the page initializes all is fine.
If someone can help me I would be grateful!
Peter
Try:
<Grid>
<WebView x:Name="Browser" />
</Grid>
Grid by default fills the full page. If you don't set any horizontal or vertical options on the web view it should fill the whole grid.
I'm fairly new to this, so sorry if this is a dumb question. How do I get my Xamarin.Forms app to start below the status bar or the notch when applicable? I've tried using a NavigationPage, but then it started wear below the top of the screen. I've also seen a few other solutions, but I can't make it work.
Can anyone help?
Thanks!
use UseSafeArea
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
using Xamarin.Forms;
namespace iPhoneX
{
public partial class ItemsPage : ContentPage
{
public ItemsPage()
{
InitializeComponent();
On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUseSafeArea(true);
}
}
}
You'll need to consider the safe area but have the background colors expand to take the full screen. So you shouldn't use
On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUseSafeArea(true);
this will box your page with large empty spaces on the bottom and top edges.
Instead, you should measure the safe areas and apply it as padding to your root view.
[assembly: ResolutionGroupName("Enterprise")]
[assembly: ExportEffect(typeof(SafeAreaPaddingEffect), nameof(SafeAreaPaddingEffect))]
namespace Enterprise.iOS.Effects
{
class SafeAreaPaddingEffect : PlatformEffect
{
Thickness _padding;
protected override void OnAttached()
{
if (Element is Layout element)
{
if (UIDevice.CurrentDevice.CheckSystemVersion(11, 0))
{
_padding = element.Padding;
var insets = UIApplication.SharedApplication.Windows[0].SafeAreaInsets; // Can't use KeyWindow this early
if (insets.Top > 0) // We have a notch
{
element.Padding = new Thickness(_padding.Left + insets.Left, _padding.Top + insets.Top, _padding.Right + insets.Right, _padding.Bottom);
return;
}
}
// Uses a default Padding of 20. Could use an property to modify if you wanted.
element.Padding = new Thickness(_padding.Left, _padding.Top + 20, _padding.Right, _padding.Bottom);
}
}
protected override void OnDetached()
{
if (Element is Layout element)
{
element.Padding = _padding;
}
}
}
}
then in xaml:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Enterprise.View.Features.Authentication.LoginView"
xmlns:effect="clr-namespace:Enterprise.View.Effects">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="100"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentView BackgroundColor="Green">
<ContentView.Effects>
<effect:SafeAreaPaddingEffect />
</ContentView.Effects>
<Label Text="Hello, from XamarinHelp.com" />
</ContentView>
</Grid>
</ContentPage>
ref: https://xamarinhelp.com/safeareainsets-xamarin-forms-ios/ Thank Adam, not me!
is it possible to make one thing on the screen expand all the way to the edges?
(from this answer)
Stretching elements out of the bounds of the safe area is arguably a use case in the case you provided. The bar is a mere background element and not content, as the navigation bar is, which also stretches to fill the whole screen.
Having said that, you unfortunately don't get this for free, but have to implement this by yourself. Assume you have the following XAML
<ContentPage ...>
<StackLayout>
<ContentView BackgroundColor="LightSkyBlue" HorizontalOptions="Fill" x:Name="Header">
<!-- Header -->
</ContentView>
<ContentView x:Name="Content">
<!-- Content -->
</ContentView>
</StackLayout>
</ContentPage>
In your code-behind (I would not use it like this, but to make the point it suffices. For a real application I have written a utility class, which is attached to the view and manages the insets.) you can now check for the property SafeAreaInsets being changed
class SafeAreaPage : ContentPage
{
// elided constructor
protected override void OnPropertyChanged(string propertyName)
{
if(propertyName = "SafeAreaInsets")
{
var insets = On<Xamarin.Forms.PlatformConfiguration.iOS>.GetSafeAreaInsets();
var headerInsets = insets; // Thickness is a value type
headerInsets.Bottom = 0;
var contentInsets = insets;
contentInsets.Top = 0;
Header.Padding = headerInsets;
Content.Padding = contentInsets;
}
}
}
How you set the Paddings of your views depends on your layouts, but this way you have a bit more control on how the safe area insets are used, although it is a bit fiddly.
I have a Xamarin.Forms app where I need to drag irregularly shaped controls (TwinTechForms SvgImageView) around, like this one:
I want it to only respond to touches on the black area and not on transparent (checkered) areas
I tried using MR.Gestures package. Hooking up to the Panning event lets me drag the image but it also starts dragging when I touch the transparent parts of it.
My setup looks like this:
<mr:ContentView x:Name="mrContentView" Panning="PanningEventHandler" Panned="PannedEventHandler" Background="transparent">
<ttf:SvgImageView x:Name="svgView" Background="transparent" SvgPath=.../>
</mr:ContentView>
and code-behind
private void PanningEventHandler(object sender, PanningEventParameters arg){
svgView.TranslateX = arg.IsCancelled ? 0: arg.TotalDistance.X;
svgView.TranslateY = arg.IsCancelled ? 0: arg.TotalDistance.Y;
}
private void PannedEventHandler(object sender, PanningEventParameters arg){
if (!arg.IsCancelled){
mrContentView.TranslateX = svgView.TranslateX;
mrContentView.TranslateY = svgView.TranslateY;
}
svgView.TranslateX = 0;
svgView.TranslateY = 0;
}
In this code-behind how should I check if I'm hitting a transparent point on the target object and when that happens how do I cancel the gesture so that another view under this one may respond to it? In the right side image touching the red inside the green O's hole should start dragging the red O
Update: SOLVED
The accepted answer's suggestion worked but was not straightforward.
I had to fork and modify both NGraphics (github fork) and TwinTechsFormsLib (TTFL, github fork)
In the NGraphics fork I added an XDocument+filter ctor to SvgReader so the same XDocument can be passed into different SvgImageView instances with a different parse filter, effectively splitting up the original SVG into multiple SvgImageView objects that can be moved independently without too much of a memory hit. I had to fix some brush inheritance for my SVGs to show as expected.
The TTFL fork exposes the XDocument+filter ctor and adds platform-specific GetPixelColor to the renderers.
Then in my Xamarin.Forms page I can load the original SVG file into multiple SvgImageView instances:
List<SvgImageView> LoadSvgImages(string resourceName, int widthRequest = 500, int heightRequest = 500)
{
var svgImageViews = new List<SvgImageView>();
var assembly = this.GetType().GetTypeInfo().Assembly;
Stream stream = assembly.GetManifestResourceStream(resourceName);
XDocument xdoc = XDocument.Load(stream);
// only groups that don't have other groups
List<XElement> leafGroups = xdoc.Descendants()
.Where(x => x.Name.LocalName == "g" && x.HasElements && !x.Elements().Any(dx => dx.Name.LocalName == "g"))
.ToList();
leafGroups.Insert(0, new XElement("nonGroups")); // this one will
foreach (XElement leafGroup in leafGroups)
{
var svgImage = new SvgImageView
{
HeightRequest = widthRequest,
WidthRequest = heightRequest,
HorizontalOptions = LayoutOptions.Start,
VerticalOptions = LayoutOptions.End,
StyleId = leafGroup.Attribute("id")?.Value, // for debugging
};
// this loads the original SVG as if only there's only one leaf group
// and its parent groups (to preserve transformations, brushes, opacity etc)
svgImage.LoadSvgFromXDocument(xdoc, (xe) =>
{
bool doRender = xe == leafGroup ||
xe.Ancestors().Contains(leafGroup) ||
xe.Descendants().Contains(leafGroup);
return doRender;
});
svgImageViews.Add(svgImage);
}
return svgImageViews;
}
Then I add all of the svgImageViews to a MR.Gesture <mr:Grid x:Name="movableHost"> and hook up Panning and Panned events to it.
SvgImageView dragSvgView = null;
Point originalPosition = Point.Zero;
movableView.Panning += (sender, pcp) =>
{
// if we're not dragging anything - check the previously loaded SVG images
// if they have a non-transparent pixel at the touch point
if (dragSvgView==null){
dragSvgView = svgImages.FirstOrDefault(si => {
var c = si.GetPixelColor(pcp.Touches[0].X - si.TranslationX, pcp.Touches[0].Y - si.TranslationY);
return c.A > 0.0001;
});
if (dragSvgView != null)
{
// save the original position of this item so we can put it back in case dragging was canceled
originalPosition = new Point (dragSvgView.TranslationX, dragSvgView.TranslationY);
}
}
// if we're dragging something - move it along
if (dragSvgView != null)
{
dragSvgView.TranslationX += pcp.DeltaDistance.X;
dragSvgView.TranslationY += pcp.DeltaDistance.Y;
}
}
Neither MR.Gestures nor any underlying platform checks if a touched area within the view is transparent. The elements which listen to the touch gestures are always rectangular. So you have to do the hit testing yourself.
The PanningEventParameters contain a Point[] Touches with the coordinates of all touching fingers. With these coordinates you can check if they match any visible area in the SVG.
Hit-testing for the donut from your sample is easy, but testing for a general shape is not (and I think that's what you want). If you're lucky, then SvgImage already supports it. If not, then you may find the principles how this can be done in the SVG Rendering Engine, Point-In-Polygon Algorithm — Determining Whether A Point Is Inside A Complex Polygon or 2D collision detection.
Unfortunately overlapping elements are a bit of a problem. I tried to implement that with the Handled flag when I originally wrote MR.Gestures, but I couldn't get it to work on all platforms. As I thought it's more important to be consistent than to make it work on just one platform, I ignore Handled on all platforms and rather raise the events for all overlapping elements. (I should've removed the flag altogether)
In your specific case I'd propose you use a structure like this for multiple SVGs:
<mr:ContentView x:Name="mrContentView" Panning="PanningEventHandler" Panned="PannedEventHandler" Background="transparent">
<ttf:SvgImageView x:Name="svgView1" Background="transparent" SvgPath=.../>
<ttf:SvgImageView x:Name="svgView2" Background="transparent" SvgPath=.../>
<ttf:SvgImageView x:Name="svgView3" Background="transparent" SvgPath=.../>
</mr:ContentView>
In the PanningEventHandler you can check if the Touches are on any SVG and if yes, which one is on top.
If you'd do multiple ContentViews each with only one SVG in it, then the PanningEventHandler would be called for each overlapping rectangular element which is not what you want.