Skip to content

Instantly share code, notes, and snippets.

@georgejecook
Last active November 7, 2018 10:08
Show Gist options
  • Save georgejecook/f42f5ccc307d0aa82a25 to your computer and use it in GitHub Desktop.
Save georgejecook/f42f5ccc307d0aa82a25 to your computer and use it in GitHub Desktop.
ADVANCED XAMARIN FORMS TECHNIQUES FOR FLEXIBLE AND PERFORMANT CROSS PLATFORM APPS - PART 5, PAGE IN PAGE EMBEDDING.
public interface INavigationServiceProvider
{
/// <summary>
/// Toggles the toolbar visibility
/// </summary>
/// <param name="showing">If set to <c>true</c> showing.</param>
/// <param name="animated">If set to <c>true</c> animate.</param>
void ToggleToolbar (bool showing, bool animated = true);
/// <summary>
/// Presents the page modally.
/// </summary>
/// <param name="page">Page.</param>
/// <param name="animated">If set to <c>true</c> animated.</param>
/// <param name = "wrapInNavigationPage"></param>
Task PresentPage (Page page, bool animated = true, bool wrapInNavigationPage = false);
/// <summary>
/// Dismisses the presented page.
/// </summary>
/// <param name="animated">If set to <c>true</c> animated.</param>
void DismissPresentedPage (bool animated = true);
/// <summary>
/// Toggles the user interface locked by showing an overlay to stop any interaction
/// </summary>
/// <param name="isLocked">If set to <c>true</c> is locked.</param>
/// <param name="message">Message.</param>
void ToggleUILocked (bool isLocked, string message = "", bool showTryAgainButton = false, Action<object, EventArgs> tryAgainAction = null);
/// <summary>
/// Gets a value indicating whether this instance is tool bar showing.
/// </summary>
/// <value><c>true</c> if this instance is tool bar showing; otherwise, <c>false</c>.</value>
bool IsToolbarShowing { get; }
}
public interface INavigationService : INavigationServiceProvider
{
/// <summary>
/// Gets or sets the current page based on the view model.
/// Must be one of the main pages supported by the main navigation
/// </summary>
/// <value>The type of viewmodel for the target view.</value>
void NavigateToViewModel<TViewModel> (Action<TViewModel> vmInitializer = null) where TViewModel : class, IViewModel;
/// <summary>
/// Gets or sets the footer view.
/// </summary>
/// <value>The footer view.</value>
Xamarin.Forms.View FooterView { get; set; }
/// <summary>
/// Creates a page for the specified view model, and presents it modally
/// </summary>
/// <param name="animated">If set to <c>true</c> animated.</param>
/// <param name = "wrapInNavigationPage">Creates a navigation page to wrap the view model's page in</param>
Task PresentPage<TViewModel> (bool animated = true, bool wrapInNavigationPage = false) where TViewModel : class, IViewModel;
/// <summary>
/// Returns the page which is currently being presented, or null
/// </summary>
/// <returns>The page.</returns>
Page PresentedPage ();
/// <summary>
/// Returns the page which is currently set as the main page type
/// </summary>
/// <returns>The page.</returns>
Page CurrentPage { get; set; }
/// <summary>
/// Alert the specified title, message, accept and cancel.
/// </summary>
/// <param name="title">Title.</param>
/// <param name="message">Message.</param>
/// <param name="accept">Accept.</param>
/// <param name="cancel">Cancel.</param>
Task<bool> Alert (string title, string message, string accept, string cancel);
/// <summary>
/// Alert the specified title, message and cancel.
/// </summary>
/// <param name="title">Title.</param>
/// <param name="message">Message.</param>
/// <param name="cancel">Cancel.</param>
Task Alert (string title, string message, string cancel);
/// <summary>
/// Hides the popup.
/// </summary>
/// <param name="popupViewDetails">Popup view details.</param>
void HidePopup (PopupViewDetails popupViewDetails);
/// <summary>
/// Shows the popup.
/// </summary>
/// <param name="popupViewDetails">Popup view details.</param>
/// <param name = "hideAction"></param>
void ShowPopup (PopupViewDetails popupViewDetails, Action hideAction);
/// <summary>
/// Removes the view model from cache - this is useful for views that are not long lived; but are loaded from the
/// main navigation (such as play/record selfeo)
/// </summary>
/// <typeparam name="TViewModel">The 1st type parameter.</typeparam>
void RemoveViewModelFromCache<TViewModel> () where TViewModel : class, IViewModel;
/// <summary>
/// Returns a new navigation page with rootPage as the root
/// </summary>
/// <returns>The navigation page.</returns>
/// <param name="rootPage">Root page.</param>
Selfeo.Controls.NavigationPage GetNavigationPage (ContentPage rootPage);
}
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="TwinEvents.Core.Main.View.MainPage"
xmlns:controls="clr-namespace:TwinEvents.Core.Controls;assembly=TwinEvents" xmlns:gestures="clr-namespace:TwinTechs.Gestures;assembly=TwinTechsLib">
<controls:ExtendedAbsoluteLayout x:Name="MainLayout"
BackgroundColor="Black">
<controls:ExtendedAbsoluteLayout x:Name="MainContentLayout">
<controls:CustomTabPageHeader x:Name="HeaderView" />
<controls:SwipablePageContainer x:Name="PageViews"
VerticalOptions="FillAndExpand"
BackgroundColor="Black">
</controls:SwipablePageContainer>
</controls:ExtendedAbsoluteLayout>
<AbsoluteLayout x:Name="LoginOverlay"
IsVisible="false">
<AbsoluteLayout BackgroundColor="Black"
AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0.5,0.5,250,100">
<Button Text="Login with your google+ account"
x:Name="LoginButton"
Clicked="OnClickedLogin"
AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0.5,0.5,250,100"
BackgroundColor="Black"
TextColor="White" />
<Label Text="Uploading"
IsVisible="false"
x:Name="OverlayLabel"
TextColor="White"
XAlign="Center"
AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds="0.5,20,250,40"
BackgroundColor="Transparent" />
<ActivityIndicator IsRunning="false"
IsVisible="false"
x:Name="OverlaySpinner"
Color="White"
AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds="0.5,40,250,40" />
</AbsoluteLayout>
</AbsoluteLayout>
</controls:ExtendedAbsoluteLayout>
</ContentPage>
using System;
using System.Collections.Generic;
using Xamarin.Forms;
using TwinEvents.Core.Events.View;
using TwinEvents.Core.App.View;
using TwinTechs.Controls;
using TwinEvents.Core.Info.View;
using TwinEvents.Core.Media.View;
using TwinEvents.Core.Sessions.View;
using TwinTechs.Gestures;
using TwinEvents.Core.App.Logging;
using XLabs.Ioc;
using System.Windows.Input;
using TwinEvents.Core.Controls;
using System.Threading.Tasks;
using TwinEvents.Core.App;
namespace TwinEvents.Core.Main.View
{
public partial class MainPage : ContentPage, INavigationService
{
bool _isChildrenCreated;
ILog _log;
public MainPage ()
{
InitializeComponent ();
}
protected override void OnBindingContextChanged ()
{
if (ViewModel != null) {
//Main page is an exception becaut it IS the navigation service, so it was created before we set it
ViewModel.NavigationService = this;
}
base.OnBindingContextChanged ();
if (ViewModel != null && !_isChildrenCreated) {
AppHelper.NavigationService = this;
ViewFactory.NavigationService = this;
LoginOverlay.BackgroundColor = Color.FromRgba (0, 0, 0, 0.5);
var eventPage = (EventPage)ViewFactory.CreatePageWithExistingViewModel (ViewModel.EventPageVM);
var infoPage = (InfoPage)ViewFactory.CreatePageWithExistingViewModel (ViewModel.InfoPageVM);
var sessionsPage = (SessionsPage)ViewFactory.CreatePageWithExistingViewModel (ViewModel.SessionPageVM);
var mediaPage = (MediaPage)ViewFactory.CreatePageWithExistingViewModel (ViewModel.MediaPageVM);
_isChildrenCreated = true;
MainLayout.IsExclusivelyHandlingLayout = true;
MainLayout.OnLayoutChildren += MainLayout_OnLayoutChildren;
MainContentLayout.IsExclusivelyHandlingLayout = true;
MainContentLayout.OnLayoutChildren += MainContentLayout_OnLayoutChildren;
var pages = new Page[]{ eventPage, infoPage, sessionsPage, mediaPage };
PageViews.ButtonsView = HeaderView;
PageViews.ChildPages = new List<Page> (pages);
}
}
void MainLayout_OnLayoutChildren (bool isChanged, double x, double y, double width, double height)
{
if (isChanged) {
MainContentLayout.Layout (new Rectangle (0, 0, width, height));
if (_presentedContainer != null) {
_presentedContainer.Layout (new Rectangle (0, 0, width, height));
}
if (LoginOverlay.IsVisible) {
LoginOverlay.Layout (new Rectangle (0, 0, width, height));
}
}
}
void MainContentLayout_OnLayoutChildren (bool isChanged, double x, double y, double width, double height)
{
if (isChanged) {
double headerHeight = width > height ? 50 : 80;
HeaderView.Layout (new Rectangle (0, 0, width, headerHeight));
PageViews.Layout (new Rectangle (0, headerHeight, width, height - headerHeight));
}
}
protected override void OnAppearing ()
{
base.OnAppearing ();
ViewModel.OnViewAppearing ();
NavigationPage.SetHasNavigationBar (this, true);
}
MainPageVM ViewModel {
get {
return BindingContext as MainPageVM;
}
}
#region INavigationService implementation
Page _presentedPage;
PageViewContainer _presentedContainer;
public async Task PresentPage (Page page, bool animated = true, bool isOverlayAnimation = false)
{
if (_presentedPage != null) {
_log.Warn ("trying to present page when one is already presented - call dismiss first! Ignoring");
return;
}
if (page != null) {
_presentedPage = page;
if (page.BindingContext is BaseViewModel) {
((BaseViewModel)page.BindingContext).OnViewAppearing ();
}
var presentedContainer = new PageViewContainer ();
presentedContainer.Layout (new Rectangle (isOverlayAnimation ? 0 : Width, 0, Width, Height));
presentedContainer.Content = _presentedPage;
MainLayout.Children.Add (presentedContainer);
if (isOverlayAnimation) {
presentedContainer.Opacity = 0;
presentedContainer.FadeTo (1, 100);
} else {
var scale = MainContentLayout.ScaleTo (0.95, 400);
var fade = MainContentLayout.FadeTo (0.8, 400);
var move = presentedContainer.LayoutTo (new Rectangle (0, 0, Width, Height), 500, Easing.SpringIn);
await Task.WhenAll (scale, fade, move);
if (Bounds != presentedContainer.Bounds) {
presentedContainer.Layout (new Rectangle (0, 0, Width, Height));
}
}
_presentedContainer = presentedContainer;
}
}
bool _isDismissingPresentedPage;
public async Task DismissPresentedPage (bool animated = true, bool isOverlayAnimation = false)
{
if (_presentedPage != null && !_isDismissingPresentedPage) {
_isDismissingPresentedPage = true;
if (_presentedPage.BindingContext is BaseViewModel) {
((BaseViewModel)_presentedPage.BindingContext).OnViewDisappearing ();
}
if (isOverlayAnimation) {
await _presentedContainer.FadeTo (0, 100);
} else {
MainContentLayout.ScaleTo (1, 300);
MainContentLayout.FadeTo (1, 300);
await _presentedContainer.LayoutTo (new Rectangle (Width, 0, Width, Height), 400, Easing.SpringOut);
}
_presentedPage.BindingContext = null;
_presentedPage = null;
MainLayout.Children.Remove (_presentedContainer);
_presentedContainer.Content = null;
_isDismissingPresentedPage = false;
}
}
public async Task PresentPage<TViewModel> (bool animated = true) where TViewModel : class, IViewModel
{
var page = (Page)ViewFactory.CreatePage<TViewModel> ();
if (page != null) {
await PresentPage (page, animated);
} else {
_log.Error ("tried to show modal for non-registered page type");
}
}
public Page PresentedPage ()
{
return _presentedPage;
}
public void ToggleLoginOverlay (bool _isShowing)
{
LoginButton.IsVisible = _isShowing;
OverlaySpinner.IsVisible = false;
OverlaySpinner.IsRunning = false;
OverlayLabel.IsVisible = false;
LoginOverlay.IsVisible = _isShowing;
LoginOverlay.Layout (new Rectangle (0, 0, Width, Height));
MainLayout.RaiseChild (LoginOverlay);
}
public void ToggleBusyOverlay (bool _isShowing, string title = "")
{
OverlaySpinner.IsVisible = _isShowing;
OverlaySpinner.IsRunning = _isShowing;
OverlayLabel.IsVisible = _isShowing;
OverlayLabel.Text = title;
LoginButton.IsVisible = false;
LoginOverlay.IsVisible = _isShowing;
LoginOverlay.Layout (new Rectangle (0, 0, Width, Height));
MainLayout.RaiseChild (LoginOverlay);
}
#endregion
void OnClickedLogin (object sender, EventArgs ev)
{
ViewModel.DoLogin ();
}
}
}
//specialized class for showing a page within a page
public class PageViewContainer : View
{
public PageViewContainer ()
{
}
public static readonly BindableProperty ContentProperty = BindableProperty.Create<PageViewContainer,Page> (s => s.Content, null);
public Page Content {
get{ return (Page)GetValue (ContentProperty); }
set{ SetValue (ContentProperty, value); }
}
}
public class PageViewContainerRenderer : ViewRenderer<PageViewContainer,UIView>
{
public PageViewContainerRenderer ()
{
}
ViewControllerContainer _viewControllerContainer;
protected override void OnElementChanged (ElementChangedEventArgs<PageViewContainer> e)
{
base.OnElementChanged (e);
var pageViewContainer = e.NewElement as PageViewContainer;
if (_viewControllerContainer != null) {
_viewControllerContainer.ViewController = null;
_viewControllerContainer = null;
}
if (e.NewElement != null) {
_viewControllerContainer = new ViewControllerContainer (Bounds);
SetNativeControl (_viewControllerContainer);
}
}
Page _currentPage;
void ChangePage (Page page)
{
if (_currentPage == page) {
return;
}
//TODO call page dissapaering/appearing methods
if (page != null) {
var pageRenderer = page.GetRenderer ();
UIViewController viewController = null;
if (pageRenderer?.ViewController != null) {
viewController = pageRenderer.ViewController;
} else {
viewController = page.CreateViewController ();
}
var parentPage = Element.GetParentPage ();
var renderer = parentPage.GetRenderer ();
if (_viewControllerContainer == null) {
_viewControllerContainer = new ViewControllerContainer (Bounds);
SetNativeControl (_viewControllerContainer);
}
_viewControllerContainer.ParentViewController = renderer.ViewController;
_viewControllerContainer.ViewController = viewController;
_currentPage = page;
FixPageLayouts ();
SetNeedsLayout ();
} else {
_viewControllerContainer = null;
}
}
public override void LayoutSubviews ()
{
base.LayoutSubviews ();
var page = Element?.Content;
if (page != null) {
page.Layout (new Rectangle (0, 0, Bounds.Width, Bounds.Height));
}
_viewControllerContainer.Frame = Bounds;
}
protected override void OnElementPropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged (sender, e);
if (e.PropertyName == "Content" || e.PropertyName == "Renderer") {
Device.BeginInvokeOnMainThread (() => ChangePage (Element?.Content));
}
}
void FixPageLayouts ()
{
var contentPage = _currentPage as ContentPage;
if (contentPage != null) {
contentPage.Layout (new Rectangle (0, 0, Bounds.Width, Bounds.Height));
contentPage.ForceLayout ();
var layout = contentPage.Content as Layout<View>;
if (layout != null) {
layout.Layout (new Rectangle (0, 0, Bounds.Width, Bounds.Height));
layout.ForceLayout ();
}
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="TwinTechs.Example.Gestures.GestureYoutubeLikeExample"
xmlns:gestures="clr-namespace:TwinTechs.Gestures;assembly=TwinTechsForms"
xmlns:cells="clr-namespace:TwinTechs.Example.Gestures.Cells;assembly=TwinTechsFormsExample"
xmlns:local="clr-namespace:TwinTechs.Example.Gestures;assembly=TwinTechsFormsExample"
xmlns:controls="clr-namespace:TwinTechs.Controls;assembly=TwinTechsForms"
BackgroundColor="Silver"
Title="SwipeExample">
<local:SimpleLayout
BackgroundColor="Silver"
x:Name="MainLayout"
IsHandlingLayoutManually="true">
<ListView
BackgroundColor="Silver"
x:Name="MediaItemsListView"
SeparatorVisibility="None"
ItemSelected="OnItemSelected"
RowHeight="100">
<ListView.ItemTemplate>
<DataTemplate>
<cells:VideoCell />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<controls:PageViewContainer
BackgroundColor="Transparent"
x:Name="PageContainer" />
</local:SimpleLayout>
</ContentPage>
using System;
using System.Collections.Generic;
using Xamarin.Forms;
using TwinTechs.Gestures;
using System.Diagnostics;
namespace TwinTechs.Example.Gestures
{
public partial class GestureYoutubeLikeExample : ContentPage
{
Rectangle _contentBounds = new Rectangle (100, 200, 150, 150);
YoutubeStyleContentPage _contentPage;
PanGestureRecognizer _panGesture;
bool _didLayoutContainer;
public GestureYoutubeLikeExample ()
{
InitializeComponent ();
MainLayout.OnLayoutChildren += MainLayout_OnLayoutChildren;
MediaItemsListView.ItemsSource = DataProvider.GetMediaItems ();
}
protected override void LayoutChildren (double x, double y, double width, double height)
{
base.LayoutChildren (x, y, width, height);
if (_contentPage != null) {
_contentPage.ParentHeight = height;
}
}
void MainLayout_OnLayoutChildren (double x, double y, double width, double height)
{
MediaItemsListView.Layout (new Rectangle (0, 0, Width, Height));
if (!_didLayoutContainer) {
_contentBounds.Y = height - 100;
_contentBounds.X = width - 160;
_contentBounds.Width = 160;
_contentBounds.Height = 100;
_didLayoutContainer = true;
}
PageContainer.Layout (_contentBounds);
}
Rectangle _startBounds;
void Gesture_OnAction (BaseGestureRecognizer recgonizer, GestureRecognizerState state)
{
if (recgonizer.View != _contentPage.VideoPlayerView) {
return;
}
var panGesture = recgonizer as PanGestureRecognizer;
Point translation = panGesture.GetTranslationInView (MainLayout);
Point velocity = panGesture.GetVelocityInView (MainLayout);
panGesture.SetTranslationInView (new Point (0, 0), MainLayout);
switch (panGesture.State) {
case GestureRecognizerState.Began:
break;
case GestureRecognizerState.Changed:
var newY = _contentBounds.Y + translation.Y;
if (newY > 0 && newY < Height - _contentPage.MinimumHeightRequest) {
var minHeight = _contentPage.MinimumHeightRequest;
var minWidth = _contentPage.MinimumWidthRequest;
_contentBounds.Y = newY;
var complete = Math.Min (1, (Height - (_contentBounds.Y + minHeight)) / Height);
// Debug.WriteLine ("complete {0} newY {1} h{2}", complete, newY, Height);
var inverseCompletion = 1 - complete;
_contentBounds.X = (Width - minWidth) * inverseCompletion;
_contentBounds.Width = (minWidth) + ((Width - minWidth) * complete);
_contentBounds.Height = Math.Max (minHeight, (Height + minHeight) * complete);
PageContainer.Layout (_contentBounds);
}
break;
case GestureRecognizerState.Cancelled:
case GestureRecognizerState.Ended:
case GestureRecognizerState.Failed:
var isShowing = _contentBounds.Y < 200;
ToggleShowing (isShowing, true);
break;
default:
break;
}
}
protected override void OnDisappearing ()
{
base.OnDisappearing ();
if (_contentPage != null) {
_contentPage.VideoPlayerView.RemoveAllGestureRecognizers ();
}
}
void OnItemSelected (object sender, SelectedItemChangedEventArgs e)
{
ToggleShowing (true, true);
}
void ToggleShowing (bool isShowing, bool animated)
{
if (_contentPage == null) {
_contentPage = new YoutubeStyleContentPage ();
_contentPage.ParentHeight = Height;
PageContainer.Content = _contentPage;
_panGesture = new PanGestureRecognizer ();
_panGesture.OnAction += Gesture_OnAction;
_panGesture.IsConsumingTouchesInParallel = true;
_contentPage.VideoPlayerView.AddGestureRecognizer (_panGesture);
}
var minHeight = _contentPage.MinimumHeightRequest;
var minWidth = _contentPage.MinimumWidthRequest;
_contentBounds.Y = isShowing ? 0 : Height - minHeight;
_contentBounds.X = isShowing ? 0 : Width - minWidth;
_contentBounds.Width = isShowing ? Width : minWidth;
_contentBounds.Height = isShowing ? Height : minHeight;
if (MediaItemsListView.SelectedItem != null) {
_contentPage.Item = MediaItemsListView.SelectedItem as MediaItem;
}
if (animated) {
PageContainer.LayoutTo (_contentBounds);
} else {
PageContainer.Layout (_contentBounds);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment