How can a scroll a specific row or section to the top of a Xamarin.Forms.ListView - xamarin

I have a Xamarin.Forms.ListView that contains events that are grouped by date. There are events that occur in future and events that occur in the past.
Users would like to have their screen load with a future event closest to the current date in view so that they do not need to manually scroll down to view it.
What options do I have with a Xamarin.Forms.ListView to accomplish this for iOS and Android users?

I have made some progress. I am able to accomplish my goal in iOS by creating a CustomListView and an iOS render to support it.
In Xamarin.Forms you create a CustomListView then after you have loaded the list you an call ScrollToRow(item,section) to manually scroll to the row you need.
In iOS the renderer maps the method to UITableView message ScrollToRow(...);
For Android I still need to create the renderer but I do know that I need to map to the calls getListView().setSelection(...); or getListView().smoothScrollToPosition(...);
I am sure there is a more elegant way to do this but for now it is getting the job done
Source For: Common.CustomListView
using System;
using Xamarin.Forms;
namespace Common {
public class CustomListView : ListView {
public Action<int, int, bool> ScrollToRowDelegate { get; set; }
public void ScrollToRow(int itemIndex, int sectionIndex = 0, bool animated = false) {
if (ScrollToRowDelegate != null) {
ScrollToRowDelegate (itemIndex, sectionIndex, animated);
}
}
}
}
iOS Renderer Source:YourApplication.iOS.Renderers.CustomListViewRenderer
using System;
using Xamarin.Forms.Platform.iOS;
using Xamarin.Forms;
using Common;
using MonoTouch.UIKit;
using MonoTouch.Foundation;
using YourApplication.iOS.Renderers;
[assembly: ExportRenderer (typeof(CustomListView), typeof(CustomListViewRenderer))]
namespace YourApplication.iOS.Renderers
{
public class CustomListViewRenderer : ListViewRenderer
{
protected override void OnModelSet (VisualElement view) {
base.OnModelSet (view);
var listView = view as CustomListView;
listView.ScrollToRowDelegate = (itemIndex, sectionIndex, animated) => {
ScrollToRow(itemIndex, sectionIndex, animated);
};
}
private void ScrollToRow(int itemIndex, int sectionIndex, bool animated) {
var tableView = this.Control as UITableView;
var indexPath = NSIndexPath.FromItemSection (itemIndex, sectionIndex);
tableView.ScrollToRow (indexPath, UITableViewScrollPosition.Top, animated);
}
}
}

Related

Xamarin: detect page pushed on NavigationRenderer

I order to apply some navigationBar properties (like as the background image) for different page, I think to have a condition on my custom NavigationRenderer.
My idea is to have some condition like (in my working code)
public class CustomNavigationRenderer : NavigationRenderer
{
public override void ViewDidLoad()
{
base.ViewDidLoad();
if (pagePushed is 1)
{
NavigationBar.SetBackgroundImage(new UIImage(), UIBarMetrics.Default);
NavigationBar.ShadowImage = new UIImage();
}
else (ahother page){
var img = UIImage.FromBundle("MyImage");
NavigationBar.SetBackgroundImage(img, UIBarMetrics.Default);
}
}
}
that allows me to have at least a condition to apply a different navigation properties. Another way is to have 2 Navigationrenderer class but I think is not possible.
Any idea to how do that?
If you look at the source code for NavigationRenderer here, you will notice there are quite a few methods and callbacks you can take advantage of.
I would suggest you can do something like this:
1) Code for your custom NavigationRenderer (iOS project, you will have to do something similar on Android):
using System.Threading.Tasks;
using MyProject.iOS;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(NavigationPage), typeof(NavRenderer))]
namespace MyProject.iOS
{
public class NavRenderer : NavigationRenderer
{
protected override async Task<bool> OnPushAsync(Page page, bool animated)
{
var result = await base.OnPushAsync(page, animated);
if(result)
{
if (page is IMyPageType1)
{
NavigationBar.SetBackgroundImage(new UIImage(), UIBarMetrics.Default);
NavigationBar.ShadowImage = new UIImage();
}
else if(page is IMyPageType2)
{
var img = UIImage.FromBundle("MyImage");
NavigationBar.SetBackgroundImage(img, UIBarMetrics.Default);
}
}
return result;
}
}
}
2) Based on the code above, you need to add two interfaces. These should be located in the same project / dll where your Pages are located (all your Xamarin.Forms UI):
public interface IMyPageType1
{
}
public interface IMyPageType2
{
}
3) Now everything that's remaining is implement the interfaces on the pages where you need it. For example:
public partial class MyPage1 : ContentPage, IMyPageType1
{
//...
}
From here, possibilities are endless! You can add for example a method to IMyPageType1 that would return a color, and then inside your renderer, once you know the page being pushed is implementing IMyPageType1, you can call the method and get the color to use.

