Skip to content

Instantly share code, notes, and snippets.

@ChaseFlorell
Created May 7, 2014 13:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ChaseFlorell/fca61e6b0f40b1acac8f to your computer and use it in GitHub Desktop.
Save ChaseFlorell/fca61e6b0f40b1acac8f to your computer and use it in GitHub Desktop.
Just trying to figure out how to better structure a FragmentActivity in order to reduce "noise" in the RootView
using System;
using Cirrious.CrossCore;
using Cirrious.MvvmCross.Droid.Fragging;
using Cirrious.MvvmCross.Droid.Fragging.Fragments;
using Cirrious.MvvmCross.ViewModels;
namespace FutureState.BreathingRoom.Droid.Ui.Views
{
internal class FragmentActivityBase : MvxFragmentActivity
{
/// <summary>
/// Show a Fragment with an unpopulated ViewModel.
/// (Leverages <see cref="Mvx.IocConstruct{T}()"/> to construct the ViewModel on the fly.)
/// </summary>
/// <typeparam name="TFragment">The MvxFragment you wish to launch</typeparam>
/// <typeparam name="TViewModel">The IMvxViewModel you wish to use</typeparam>
/// <param name="frameLayout">The FrameLayout that the Fragment is being injected into</param>
/// <remarks>TViewModel is constructed on the fly.
/// If you wish to pass in a pre-populated ViewModel, use <see cref="ShowFragment{TFragment}(IMvxViewModel, int)"/></remarks>
/// <example>
/// ShowFragment{FooFragment, FooFragmentViewModel}(Resource.Id.ContentFrame);
/// </example>
protected void ShowFragment<TFragment, TViewModel>(int frameLayout)
where TFragment : MvxFragment
where TViewModel : IMvxViewModel
{
var fragment = (TFragment)Activator.CreateInstance(typeof(TFragment));
fragment.ViewModel = Mvx.IocConstruct<TViewModel>();
RunOnUiThread(() => SupportFragmentManager.BeginTransaction()
.Replace(frameLayout, fragment)
.AddToBackStack(null)
.Commit());
}
/// <summary>
/// Show a Fragment with a pre-populated ViewModel
/// </summary>
/// <typeparam name="TFragment">The MvxFragment you wish to launch</typeparam>
/// <param name="viewModel">The pre-populated ViewModel you wish to use</param>
/// <param name="frameLayout">The FrameLayout that the Fragment is being injected into</param>
/// <remarks> Use when you need to pass data into the ViewModel
/// If you wish to have an unpopulated ViewModel constructed, use <see cref="ShowFragment{TFragment, TViewModel}(int)"/>
/// </remarks>
/// <example>
/// var viewModel = Mvx.IocConstruct{FooViewModel}();
/// viewModel.Bar = "This is the bar string."
/// ShowFragment{FooFragment}(viewModel, Resource.Id.ContentFrame);
/// </example>
protected void ShowFragment<TFragment>(IMvxViewModel viewModel, int frameLayout) where TFragment : MvxFragment
{
var fragment = (TFragment)Activator.CreateInstance(typeof(TFragment));
fragment.ViewModel = viewModel;
RunOnUiThread(() => SupportFragmentManager.BeginTransaction()
.Replace(frameLayout, fragment)
.AddToBackStack(null)
.Commit());
}
protected void SetActionBarTitle(string title)
{
RunOnUiThread(() => ActionBar.Title = title);
}
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
using Android.App;
using Android.Content.PM;
using Android.Content.Res;
using Android.OS;
using Android.Support.V4.Widget;
using Android.Views;
using Android.Views.Animations;
using Android.Widget;
using Cirrious.CrossCore;
using Cirrious.MvvmCross.Binding.BindingContext;
using Cirrious.MvvmCross.Binding.Droid.Views;
using Cirrious.MvvmCross.Plugins.Messenger;
using FutureState.AppCore;
using FutureState.AppCore.Audio;
using FutureState.AppCore.Exceptions;
using FutureState.AppCore.Helpers;
using FutureState.AppCore.ProxyServices;
using FutureState.AppCore.Services;
using FutureState.AppCore.ViewModels;
using FutureState.BreathingRoom.Droid.Ui.Controls;
using FutureState.BreathingRoom.Droid.Ui.Fragments;
using FutureState.BreathingRoom.Droid.Ui.Fragments.User;
using FutureState.BreathingRoom.Droid.Ui.Helpers;
using Fragment = Android.Support.V4.App.Fragment;
using Uri = Android.Net.Uri;
namespace FutureState.BreathingRoom.Droid.Ui.Views
{
[Activity(ScreenOrientation = ScreenOrientation.Portrait)]
internal class RootView : FragmentActivityBase
{
private Guid _audioMediaId;
private RelativeLayout _audioPlayerLayout;
private DrawerToggler _drawerToggler;
private LinearLayout _leftDrawerLayout;
private ListView _leftDrawerList; // MvxListView if you want to have 'ItemClick" an ICommand
private int _leftDrawerPosition = -1; // make sure we don't have a position when we start.
private FsmIconTextView _leftDrawerQuickSyncIcon;
private FsmTextView _leftDrawerUserName;
private IMediaService _mediaService;
private IModuleService _moduleService;
private IMvxMessenger _mvxMessenger;
private FsmIconTextView _playPauseIcon;
private ListView _rightDrawerExtraStuffList;
private RelativeLayout _rightDrawerLayout;
private ListView _rightDrawerLifeStoryList;
private ListView _rightDrawerMastermindList;
private ScrollView _rightDrawerScrolLView;
private ListView _rightDrawerTryItList; // MvxListView if you want to have 'ItemClick" an ICommand
private DrawerLayout _rootViewLayout;
private FsmIconTextView _syncButton;
private Guid _textMediaId;
// ReSharper disable NotAccessedField.Local
private MvxSubscriptionToken _token; // unused on purpose... dont remove.
// ReSharper restore NotAccessedField.Local
private FsmTextView _userProfileButton;
private VimeoProxyService _vimeoService;
#region Overrides
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.RootView);
ResolveDependencies();
FindControls();
SetupBindings();
SetClickHandlers();
SetNavigationDrawerConfiguration();
FetchLocalDataAndShowFirstFragmentAsync();
}
public override void OnAttachFragment(Fragment fragment)
{
base.OnAttachFragment(fragment);
// if we navigate away from the ModuleFragment, we need to make sure to reset the left position
if (fragment.GetType() != typeof(ModuleFragment))
{
_leftDrawerPosition = -1;
}
if (fragment.GetType() != typeof(TextMediaFragment))
{
// reset the text media ID when you navigate away from a text fragment.
// this does not take into account when a user pressed the back button.
_textMediaId = Guid.Empty;
}
}
public override void OnBackPressed()
{
// if there's only one item remaining in the backstack
// let's just open the drawer instead of exiting the app
if (SupportFragmentManager.BackStackEntryCount <= 1)
{
_rootViewLayout.OpenDrawer(_leftDrawerLayout);
}
else
{
// otherwise just manage the back button as normal
base.OnBackPressed();
// since OnAttachFragment isn't triggered when a user presses Back
// we have to reset the text media ID here as well.
_textMediaId = Guid.Empty;
}
}
protected override void OnPostCreate(Bundle savedInstanceState)
{
base.OnPostCreate(savedInstanceState);
_drawerToggler.SyncState();
}
public override void OnConfigurationChanged(Configuration newConfig)
{
base.OnConfigurationChanged(newConfig);
_drawerToggler.OnConfigurationChanged(newConfig);
}
public override bool OnCreateOptionsMenu(IMenu menu)
{
MenuInflater.Inflate(Resource.Menu.ActionBarMenu, menu);
return base.OnCreateOptionsMenu(menu);
}
public override bool OnOptionsItemSelected(IMenuItem item)
{
// Close Drawer A when Drawer B is open, and Vice Versa
switch (item.ItemId)
{
// Handle Right Menu button click
case Resource.Id.ActionBarMenu_Media:
if (_rootViewLayout.IsDrawerOpen(_rightDrawerLayout))
{
// close the right drawer if it's already open
_rootViewLayout.CloseDrawer(_rightDrawerLayout);
}
else
{
// close the right drawer (if it's open) and open the left one
_rootViewLayout.CloseDrawer(_leftDrawerLayout);
_rootViewLayout.OpenDrawer(_rightDrawerLayout);
}
return true;
// Handle Left Menu button click
case Android.Resource.Id.Home:
if (_rootViewLayout.IsDrawerOpen(_leftDrawerLayout))
{
_rootViewLayout.CloseDrawer(_leftDrawerLayout);
}
else
{
_rootViewLayout.CloseDrawer(_rightDrawerLayout);
_rootViewLayout.OpenDrawer(_leftDrawerLayout);
}
return true;
}
// Handle your other action bar items...
return base.OnOptionsItemSelected(item);
}
#endregion
// todo: theme this http://stackoverflow.com/a/12824819/124069
public override ActionBar ActionBar
{
get
{
var actionBar = base.ActionBar;
actionBar.SetDisplayHomeAsUpEnabled(true); // makes the app icon go up a level instead of back
actionBar.SetHomeButtonEnabled(true);
actionBar.SetIcon(Resource.Drawable.breathingroom_man);
return actionBar;
}
}
public new RootViewModel ViewModel
{
get { return (RootViewModel)base.ViewModel; }
}
private void ResolveDependencies()
{
_mvxMessenger = Mvx.Resolve<IMvxMessenger>();
_moduleService = Mvx.Resolve<IModuleService>();
_mediaService = Mvx.Resolve<IMediaService>();
_vimeoService = Mvx.Resolve<VimeoProxyService>();
_token = _mvxMessenger.Subscribe<AudioPlayerMessage>(OnAudioPositionChanged);
}
/// <summary>
/// Initialize controls and ViewModel data.
/// </summary>
private void FindControls()
{
// Find RootView controls
_rootViewLayout = FindViewById<DrawerLayout>(Resource.Id.RootView_Layout);
// left drawer
_leftDrawerList = FindViewById<MvxListView>(Resource.Id._LeftDrawer_List);
_leftDrawerLayout = FindViewById<LinearLayout>(Resource.Id._LeftDrawer_DrawerLayout);
_leftDrawerUserName = FindViewById<FsmTextView>(Resource.Id._LeftDrawer_UserName);
_leftDrawerQuickSyncIcon = FindViewById<FsmIconTextView>(Resource.Id._LeftDrawer_QuickSync_Icon);
// right drawer
_rightDrawerTryItList = FindViewById<MvxListView>(Resource.Id._RightDrawer_TryItList);
_rightDrawerLifeStoryList = FindViewById<MvxListView>(Resource.Id._RightDrawer_LifeStoryList);
_rightDrawerMastermindList = FindViewById<MvxListView>(Resource.Id._RightDrawer_MastermindList);
_rightDrawerExtraStuffList = FindViewById<MvxListView>(Resource.Id._RightDrawer_ExtraStuffList);
_rightDrawerLayout = FindViewById<RelativeLayout>(Resource.Id._RightDrawer_DrawerLayout);
_playPauseIcon = FindViewById<FsmIconTextView>(Resource.Id._AudioPlayer_PlayIcon);
_audioPlayerLayout = FindViewById<RelativeLayout>(Resource.Id._MediaDrawerListItem_AudioPlayerLayout);
_rightDrawerScrolLView = FindViewById<ScrollView>(Resource.Id._RightDrawer_ScrollView);
_syncButton = FindViewById<FsmIconTextView>(Resource.Id._LeftDrawer_QuickSync_Icon);
_userProfileButton = FindViewById<FsmTextView>(Resource.Id._LeftDrawer_UserName);
}
/// <summary>
/// Create Mvx Bindings
/// </summary>
/// <remarks>Bindings for ListViews are still happening in the XML because I don't feel like writing a custom adapter</remarks>
private void SetupBindings()
{
var set = this.CreateBindingSet<RootView, RootViewModel>();
set.Bind(_leftDrawerUserName).For(v => v.Text).To(vm => vm.FullName);
set.Bind(_leftDrawerQuickSyncIcon).For(v => v.Command).To(vm => vm.QuickSyncCommand);
set.Bind(_leftDrawerQuickSyncIcon).For(v => v.Animate).To(vm => vm.AnimateSyncIcon);
set.Bind(_playPauseIcon).For(v => v.Text).To(vm => vm.PlayPauseIcon);
set.Bind(_playPauseIcon).For(v => v.Command).To(vm => vm.PlayAudioCommand);
set.Bind(_playPauseIcon).For(v => v.CommandParameter).To(vm => vm);
set.Apply();
}
/// <summary>
/// Grab data from the local database and populate the viewmodel
/// </summary>
private void FetchLocalDataAndShowFirstFragmentAsync()
{
// Grab View Data. Cannot be async since we trigger a select in this same run.
// todo: add a task that allows this to be async, and don't trigger the select until the task returns.
new Task(() =>
{
ViewModel.LeftNavigationList = _moduleService.Find();
SelectLeftNavigationListItem(0);
}).Start();
}
/// <summary>
/// All button click events for RootView controls go here.
/// </summary>
private void SetClickHandlers()
{
// drawer listview clicks
_leftDrawerList.ItemClick += (sender, args) => SelectLeftNavigationListItem(args.Position);
_rightDrawerTryItList.ItemClick += (sender, args) => SelectTryItNavItem(args.Position);
_rightDrawerLifeStoryList.ItemClick += (sender, args) => SelectLifeStoryNavItem(args.Position);
_rightDrawerMastermindList.ItemClick += (sender, args) => SelectMastermindNavItem(args.Position);
_rightDrawerExtraStuffList.ItemClick += (sender, args) => SelectExtraStuffNavItem(args.Position);
// button clicks
_syncButton.Click += (sender, args) => ViewModel.QuickSyncCommand.Execute(ViewModel);
// this particular click navigates away from this fragment.
_userProfileButton.Click +=
(sender, eventargs) =>
{
_leftDrawerPosition = -1;
// this ensures that next time the user clicks a nav item, they will be taken to the right spot.
var vm = Mvx.IocConstruct<EditProfileViewModel>();
Task.Factory.StartNew(() =>
{
vm.FirstName = App.CurrentUserModel.FirstName;
vm.Email = App.CurrentUserModel.Email;
});
ShowFragment<EditProfileFragment>(vm, Resource.Id.RootView_ContentFrame);
_rootViewLayout.CloseDrawer(_leftDrawerLayout);
};
}
/// <summary>
/// Navigation Drawer Configuration
/// </summary>
private void SetNavigationDrawerConfiguration()
{
// Left Drawer Config
_drawerToggler = new DrawerToggler(this,
_rootViewLayout,
Resource.Drawable.ic_drawer_light,
Resource.String.drawer_open,
Resource.String.drawer_close,
_leftDrawerLayout,
_rightDrawerLayout);
_drawerToggler.DrawerClosed += delegate { InvalidateOptionsMenu(); };
_drawerToggler.DrawerOpened += delegate
{
_leftDrawerList.SetItemChecked(_leftDrawerPosition, true);
InvalidateOptionsMenu();
};
_rootViewLayout.SetDrawerShadow(Resource.Drawable.drawer_shadow_dark, (int)GravityFlags.Left);
// left drawer shadow
_rootViewLayout.SetDrawerShadow(Resource.Drawable.right_drawer_shadow_dark, (int)GravityFlags.Right);
// right drawer shadow
_rootViewLayout.SetDrawerListener(_drawerToggler);
}
/// <summary>
/// Update the view model with the current postion of the audio player.
/// </summary>
/// <param name="audioPlayerMessage">message being sent back from the Mvx.Messenger plugin </param>
private void OnAudioPositionChanged(AudioPlayerMessage audioPlayerMessage)
{
if ((ViewModel.CurrentPositionMsec == audioPlayerMessage.CurrentPositionMsec) ||
(audioPlayerMessage.PlayState != PlayState.Playing &&
audioPlayerMessage.PlayState != PlayState.Completed)) return;
if (audioPlayerMessage.CurrentPositionMsec >= ViewModel.DurationMsec ||
audioPlayerMessage.PlayState == PlayState.Completed)
{
if (ViewModel.StopCommand.CanExecute(ViewModel))
{
ViewModel.CurrentPositionMsec = 0;
}
}
else
{
ViewModel.CurrentPositionMsec = audioPlayerMessage.CurrentPositionMsec;
}
}
/// <summary>
/// Triggered when an Item is selected in the <see cref="_leftDrawerList"/>.
/// </summary>
/// <param name="position">The zero based position of the selected list item</param>
private void SelectLeftNavigationListItem(int position)
{
if (_leftDrawerPosition != position)
{
ThreadPool.QueueUserWorkItem(state =>
{
_leftDrawerPosition = position;
// Clear the back stack
for (var i = 0; i < SupportFragmentManager.BackStackEntryCount; i++)
{
SupportFragmentManager.PopBackStack();
}
var vm = Mvx.IocConstruct<ModuleViewModel>();
vm.Id = ViewModel.LeftNavigationList[position].Id; // Required for fetching data in the Fragment
vm.Number = ViewModel.LeftNavigationList[position].Number;
vm.Title = ViewModel.LeftNavigationList[position].Title;
vm.Description = ViewModel.LeftNavigationList[position].Description;
ViewModel.RightNavigationList = _mediaService.FindModuleMediaByModuleId(vm.Id);
ShowFragment<ModuleFragment>(vm, Resource.Id.RootView_ContentFrame);
SetActionBarTitle(ViewModel.LeftNavigationList[position].Title);
});
}
_rootViewLayout.CloseDrawer(_leftDrawerLayout);
}
/// <summary>
/// Make the Audio Player animate in
/// </summary>
private void RevealAudioPlayer()
{
RunOnUiThread(() =>
{
if (_audioPlayerLayout.Visibility == ViewStates.Visible) return;
var slideInAnimation = AnimationUtils.LoadAnimation(this, Resource.Animation.slide_in);
_audioPlayerLayout.Visibility = ViewStates.Visible;
_audioPlayerLayout.StartAnimation(slideInAnimation);
var scrollViewLayoutParams = (RelativeLayout.LayoutParams)_rightDrawerScrolLView.LayoutParameters;
scrollViewLayoutParams.BottomMargin = (int)(80f * (Resources.DisplayMetrics.Density));
_rightDrawerScrolLView.LayoutParameters = scrollViewLayoutParams;
});
}
#region Module Section Specific Select Handlers
/// <summary>
/// Selected Navigation Item for the "Try It" section
/// </summary>
/// <param name="position">selected position within the list</param>
private void SelectTryItNavItem(int position)
{
ThreadPool.QueueUserWorkItem(state =>
{
var model = ViewModel.TryItMediaList[position];
SelectMediaItem(model);
});
}
/// <summary>
/// Selected Navigation Item for the "Life Story" section
/// </summary>
/// <param name="position">selected position within the list</param>
private void SelectLifeStoryNavItem(int position)
{
ThreadPool.QueueUserWorkItem(state =>
{
var model = ViewModel.LifeStoryMediaList[position];
SelectMediaItem(model);
});
}
/// <summary>
/// Selected Navigation Item for the "Mastermind" section
/// </summary>
/// <param name="position">selected position within the list</param>
private void SelectMastermindNavItem(int position)
{
ThreadPool.QueueUserWorkItem(state =>
{
var model = ViewModel.MastermindMediaList[position];
SelectMediaItem(model);
});
}
/// <summary>
/// Selected Navigation Item for the "Extra Stuff" section
/// </summary>
/// <param name="position">selected position within the list</param>
private void SelectExtraStuffNavItem(int position)
{
ThreadPool.QueueUserWorkItem(state =>
{
var model = ViewModel.ExtraStuffMediaList[position];
SelectMediaItem(model);
});
}
/// <summary>
/// Triggered when an Item is selected in one of the Media Section Lists.
/// </summary>
/// <param name="model">The media model of the selected item</param>
/// <remarks>This is happening in a thread pool</remarks>
private void SelectMediaItem(MediaViewModel model)
{
switch (model.MediaType.Name)
{
case "Audio":
TriggerAudioMedia(model);
break;
case "Video":
TriggerVideoMedia(model);
break;
case "Text":
TriggerTextMedia(model);
break;
default:
throw new MediaTypeNotSupportedException();
}
}
private void TriggerAudioMedia(MediaViewModel model)
{
if (_audioMediaId == model.Id) return;
_audioMediaId = model.Id;
// todo: I don't think we need a MediaObject on the RootViewModel
// however we're currently using it to init the audio track
// must find a better way.
ViewModel.Media = model;
// stop an existing track if the player has one
if (ViewModel.StopCommand.CanExecute())
ViewModel.StopCommand.Execute(ViewModel);
// start new track
if (ViewModel.PlayAudioCommand.CanExecute())
ViewModel.PlayAudioCommand.Execute(ViewModel);
RevealAudioPlayer();
}
private void TriggerVideoMedia(MediaViewModel model)
{
if (ViewModel.StopCommand.CanExecute())
ViewModel.StopCommand.Execute(ViewModel);
var videoId = model.FileLocation.ParseVimeoId();
var videoInfo = _vimeoService.GetVideoInfo(videoId);
var uri = Uri.Parse(videoInfo.Files[0].Link);
var mimeType = videoInfo.Files[0].Type;
this.Intent(uri, mimeType).AsActionView().StartIntent();
}
private void TriggerTextMedia(MediaViewModel model)
{
if (_textMediaId != model.Id)
{
_textMediaId = model.Id;
ShowFragment<TextMediaFragment>(model, Resource.Id.RootView_ContentFrame);
}
RunOnUiThread(() => _rootViewLayout.CloseDrawer(_rightDrawerLayout));
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment