Frame goes transparent on tap - xamarin

I am working on a Xamarin.Forms project with a ListView and each item has a Frame with a white background. My problem is I recently noticed when I tap the Item it makes the Frame's background white. It still shows all the other objects but the frame goes transparent.
The frame is simply
<Frame CornerRadius="10" Padding="0" Margin="10, 10, 10, 5" BackgroundColor="White">...</Frame>

Promoting above comments-answer to real-answer for readability sake:
The behavior you are seeing is specific to iOS. You can solve it by overriding the default behavior using an effect, like so:
[assembly: ResolutionGroupName("MyEffects")]
[assembly: ExportEffect(typeof(ListViewHighlightEffect), nameof(ListViewHighlightEffect))]
namespace MyProject.iOS.Effects
{
public class ListViewHighlightEffect : PlatformEffect
{
protected override void OnAttached()
{
var listView = (UIKit.UITableView)Control;
listView.AllowsSelection = false;
}
protected override void OnDetached()
{
}
}
}
Then, you can apply it in your view code-behind:
MyListView.Effects.Add(Effect.Resolve($"MyEffects.ListViewHighlightEffect"));
I did a short write-up on the full solution here
Alternatively, if you want to maintain the ability to select an item, one possible fix would be to add an ItemTapped handler in your code behind and null out
MyListView.SelectedItem = null;

Related

.net maui CarouselView is not quite compatible with Slider control: CarouselView swipe operation takes over Slider's drag action

.net maui app.
Dragging value element along the slider bar does not work if the the slider put into CarouselView's template like this:
<CarouselView ItemsSource="{Binding Items}">
<CarouselView.ItemTemplate>
<DataTemplate>
<Slider Minimum="0" Maximum="30" WidthRequest="200" />
</DataTemplate>
</CarouselView.ItemTemplate>
</CarouselView>
CarouselView takes over the swipe event for scrolling through the items, and Slider does not get the event (DragStarted is not even called). You can actually click along the slider bar to change its value, so it's not completely frozen, but not how it's supposed to work. Drag & drop is main way user deal with slider control.
Could anyone advise any workaround? I want users to be able scroll through carousel view items also. It's just if they swipe inside the control, event should not handed over to its parent container, if it's possible to do so.
If I add it outside of the corouselview, combine both in Grid and use padding to align slider inside the corouselview, it works as expected, but I need to add lots of additional code, calculate the desirable location and redirect all bindings, which ends up to be an awkward workaround.
At first, I don't suggest you use the slider in the CarouselView. Becasue you want the same behavior has two effects. There is a conflict between them.
But for the android, you can use the custom handler to deal with the swipe event.
Put the Handler class in the /Platform/Android:
public class MySliderHander : SliderHandler
{
protected override void ConnectHandler(SeekBar platformView)
{
base.ConnectHandler(platformView);
platformView.SetOnTouchListener(new SliderListener());
// the listener will make the slider deal with the swip event not the CarouselView.
}
}
Put the SliderListener class in the /Platform/Android
public class SliderListener : Java.Lang.Object, IOnTouchListener
{
public bool OnTouch(global::Android.Views.View v, MotionEvent e)
{
if (e.Action == MotionEventActions.Down || e.Action == MotionEventActions.Move)
{
v.Parent.RequestDisallowInterceptTouchEvent(true);
}
else
{
v.Parent.RequestDisallowInterceptTouchEvent(false);
}
return false;
}
}
And in the MauiProgram.cs:
builder
                  .UseMauiApp<App>()      
                  .ConfigureMauiHandlers(handlers => {
#if ANDROID
                        handlers.AddHandler(typeof(Slider), typeof(YourProjectName.Platforms.Android.MySliderHander));
                  #endif
                  })
In addition, the Slider's height is same as the CarouselView. So you can use a frame to contain the Slider and swipe the CarouselView by swiping the frame.

How to run control animation inside Template by ViewModel property change

