How do I read a view's bounds in the OnAppearing() method? - xamarin

Currently I have this:
protected override void OnAppearing()
{
var collapsedHeight = ReportFormButtonBar.Height;
var reportFormExpandedPosition = ReportForm.Bounds;
var reportFormCollapsedPosition = ReportForm.Bounds;
reportFormCollapsedPosition.Y = ( ReportForm.Height - collapsedHeight );
reportFormExpandedPosition.Y = 0;
ReportForm.TranslateTo( reportFormCollapsedPosition.X, reportFormCollapsedPosition.Y, 200, Easing.CubicOut );
base.OnAppearing();
}
Which is supposed to position the ReportForm in a collapsed position. To do that, I need to read some bounds. However, the bounds are all set to x:0, y:0 with width and height to -1. I've been struggling with this for a long while now and can't find any answer.
TLDR: What should I do to be able to read the correct bounds in this method? Or is there some other way to accomplish view setup?

To work with view bounds, I would recommend overriding OnSizeAllocated or SizeChanged event.
//in view class
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
// retreive view bounds here..
}
//OR..
ReportForm.SizeChanged += (sender, e) => {
if(Width > 0 && Height > 0)
{
// retreive view bounds here..
}
};

Related

Can't Trigger MouseEntered on NSButton in Xamarin.Mac

I'm trying to programmatically add a MouseEntered event to a custom NSButton class, and I can't seem to get it to fire. I'm writing a Mac OS application in Visual Studio for Mac, using Xamarin.Mac. I need to add the event in code because I'm creating the buttons dynamically.
Here's my ViewController where these buttons are being created. They're instantiated in the DrawHexMap method near the bottom.
public partial class MapController : NSViewController
{
public MapController (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
DrawHexMap();
}
partial void GoToHex(Foundation.NSObject sender)
{
string[] coordinateStrings = ((NSButton)sender).Title.Split(',');
Hex chosenHex = HexRepo.GetHex(coordinateStrings[0], coordinateStrings[1]);
HexService.currentHex = chosenHex;
NSTabViewController tp = (NSTabViewController)ParentViewController;
tp.TabView.SelectAt(1);
}
private void DrawHexMap()
{
double height = 60;
for (int x = 0; x < 17; x++) {
for (int y = 0; y < 13; y++) {
HexButton button = new HexButton(x, y, height);
var handle = ObjCRuntime.Selector.GetHandle("GoToHex:");
button.Action = ObjCRuntime.Selector.FromHandle(handle);
button.Target = this;
View.AddSubview(button);
}
}
}
}
And here's the custom button class.
public class HexButton : NSButton
{
public NSTrackingArea _trackingArea;
public HexButton(int x, int y, double height)
{
double width = height/Math.Sqrt(3);
double doubleWidth = width * 2;
double halfHeight = height/2;
double columnNudge = decimal.Remainder(x, 2) == 0 ? 0 : halfHeight;
Title = x + "," + y;
//Bordered = false;
//ShowsBorderOnlyWhileMouseInside = true;
SetFrameSize(new CGSize(width, height));
SetFrameOrigin(new CGPoint(width + (x * doubleWidth), (y * height) + columnNudge));
_trackingArea = new NSTrackingArea(Frame, NSTrackingAreaOptions.ActiveInKeyWindow | NSTrackingAreaOptions.MouseEnteredAndExited, this, null);
AddTrackingArea(_trackingArea);
}
public override void MouseEntered(NSEvent theEvent)
{
base.MouseEntered(theEvent);
Console.WriteLine("mouse enter");
}
}
So as you can see, I'm creating a tracking area for the button and adding it in the constructor. Yet I can't seem to get a MouseEntered to fire. I know the MouseEntered override in this class works, because when I call button.MouseEntered() directly from my code, the method fires.
A few other things I've tried include: Commenting out the lines that set the Action and Target in the ViewController, in case those were overriding the MouseEntered handler somehow. Setting those values inside the HexButton constructor so that the Target was the button instead of the ViewController. Putting the MouseEntered override in the ViewController instead of the button class. Creating the tracking area after the button was added as a subview to the ViewController. None of these made a difference.
Any help would be much appreciated! It's quite difficult to find documentation for Xamarin.Mac...
Thanks!
You are adding the Frame as the region tracked, but you are attaching the tracking area to the view that you are creating the region from, thus you need to track the Bounds coords.
_trackingArea = new NSTrackingArea(Bounds, NSTrackingAreaOptions.ActiveInKeyWindow | NSTrackingAreaOptions.MouseEnteredAndExited, this, null);
Note: If you were tracking from the view that you are adding the buttons to, then you would need to track the "Frame" as it is the tracking region is relative to the view being tracked, not its children.

Adding a bottom border to an Entry in Xamarin Forms iOS with an image at the end

Now before anyone ignores this as a duplicate please read till the end. What I want to achieve is this
I've been doing some googling and looking at objective c and swift responses on stackoverflow as well. And this response StackOverFlowPost seemed to point me in the right direction. The author even told me to use ClipsToBounds to clip the subview and ensure it's within the parents bounds. Now here's my problem, if I want to show an image on the right side of the entry(Gender field), I can't because I'm clipping the subview.
For clipping, I'm setting the property IsClippedToBounds="True" in the parent stacklayout for all textboxes.
This is the code I'm using to add the bottom border
Control.BorderStyle = UITextBorderStyle.None;
var myBox = new UIView(new CGRect(0, 40, 1000, 1))
{
BackgroundColor = view.BorderColor.ToUIColor(),
};
Control.AddSubview(myBox);
This is the code I'm using to add an image at the beginning or end of an entry
private void SetImage(ExtendedEntry view)
{
if (!string.IsNullOrEmpty(view.ImageWithin))
{
UIImageView icon = new UIImageView
{
Image = UIImage.FromFile(view.ImageWithin),
Frame = new CGRect(0, -12, view.ImageWidth, view.ImageHeight),
ClipsToBounds = true
};
switch (view.ImagePos)
{
case ImagePosition.Left:
Control.LeftView.AddSubview(icon);
Control.LeftViewMode = UITextFieldViewMode.Always;
break;
case ImagePosition.Right:
Control.RightView.AddSubview(icon);
Control.RightViewMode = UITextFieldViewMode.Always;
break;
}
}
}
After analysing and debugging, I figured out that when OnElementChanged function of the Custom Renderer is called, the control is still not drawn so it doesn't have a size. So I subclassed UITextField like this
public class ExtendedUITextField : UITextField
{
public UIColor BorderColor;
public bool HasBottomBorder;
public override void Draw(CGRect rect)
{
base.Draw(rect);
if (HasBottomBorder)
{
BorderStyle = UITextBorderStyle.None;
var myBox = new UIView(new CGRect(0, 40, Frame.Size.Width, 1))
{
BackgroundColor = BorderColor
};
AddSubview(myBox);
}
}
public void InitInhertedProperties(UITextField baseClassInstance)
{
TextColor = baseClassInstance.TextColor;
}
}
And passed the hasbottomborder and bordercolor parameters like this
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
var view = e.NewElement as ExtendedEntry;
if (view != null && Control != null)
{
if (view.HasBottomBorder)
{
var native = new ExtendedUITextField
{
BorderColor = view.BorderColor.ToUIColor(),
HasBottomBorder = view.HasBottomBorder
};
native.InitInhertedProperties(Control);
SetNativeControl(native);
}
}
But after doing this, now no events fire :(
Can someone please point me in the right direction. I've already built this for Android, but iOS seems to be giving me a problem.
I figured out that when OnElementChanged function of the Custom Renderer is called, the control is still not drawn so it doesn't have a size.
In older versions of Xamarin.Forms and iOS 9, obtaining the control's size within OnElementChanged worked....
You do not need the ExtendedUITextField, to obtain the size of the control, override the Frame in your original renderer:
public override CGRect Frame
{
get
{
return base.Frame;
}
set
{
if (value.Width > 0 && value.Height > 0)
{
// Use the frame size now to update any of your subview/layer sizes, etc...
}
base.Frame = value;
}
}

Prevent auto-scroll to top on orientation change in a XF ScrollView

Rather annoying issue, but it seems as though there's some default behavior on a scroll view, that when you change orientation, it auto-scrolls to top. ShouldScrollToTop is a property available on a UIScrollView but doesn't seem to be available in a XF ScrollView. But it does it anyway? Is there a way to stop?
I just did a quick test on this and only found it to be an issue on iOS. On Android the ScrollView did not scroll to the top after rotation. I did not test UWP/WinPhone. If it is iOS only, a bug should be filed against Xamarin.Forms.
In the meantime, here is a workaround for iOS. You will need to create a custom renderer [1] for your ScrollView. And the renderer code would look like this:
public class MyScrollViewRenderer : ScrollViewRenderer
{
CGPoint offset;
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
UIScrollView sv = NativeView as UIScrollView;
sv.Scrolled += (sender, evt) => {
// Checking if sv.ContentOffset is not 0,0
// because the ScrollView resets the ContentOffset to 0,0 when rotation starts
// even if the ScrollView had been scrolled (I believe this is likely the cause for the bug).
// so you only want to set offset variable if the ScrollView is scrolled away from 0,0
// and I do not want to reset offset to 0,0 when the rotation starts, as it would overwrite my saved offset.
if (sv.ContentOffset.X != 0 || sv.ContentOffset.Y != 0)
offset = sv.ContentOffset;
};
// Subscribe to the oreintation changed event.
NSNotificationCenter.DefaultCenter.AddObserver(this, new Selector("handleRotation"), new NSString("UIDeviceOrientationDidChangeNotification"), null );
}
[Export("handleRotation")]
void HandleRotation()
{
if (offset.X != 0 || offset.Y != 0) {
UIScrollView sv = NativeView as UIScrollView;
// Reset the ScrollView offset from the last saved offset.
sv.ContentOffset = offset;
}
}
}
[1] https://developer.xamarin.com/guides/xamarin-forms/custom-renderer/
I have extended #jgoldberger's answer and come up with this:
[assembly: ExportRenderer(typeof(Xamarin.Forms.ScrollView), typeof(MyApp.iOS.CustomRenderers.Controls.FieldStrikeScrollViewRenderer))]
namespace MyApp.iOS.CustomRenderers.Controls
{
public class MyScrollViewRenderer : ScrollViewRenderer
{
CGPoint offset;
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
UIScrollView sv = NativeView as UIScrollView;
sv.Scrolled += (sender, evt) => {
// Checking if sv.ContentOffset is not 0,0
// because the ScrollView resets the ContentOffset to 0,0 when rotation starts
// even if the ScrollView had been scrolled (I believe this is likely the cause for the bug).
// so you only want to set offset variable if the ScrollView is scrolled away from 0,0
// and I do not want to reset offset to 0,0 when the rotation starts, as it would overwrite my saved offset.
if (sv.ContentOffset.X != 0 || sv.ContentOffset.Y != 0)
offset = sv.ContentOffset;
};
}
public override void LayoutSubviews()
{
if (offset.X != 0 || offset.Y != 0)
{
UIScrollView sv = NativeView as UIScrollView;
// Reset the ScrollView offset from the last saved offset.
sv.ContentOffset = offset;
}
base.LayoutSubviews();
}
}
}
but he's right, this should really be fixed by Xamarin
Be careful with those answers in the newest Xamarin version. They will cause issues with the scroll offset behaviour when they keyboard is shown/hidden. The Scrollview might stay in an invalid offset position and can't be scrolled down by the user anymore.
I am going to add a new seperate answer. In the later version of Xamarin.Forms (3.0+)
this is a better fix:
[assembly: ExportRenderer(typeof(Xamarin.Forms.ScrollView), typeof(MyApp.iOS.CustomRenderers.Controls.ExtendedScrollViewRenderer))]
namespace FieldStrikeMove.iOS.CustomRenderers.Controls
{
public class ExtendedScrollViewRenderer : ScrollViewRenderer
{
public override CGSize ContentSize
{
get => base.ContentSize;
set
{
if (Element is ScrollView scrollView)
{
if (!scrollView.ContentSize.IsZero && value != scrollView.ContentSize.ToSizeF())
{
return;
}
}
base.ContentSize = value;
}
}
}
}
Linked to this issue

