I have a modal page that contains an absolute layout space in which I insert my widgets, in particular a scrollable list widget, which is a custom class derived from AbsoluteLayout:
public ListWidget(List<ListItem> items)
{
list_items = items;
BackgroundColor = Color.Transparent;
tap = new TapGestureRecognizer();
tap.Tapped += Tap_Handler;
list = new ListView()
{
BackgroundColor = Color.Transparent,
HasUnevenRows = true,
VerticalOptions = LayoutOptions.Center
};
list.ItemTapped += Tap_Handler;
list.ItemTemplate = new DataTemplate(typeof(CustomCell));
list.ItemsSource = list_items;
SetLayoutBounds(list, new Rectangle(0.0, 0.0, 1.0, 1.0));
SetLayoutFlags(list, AbsoluteLayoutFlags.All);
Children.Add(list);
}
Nothing fancy, as you see. Now, the page (a ContentPage) in question is loaded by a PushModalAsync() call, but here comes the problem:
If I swipe down on the list, the modal page gets hidden and I cannot get it back, since when I try I get this warning:
Warning: Attempt to present <Xamarin_Forms_Platform_iOS_ModalWrapper: 0x7f970601c580> on <Xamarin_Forms_Platform_iOS_PageRenderer: 0x7f9703f54b50> whose view is not in the window hierarchy!
Apparently, no PopModalAsync() is called in this case.
I have tried overriding this by using a gesture recognizer:
// in the constructor...
space.GestureRecognizers.Add(swipeDown);
listwidget.GestureRecognizers.Add(swipeDown);
}
// where my hopes crushed
void Swiper_Handler(object sender, SwipedEventArgs e)
{
System.Diagnostics.Debug.Print("SWIPED sender {0} args {1}", sender, e);
if (sender == listwidget)
{
System.Diagnostics.Debug.Print("LIST WIDGET! ABORT!");
return;
}
switch (e.Direction)
{
case SwipeDirection.Left:
System.Diagnostics.Debug.Print("Swiped to LEFT");
break;
case SwipeDirection.Right:
System.Diagnostics.Debug.Print("Swiped to RIGHT");
break;
case SwipeDirection.Up:
System.Diagnostics.Debug.Print("Swiped to UP");
break;
case SwipeDirection.Down:
System.Diagnostics.Debug.Print("Swiped to DOWN");
Navigation.PopModalAsync();
break;
}
}
But it won't work:
enabling swipe down on space will pop the page, but I cannot scroll the list
enabling swipe down on listwidget will hide the page, I cannot scroll the list, and I get the above error
removing gestures will hide the page, I cannot scroll the list, and I get the above error
enabling swipe on both space and listwidget will pop the page, but I cannot scroll the list
How can I make a scrollable list with a modal page coexist?
Any hints are greatly appreciated.
Related
I have a Prism based Xamarin Forms app that contains an edit page that is wrapped in a Navigation page so there is a back button at top left on both Android and iOS. To avoid the user accidentally losing an edit in progress by accidentally clicking the back button (in particular on Android) we want to prompt them to confirm that they definitely want to cancel.
Thing is, this seems like something that is not baked in to Xamarin forms. You can override OnBackButtonPressed in a navigation page, but that only gets called for the hardware/software back button on Android. There are articles detailing techniques to intercept the actual arrow button at the top left on Android (involving overriding OnOptionsItemSelected in the Android MainActivity), but on iOS I'm not sure it is even possible.
So I can't help but wonder if I am going about this the wrong way? Should I not be intercepting the top left / hardware / software back button in this way? Seems like a pretty common thing to do (e.g. press back when editing a new contact in the android built in Contacts app and you get a prompt) but it really feels like I am fighting the system here somehow.
There are previous questions around this, most relevant appears to be How to intercept Navigation Bar Back Button Clicked in Xamarin Forms? - but I am looking for some broad brush suggestions for an approach here. My objective is to show the user a page with the <- arrow at top left for Android, "Cancel" for iOS but I would like to get some views about the best way to go about it that does not involve me fighting against prism / navigation pages / xamarin forms and (where possible) not breaking the various "best practices" on Android and iOS.
After going down the same path as you and being told not to prevent users from going back, I decided on showing an alert after they tap the back button (within ContentPage.OnDisappearing()) that says something like Would you like to save your work?.
If you go with this approach, be sure to use Application.MainPage.DisplayAlert() instead of just this.DisplayAlert() since your ContentPage might not be visible at that point.
Here is how I currently handle saving work when they click the back button (I consolidated a good bit of code and changed some things):
protected override async void OnDisappearing() {
base.OnDisappearing();
// At this point the page is gone or is disappearing, but all properties are still available
#region Auto-save Check and Execution
/*
* Checks to see if any edits have been made and if a save is not in progress, if both are true, it asks if they want to save, if yes, it checks for validation errors.
* If it finds them, it marks it as such in the model before saving the model to the DB and showing an alert stating what was done
*/
if(!_viewModel.WorkIsEdited || _viewModel.SaveInProgress) { //WorkIsEdited changes if they enter/change data or focus on certain elements such as a Picker
return;
}
if(!await Application.Current.MainPage.DisplayAlert("ALERT", "You have unsaved work! Would you like to save now?", "Yes", "No")) {
return;
}
if(await _viewModel.SaveClaimErrorsOrNotAsync()) { //The return value is whether validation succeeds or not, but it gets saved either way
App.SuccessToastConfig.Message = "Work saved successfully. Try saving it yourself next time!";
UserDialogs.Instance.Toast(App.SuccessToastConfig);
} else if(await Application.Current.MainPage.DisplayAlert("ERROR", "Work saved successfully but errors were detected. Tap the button to go back to your work.", "To Work Entry", "OK")) {
await Task.Delay(200); //BUG: On Android, the alert above could still be displayed when the page below is pushed, which prevents the page from displaying //BUG: On iOS 10+ currently the alerts are not fully removed from the view hierarchy when execution returns (a fix is in the works)
await Application.Current.MainPage.Navigation.PushAsync(new WorkPage(_viewModel.SavedWork));
}
#endregion
}
What you ask for is not possible. The back button tap cannot be canceled on iOS even in native apps. You can do some other tricks like having a custom 'back' button, but in general you shouldn't do that - you should instead have a modal dialog with the Done and Cancel buttons (or something similar).
If you use xamarin forms that code it is work.
CrossPlatform source
public class CoolContentPage : ContentPage
{
public Action CustomBackButtonAction { get; set; }
public static readonly BindableProperty EnableBackButtonOverrideProperty =
BindableProperty.Create(nameof(EnableBackButtonOverride), typeof(bool), typeof(CoolContentPage), false);
public bool EnableBackButtonOverride{
get { return (bool)GetValue(EnableBackButtonOverrideProperty); }
set { SetValue(EnableBackButtonOverrideProperty, value); }
}
}
}
Android source
public override bool OnOptionsItemSelected(IMenuItem item)
{
if (item.ItemId == 16908332)
{
var currentpage = (CoolContentPage)
Xamarin.Forms.Application.
Current.MainPage.Navigation.
NavigationStack.LastOrDefault();
if (currentpage?.CustomBackButtonAction != null)
{
currentpage?.CustomBackButtonAction.Invoke();
return false;
}
return base.OnOptionsItemSelected(item);
}
else
{
return base.OnOptionsItemSelected(item);
}
}
public override void OnBackPressed()
{
var currentpage = (CoolContentPage)
Xamarin.Forms.Application.
Current.MainPage.Navigation.
NavigationStack.LastOrDefault();
if (currentpage?.CustomBackButtonAction != null)
{
currentpage?.CustomBackButtonAction.Invoke();
}
else
{
base.OnBackPressed();
}
}
iOS source
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (((CoolContentPage)Element).EnableBackButtonOverride)
{
SetCustomBackButton();
}
}
private void SetCustomBackButton()
{
var backBtnImage = UIImage.FromBundle("iosbackarrow.png");
backBtnImage = backBtnImage.ImageWithRenderingMode
(UIImageRenderingMode.AlwaysTemplate);
var backBtn = new UIButton(UIButtonType.Custom)
{
HorizontalAlignment =
UIControlContentHorizontalAlignment.Left,
TitleEdgeInsets =
new UIEdgeInsets(11.5f, 15f, 10f, 0f),
ImageEdgeInsets =
new UIEdgeInsets(1f, 8f, 0f, 0f)
};
backBtn.SetTitle("Back", UIControlState.Normal);
backBtn.SetTitleColor(UIColor.White, UIControlState.Normal);
backBtn.SetTitleColor(UIColor.LightGray, UIControlState.Highlighted);
backBtn.Font = UIFont.FromName("HelveticaNeue", (nfloat)17);
backBtn.SetImage(backBtnImage, UIControlState.Normal);
backBtn.SizeToFit();
backBtn.TouchDown += (sender, e) =>
{
// Whatever your custom back button click handling
if(((CoolContentPage)Element)?.
CustomBackButtonAction != null)
{
((CoolContentPage)Element)?.
CustomBackButtonAction.Invoke();
}
};
backBtn.Frame = new CGRect(
0,
0,
UIScreen.MainScreen.Bounds.Width / 4,
NavigationController.NavigationBar.Frame.Height);
var btnContainer = new UIView(
new CGRect(0, 0,
backBtn.Frame.Width, backBtn.Frame.Height));
btnContainer.AddSubview(backBtn);
var fixedSpace =
new UIBarButtonItem(UIBarButtonSystemItem.FixedSpace)
{
Width = -16f
};
var backButtonItem = new UIBarButtonItem("",
UIBarButtonItemStyle.Plain, null)
{
CustomView = backBtn
};
NavigationController.TopViewController.NavigationItem.LeftBarButtonItems = new[] { fixedSpace, backButtonItem };
}
using in xamarin forms
public Page2()
{
InitializeComponent();
if (EnableBackButtonOverride)
{
this.CustomBackButtonAction = async () =>
{
var result = await this.DisplayAlert(null, "Go back?" Yes go back", "Nope");
if (result)
{
await Navigation.PopAsync(true);
}
};
}
}
First thing, I have an Editor.I can Identify if the user entered any text in that Edior.But i need to return something when the user tapped on the Editor. How to achieve this?
var msgEditor = new Editor
{
Margin = new Thickness(10, 0, 10, 0),
HeightRequest = App.ScreenHeight,
};
Secong thing, Editor is inside the scrollview.When i tapped on the Editor scrollview scrolls down.I need to manually pull down to see the cursor.How to set content offset when i tapped on Editor?
ScrollView scroll = new ScrollView
{
Content = msgEditor,
};
Content = scroll;
On the editor, you have the focus event that notifies you that the user tapped the editor. You can do as follow :
{
var editor = new Editor();
editor.Focused += EditorOnFocused;
}
private void EditorOnFocused(object sender, FocusEventArgs focusEventArgs)
{
//do your stuff
}
I have a list view in which inside each cell I have two textview, I need to handle the click event for one of the textview element. But when I put the click event inside the GetView() of the adapter, it is called multiple times.
public override View GetView(int position, View convertView, ViewGroup parent)
{
var item = tableItems[position];
ViewHolder holder;
View view = convertView;
if (view == null)
{
LayoutInflater layoutInflator = LayoutInflater.From(mContext);
view = layoutInflator.Inflate(Resource.Layout.myListViewCell, null);
holder = new ViewHolder();
holder.tvEmpName = view.FindViewById<TextView>(Resource.Id.tv_EmpName);
holder.tvEmpPhone = view.FindViewById<TextView>(Resource.Id.tv_EmpPhone);
view.Tag = holder;
}
else
{
holder = (ViewHolder)view.Tag;
}
holder.tvEmpName.Text = item.FullName;
holder.tvEmpPhone.Text = item.Phone;
holder.tvEmpPhone.Click += (sender, e) => {
// Click event to launch the Popup menu
// This event is being called multiple times, as Get view() being called multiple times.
};
return view;
}
I gone through this similar thread, but didn't find any solution.
The event is called multiple times because evertime when the metod getview is called you add a click event with +=.
In this case you can put the click event inside the if, like that:
if (view == null)
{
LayoutInflater layoutInflator = LayoutInflater.From(mContext);
view = layoutInflator.Inflate(Resource.Layout.myListViewCell, null);
holder = new ViewHolder();
holder.tvEmpName = view.FindViewById<TextView>(Resource.Id.tv_EmpName);
holder.tvEmpPhone = view.FindViewById<TextView>(Resource.Id.tv_EmpPhone);
view.Tag = holder;
holder.tvEmpPhone.Click += (sender, e) => {
// Code here
};
}
But the recyclerview is better than listview for this.
In my Xamarin.Forms app I have a button at the bottom. This button is hidden on a Windows 10 Mobile phone. Isn't there a setting were the size of my page is adapting to the available size? In such a case the height of my page would decrease if the navigation bar is shown, and it would increase if the navigation bar is hidden.
I saw solutions suggesting to programmaticaly hidding the navigation bar. E.g.
ApplicationView.GetForCurrentView().TryEnterFullScreenMode();
Where should it be placed? I put it in the App.xaml.cs before rootFrame.Navigate in OnLaunched. If I run the app on my local machine it changed to fullscreen. On the mobile phone the navigation bar was hidden, but there remained a white area at the bottom.
Furthermore I tried
ApplicationView.GetForCurrentView().FullScreenSystemOverlayMode = FullScreenSystemOverlayMode.Minimal;
but I don't see something different.
How should a developer handle the navigation bar without hiding the content beneath it?
This seems to fix the issue:
ApplicationView.GetForCurrentView().SuppressSystemOverlays = true;
The result is that the navigation bar isn't shown when starting the app. The user has the possibility to show the navigation bar with a swipe up. Here the page automatically resizes as desired.
I put it in the App.xaml.cs in the OnLaunched method:
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
Frame rootFrame = Window.Current.Content as Frame;
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (rootFrame == null)
{
// Create a Frame to act as the navigation context and navigate to the first page
rootFrame = new Frame();
rootFrame.NavigationFailed += OnNavigationFailed;
Xamarin.Forms.Forms.Init(e);
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
//TODO: Load state from previously suspended application
}
// Place the frame in the current Window
Window.Current.Content = rootFrame;
}
if (rootFrame.Content == null)
{
// When the navigation stack isn't restored navigate to the first page,
// configuring the new page by passing required information as a navigation
// parameter
ApplicationView.GetForCurrentView().SuppressSystemOverlays = true;
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
// Ensure the current window is active
Window.Current.Activate();
}
You can also use:
ApplicationView.GetForCurrentView().SetDesiredBoundsMode(ApplicationViewBoundsMode.UseVisible);
In the App.xaml.cs file for UWP in the onLauched function like this:
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
if (System.Diagnostics.Debugger.IsAttached)
{
this.DebugSettings.EnableFrameRateCounter = true;
}
Frame rootFrame = Window.Current.Content as Frame;
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (rootFrame == null)
{
// Create a Frame to act as the navigation context and navigate to the first page
rootFrame = new Frame();
rootFrame.NavigationFailed += OnNavigationFailed;
Xamarin.Forms.Forms.Init(e);
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
//TODO: Load state from previously suspended application
}
// Place the frame in the current Window
Window.Current.Content = rootFrame;
}
if (rootFrame.Content == null)
{
// When the navigation stack isn't restored navigate to the first page,
// configuring the new page by passing required information as a navigation
// parameter
ApplicationView.GetForCurrentView().SetDesiredBoundsMode(ApplicationViewBoundsMode.UseVisible);
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
// Ensure the current window is active
Window.Current.Activate();
}
This will make sure your app stays inside the visible area, thus with no overlay from the bottom navigation bar.
I have created a RecyclerView adapter and I'm trying to start an activity when a row is clicked:
public override OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
MyViewHolder viewHolder = (MyViewHolder)holder;
viewHolder.MyView.Click += (sender, e) =>
{
var context = viewHolder.MyView.Context;
var intent = new Intent(context, typeof(DetailActivity));
context.StartActivity(intent);
}
}
When I click the first row it will take me to the activity like I want. If I scroll down so that the first row is rebound and then scroll back to the top again and then click the first row then my Click event fires twice. Once for the first row that was bound and then again for a row that was bound when I scrolled.
Is there an event you need to handle to unregister the click events?
I believe the standard pattern is to setup your clickhandlers in the constructor of the ViewHolder. Then in OnBindViewHolder, you update the Views/Data inside the ViewHolder.
Something like this (not compiled code):
Adapter:
public override OnBindViewHolder()
{
MyViewHolder viewHolder = (MyViewHolder)holder;
viewHolder.SetData(whatever data you care about);
}
MyViewHolder:
public MyViewHolder(View view) : base(view)
{
MainView = view;
MainView.Click += (sender, e) =>
{
var context = MainView.Context;
var intent = new Intent(context, typeof(DetailActivity));
context.StartActivity(intent);
}
}
Doing it this way keeps the Adapter cleaner by putting business logic in the ViewHolder, and also prevents your click handlers from being constantly setup and torn down as you scroll.