I'm looking to add a swipe function in my app that is pretty much identical to the unlock mechanic on (old?) iPhones (See pictures).
I am struggling with how this could be achieved on a crossplatform solution. My immediate thought would be to use a slider and a custom renderer but unsure how to create the function of snapping to start if the user lets go before finishing the slide. Would appreciate if anyone could either assist with that function or if they have a better suggestion on how achieve this.
Unless and until - you really need a particularly native look for every platform; you can pretty much write your own custom slider control using PanGestureRecognizer, and AbsoluteLayout (without any need for custom-renderers). For that snapping effect you can use Translation animation with Cubic easing effect.
For example, you can define a control as following; this sample control extends AbsoluteLayout while allowing you to define your own controls representing thumb and track-bar. It also creates an almost invisible top-most layer to act as pan-gesture listener. Once, gesture is completed, it checks to see if slide for complete (i.e entire width of track-bar) - and then raises SlideCompleted event.
public class SlideToActView : AbsoluteLayout
{
public static readonly BindableProperty ThumbProperty =
BindableProperty.Create(
"Thumb", typeof(View), typeof(SlideToActView),
defaultValue: default(View), propertyChanged: OnThumbChanged);
public View Thumb
{
get { return (View)GetValue(ThumbProperty); }
set { SetValue(ThumbProperty, value); }
}
private static void OnThumbChanged(BindableObject bindable, object oldValue, object newValue)
{
((SlideToActView)bindable).OnThumbChangedImpl((View)oldValue, (View)newValue);
}
protected virtual void OnThumbChangedImpl(View oldValue, View newValue)
{
OnSizeChanged(this, EventArgs.Empty);
}
public static readonly BindableProperty TrackBarProperty =
BindableProperty.Create(
"TrackBar", typeof(View), typeof(SlideToActView),
defaultValue: default(View), propertyChanged: OnTrackBarChanged);
public View TrackBar
{
get { return (View)GetValue(TrackBarProperty); }
set { SetValue(TrackBarProperty, value); }
}
private static void OnTrackBarChanged(BindableObject bindable, object oldValue, object newValue)
{
((SlideToActView)bindable).OnTrackBarChangedImpl((View)oldValue, (View)newValue);
}
protected virtual void OnTrackBarChangedImpl(View oldValue, View newValue)
{
OnSizeChanged(this, EventArgs.Empty);
}
private PanGestureRecognizer _panGesture = new PanGestureRecognizer();
private View _gestureListener;
public SlideToActView()
{
_panGesture.PanUpdated += OnPanGestureUpdated;
SizeChanged += OnSizeChanged;
_gestureListener = new ContentView { BackgroundColor = Color.White, Opacity = 0.05 };
_gestureListener.GestureRecognizers.Add(_panGesture);
}
public event EventHandler SlideCompleted;
private const double _fadeEffect = 0.5;
private const uint _animLength = 50;
async void OnPanGestureUpdated(object sender, PanUpdatedEventArgs e)
{
if (Thumb == null | TrackBar == null)
return;
switch (e.StatusType)
{
case GestureStatus.Started:
await TrackBar.FadeTo(_fadeEffect, _animLength);
break;
case GestureStatus.Running:
// Translate and ensure we don't pan beyond the wrapped user interface element bounds.
var x = Math.Max(0, e.TotalX);
if (x > (Width - Thumb.Width))
x = (Width - Thumb.Width);
if (e.TotalX < Thumb.TranslationX)
return;
Thumb.TranslationX = x;
break;
case GestureStatus.Completed:
var posX = Thumb.TranslationX;
// Reset translation applied during the pan (snap effect)
await TrackBar.FadeTo(1, _animLength);
await Thumb.TranslateTo(0, 0, _animLength * 2, Easing.CubicIn);
if (posX >= (Width - Thumb.Width - 10/* keep some margin for error*/))
SlideCompleted?.Invoke(this, EventArgs.Empty);
break;
}
}
void OnSizeChanged(object sender, EventArgs e)
{
if (Width == 0 || Height == 0)
return;
if (Thumb == null || TrackBar == null)
return;
Children.Clear();
SetLayoutFlags(TrackBar, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(TrackBar, new Rectangle(0, 0, 1, 1));
Children.Add(TrackBar);
SetLayoutFlags(Thumb, AbsoluteLayoutFlags.None);
SetLayoutBounds(Thumb, new Rectangle(0, 0, this.Width/5, this.Height));
Children.Add(Thumb);
SetLayoutFlags(_gestureListener, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(_gestureListener, new Rectangle(0, 0, 1, 1));
Children.Add(_gestureListener);
}
}
Sample usage:
<StackLayout Margin="40">
<local:SlideToActView HeightRequest="50" SlideCompleted="Handle_SlideCompleted">
<local:SlideToActView.Thumb>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Silver" Padding="0">
<Image Source="icon.png" HorizontalOptions="Center" VerticalOptions="Center" HeightRequest="40" WidthRequest="40" />
</Frame>
</local:SlideToActView.Thumb>
<local:SlideToActView.TrackBar>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Gray" Padding="0">
<Label Text="Slide 'x' to cancel" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" />
</Frame>
</local:SlideToActView.TrackBar>
</local:SlideToActView>
<Label x:Name="MessageLbl" FontAttributes="Bold" TextColor="Green" />
</StackLayout>
Code-Behind
void Handle_SlideCompleted(object sender, System.EventArgs e)
{
MessageLbl.Text = "Success!!";
}
Update : 08/30
As #morten-j-petersen wanted support for a fill-bar like implementation; added support for that.
Updated control code
public class SlideToActView : AbsoluteLayout
{
public static readonly BindableProperty ThumbProperty =
BindableProperty.Create(
"Thumb", typeof(View), typeof(SlideToActView),
defaultValue: default(View));
public View Thumb
{
get { return (View)GetValue(ThumbProperty); }
set { SetValue(ThumbProperty, value); }
}
public static readonly BindableProperty TrackBarProperty =
BindableProperty.Create(
"TrackBar", typeof(View), typeof(SlideToActView),
defaultValue: default(View));
public View TrackBar
{
get { return (View)GetValue(TrackBarProperty); }
set { SetValue(TrackBarProperty, value); }
}
public static readonly BindableProperty FillBarProperty =
BindableProperty.Create(
"FillBar", typeof(View), typeof(SlideToActView),
defaultValue: default(View));
public View FillBar
{
get { return (View)GetValue(FillBarProperty); }
set { SetValue(FillBarProperty, value); }
}
private PanGestureRecognizer _panGesture = new PanGestureRecognizer();
private View _gestureListener;
public SlideToActView()
{
_panGesture.PanUpdated += OnPanGestureUpdated;
SizeChanged += OnSizeChanged;
_gestureListener = new ContentView { BackgroundColor = Color.White, Opacity = 0.05 };
_gestureListener.GestureRecognizers.Add(_panGesture);
}
public event EventHandler SlideCompleted;
private const double _fadeEffect = 0.5;
private const uint _animLength = 50;
async void OnPanGestureUpdated(object sender, PanUpdatedEventArgs e)
{
if (Thumb == null || TrackBar == null || FillBar == null)
return;
switch (e.StatusType)
{
case GestureStatus.Started:
await TrackBar.FadeTo(_fadeEffect, _animLength);
break;
case GestureStatus.Running:
// Translate and ensure we don't pan beyond the wrapped user interface element bounds.
var x = Math.Max(0, e.TotalX);
if (x > (Width - Thumb.Width))
x = (Width - Thumb.Width);
//Uncomment this if you want only forward dragging.
//if (e.TotalX < Thumb.TranslationX)
// return;
Thumb.TranslationX = x;
SetLayoutBounds(FillBar, new Rectangle(0, 0, x + Thumb.Width / 2, this.Height));
break;
case GestureStatus.Completed:
var posX = Thumb.TranslationX;
SetLayoutBounds(FillBar, new Rectangle(0, 0, 0, this.Height));
// Reset translation applied during the pan
await Task.WhenAll(new Task[]{
TrackBar.FadeTo(1, _animLength),
Thumb.TranslateTo(0, 0, _animLength * 2, Easing.CubicIn),
});
if (posX >= (Width - Thumb.Width - 10/* keep some margin for error*/))
SlideCompleted?.Invoke(this, EventArgs.Empty);
break;
}
}
void OnSizeChanged(object sender, EventArgs e)
{
if (Width == 0 || Height == 0)
return;
if (Thumb == null || TrackBar == null || FillBar == null)
return;
Children.Clear();
SetLayoutFlags(TrackBar, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(TrackBar, new Rectangle(0, 0, 1, 1));
Children.Add(TrackBar);
SetLayoutFlags(FillBar, AbsoluteLayoutFlags.None);
SetLayoutBounds(FillBar, new Rectangle(0, 0, 0, this.Height));
Children.Add(FillBar);
SetLayoutFlags(Thumb, AbsoluteLayoutFlags.None);
SetLayoutBounds(Thumb, new Rectangle(0, 0, this.Width/5, this.Height));
Children.Add(Thumb);
SetLayoutFlags(_gestureListener, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(_gestureListener, new Rectangle(0, 0, 1, 1));
Children.Add(_gestureListener);
}
}
XAML Usage
<StackLayout Margin="40">
<local:SlideToActView HeightRequest="50" SlideCompleted="Handle_SlideCompleted">
<local:SlideToActView.Thumb>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Silver" Padding="0">
<Image Source="icon.png" HorizontalOptions="Center" VerticalOptions="Center" HeightRequest="40" WidthRequest="40" />
</Frame>
</local:SlideToActView.Thumb>
<local:SlideToActView.TrackBar>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Gray" Padding="0">
<Label Text="Slide 'x' to cancel" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" />
</Frame>
</local:SlideToActView.TrackBar>
<local:SlideToActView.FillBar>
<Frame CornerRadius="10" HasShadow="false" BackgroundColor="Red" Padding="0" />
</local:SlideToActView.FillBar>
</local:SlideToActView>
<Label x:Name="MessageLbl" FontAttributes="Bold" TextColor="Green" />
</StackLayout>
using custom renders for xamarin forms so that you could define how the slider should look in each platform, In android SeekBars are commonly used for sliders and in iOS UiSlider
https://blog.xamarin.com/customizing-xamarin-forms-controls-with-effects/
https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/custom-renderer/
also since if you have decided to use custom render, you can use your own slider derived from android seek bar with animations
http://www.viralandroid.com/2015/11/android-custom-seekbar-example.html
also a custom UIslider for iOS
you can hold up your generic methods in a portable class , as you have explained the behavior which only have two states this might also be achievable using a custom switch widget
There is a bug in Android in which the Gesture Recognizer does not trigger the Started or Completed event! Here the link: https://bugzilla.xamarin.com/show_bug.cgi?id=39768
So, I implemented this workaround which checks if the pan is stopped every two seconds and restarts the position. It only runs the timer in Android as in iOS runs ok. Here the code:
public class SlideToOpenView : AbsoluteLayout
{
public static readonly BindableProperty ThumbProperty =
BindableProperty.Create(
"Thumb", typeof(View), typeof(SlideToOpenView),
defaultValue: default(View));
public View Thumb
{
get { return (View)GetValue(ThumbProperty); }
set { SetValue(ThumbProperty, value); }
}
public static readonly BindableProperty TrackBarProperty =
BindableProperty.Create(
"TrackBar", typeof(View), typeof(SlideToOpenView),
defaultValue: default(View));
public View TrackBar
{
get { return (View)GetValue(TrackBarProperty); }
set { SetValue(TrackBarProperty, value); }
}
public static readonly BindableProperty FillBarProperty =
BindableProperty.Create(
"FillBar", typeof(View), typeof(SlideToOpenView),
defaultValue: default(View));
public View FillBar
{
get { return (View)GetValue(FillBarProperty); }
set { SetValue(FillBarProperty, value); }
}
private PanGestureRecognizer _panGesture = new PanGestureRecognizer();
private View _gestureListener;
private bool _android = false;
public SlideToOpenView()
{
_panGesture.PanUpdated += OnPanGestureUpdated;
SizeChanged += OnSizeChanged;
_gestureListener = new ContentView { BackgroundColor = Color.White, Opacity = 0.05 };
_gestureListener.GestureRecognizers.Add(_panGesture);
if (Device.RuntimePlatform == Device.Android) {
_android = true;
}
}
public event EventHandler SlideCompleted;
private const double _fadeEffect = 0.5;
private const uint _animLength = 50;
//Variable that stores the last state in axis X
private double _lastX = -1;
private bool _panRunning = false;
async void OnPanGestureUpdated(object sender, PanUpdatedEventArgs e)
{
if (Thumb == null || TrackBar == null || FillBar == null)
return;
switch (e.StatusType)
{
case GestureStatus.Started:
Debug.WriteLine("GestureStatus.Started");
await TrackBar.FadeTo(_fadeEffect, _animLength);
break;
case GestureStatus.Running:
// Translate and ensure we don't pan beyond the wrapped user interface element bounds.
var x = Math.Max(0, e.TotalX);
if (x > (Width - Thumb.Width))
x = (Width - Thumb.Width);
//Uncomment this if you want only forward dragging.
//if (e.TotalX < Thumb.TranslationX)
// return;
Thumb.TranslationX = x;
SetLayoutBounds(FillBar, new Rectangle(0, 0, x + Thumb.Width / 2, this.Height));
if (_panRunning == false && _android == true)
{
Device.StartTimer(TimeSpan.FromMilliseconds(2000), TimerHandle);
_panRunning = true;
}
break;
case GestureStatus.Completed:
_panRunning = false;
var posX = Thumb.TranslationX;
SetLayoutBounds(FillBar, new Rectangle(0, 0, 0, this.Height));
// Reset translation applied during the pan
await Task.WhenAll(new Task[]{
TrackBar.FadeTo(1, _animLength),
Thumb.TranslateTo(0, 0, _animLength * 2, Easing.CubicIn),
});
//await TrackBar.FadeTo(1, _animLength);
//await Thumb.TranslateTo(0, 0, _animLength * 2, Easing.CubicIn);
if (posX >= (Width - Thumb.Width - 10/* keep some margin for error*/))
SlideCompleted?.Invoke(this, EventArgs.Empty);
break;
}
}
//Timer handle for Android Xamarin.Forms Gesture Bug
bool TimerHandle()
{
if (_lastX == 0) {
_lastX = -1;
return false;
}
if (Thumb.TranslationX == _lastX && _lastX != -1) {
_panRunning = false;
var posX = Thumb.TranslationX;
SetLayoutBounds(FillBar, new Rectangle(0, 0, 0, this.Height));
// Reset translation applied during the pan
TrackBar.FadeTo(1, _animLength);
Thumb.TranslateTo(0, 0, _animLength * 2, Easing.CubicIn);
if (posX >= (Width - Thumb.Width - 10/* keep some margin for error*/))
SlideCompleted?.Invoke(this, EventArgs.Empty);
_lastX = -1;
return false;
}
_lastX = Thumb.TranslationX;
return true;
}
void OnSizeChanged(object sender, EventArgs e)
{
Debug.WriteLine("OnSizeChanged");
if (Width == 0 || Height == 0)
return;
if (Thumb == null || TrackBar == null || FillBar == null)
return;
Children.Clear();
SetLayoutFlags(TrackBar, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(TrackBar, new Rectangle(0, 0, 1, 1));
Children.Add(TrackBar);
SetLayoutFlags(FillBar, AbsoluteLayoutFlags.None);
SetLayoutBounds(FillBar, new Rectangle(0, 0, 0, this.Height));
Children.Add(FillBar);
SetLayoutFlags(Thumb, AbsoluteLayoutFlags.None);
SetLayoutBounds(Thumb, new Rectangle(0, 0, this.Width / 5, this.Height));
Children.Add(Thumb);
SetLayoutFlags(_gestureListener, AbsoluteLayoutFlags.SizeProportional);
SetLayoutBounds(_gestureListener, new Rectangle(0, 0, 1, 1));
Children.Add(_gestureListener);
}
}
Related
I followed this tutorial to create an Image cropping page https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/bitmaps/cropping. However i now bound two views of the PhotoCropperCanvasView to an carousel view an wanted to Bind the CroppedBitMap so that i can access this property directl in the viewmodel. I just cannot figure ouit how i would achieve that. I f i just make this a Bindable Property the property does not change when i make a new rectangle. So i think i kind of have to exclude the Code of the property but i am very confused.
The whole ocde:
public class PhotoCropperCanvasView : SKCanvasView, INotifyPropertyChanged
{
const int CORNER = 50; // pixel length of cropper corner
const int RADIUS = 100; // pixel radius of touch hit-test
//SKBitmap bitmap;
//CroppingRectangle croppingRect;
SKMatrix inverseBitmapMatrix;
public SKBitmap testmap;
//public SKBitmap bitmap { get; set; }
public Image testImage { get; set; }
public static readonly BindableProperty mapProperty =
BindableProperty.Create(nameof(map), typeof(SKBitmap), typeof(PhotoCropperCanvasView), null);
public SKBitmap map
{
get
{
return (SKBitmap)GetValue(mapProperty);
}
set
{
SetValue(mapProperty, value);
}
}
public CroppingRectangle croppingRect { get; set; }
//public SKMatrix inverseBitmapMatrix { get; set; }
public static readonly BindableProperty bitmapProperty =
BindableProperty.Create(nameof(bitmap), typeof(SKBitmap), typeof(Image),null,propertyChanged: OnbitmapChanged);
static void OnbitmapChanged(BindableObject bindable, object oldValue, object newValue)
{
Console.WriteLine("test");
}
public SKBitmap bitmap
{
get
{
return (SKBitmap)GetValue(bitmapProperty);
}
set
{
SetValue(bitmapProperty, value);
}
}
public SKBitmap CroppedBitmap
{
get
{
SKRect cropRect = new SKRect(croppingRect.Rect.Left,croppingRect.Rect.Top,croppingRect.Rect.Right,croppingRect.Rect.Bottom);
SKBitmap croppedBitmap = new SKBitmap((int)cropRect.Width,
(int)cropRect.Height);
SKRect dest = new SKRect(0, 0, cropRect.Width, cropRect.Height);
SKRect source = new SKRect(cropRect.Left, cropRect.Top,
cropRect.Right, cropRect.Bottom);
using (SKCanvas canvas = new SKCanvas(croppedBitmap))
{
canvas.DrawBitmap(bitmap, source, dest);
}
return croppedBitmap;
}
}
// Touch tracking
TouchEffect touchEffect = new TouchEffect();
struct TouchPoint
{
public int CornerIndex { set; get; }
public SKPoint Offset { set; get; }
}
Dictionary<long, TouchPoint> touchPoints = new Dictionary<long, TouchPoint>();
// Drawing objects
SKPaint cornerStroke = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.White,
StrokeWidth = 10
};
SKPaint edgeStroke = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.White,
StrokeWidth = 2
};
// this constructor for profile image
public PhotoCropperCanvasView(SKBitmap bitmap, float? aspectRatio = null)
{
this.bitmap = bitmap;
SKRect bitmapRect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
croppingRect = new CroppingRectangle(bitmapRect, aspectRatio);
touchEffect.TouchAction += OnTouchEffectTouchAction;
}
// this constructor for post images
public PhotoCropperCanvasView()
{
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = nameof(bitmap))
{
base.OnPropertyChanged(propertyName);
if (bitmap != null)
{
SKRect bitmapRect = new SKRect(0, 0, bitmap.Width, bitmap.Width);
croppingRect = new CroppingRectangle(bitmapRect, 1);
touchEffect.TouchAction += OnTouchEffectTouchAction;
}
}
protected override void OnParentSet()
{
base.OnParentSet();
// Attach TouchEffect to parent view
Parent.Effects.Add(touchEffect);
}
protected override void OnPaintSurface(SKPaintSurfaceEventArgs args)
{
base.OnPaintSurface(args);
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.Gray);
// Calculate rectangle for displaying bitmap
float scale = Math.Min((float)info.Width / bitmap.Width, (float)info.Height / bitmap.Height);
float x = (info.Width - scale * bitmap.Width) / 2;
float y = (info.Height - scale * bitmap.Height) / 2;
SKRect bitmapRect = new SKRect(x, y, x + scale * bitmap.Width, y + scale * bitmap.Height);
canvas.DrawBitmap(bitmap, bitmapRect);
// Calculate a matrix transform for displaying the cropping rectangle
SKMatrix bitmapScaleMatrix = SKMatrix.MakeIdentity();
bitmapScaleMatrix.SetScaleTranslate(scale, scale, x, y);
// Display rectangle
SKRect scaledCropRect = bitmapScaleMatrix.MapRect(croppingRect.Rect);
canvas.DrawRect(scaledCropRect, edgeStroke);
// Display heavier corners
using (SKPath path = new SKPath())
{
path.MoveTo(scaledCropRect.Left, scaledCropRect.Top + CORNER);
path.LineTo(scaledCropRect.Left, scaledCropRect.Top);
path.LineTo(scaledCropRect.Left + CORNER, scaledCropRect.Top);
path.MoveTo(scaledCropRect.Right - CORNER, scaledCropRect.Top);
path.LineTo(scaledCropRect.Right, scaledCropRect.Top);
path.LineTo(scaledCropRect.Right, scaledCropRect.Top + CORNER);
path.MoveTo(scaledCropRect.Right, scaledCropRect.Bottom - CORNER);
path.LineTo(scaledCropRect.Right, scaledCropRect.Bottom);
path.LineTo(scaledCropRect.Right - CORNER, scaledCropRect.Bottom);
path.MoveTo(scaledCropRect.Left + CORNER, scaledCropRect.Bottom);
path.LineTo(scaledCropRect.Left, scaledCropRect.Bottom);
path.LineTo(scaledCropRect.Left, scaledCropRect.Bottom - CORNER);
canvas.DrawPath(path, cornerStroke);
}
// Invert the transform for touch tracking
bitmapScaleMatrix.TryInvert(out inverseBitmapMatrix);
}
void OnTouchEffectTouchAction(object sender, TouchActionEventArgs args)
{
int i = 0;
SKPoint pixelLocation = ConvertToPixel(args.Location);
SKPoint bitmapLocation = inverseBitmapMatrix.MapPoint(pixelLocation);
switch (args.Type)
{
case TouchActionType.Pressed:
// Convert radius to bitmap/cropping scale
float radius = inverseBitmapMatrix.ScaleX * RADIUS;
// Find corner that the finger is touching
int cornerIndex = croppingRect.HitTest(bitmapLocation, radius);
if (cornerIndex != -1 && !touchPoints.ContainsKey(args.Id))
{
TouchPoint touchPoint = new TouchPoint
{
CornerIndex = cornerIndex,
Offset = bitmapLocation - croppingRect.Corners[cornerIndex]
};
touchPoints.Add(args.Id, touchPoint);
}
break;
case TouchActionType.Moved:
if (touchPoints.ContainsKey(args.Id))
{
TouchPoint touchPoint = touchPoints[args.Id];
croppingRect.MoveCorner(touchPoint.CornerIndex,
bitmapLocation - touchPoint.Offset);
InvalidateSurface();
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (touchPoints.ContainsKey(args.Id))
{
touchPoints.Remove(args.Id);
//map = CroppedBitmap;
}
break;
}
}
SKPoint ConvertToPixel(Xamarin.Forms.Point pt)
{
return new SKPoint((float)(CanvasSize.Width * pt.X / Width),
(float)(CanvasSize.Height * pt.Y / Height));
}
}
}
Since the case TouchActionType.Cancelled gets only triggerd once everytime the rectangel was moved, i thought i would set thew bindable Proeprty map to the Cropped bitmap property so that i can get the Cropped Image from the view obver a Binding to the viewmodel. This part works, however, when i activate the line map = CroppedBitmap the cropping rectangle can only be moved by opposite corners. So if i start moving it with the bottom right corner i con only use the top left or bottom right. If i leave the line map = CroppedBitman(249) deactivated i can move the rectangle on all corners at every times. I do not understand this behaviour.
the view:
<CarouselView Grid.Row="0"
IsSwipeEnabled="False"
x:Name="carousel"
Margin="0,-40,0,0"
CurrentItem="{Binding CurrentCutImage, Mode=TwoWay}"
CurrentItemChanged="CarouselView_CurrentItemChanged"
HorizontalScrollBarVisibility="Always"
IsScrollAnimated="True"
ItemsSource="{Binding ImageObjects}"
VerticalScrollBarVisibility="Always"
>
<CarouselView.ItemTemplate>
<DataTemplate x:DataType="viewmodel:CutImages">
<Grid>
<bitmaps:PhotoCropperCanvasView bitmap="{Binding ImageSource }" map="{Binding MapSource, Mode=TwoWay}" >
</bitmaps:PhotoCropperCanvasView>
</Grid>
</DataTemplate>
</CarouselView.ItemTemplate>
</CarouselView>
and the VM:
public partial class CutImagesViewModel : ObservableObject
{
// == observable properties ==
[ObservableProperty]
public Collection<CutImages> imageObjects = new Collection<CutImages>();
[ObservableProperty]
CutImages currentCutImage;
[ObservableProperty]
public SKBitmap maps;
public CutImagesViewModel(Collection<SKBitmapImageSource> images)
{
foreach(var image in images)
{
ImageObjects.Add(new CutImages(image));
}
this.CurrentCutImage = this.ImageObjects.FirstOrDefault();
}
}
public partial class CutImages : ObservableObject
{
[ObservableProperty]
public SKBitmap imageSource;
[ObservableProperty]
public SKBitmap mapSource;
partial void OnMapSourceChanged(SKBitmap value)
{
if(!images.Contains(value))
{
images.Add(value);
}
}
[ObservableProperty]
Collection<SKBitmap> images = new Collection<SKBitmap>();
public CutImages(ImageSource imageSource)
{
SKBitmapImageSource sourceImage = (SKBitmapImageSource)imageSource;
SKBitmap image = sourceImage;
ImageSource = image;
}
}
I am sorry to be asking what seems really obvious question but I've been unable to set the properties (e.g. background color) of the list view / contents when using the picker
The Picker properties style what you see before you expand the list to select contents, but I cannot see or find how to affect the latter
In this example from my XAML the collapsed view of the Picker is styled correctly, but when it opens the background is white / transparent
Sorry, I have looked in many links and just can't find the info
<Picker
VerticalOptions="CenterAndExpand"
Grid.Column="1"
Grid.Row="1"
Title="PICKER"
BackgroundColor="Transparent"
TitleColor="White"
FontSize="Medium"
Style="{StaticResource AlphabetPicker}"
x:Name="AlphabetPicker"
ItemsSource="{Binding Alphabet}"
SelectedIndexChanged="GetLetterSelected"
HorizontalOptions="Start">
You could use custom renderer.
I follow the code in the link: Customize the Xamarin.Forms Picker Popup List
MyPicker.cs
public class MyPicker : Xamarin.Forms.Picker
{
}
MyPickerRenderer.cs
[assembly: ExportRenderer(typeof(MyPicker), typeof(MyPickerRenderer))]
namespace XamarinDemo.Droid.Renderer
{
class MyPickerRenderer : PickerRenderer
{
IElementController ElementController => Element as IElementController;
public MyPickerRenderer(Context context) : base(context)
{
}
private AlertDialog _dialog;
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Picker> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || e.OldElement != null)
return;
Control.Click += Control_Click;
}
protected override void Dispose(bool disposing)
{
Control.Click -= Control_Click;
base.Dispose(disposing);
}
private void Control_Click(object sender, EventArgs e)
{
Xamarin.Forms.Picker model = Element;
var picker = new NumberPicker(Context);
if (model.Items != null && model.Items.Any())
{
// set style here
picker.MaxValue = model.Items.Count - 1;
picker.MinValue = 0;
picker.SetBackgroundColor(Android.Graphics.Color.Yellow);
picker.SetDisplayedValues(model.Items.ToArray());
picker.WrapSelectorWheel = false;
picker.Value = model.SelectedIndex;
}
var layout = new LinearLayout(Context) { Orientation = Orientation.Vertical };
layout.AddView(picker);
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, true);
var builder = new AlertDialog.Builder(Context);
builder.SetView(layout);
builder.SetTitle(model.Title ?? "");
builder.SetNegativeButton("Cancel ", (s, a) =>
{
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
// It is possible for the Content of the Page to be changed when Focus is changed.
// In this case, we'll lose our Control.
Control?.ClearFocus();
_dialog = null;
});
builder.SetPositiveButton("Ok ", (s, a) =>
{
ElementController.SetValueFromRenderer(Xamarin.Forms.Picker.SelectedIndexProperty, picker.Value);
// It is possible for the Content of the Page to be changed on SelectedIndexChanged.
// In this case, the Element & Control will no longer exist.
if (Element != null)
{
if (model.Items.Count > 0 && Element.SelectedIndex >= 0)
Control.Text = model.Items[Element.SelectedIndex];
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
// It is also possible for the Content of the Page to be changed when Focus is changed.
// In this case, we'll lose our Control.
Control?.ClearFocus();
}
_dialog = null;
});
_dialog = builder.Create();
_dialog.DismissEvent += (ssender, args) =>
{
ElementController?.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
};
_dialog.Show();
}
}
}
Xaml:
<StackLayout>
<local:MyPicker x:Name="picker"
Title="Select a monkey"
TitleColor="Red">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>Baboon</x:String>
<x:String>Capuchin Monkey</x:String>
<x:String>Blue Monkey</x:String>
<x:String>Squirrel Monkey</x:String>
<x:String>Golden Lion Tamarin</x:String>
<x:String>Howler Monkey</x:String>
<x:String>Japanese Macaque</x:String>
</x:Array>
</Picker.ItemsSource>
</local:MyPicker>
</StackLayout>
I have a ListView in Xamarin.Forms of this way :
this.listView = new ListView();
this.listView.HasUnevenRows = true;
var dataTemplate = new DataTemplate(() =>
{
return new ViewCell { View = new CustomButtonTemplate()};
});
this.listView.ItemTemplate = dataTemplate;
CustomButtonTemplate.xaml
<local:CustomButton
Margin="6"
Padding="0"
HeightRequest="-1"
WidthRequest="-1"
Style="{StaticResource Title_LabelStyle}"
Text="{Binding DisplayText}" />
I also got one button renderer but dont work (without HeightRequest,WidthRequest,Padding dont work either):
[assembly: ExportRenderer(typeof(CustomButton), typeof(CustomButtonMultilineRenderer))]
namespace SGUK.ClassAction.IOS.Renderers
{
public class CustomButtonMultilineRenderer : ButtonRenderer
{
public CustomButtonMultilineRenderer()
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Button> e)
{
base.OnElementChanged(e);
if (this.Control != null)
{
this.Control.TitleLabel.LineBreakMode = UILineBreakMode.WordWrap;
this.Control.TitleEdgeInsets = new UIEdgeInsets(0, 10, 0, 10);
this.Control.TitleLabel.TextAlignment = UITextAlignment.Center;
this.Control.HorizontalAlignment = UIControlContentHorizontalAlignment.Center;
}
}
}
}
(with MaterialButtonRenderer dont work either)
The auto height with HasUnevenRows=true works fine on iOS if not using a custom renderer. If using a custom renderer, then it is up to the renderer to set the height of the cell, you have to calculate your own row height in the GetHeightForRow method in the custom renderer.
[assembly: ExportRenderer(typeof(ListView), typeof(MyLVRenderer))]
namespace App79.iOS
{
public class MyLVRenderer : ListViewRenderer
{
//UITableViewSource originalSource;
protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
{
base.OnElementChanged(e);
UITableViewSource originalSource = (UIKit.UITableViewSource)Control.Source;
Control.Source = new MyLVSource(originalSource, e.NewElement);
}
}
public class MyLVSource : UITableViewSource
{
UITableViewSource originalSource;
ListView myListView;
public MyLVSource(UITableViewSource origSource, ListView myListV)
{
originalSource = origSource;
myListView = myListV;
}
public override nint RowsInSection(UITableView tableview, nint section)
{
return originalSource.RowsInSection(tableview, section);
}
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
return originalSource.GetCell(tableView, indexPath);
}
public override nfloat GetHeightForFooter(UITableView tableView, nint section)
{
return originalSource.GetHeightForFooter(tableView, section);
}
public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
{
nfloat origHeight = originalSource.GetHeightForRow(tableView, indexPath);
// calculate your own row height here
ObservableCollection<Employee> employees = myListView.ItemsSource as ObservableCollection<Employee>;
string displayName = employees[indexPath.Row].DisplayName;
nfloat height = MeasureTextSize(displayName,UIScreen.MainScreen.Bounds.Size.Width-50,UIFont.SystemFontSize,null);
return height;
}
public nfloat MeasureTextSize(string text, double width, double fontSize, string fontName = null)
{
var nsText = new NSString(text);
var boundSize = new SizeF((float)width, float.MaxValue);
var options = NSStringDrawingOptions.UsesFontLeading | NSStringDrawingOptions.UsesLineFragmentOrigin;
if (fontName == null)
{
fontName = "HelveticaNeue";
}
var attributes = new UIStringAttributes
{
Font = UIFont.FromName(fontName, (float)fontSize)
};
var sizeF = nsText.GetBoundingRect(boundSize, options, attributes, null).Size;
//return new Xamarin.Forms.Size((double)sizeF.Width, (double)sizeF.Height);
return sizeF.Height + 5;
}
}
}
Here is the result:
I uploaded a sample here and you can check.
Say I have a list of items displayed as a Grid layout. Each item takes up a row and is made up of multiple items in a column. It's basically a table:
<Grid>
<Label Text="Item1" Grid.Row="0" Grid.Colum="0" />
<Image Src="something1" Grid.Row="0" Grid.Colum="1" />
<Label Text="Item2" Grid.Row="1" Grid.Colum="0" />
<Image Src="something2" Grid.Row="1" Grid.Colum="1" />
<Label Text="Item3" Grid.Row="2" Grid.Colum="0" />
<Image Src="something3" Grid.Row="2" Grid.Colum="1" />
</Grid>
Each Label/Image represents a row in my list of items to be displayed. I'm not worried about the databinding for the moment, I just want to move the Label/Image into a custom control so that I can use that custom control to add "Rows" into my Grid:
<Grid>
<customcontrol:MyCustomRowControl Text="Item1" Source="img1" Grid.Row="0"/>
<customcontrol:MyCustomRowControl Text="Item2" Source="img1" Grid.Row="1"/>
<customcontrol:MyCustomRowControl Text="Item3" Source="img1" Grid.Row="3"/>
</Grid>
I can probably set the Lable/Image/etc from my custom control to it's appropriate row/column from the code-behind.Where I get lost is what type of base class should I make this custom control? Because it is that class that will become the content of the Grid, not it's Labels and Images, therefore the Grid.Row and Grid.Column will not propagate correctly. I really hope I managed to explain this.
Can I create a custom control in Xamarin that I can add as a content to a Grid and have it's children respect the Grid's columns?
You can write about like this
DynamicGridView class
public class DynamicGridView : Grid
{
private int _rowCount;
private int _columnCount;
protected int _column;
protected int _starHeight = 0;
protected int _type;
protected int[] _starHeightList;
public DynamicGridEnum _dynamicGridEnum;
public DynamicGridView(DynamicGridEnum dynamicGridEnum, params int[] starHeightList)
{
_type = 2;
switch (dynamicGridEnum)
{
case DynamicGridEnum.Auto:
_column = starHeightList[0];
break;
case DynamicGridEnum.Star:
_column = starHeightList[0];
_starHeight = starHeightList[1];
_type = 1;
break;
case DynamicGridEnum.Custom:
_column = starHeightList.Length;
break;
default:
break;
}
_starHeightList = starHeightList;
_dynamicGridEnum = dynamicGridEnum;
_rowCount = 0;
_columnCount = 0;
Padding = 0;
Margin = 0;
ColumnSpacing = -1;
RowSpacing = -1;
}
public virtual void AddView(View view)
{
int countRow = _rowCount / _column;
if (RowDefinitions.Count <= countRow)
{
RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, (GridUnitType)_type) });
}
Children.Add(view, _columnCount, countRow);
_rowCount++;
_columnCount++;
_columnCount = _columnCount % _column;
}
}
DynamicGrid class
public class DynamicGrid : DynamicGridView
{
public DynamicGrid(DynamicGridEnum dynamicGridEnum, params int[] starHeightList) : base(dynamicGridEnum, starHeightList)
{
for (int i = 0; i < starHeightList.Length; i++) { starHeightList[i] = starHeightList[i] <= 0 ? 1 : starHeightList[i]; }
if (dynamicGridEnum == DynamicGridEnum.Custom)
{
StartCustomGrid();
}
else
StartGrid();
}
private void StartGrid()
{
int percent = 100 / _column;
for (int i = 0; i < _column; i++)
ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(percent, (GridUnitType)_type) });
}
private void StartCustomGrid()
{
foreach (var item in _starHeightList)
ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(item, GridUnitType.Star) });
}
}
And usage of dynamic grid (I defined an enum for type of grid. For
example if enum is auto it will be auto resize row/columns of grid.)
public partial class MainPage : ContentPage
{
public MainPage()
{
StackLayout sl = new StackLayout();
DynamicGrid dynamicGrid = new DynamicGrid(Enums.DynamicGridEnum.Custom, 20, 50, 20, 0);
dynamicGrid.AddView(new BoxView() { BackgroundColor = Color.AliceBlue });
dynamicGrid.AddView(new BoxView() { BackgroundColor = Color.Aqua });
dynamicGrid.AddView(new BoxView() { BackgroundColor = Color.AntiqueWhite });
dynamicGrid.AddView(new BoxView() { BackgroundColor = Color.Azure });
sl.Children.Add(new CardView(Color.Beige, Color.Bisque, 60, Color.Black, 90, 10));
sl.Children.Add(dynamicGrid);
Content = sl;
}
}
I know the default DatePicker already has a bottom line, but I'm trying to add a bottom line to the DatePicker in the custom renderer code (for some purpose).
I can set a full border of my GradientDrawable object by myGradientDrawable.SetStroke(3, myColor); but I don't know how to add only the bottom line so anyone can help me please?
Try this:
public class CustomPickerRenderer : PickerRenderer
{
public CustomPickerRenderer(Context context) : base(context)
{
}
private AlertDialog alert;
private CustomPicker element;
private int selectedIndex;
public LayerDrawable AddPickerStyles(string imagePath)
{
ColorDrawable borderColorDrawable = new ColorDrawable(Xamarin.Forms.Color.FromHex("#43addf").ToAndroid());
ColorDrawable backgroundColorDrawable = new ColorDrawable(Xamarin.Forms.Color.FromHex("#7e1b80").ToAndroid());
Drawable[] drawables = new Drawable[]
{
borderColorDrawable, backgroundColorDrawable
};
LayerDrawable layerDrawable = new LayerDrawable(drawables);
layerDrawable.SetLayerInset(1, 0, 0, 0, 5);
return layerDrawable;
}
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
element = (CustomPicker)this.Element;
if (Control != null && this.Element != null)
{
Control.Background = AddPickerStyles(element.Image);
}
}
}