.Net Maui page with CarouselView and list of cards created via data binding to the collection of items in ViewModel (VM). I'm looking for ways to animate control inside CarouselView by some property in VM set to a particular value. Animation should be done in c# code (code-behind, trigger action, behavior etc.), not by xaml. Not sure how to properly implement that. This is what I considered as possible solutions:
Declare event in VM and subscribe for it in code-behind. Works very well for non-template controls, but with CarouselView which consists of collection Card controls described inside DataTemplate I need to find that particular active control only, let's say Label that I want to animate. Not sure how to find it, there are one instance of it per each item in VM collection, but even if I do it does not look to be a good MVVM oriented design.
My big hope was on TriggerAction<Label> (given I want to animate Label), but then problem is TriggerAction seems to only work inside EventTrigger which only catches xaml control events, not VM events. What can catch VM events and property changes is DataTrigger, but it does not allow to have TriggerAction<T> declared inside on the other hand. Not sure why there is such limitation in .Net Maui, I wish I had some mix of both.
Behaviors, - as with triggers not sure how to run them by any property change or event declared in VM
// ViewModel (mvvm community toolkit is used):
public partial class CollectionOfItems : ObservableObject
{
public Item[] Items {get; set;}
}
public partial class Item : ObservableObject
{
[ObservableProperty]
public string _name;
// Setting this to false should trigger Label animation
[ObservableProperty]
public bool _isInvalid;
}
...
<CarouselView ItemsSource="{Binding Items}">
<CarouselView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding Name}">
<Label.Triggers>
<DataTrigger TargetType="Label" Binding="{Binding IsInvalid}" Value="True">
<!-- I wish I was able to trigger InvalidDataTriggerAction from here, but it's not compatible with DataTrigger -->
</DataTrigger>
</Label.Triggers>
</Label>
</DataTemplate>
</CarouselView.ItemTemplate>
</CarouselView>
...
// Animation should be implemented in c#, not XAML
// Should be triggered by IsInvalid=false, but I do not know how:
public class InvalidDataTriggerAction : TriggerAction<Label>
{
protected override void Invoke(Label label)
{
label.TranslateTo(-30, 0, 100);
label.TranslateTo(60, 0, 100);
label.TranslateTo(-30, 0, 100);
}
}
Alright, I've found a way to do it through bindable properties inside behaviors. Happened to be bit more complicated than I expected, but it works. Unfortunately .NET Maui does not provide a better more intuitive way to do that, I guess it's the area for improvement.
Here's the code:
namespace View.Behaviors;
public class AnimateWrongAnswerBehavior : BaseBehavior<VisualElement>
{
public static readonly BindableProperty ShouldAnimateProperty =
BindableProperty.CreateAttached(
"ShouldAnimate",
typeof(bool),
typeof(AnimateWrongAnswerBehavior),
false,
propertyChanged: OnShouldAnimateChanged);
public static void SetShouldAnimate(BindableObject view, VisualElement value) =>
view.SetValue(ShouldAnimateProperty, value);
static async void OnShouldAnimateChanged(BindableObject bindable, object oldValue, object newValue)
{
if ((bool)newValue)
{
await Animate((VisualElement)bindable);
}
}
/// <summary>
/// Implemenation of Animation logic
/// </summary>
static private async Task Animate(VisualElement elementToAnimate)
{
await elementToAnimate.TranslateTo(-30, 0, 100);
await elementToAnimate.TranslateTo(60, 0, 100);
await elementToAnimate.TranslateTo(-30, 0, 100);
}
}
Then you can bind animation to any visual element like Frame:
<ContentPage
xmlns:bh="clr-namespace:View.Behaviors" ...>
<Frame bh:AnimateWrongAnswerBehavior.ShouldAnimate="{Binding IsInvalid}">
</ContentPage>
Just like you said, the control in the datatemplate is hard to access. So I have done a sample to test the TranslateX property of the Label. And I found that label.TranslateTo(60, 0, 100); had the same effect as the label.TranslateX = 60.
According to this, you can create a variable in the Item and binding it to the label in the DataTemplate. And change the item's value in the page.cs. And you can also use the DataTrigger to set the value of label.TranslateX.
Please note that TranslateTo is awaitable and might need the async/await pattern for the element to be animated, especially if more than one TranslateTo is invoked.
TriggerAction with async/await added:
public class TriggerActionTranslateTo : TriggerAction<VisualElement>
{
protected override async void Invoke(VisualElement sender)
{
await sender.TranslateTo(0, 0, 250);
await sender.TranslateTo(30, 0, 250);
await sender.TranslateTo(0, 0, 250);
}
For some reason async/await is not needed with only one TranslateTo and this will animate:
protected override void Invoke(VisualElement sender)
{
sender.TranslateTo(30, 0, 250);
}
XAML:
<Label.Triggers>
<DataTrigger
TargetType="Label"
Binding="{Binding Property}"
Value="True">
<DataTrigger.EnterActions>
<ns:TriggerActionTranslateTo/>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
...

Xamarin Forms Android Autosize Label TextCompat pre android 8 doesn't autosize text

I want to utilise the auto-sizing feature of android textviews in my xamarin forms solution so that as the text length grows, the font sizes shrinks to never overflow the bounds of the label, and doesn't get truncated. I've created a custom Label control to do so and added an android custom renderer. It's not working in Android 7 and below. It is working in Android 8 and above.
According to the docs autosize support was introduced in android 8, but can be supported back to Android 4 with AppCompat.v4. However, my custom rendered label just renders the default font size in Android pre 8. It works fine in 8+ devices, the label text resizes as needed to not overflow the bounds. The accepted answer to this question with a similar issue on native android says it can be to do with not setting a width and height, I've tried setting widthrequest and heightrequest explicitly and it doesn't change anything. Also setting maxlines=1 doesn't change anything. An alternative thread suggests that custom fonts are the culprit. I created a vanilla forms solution using the default device font, and get the same effect.
My code:
internal class AutosizeLabelRenderer : LabelRenderer
{
#region constructor
public AutosizeLabelRenderer(Context context) : base(context)
{
}
#endregion
#region overridable
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || !(e.NewElement is AutoSizeLabel autoLabel) || Control == null) { return; }
TextViewCompat.SetAutoSizeTextTypeUniformWithConfiguration(Control, autoLabel.AutoSizeMinTextSize,
autoLabel.AutoSizeMaxTextSize, autoLabel.AutoSizeStepGranularity, (int)ComplexUnitType.Sp);
}
#endregion
}
public class AutoSizeLabel : Label
{
public int AutoSizeMaxTextSize
{
get => (int)GetValue(AutoSizeMaxTextSizeProperty);
set => SetValue(AutoSizeMaxTextSizeProperty, value);
}
public static readonly BindableProperty AutoSizeMaxTextSizeProperty = BindableProperty.Create(
nameof(AutoSizeMaxTextSize), // the name of the bindable property
typeof(int), // the bindable property type
typeof(AutoSizeLabel)); // the default value for the property
public int AutoSizeMinTextSize
{
get => (int)GetValue(AutoSizeMinTextSizeProperty);
set => SetValue(AutoSizeMinTextSizeProperty, value);
}
public static readonly BindableProperty AutoSizeMinTextSizeProperty = BindableProperty.Create(
nameof(AutoSizeMinTextSize), // the name of the bindable property
typeof(int), // the bindable property type
typeof(AutoSizeLabel)); // the default value for the property
public int AutoSizeStepGranularity
{
get => (int)GetValue(AutoSizeStepGranularityProperty);
set => SetValue(AutoSizeStepGranularityProperty, value);
}
public static readonly BindableProperty AutoSizeStepGranularityProperty = BindableProperty.Create(
nameof(AutoSizeStepGranularity), // the name of the bindable property
typeof(int), // the bindable property type
typeof(AutoSizeLabel)); // the default value for the property
//
}
Not working: Android 7 - text does not shrink
Working as expected: Android 8 and above
Xaml for above images:
<StackLayout HeightRequest="200" WidthRequest="100">
<Label Text="Fixed width and height, sentences get longer, text should shrink" />
<controls:AutoSizeLabel
AutoSizeMaxTextSize="50"
AutoSizeMinTextSize="8"
AutoSizeStepGranularity="1"
BackgroundColor="{StaticResource Shamrock}"
HeightRequest="40"
HorizontalOptions="Start"
MaxLines="1"
Text="A small sentence"
WidthRequest="200" />
<controls:AutoSizeLabel
AutoSizeMaxTextSize="50"
AutoSizeMinTextSize="8"
AutoSizeStepGranularity="1"
BackgroundColor="{StaticResource Shamrock}"
HeightRequest="40"
HorizontalOptions="Start"
MaxLines="1"
Text="A larger sentence that shrinks"
WidthRequest="200" />
<controls:AutoSizeLabel
AutoSizeMaxTextSize="50"
AutoSizeMinTextSize="8"
AutoSizeStepGranularity="1"
BackgroundColor="{StaticResource Shamrock}"
HeightRequest="40"
HorizontalOptions="Start"
MaxLines="1"
Text="An even larger sentence that shrinks more."
WidthRequest="200" />
</StackLayout>
TextView font size changes with the size of the control, which is new in Android 8.0 (API26),therefore, compatibility issues need to be considered when using the previous version.You could change the TextView to AppCompatTextView.
Change your
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || !(e.NewElement is AutoSizeLabel autoLabel) || Control == null) { return; }
AppCompatTextView appCompatTextView = new AppCompatTextView(_context);
appCompatTextView.Text = Element.Text;
appCompatTextView.SetMaxLines(1);
SetNativeControl(appCompatTextView);
TextViewCompat.SetAutoSizeTextTypeUniformWithConfiguration(Control,autoLabel.AutoSizeMinTextSize,autoLabel.AutoSizeMaxTextSize, autoLabel.AutoSizeStepGranularity, (int)ComplexUnitType.Sp);
}
Leo Zhu's answer got me most of the way there. There were a couple of extra steps I needed to take to get it fully working, so I'm posting the code as a separate answer here.
Differences between mine and Leo's answer:
Creating a new native control in scope like Leo suggested meant that it worked for a while but got disposed by the garbage collector and caused an exception when returning to the page after navigating away. To fix this I needed to override a property called ManageNativeControlLifetime to return false, and then manually manage disposing the object by overriding the dispose method and calling Control.RemoveFromParent();. This advice comes from a xamarin staff member in this thread.
Formatting and binding context are not automatically inherited when creating the new native control and need to be set manually. I needed to add those based on my needs using the android specific binding syntax. You may need to add other formatting and binding code based on your needs, I'm just doing font colour, gravity and binding context here.
I set the binding context with
appCompatTextView.SetBindingContext(autoLabel.BindingContext);
Once the binding context was set, I needed to add a new string property to my XF AutoSizeLabel class to pass in through XAML, then use it to set the binding path for the relevant property (In my case the text property). If more than one binding is required, you would need to add multiple new binding path properties for each required property. I set a specific binding like this:
appCompatTextView.SetBinding("Text", new Binding(autoLabel.TextBindingPath));
To facilitate this in my Xamarin Forms Xaml, my Xaml went from <Label Text="{Binding MyViewModelPropertyName}" /> to <controls:AutoSizeLabel TextBindingPath="MyViewModelPropertyName" />
Here's the full code of the renderer:
protected override bool ManageNativeControlLifetime => false;
protected override void Dispose(bool disposing)
{
Control.RemoveFromParent();
base.Dispose(disposing);
}
private AppCompatTextView appCompatTextView;
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || !(e.NewElement is AutoSizeLabel autoLabel) || Control == null) { return; }
//v8 and above supported natively, no need for the extra stuff below.
if (DeviceInfo.Version.Major >= 8)
{
Control?.SetAutoSizeTextTypeUniformWithConfiguration(
autoLabel.AutoSizeMinTextSize,
autoLabel.AutoSizeMaxTextSize, autoLabel.AutoSizeStepGranularity,
(int)ComplexUnitType.Sp);
return;
}
appCompatTextView = new AppCompatTextView(Context);
appCompatTextView.SetTextColor(Element.TextColor.ToAndroid());
appCompatTextView.SetMaxLines(1);
appCompatTextView.Gravity = GravityFlags.Center;
appCompatTextView.SetBindingContext(autoLabel.BindingContext);
appCompatTextView.SetBinding("Text", new Binding(autoLabel.TextBindingPath));
SetNativeControl(appCompatTextView);
TextViewCompat.SetAutoSizeTextTypeUniformWithConfiguration(Control, autoLabel.AutoSizeMinTextSize, autoLabel.AutoSizeMaxTextSize, autoLabel.AutoSizeStepGranularity, (int)ComplexUnitType.Sp);
}