Xamarin Forms: Change the size of scrollview on translateto

What I am trying to do is:
I have a scrollview inside my view and it's height is like 2/9 of the parent height. Then user can translate this scrollview to make it bigger. However scrollview's size does not change obviously. So even though it is bigger scrollview's size remains the same, killing the point. I could make it bigger initially. However it won't scroll since the size is big enough not to scroll.
I don't know if was able to explain it right. Hope i did.
Regards.
Edit
-------------
Some code to explain my point further
This is the scrollview
public class TranslatableScrollView : ScrollView
{
public Action TranslateUp { get; set; }
public Action TranslateDown { get; set; }
bool SwipedUp;
public TranslatableScrollView()
{
SwipedUp = false;
Scrolled += async delegate {
if (!SwipedUp && ScrollY > 0) {
TranslateUp.Invoke ();
SwipedUp = true;
} else if (SwipedUp && ScrollY <= 0) {
TranslateDown.Invoke ();
SwipedUp = false;
}
};
}
}
And this is the code in the page
sv_footer = new TranslatableScrollView {
Content = new StackLayout {
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
Children = {
l_details,
l_place
},
}
};
sv_footer.TranslateUp += new Action (async delegate {
Parent.ForceLayout();
await cv_scrollContainer.TranslateTo(0, -transX, aSpeed, easing);
});
sv_footer.TranslateDown += new Action (async delegate {
await cv_scrollContainer.TranslateTo(0, 0, aSpeed, easing);
});
cv_scrollContainer = new ContentView {
Content = sv_footer,
VerticalOptions = LayoutOptions.Fill
};
I put the scrollview inside a contentview otherwise Its scroll indexes becomes 0 when they are translated. Ref: Xamarin Forms: ScrollView returns to begging on TranslateTo
The problem was, translateTo only changes the position of the element. I could use scaleTo after the translation. However it changes the both dimensions. Someone from Xamarin Forums suggested me to use LayoutTo which I did not know existed. With layoutTo you can change both the size and location. Giving an instance of Rectangle type.