Xamarin Form - Add png icon in toolbar with color

This is my code
ToolbarItems.Add(new ToolbarItem("User", "userAvatar.png", async () => {
await Navigation.PopToRootAsync();
}));
It's not working. It's place a masked single color image instead a png in colors.
I'm trying to archive something like this...
Any clue ?
I was going mad about this issue once, too (my situation was a bit more subtle, I had a plain and a colored verion of the icon and was wondering why the heck the colored icon would not be loaded) and unfortunately it's not that easy to overcome.
The icons being monochrome is the default behavior for iOS apps and Xamarin.Forms implements this behavior. According to this post you'll need a custom renderer to show colored icons in the navigation bar.
Edit
According to this post, you'll have to set the UIImageRenderingMode for the respective images in your custom renderer
image = image.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
using the renderer implementation from this answer, it should be something in the line of
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(TabbedPage), typeof(MyProject.iOS.Renderers.IosMainMenuRenderer))]
namespace MyProject.iOS.Renderers {
public class IosMainMenuRenderer : TabbedRenderer {
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
var items = TabBar.Items;
for (var i = 0; i < items.Length; i++) {
items[i].Image = items[i].ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
items[i].SelectedImage = items[i].SelectedImage.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
}
}
}
}
but I have not tested this!
For a color logo on the right on all my navigation pages I used this custom renderer:
[assembly: ExportRenderer(typeof(CustomNavigationPage), typeof(CustomNavigationRenderer))]
namespace App.iOS
{
public class CustomNavigationRenderer : NavigationRenderer
{
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
var logo = NavigationBar.TopItem.RightBarButtonItem.Image;
if (logo == null) return;
if (logo.RenderingMode == UIImageRenderingMode.AlwaysOriginal)
{
return;
}
NavigationBar.TopItem.RightBarButtonItem.Image = logo.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
}
}
}
The easiest way is to not use a toolbar, instead, set the type of this page as a Modal when navigating to it(Using Navigation.PushModal()). And add a horizontal LinearLayout that will act as the toolbar.

How to create a code-only webview with Xamarin.Forms

I'm trying to use a library that doesn't has a .Net SDK, but as I want to use it only to return a string, I thought I could use it's JS SDK by creating a custom WebView that returns strings (https://xamarinhelp.com/xamarin-forms-webview-executing-javascript/).
The first problem that I faced was that a CustomRenderer is not called in Xamarin.Forms until the View is added to a Page (or at least I couldn't make it be called). To fix this I added a call to Platform.CreateRenderer in each platform.
It did the trick and the CustomRenderer executed. But when I tried to call a JS function to retrieve a string, the app just hung and stayed that way.
I didn't try to insert the WebView in a Page because I want it to be independent of the page that the app is current on, and as I want a "code-only" html, I don't see the point of adding it somewhere.
My classes:
JSEvaluator
namespace MyNamespace.Views
{
public class JSEvaluator : WebView
{
public static BindableProperty EvaluateJavascriptProperty = BindableProperty.Create(nameof(EvaluateJavascript), typeof(Func<string, Task<string>>), typeof(JSEvaluator), null, BindingMode.OneWayToSource);
public Func<string, Task<string>> EvaluateJavascript
{
get { return (Func<string, Task<string>>)GetValue(EvaluateJavascriptProperty); }
set { SetValue(EvaluateJavascriptProperty, value); }
}
public JSEvaluator()
{
}
}
}
UWP Renderer
[assembly: ExportRenderer(typeof(JSEvaluator), typeof(JSEvaluatorRenderer))]
namespace MyNamespace.UWP.Renderers
{
public class JSEvaluatorRenderer : WebViewRenderer
{
public JSEvaluatorRenderer() { }
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
var webView = e.NewElement as JSEvaluator;
if (webView != null)
webView.EvaluateJavascript = async (js) =>
{
return await Control.InvokeScriptAsync("eval", new[] { js });
};
}
}
}
Creation and use
if (jsEvaluator == null)
{
jsEvaluator = new JSEvaluator { Source = new HtmlWebViewSource { Html = HTML.html } };
#if __ANDROID__
Xamarin.Forms.Platform.Android.Platform.CreateRenderer(jsEvaluator);
#elif __IOS__
Xamarin.Forms.Platform.iOS.Platform.CreateRenderer(jsEvaluator);
#elif WINDOWS_UWP
Xamarin.Forms.Platform.UWP.Platform.CreateRenderer(jsEvaluator);
#endif
}
Thanks for the help :)
I had to add the WebView to a page, as #SushiHangover said in the comment. With this done, it worked as expected.