CustomRenderer not applying

The BackgroundColor property on Xamarin.Forms.Buttons started coloring the entire area assigned to the button instead of just the button, so I started delving into the Custom Renderers in Android to see if I could fix it. The following is the class I created, the .xaml where I use a custom button, and the renderer (it's just supposed to color the button green as a test):
using Xamarin.Forms;
namespace wcsmobile
{
public class wcsButton : Button
{
}
}
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:wcsmobile;assembly=wcsmobile"
x:Class="wcsmobile.Transfer"
Title="Transfer">
<ContentPage.Content>
<StackLayout HorizontalOptions="FillAndExpand">
<local:wcsButton Text="OK" HorizontalOptions="Center"/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
[assembly: ExportRenderer(typeof(wcsButton), typeof(ButtonRenderer))]
namespace wcsmobile.Droid
{
class wcsButtonRenderer : ButtonRenderer
{
public wcsButtonRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if (Control != null)
{
Control.SetBackgroundColor(global::Android.Graphics.Color.LightGreen);
}
}
}
}
There is no error, but the button stays gray, not green. Putting a breakpoint at the "base.OnElementChanged(e)" doesn't do anything; the breakpoint never gets hit. Both projects are using Xamarin.Forms version 3.1.0.583944.
Anyone with a clue as to why the Custom Renderer doesn't seem to be applying would be very helpful. (Or even an idea as to why BackgroundColor seemed to stop working in the first place).
As you mentioned "both projects" the answer seems obvious: the renderer from project A isn't applied to project B. You have the choice to move it to appropriate project OR move it to a shared project https://learn.microsoft.com/en-us/xamarin/cross-platform/app-fundamentals/shared-projects?tabs=vswin