My AsyncTask does not update UI smoothly (animation)

I want to make a TextView appear little by little, like animation. The problem is, the animation is not smooth. It gets stuck for a little while sometimes and then resumes. Sometimes even worse, it goes back... I mean, the TextView gets bigger and bigger but suddenly gets smaller then bigger again. Could anyone help me?
private class UnfoldTask extends AsyncTask<Integer, Integer, Integer> {
View view;
public UnfoldTask(View v) {
this.view = v;
ViewGroup.LayoutParams pa = view.getLayoutParams();
pa.height = 0;
view.setLayoutParams(pa);
}
#Override
protected Integer doInBackground(Integer... maxHeight) {
ViewGroup.LayoutParams pa = view.getLayoutParams();
while (pa.height < maxHeight[0]) {
pa.height += (int) (24 * getResources().getDisplayMetrics().density + 0.5f);
sleep(100);
publishProgress(pa.height);
}
return maxHeight[0];
}
private void sleep(int i) {
try {
Thread.sleep(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
#Override
protected void onProgressUpdate(Integer... values) {
ViewGroup.LayoutParams pa = view.getLayoutParams();
pa.height = values[0];
view.setLayoutParams(pa);
}
#Override
protected void onPostExecute(Integer result) {
ViewGroup.LayoutParams pa = view.getLayoutParams();
pa.height = result;
view.setLayoutParams(pa);
}
}
You should be using a scale animation for this. Here's an example:
ScaleAnimation animation = new ScaleAnimation(1, 2, 1, 2, centerX, centerY); // Scales from normal size (1) to double size (2). centerX/Y is the center of your text view. Change this to set the pivot point of your animation.
animation.setDuration(1000);
myTextView.startAnimation(animation);
You can use droidQuery to simplify this:
//this will set the height of myView to 0px.
$.with(myView).height(0);
//when you are ready to animate to height (in pixels):
$.with(myView).animate("{height:" + height + "}", new AnimationOptions());
Check the documentation if you want to get fancy - such as adding duration, and event callbacks. If you are still noticing non-smooth animation, consider adding the application attribute to your AndroidManifest:
android:hardwareAccelerated="true"

Resources