Created
May 7, 2014 13:50
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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