Xamarin.Forms custom Android NavigationPageRenderer title and subtitle

Currently working on a project where I want to use AppCompat and have a requirement setting title and subtitle on most of the pages.
It doesn't work using AppCompat at all - neither setting the properties nor using a custom view.
When not using AppCompat both works as expected. The full source code is available here so just run the app if you're curious :)
using System.ComponentModel;
using Android.App;
using Android.Widget;
using App1.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
#if __APPCOMPAT__
using NavigationRenderer = Xamarin.Forms.Platform.Android.AppCompat.NavigationPageRenderer;
#else
using NavigationRenderer = Xamarin.Forms.Platform.Android.NavigationRenderer;
#endif
[assembly: ExportRenderer(typeof(NavigationPage), typeof(NavigationPageRenderer))]
namespace App1.Droid.Renderers
{
public class NavigationPageRenderer : NavigationRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<NavigationPage> e)
{
base.OnElementChanged(e);
SetCustomView(e.NewElement.CurrentPage.GetType().Name);
}
private void SetCustomView(string view)
{
var activity = (Activity)Context;
#if __APPCOMPAT__
var actionBar = ((FormsAppCompatActivity)Context).SupportActionBar;
#else
var actionBar = activity.ActionBar;
#endif
actionBar.Title = view;
actionBar.Subtitle = " -> " + view;
var abv = new LinearLayout(activity)
{
Orientation = Orientation.Vertical
};
var main = new TextView(activity)
{
Text = view,
};
main.SetTextColor(Color.Aqua.ToAndroid());
main.SetPadding(4, 4, 2, 6);
abv.AddView(main);
abv.AddView(new TextView(activity)
{
Text = " -> " + view
});
actionBar.SetDisplayShowCustomEnabled(true);
actionBar.CustomView = abv;
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName.Equals("CurrentPage"))
{
SetCustomView(((NavigationPage)sender).CurrentPage.GetType().Name);
}
}
}
}
Edit: Thanks #jimmgarr. Altered the code slightly to keep alternating between AppCompbat and "normal mode". The code is available here
So it looks like the NavigationPage uses its own Toolbar instance. That's why setting the properties on SupportActionBar isn't doing anything.
I was able to get it working by overriding OnViewAdded() to get a reference to the new Toolbar when it's added:
public override void OnViewAdded(Android.Views.View child)
{
base.OnViewAdded(child);
if (child.GetType() == typeof(Support.Toolbar))
toolbar = (Support.Toolbar)child;
}
Then using the reference inside SetCustomView() to set only the Subtitle since the Title is already set automatically.
Here's the complete renderer class :)

How to show iOS add contact screen from Xamarin Forms?

