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.
Related
I need to know VisualElement coordinates in device screen coordinate space on iOS in Xamarin project.
Android has its own method GetLocationOnScreen
But iOS hasn't
I found this solution:
public static Point GetScreenCoords(this VisualElement view)
{
var result = new Point(view.X, view.Y);
while (view.Parent is VisualElement parent)
{
result = result.Offset(parent.X, parent.Y);
view = parent;
}
return result;
}
but properties X ad Y of VisualElement are relatives to parents bounds and don't provide required values.
You can use DependencyService to implement the function. I wrote a simple case to get the coordinates of VisualElement. In my simple, I need to get the border of the element relative to the parent element (the child element is inside the parent element). If no parent element is specified, the method returns the bounds of the element relative to the window. You can refer to this:
Create a dependent interface:
public interface IMyLocation
{
RectangleF GetCoordinates(VisualElement element, VisualElement parentElement);
}
iOS implementation:
[assembly: Xamarin.Forms.Dependency(typeof(MyLocation))]
namespace FormsDemoWang.iOS
{
public class MyLocation : IMyLocation
{
public RectangleF GetCoordinates(VisualElement element, VisualElement parentElement)
{
IVisualElementRenderer renderer = Platform.GetRenderer(element);
UIView elementNativeView = renderer.NativeView;
UIView parentNativeView = null;
if (parentNativeView != null) {
parentNativeView = Platform.GetRenderer(parentElement).NativeView;
}
CGRect rect = elementNativeView.ConvertRectToView(elementNativeView.Frame, parentNativeView);
float x = (float)Math.Round(rect.X);
float y = (float)Math.Round(rect.Y);
float width = (float)Math.Round(rect.Width);
float height = (float)Math.Round(rect.Height);
return new RectangleF(x, y, width, height);
}
}
}
Used in Forms:
var coordinates = DependencyService.Get<IMyLocation>().GetCoordinates(myElement, parentElement);
var xlable = new Label { Text = $"X:{coordinates.X}" };
var ylable = new Label { Text = $"Y:{coordinates.Y}" };
We have discovered in our Xamarin app in iOS 13 that the disclosure button action is not firing. Our basic code looks like this in a GridCell Class. It had been firing in iOS 12. Is there another event delegate in iOS 13?
[EventDelegate("accessorySelected")]
public event EventHandler AccessorySelected
{
add
{
accessorySelected += value;
SetAccessory();
}
remove
{
accessorySelected -= value;
SetAccessory();
}
}
private event EventHandler accessorySelected;
private void SelectAccessory(object sender, EventArgs args)
{
var handler = accessorySelected;
if (handler != null)
{
handler(Pair ?? this, EventArgs.Empty);
}
else
{
... Action Code ....
}
}
Delving into this code, it was not what was expected. It actually dates from 2011 and the code to load the disclosure event was unconventional, or as another one of our developers called it during this review, "Anti-pattern".
public class GridCell : UITableViewCell, INotifyPropertyChanged
{
...
public override void LayoutSubviews()
{
base.LayoutSubviews();
// I do not like going off of the frame width. The ContentView frame width would be preferred,
// but we would first need to ensure that its value is correctly adjusted in case of a disclosure.
// This should happen automatically, but it doesn't always happen in time for PerformLayout.
// There also seem to be cases where the adjusted value is not entirely accurate.
float width = (float)Frame.Width;
if (UIDevice.CurrentDevice.CheckSystemVersion(7, 0))
{
width -= (Accessory == UITableViewCellAccessory.DetailDisclosureButton ? 67 :
Accessory == UITableViewCellAccessory.DisclosureIndicator ? 33 : 0);
}
else
{
width -= (Accessory == UITableViewCellAccessory.DetailDisclosureButton ? 33 :
Accessory == UITableViewCellAccessory.DisclosureIndicator ? 20 : 0);
}
width = Math.Max(width, 0);
var size = this.PerformLayout(new iFactr.UI.Size(width, Math.Max(MinHeight - 1, 0)), new iFactr.UI.Size(width, Math.Max(MaxHeight - 1, 0)));
ContentView.Frame = new CGRect(0, 0, (float)size.Width, (float)size.Height);
Frame = new CGRect(Frame.Location, new CGSize(Frame.Width, ContentView.Frame.Height));
// Checking subview for Disclosure - iOS 13 changed 2019 subview
var disclosure = this.GetSubview<UIControl>(c => c.Description.StartsWith("<UITableViewCellDetailDisclosureView") || c.Description.StartsWith("<UIButton"));
var dlog = this.GetSubview<UIControl>();
if (disclosure != null)
{
//Setting Disclosure button (i) event
disclosure.TouchUpInside -= SelectAccessory;
disclosure.TouchUpInside += SelectAccessory;
}
}
...
}
And the change was to this line to look for UIButton now along with UITableViewCellDetailDisclosureView
var disclosure = this.GetSubview<UIControl>(
c => c.Description.StartsWith("<UITableViewCellDetailDisclosureView")
|| c.Description.StartsWith("<UIButton")); // <-- the "fix"
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..
}
};
Its very common problem in iOS mobile development and that is while you are done with your UI and It contains too many UITextFields, If you try to input value in UITextFields those are added center bottom of the screen; these fields hides behind the keyboard. How can we get rid of this general problem?
You could use the AddObserver method in NSNotificationCenter for when keyboard is visible and hidden.
sample code(FYI: Got the code below from another post sometime last year, I can't remember link to the post but it works fine.)
Call the AddObserver in your viewdidload method
// Keyboard popup NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.DidShowNotification, KeyBoardUpNotification);
// Keyboard Down NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillHideNotification, KeyBoardDownNotification);
you can add the methods below in your base controller base if you have one
public void KeyBoardUpNotification(NSNotification notification) {
CGRect keyboardSize = UIKeyboard.BoundsFromNotification(notification);
// Find what opened the keyboard
foreach (UIView view in this.View.Subviews) {
if (view.IsFirstResponder)
activeview = view;
}
bottom = (activeview.Frame.Y + activeview.Frame.Height + offset);
scrollamount = (keyboardSize.Height - (View.Frame.Size.Height - bottom));
if (scrollamount > 0) {
moveViewUp = true;
MoveView(moveViewUp);
} else {
moveViewUp = false;
}
}
public void KeyBoardDownNotification(NSNotification notification) {
if (moveViewUp) {
MoveView(false);
}
}
private void MoveView(bool move) {
UIView.BeginAnimations(string.Empty, IntPtr.Zero);
UIView.SetAnimationDuration(0.3);
CGRect frame = View.Frame;
if (move) {
frame.Y -= scrollamount;
} else {
frame.Y += scrollamount;
scrollamount = 0;
}
View.Frame = frame;
UIView.CommitAnimations();
}
I have used a nuget package to get rid of this problem. I have overrides two methods and initialized code inside these methods.
Download KeyboardHandler and use as following:
using KeyboardHandler;
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
this.yourScrollView.SubscribeKeyboardManaqger();
}
public override void ViewWillDisappear(bool animated)
{
base.ViewWillDisappear(animated);
this.yourScrollView.UnsubscribeKeyboardManaqger();
}
I want to make horizontal collection and add new cell at the first position.
I make custom UICollectionViewFlowLayout and override
LayoutAttributesForElementsInRect like this (to see reverse order of items):
public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect)
{
var attributes = new UICollectionViewLayoutAttributes[cellCount];
NSIndexPath indexPath;
// Setting first position
int shift = 25, reverseXCenter;
for (int i = 0; i < cellCount; i++)
{
indexPath = NSIndexPath.FromItemSection(i, 0);
attributes[i] = LayoutAttributesForItem(indexPath);
// We have to make opposite order
reverseXCenter = (cellCount -1 - i) * 50;
attributes[i].Center = new CGPoint(reverseXCenter + shift, attributes[i].Center.Y);
}
return attributes;
}
I think is bad solution, because when I have more than 5 cells, the displayed collection is filled and I can't scroll.
I had debugger in GetOrCreateCellFor in CollectionViewSource
and indexPath.Item's had value for example (10,9,8,7,6...), where 10 is the last I added.
Maybe is another way to do this, or just I miss something?
Ok I am gonna help you out with a flow .
Suppose you are in a ViewController called "A" which will present a controller ViewController called "B"
Inside A use this function to push B
Public void PushNewHorizontalCollectionController(){
using (var vc =
new B(this.PageViewControllerLayout(), indexPath))
{
this.controller.PushViewController(pageViewController, true);
}
}
public UICollectionViewFlowLayout PageViewControllerLayout()
{
var flowLayout = new UICollectionViewFlowLayout();
var itemSize = this.controller.NavigationBarHidden ? new CGSize(Constants.ScreenWidth, Constants.ScreenHeight + 20) : new CGSize(Constants.ScreenWidth, Constants.ScreenHeight - Constants.NavigationHeaderAndStatusbarHeight);
flowLayout.ItemSize = itemSize;
flowLayout.MinimumLineSpacing = 0;
flowLayout.MinimumInteritemSpacing = 0;
flowLayout.ScrollDirection = UICollectionViewScrollDirection.Horizontal;
return flowLayout;
}
Here itemSize is CGSize which u can assign what size you can ignore my constants which are self explainable .
Inside B your constructor should look like this .
public HorizontalViewController(UICollectionViewLayout layout, NSIndexPath indexpath) : base(layout)
{
Do your initialisation like registering UICollectionViewCell class nibs for use with this Controller
}
Handling real requirement from comment .
You wanna add something always as 1st element in collectionView ,Go to this method which should look like below
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
Console.WriteLine("getcellHori");
var isQuoteType = Photo.PhotoCollection[(int)indexPath.Item].Type;
if (isQuoteType == 3 || isQuoteType == 4)
{
var item = Photo.PhotoCollection[(int)indexPath.Item];
var cell = (HorizontalviewForQuoteType)collectionView.DequeueReusableCell("HorizontalviewForQuoteType", indexPath);
cell.PopulateCell(item.Caption,item.TextLabel,item.Likes,item.TimeLabel,item.Id);
HorizontalviewForQuoteType.pullAction = (offset) =>
{
this.pullOffset = offset;
this.NavigationController.PopViewController(true);
};
cell.SetNeedsLayout();
return cell;
}
Here you this line
var item = Photo.PhotoCollection[(int)indexPath.Item];
this is the data source , so whenever you adding something at index first and everything will be solved .