Xamarin Forms Clickable Images

I originally implemented this feature but simply adding an image to a button. Then I realized I could simply add a tap gesture to an image (w/o using a button). Any recommendations which is the best way to go and why? Thanks.
I use my own "OnClick" event for Image :) with a custom control:
public class MyImage : Xamarin.Forms.Image
{
public static BindableProperty OnClickProperty =
BindableProperty.Create("OnClick", typeof(Command), typeof(MyImage));
public Command OnClick
{
get { return (Command)GetValue(OnClickProperty); }
set { SetValue(OnClickProperty, value); }
}
public MyImage()
{
GestureRecognizers.Add(new TapGestureRecognizer() {Command = new Command(DisTap)});
}
private void DisTap(object sender)
{
if (OnClick != null)
{
OnClick.Execute(sender);
}
}
}
Then use it with MVVM like:
<local:MyImage Source="{Binding Img}" OnClick="{Binding ImgTapCommand}" />
It depends of visual effect you want to achieve.
If you use Button you'll have tapped animation (depens of platform) and specific buttton border. You have much less control how the image will look like (it's on the left side of button text).
If you use a plain TapGestureRecognizer you'll have a normal image with full control of aspect ratio/size etc.
You could use absolute layout, which can be used to place two elements above each other, make sure to make the button is the second element.
<AbsoluteLayout>
<Image Source="clock.png" AbsoluteLayout.LayoutBounds="0.2,0.2,35,35" AbsoluteLayout.LayoutFlags="PositionProportional"/>
<Button AbsoluteLayout.LayoutBounds="0.2,0.2,35,35" AbsoluteLayout.LayoutFlags="PositionProportional" BorderColor="Transparent" BackgroundColor="Transparent" Command="{Binding AlertMeCommand}"/>
</AbsoluteLayout>

Resources