I'm trying to show the iOS add contact screen using Xamarin Forms. From what I can see Xamarin Forms does not support this out of the box but Xamarin iOS does. Unfortunately I can't get them to work together. What I mean by "together" is that I need get access to NavigationController from Xamarin Forms Page.
Can this be done?
I have a sample solution that demonstrates the problem here: https://github.com/pawelpabich/XamarinFormsContacts. I also put the most important code below.
public void ShowContact(NavigationPage page)
{
var newPersonController = new ABNewPersonViewController();
var person = new ABPerson();
person.FirstName = "John";
person.LastName = "Doe";
newPersonController.DisplayedPerson = person;
var controller = page.CreateViewController();
//!!!!---> controller.NavigationController is null !!!!!<----
controller.NavigationController.PushViewController(newPersonController, true);
}
I updated the repo and it now contains code that works.
There is a UINavigationController when using Xamarin.Forms (when using a NavigationPage), but you have to search for it. This was the only way I could get a hold of it. Those other methods, CreateViewController and RendererFactory actually create a new ViewController which isn't what you wanted.
public void ShowContact(NavigationPage page)
{
var newPersonController = new ABNewPersonViewController();
var person = new ABPerson();
person.FirstName = "John";
person.LastName = "Doe";
newPersonController.Title = "This is a test";
newPersonController.DisplayedPerson = person;
UINavigationController nav = null;
foreach (var vc in
UIApplication.SharedApplication.Windows[0].RootViewController.ChildViewControllers)
{
if (vc is UINavigationController)
nav = (UINavigationController)vc;
}
nav.PresentModalViewController(new UINavigationController (newPersonController), true);
}
I also attempted to Create a PersonPage and PersonPageRenderer, as that would be the cleanest solution, but I couldn't get it working. This could work if you spent some time.
[assembly: ExportRenderer(typeof(PersonPage), typeof(PersonPageRenderer))]
public class PersonPageRenderer : ABNewPersonViewController, IVisualElementRenderer, IDisposable, IRegisterable
Pawel, the problem is that when you use Xamarin.Forms no NavigationController is created (as I know at least in X.F 1.3+, maybe Michael will prove me wrong). If you want to create new address boo element you can use this approach - How do you add contacts to the iPhone Address book with monotouch?
Because iOS Add Contact screen is a Native iOS API and your application logic is in a PCL you need to use a DependancyService.
1) To do this in the PCL create a Interface which provides the functionality, like
public interface ILocalAddContact
{
void DisplayContactScreen(Contact contact)
}
2) Implement the Interface in the Native Applications:
public class LocalAddContactiOS : ILocalAddContact
{
public void DisplayContactScreen(Contact contact)
{
//... do iOS Magic
}
}
3) Register the Dependancy in the Top of the Native File
[assembly: Xamarin.Forms.Dependency(typeof(LocalAddContactiOS))]
4) Obtain the Dependancy from the iOS Project from the
var addContact = DependencyService.Get<ILocalAddContact> ();
addContact.DisplayContactScreen (contact);
If you take a look at this sample application on github, it's very similar (but is used for CreateCalendar).
Ok, this is how I finally implemented. I created UINavigationController manually and use it for navigations outside Xamarin.Forms.
using System;
using System.Collections.Generic;
using System.Linq;
using Foundation;
using UIKit;
using Xamarin.Forms;
using AddressBookUI;
using AddressBook;
namespace TestContacts.iOS
{
[Register ("AppDelegate")]
public partial class AppDelegate : UIApplicationDelegate
{
UIWindow window;
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
global::Xamarin.Forms.Forms.Init ();
window = new UIWindow(UIScreen.MainScreen.Bounds);
var nav =
new UINavigationController(new App ().MainPage.CreateViewController());
ContactsShared.Instance = new TouchContacts (nav);
window.RootViewController = nav;
window.MakeKeyAndVisible();
return true;
}
}
public class TouchContacts : IContactsShared {
UINavigationController nav;
public TouchContacts(UINavigationController nav){
this.nav = nav;
}
public void Show() {
var newPersonController = new ABNewPersonViewController();
newPersonController.NewPersonComplete +=
(object sender, ABNewPersonCompleteEventArgs e) =>
{
nav.PopViewController(true);
};
var person = new ABPerson();
person.FirstName = "John";
person.LastName = "Doe";
newPersonController.DisplayedPerson = person;
nav.PushViewController(newPersonController, true);
}
}
}

Resources