I've created a small app that has three fragments for top-level navigation through a BottomNavigationView. If you launch the app and click on a navigation button on the bottom nav, you are presented with an up button in the action bar. Here is the code for the activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
setSupportActionBar(toolbar)
val navController = navHostFragment.findNavController()
setupActionBarWithNavController(this, navController)
setupWithNavController(bottomNav, navController)
}
override fun onSupportNavigateUp(): Boolean
= findNavController(navHostFragment).navigateUp()
}
Here is a screenshot of the result. The app is launched on the home screen and all I've done is simply click the profile button from the BottomNavigationView.
I've tried listening to the BottomNavigationView's item selections and navigating manually using different NavOptions to no avail. Is there anything we can do to avoid showing an up button in the action bar while the user is navigating with a BottomNavigationView?
Starting with 1.0.0-alpha07 you can use AppBarConfiguration to configure that behaviour.
AppBarConfiguration has a Builder constructor so you can create a new Builder with a specific set of top level destinations, referenced by their id (this id is the one you set on your navigation layout).
Create new AppBarConfiguration:
val appBarConfiguration = AppBarConfiguration
.Builder(
R.id.navigationHomeFragment,
R.id.navigationListFragment,
R.id.navigationProfileFragment)
.build()
Then, instead of setupActionBarWithNavController(this, navController) you need to call setupActionBarWithNavController(this, navController, appBarConfiguration)
This is the right way to handle top navigation behaviours.
I had the same problem and I found a way to get the current fragment with the NavController.addOnNavigatedListener, so I applied the following logic within my Activity and for now it works for me
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
val navHost = supportFragmentManager
.findFragmentById(R.id.navHostFragment) as NavHostFragment
navHost.navController.addOnNavigatedListener { _, destination ->
val showButton = showUpButton(destination.id)
//Here occurs the magic
supportActionBar?.setDisplayShowHomeEnabled(showButton)
supportActionBar?.setDisplayHomeAsUpEnabled(showButton)
}
}
//Check according to your convenience which fragment id
//should show or hide the "Up Button"
private fun showUpButton(id: Int): Boolean {
return id != R.id.your_home_fragment && id != R.id.your_list_fragment
&& id != R.id.your_profile_fragment
}
And this is my project...
I do not know if it is the best option but if someone has any better suggestion, it will be welcome
Use this instead of setupWithNavController:
navController.addOnNavigatedListener(new NavController.OnNavigatedListener() {
#Override
public void onNavigated(#NonNull NavController controller, #NonNull NavDestination destination) {
toolbar.setTitle(destination.getLabel());
}
});
Or the equivalent in Kotlin.
The only thing setupWithNavController does is add a listener to change the toolbar title, and creates the up button. If you don't want the up button, just add a listener which changes the toolbar title only.
You could set the navigation icon to null, or change the icon only for some destinations
eg:
1) In Kotlin:
navController.addOnDestinationChangedListener { _, destination, _ ->
toolbar.title = destination.label
toolbar.navigationIcon = null
}
2) In Java:
hostFragment.getNavController().addOnDestinationChangedListener(new OnDestinationChangedListener() {
public final void onDestinationChanged(#NotNull NavController controller, #NotNull NavDestination destination, #Nullable Bundle arguments) {
toolbar.setTitle(destination.getLabel());
toolbar.setNavigationIcon(null);
}
});
If you are using NavigationUI.setupWithNavController check out onNavDestinationSelected.
Inside your bottom navigation view menu.xml, add android:menuCategory="secondary" to the corresponding MenuItem:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/profileFragment"
android:icon="#drawable/bottom_nav_icon_profile"
android:menuCategory="secondary"
android:title="#string/profile" />
In Java this works for me:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
navController = Navigation.findNavController(this, R.id.nav_host_fragment);
AppBarConfiguration appBarConfiguration = new AppBarConfiguration
.Builder(navController.getGraph())
.build();
NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration);
navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
toolbar.setTitle(destination.getLabel());
toolbar.setNavigationIcon(null);
});
Related
I have a textview and a button on screen. User taps on the text view which shows up keyboard, user types some text and then clicks on the button whose onclick is bound to a command defined in the view model.
I want to dismiss keyboard from view model by sending a message or calling a method in the view but still want to keep loose coupling between view and view model. I see mvvmmessenger, mvxinteraction etc to accomplish this. What is the best way to handle this?
Don't know if this is the best way to handle this, but hopefully the way i did it can help you out.
I made an interface called IPlatformAction in the Core project, which implements a method called DismissKeyboard.
public interface IPlatformAction
{
void DismissKeyboard();
}
Then I made a service called PlatformActionService in the Droid project, that implements that interface.
public class PlatformService : IPlatformAction
{
protected Activity CurrentActivity =>
Mvx.IoCProvider.Resolve<IMvxAndroidCurrentTopActivity>().Activity;
public void DismissKeyboard()
{
var currentFocus = CurrentActivity.CurrentFocus;
if (currentFocus != null)
{
InputMethodManager inputMethodManager = (InputMethodManager)CurrentActivity.GetSystemService(Context.InputMethodService);
inputMethodManager.HideSoftInputFromWindow(currentFocus.WindowToken, HideSoftInputFlags.None);
}
}
}
Lastly, I inject the interface in the viewmodel and call the dismiss keyboard method
public class SomeViewModel : BaseViewModel<SomeModel>
{
private readonly IPlatformAction _platformAction;
public SomeViewModel(IPlatformAction platformAction)
{
_platformAction = platformAction;
}
public async Task DoSomething()
{
//Some code
_platformAction.DismissKeyboard();
}
}
Currently I have strange problems that remind me of working with the WPF Dispatcher from earlier times.
In WPF my workaround was to increase the dispatcher invoke priority.
In Xamarin Android I work around this issue by calling
uiProgressBar.Invalidate(); uiProgressBar.RequestLayout();
at specific stages to force and update.
A short description of my scenario:
I am using a ListView. Every list element has a ProgressBar. When I start updating serveral ProgressBar's in parallel the GUI descides to stop updating the progress after some time but keeps responsive to touch events, pan, etc.
I am working with async await.
I have also noticed that the problem mainly occures when using Release Mode with the option "Bundle assemblies into native code".
Hopefully someone can help me, or tell me if this is a bug or expected behavior.
Thank you for help in advance.
Hopefully someone can help me, or tell me if this is a bug or expected behavior. Thank you for help in advance.
For any data changes inside a ListView, you should use adapter.NotifyDataSetInvalidated(); to let ListView update all the items. I don't see your detailed codes, so what I can provide is just an Example:
ProgressBarAdapter.cs:
public class ProgressBarAdapter:BaseAdapter
{
Context _context;
public List<Model> _items;
public ProgressBarAdapter(Context context,List<Model> items)
{
_context = context;
_items = items;
}
public override int Count => _items.Count;
public override Java.Lang.Object GetItem(int position)
{
return _items[position];
}
public override long GetItemId(int position)
{
return _items[position].id;
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
ProgressBar bar = null;
if (convertView != null)
{
bar = convertView as ProgressBar;
}
else
{
bar=(ProgressBar)(_context as MainActivity).LayoutInflater.Inflate(Resource.Layout.ListViewItem,null);
}
bar.Progress = _items[position].value;
return bar;
}
}
If you want to change the data, instead of accessing the Android View directly, you should simply change the _items of Adapter:
adapter._items= InitList(20);
private List<Model> InitList(int start=0)
{
List<Model> list = new List<Model>();
for (int i = start; i < 50; i++)
{
list.Add(new Model {
id=i,
value=i
});
}
return list;
}
And call NotifyDataSetInvalidated:
adapter.NotifyDataSetInvalidated();
I have a problem...I have MvxGridView with Menu items and with ItemClick ShowMenuCommand
Like this:
private ICommand _showMenuCommand;
public ICommand ShowMenuCommand
{
get
{
_showMenuCommand = _showMenuCommand ?? new MvxCommand<Menu>(DoShowMenuCommand);
return _showMenuCommand;
}
}
private void DoShowMenuCommand(Menu menu)
{
ShowViewModel<MenuCardViewModel>(menu);
}
Menu contains some properties like header, name, image, etc.. but contains also List menuItems. When I debug and breakpoint in DoShowMenuCommand Menu has List of menuItems but when I debug and breakpoint in MenuCardViewModel in method Init:
public void Init(Menu menu)
{
// HERE..
}
So here Menu has everything but MenuItems list is null. I dont know why... some tips why everything is here but list is null?
MvvmCross serialises complex DTO's into JSON. I'm not sure how you have implemented this but you could try it this way and see if that helps:
private void DoShowMenuCommand(Menu menu)
{
ShowViewModel<MenuCardViewModel,Menu>(menu);
}
So the second generic is the model that you want to pass. The next step is to add the menu generic to the "MenuCardViewModel"
public class MenuCardViewModel : MvxViewModel<Menu>
This will require to implement the init method:
protected override Task Init(Menu menu)
{
}
If this doesn't work i would suggest diving into why your list is not serializable/deserializable.
I'm making an app with using Xamarin.forms.
You might know forms' button is not enough to use as image button if you tried one.
So I use Image as a button and add gesturerecogniger. It's working fine.
Good thing is that I can use all Image's bindable property same like using Image. (like 'Aspect property' and else)
Only problem is that Android button has sound effect when it's pressed.
Mine doesn't have.
How to play default button sound on Android?
[another try]
I tried to make layout and put Image and empty dummy button on it.
But If I do this, I can't use any property of Image or Button unless I manually link it.
So I think it's not the right way.
Thanks.
Xamarin.Android:
var root = FindViewById<View>(Android.Resource.Id.Content);
root.PlaySoundEffect(SoundEffects.Click);
Android playSoundEffect(int soundConstant)
Xamarin.iOS
UIDevice.CurrentDevice.PlayInputClick();
Xamarin.Forms via Dependency Service:
public interface ISound
{
void KeyboardClick () ;
}
And then implement the platform specific function.
iOS:
public void KeyboardClick()
{
UIDevice.CurrentDevice.PlayInputClick();
}
Android:
public View root;
public void KeyboardClick()
{
if (root == null)
{
root = FindViewById<View>(Android.Resource.Id.Content);
}
root.PlaySoundEffect(SoundEffects.Click);
}
Xamarin Forms:
PCL interface:
interface ISoundService { void Click(); }
Click handler:
void Handle_OnClick(object sender, EventArgs e) {
DependencyService.Get<ISoundService>().Click();
}
Android:
public class MainActivity {
static MainActivity Instance { get; private set; }
OnCreate() {
Instance = this;
}
}
class SoundService : ISoundService {
public void Click() {
var activity = MainActivity.Instance;
var view = activity.FindViewById<View>(
Android.Resource.Id.Content);
view.PlaySoundEffect(SoundEffects.Click);
}
}
Take a look at the following:
MonoTouch.UIKit.IUIInputViewAudioFeedback
Interface that, together with the UIInputViewAudioFeedback_Extensions class, comprise the UIInputViewAudioFeedback protocol.
See Also: IUIInputViewAudioFeedback
https://developer.xamarin.com/api/type/MonoTouch.UIKit.IUIInputViewAudioFeedback/
You'll want something like this (untested):
public void SomeButtonFunction()
{
SomeBtn.TouchUpInside += (s, e) => {
UIDevice.CurrentDevice.PlayInputClick();
};
}
I have the following code in my Android app, it basically uses one page (using a NavigationDrawer) and swaps fragments in/out of the central view. This allows the navigation to occur on one page instead of many pages:
Setup.cs:
protected override IMvxAndroidViewPresenter CreateViewPresenter()
{
var customPresenter = new MvxFragmentsPresenter();
Mvx.RegisterSingleton<IMvxFragmentsPresenter>(customPresenter);
return customPresenter;
}
ShellPage.cs
public class ShellPage : MvxCachingFragmentCompatActivity<ShellPageViewModel>, IMvxFragmentHost
{
.
.
.
public bool Show(MvxViewModelRequest request, Bundle bundle)
{
if (request.ViewModelType == typeof(MenuContentViewModel))
{
ShowFragment(request.ViewModelType.Name, Resource.Id.navigation_frame, bundle);
return true;
}
else
{
ShowFragment(request.ViewModelType.Name, Resource.Id.content_frame, bundle, true);
return true;
}
}
public bool Close(IMvxViewModel viewModel)
{
CloseFragment(viewModel.GetType().Name, Resource.Id.content_frame);
return true;
}
.
.
.
}
How can I achieve the same behavior in a Windows UWP app? Or rather, is there ANY example that exists for a Windows MvvmCross app which implements a CustomPresenter? That may at least give me a start as to how to implement it.
Thanks!
UPDATE:
I'm finally starting to figure out how to go about this with a customer presenter:
public class CustomPresenter : IMvxWindowsViewPresenter
{
IMvxWindowsFrame _rootFrame;
public CustomPresenter(IMvxWindowsFrame rootFrame)
{
_rootFrame = rootFrame;
}
public void AddPresentationHintHandler<THint>(Func<THint, bool> action) where THint : MvxPresentationHint
{
throw new NotImplementedException();
}
public void ChangePresentation(MvxPresentationHint hint)
{
throw new NotImplementedException();
}
public void Show(MvxViewModelRequest request)
{
if (request.ViewModelType == typeof(ShellPageViewModel))
{
//_rootFrame?.Navigate(typeof(ShellPage), null); // throws an exception
((Frame)_rootFrame.UnderlyingControl).Content = new ShellPage();
}
}
}
When I try to do a navigation to the ShellPage, it fails. So when I set the Content to the ShellPage it works, but the ShellPage's ViewModel is not initialized automatically when I do it that way. I'm guessing ViewModels are initialized in MvvmCross using OnNavigatedTo ???
I ran into the same issue, and built a custom presenter for UWP. It loans a couple of ideas from an Android sample I found somewhere, which uses fragments. The idea is as follows.
I have a container view which can contain multiple sub-views with their own ViewModels. So I want to be able to present multiple views within the container.
Note: I'm using MvvmCross 4.0.0-beta3
Presenter
using System;
using Cirrious.CrossCore;
using Cirrious.CrossCore.Exceptions;
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.Views;
using Cirrious.MvvmCross.WindowsUWP.Views;
using xxxxx.WinUniversal.Extensions;
namespace xxxxx.WinUniversal.Presenters
{
public class MvxWindowsMultiRegionViewPresenter
: MvxWindowsViewPresenter
{
private readonly IMvxWindowsFrame _rootFrame;
public MvxWindowsMultiRegionViewPresenter(IMvxWindowsFrame rootFrame)
: base(rootFrame)
{
_rootFrame = rootFrame;
}
public override async void Show(MvxViewModelRequest request)
{
var host = _rootFrame.Content as IMvxMultiRegionHost;
var view = CreateView(request);
if (host != null && view.HasRegionAttribute())
{
host.Show(view as MvxWindowsPage);
}
else
{
base.Show(request);
}
}
private static IMvxWindowsView CreateView(MvxViewModelRequest request)
{
var viewFinder = Mvx.Resolve<IMvxViewsContainer>();
var viewType = viewFinder.GetViewType(request.ViewModelType);
if (viewType == null)
throw new MvxException("View Type not found for " + request.ViewModelType);
// Create instance of view
var viewObject = Activator.CreateInstance(viewType);
if (viewObject == null)
throw new MvxException("View not loaded for " + viewType);
var view = viewObject as IMvxWindowsView;
if (view == null)
throw new MvxException("Loaded View is not a IMvxWindowsView " + viewType);
view.ViewModel = LoadViewModel(request);
return view;
}
private static IMvxViewModel LoadViewModel(MvxViewModelRequest request)
{
// Load the viewModel
var viewModelLoader = Mvx.Resolve<IMvxViewModelLoader>();
return viewModelLoader.LoadViewModel(request, null);
}
}
}
IMvxMultiRegionHost
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.WindowsUWP.Views;
namespace xxxxx.WinUniversal.Presenters
{
public interface IMvxMultiRegionHost
{
void Show(MvxWindowsPage view);
void CloseViewModel(IMvxViewModel viewModel);
void CloseAll();
}
}
RegionAttribute
using System;
namespace xxxxx.WinUniversal.Presenters
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class RegionAttribute
: Attribute
{
public RegionAttribute(string regionName)
{
Name = regionName;
}
public string Name { get; private set; }
}
}
These are the three foundational classes you need. Next you'll need to implement the IMvxMultiRegionHost in a MvxWindowsPage derived class.
This is the one I'm using:
HomeView.xaml.cs
using System;
using System.Diagnostics;
using System.Linq;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
using Cirrious.MvvmCross.ViewModels;
using Cirrious.MvvmCross.WindowsUWP.Views;
using xxxxx.Shared.Controls;
using xxxxx.WinUniversal.Extensions;
using xxxxx.WinUniversal.Presenters;
using xxxxx.Core.ViewModels;
namespace xxxxx.WinUniversal.Views
{
public partial class HomeView
: MvxWindowsPage
, IMvxMultiRegionHost
{
public HomeView()
{
InitializeComponent();
}
// ...
public void Show(MvxWindowsPage view)
{
if (!view.HasRegionAttribute())
throw new InvalidOperationException(
"View was expected to have a RegionAttribute, but none was specified.");
var regionName = view.GetRegionName();
RootSplitView.Content = view;
}
public void CloseViewModel(IMvxViewModel viewModel)
{
throw new NotImplementedException();
}
public void CloseAll()
{
throw new NotImplementedException();
}
}
}
The last piece to make this work is the way the actual xaml in the view is set-up. You'll notice that I'm using a SplitView control, and that I'm replacing the Content property with the new View that's coming in in the ShowView method on the HomeView class.
HomeView.xaml
<SplitView x:Name="RootSplitView"
DisplayMode="CompactInline"
IsPaneOpen="false"
CompactPaneLength="48"
OpenPaneLength="200">
<SplitView.Pane>
// Some ListView with menu items.
</SplitView.Pane>
<SplitView.Content>
// Initial content..
</SplitView.Content>
</SplitView>
EDIT:
Extension Methods
I forgot to post the two extension methods to determine if the view declares a [Region] attribute.
public static class RegionAttributeExtentionMethods
{
public static bool HasRegionAttribute(this IMvxWindowsView view)
{
var attributes = view
.GetType()
.GetCustomAttributes(typeof(RegionAttribute), true);
return attributes.Any();
}
public static string GetRegionName(this IMvxWindowsView view)
{
var attributes = view
.GetType()
.GetCustomAttributes(typeof(RegionAttribute), true);
if (!attributes.Any())
throw new InvalidOperationException("The IMvxView has no region attribute.");
return ((RegionAttribute)attributes.First()).Name;
}
}
Hope this helps.
As the link to the blog of #Stephanvs is no longer active I was able to pull the content off the Web Archive, i'll post it here for who ever is looking for it:
Implementing a Multi Region Presenter for Windows 10 UWP and MvvmCross
18 October 2015 on MvvmCross, Xamarin, UWP, Windows 10, Presenter > Universal Windows Platform
I'm upgrading a Windows Store app to the new Windows 10 Universal
Windows Platform. MvvmCross has added support for UWP in v4.0-beta2.
A new control in the UWP is the SplitView control. Basically it
functions as a container view which consist of two sub views, shown
side-by-side. Mostly it's used to implement the (in)famous hamburger
menu.
By default MvvmCross doesn't know how to deal with the SplitView, and
just replaces the entire screen contents with a new View when
navigating between ViewModels. If however we want to lay-out our views
differently and show multiple views within one window, we need a
different solution. Luckily we can plug-in a custom presenter, which
will take care of handling the lay-out per platform.
Registering the MultiRegionPresenter
In the Setup.cs file in your UWP project, you can override the
CreateViewPresenter method with the following implementation.
protected override IMvxWindowsViewPresenter CreateViewPresenter(IMvxWindowsFrame rootFrame)
{
return new MvxWindowsMultiRegionViewPresenter(rootFrame);
}
Using Regions
We can define a region by declaring a
element. At this point it has to be a Frame type because then we can
also show a nice transition animation when switching views.
<mvx:MvxWindowsPage ...>
<Grid>
<!-- ... -->
<SplitView>
<SplitView.Pane>
<!-- Menu Content as ListView or something similar -->
</SplitView.Pane>
<SplitView.Content>
<Frame x:Name="MainContent" />
</SplitView.Content>
</SplitView>
</Grid>
</mvx:MvxWindowsPage>
Now we want to be able when a ShowViewModel(...) occurs to swap out
the current view presented in the MainContent frame.
Showing Views in a Region
In the code-behind for a View we can now declare a MvxRegionAttribute,
defining in which region we want this View to be rendered. This name
has to match a Frame element in the view.
[MvxRegion("MainContent")]
public partial class PersonView
{
// ...
}
It's also possible to declare multiple regions within the same view.
This would allow you to split up your UI in more re-usable pieces.
Animating the Transition between Content Views
If you want a nice animation when transitioning between views in the
Frame, you can add the following snippet to the Frame declaration.
<Frame x:Name="MainContent">
<Frame.ContentTransitions>
<TransitionCollection>
<NavigationThemeTransition>
<NavigationThemeTransition.DefaultNavigationTransitionInfo>
<EntranceNavigationTransitionInfo />
</NavigationThemeTransition.DefaultNavigationTransitionInfo>
</NavigationThemeTransition>
</TransitionCollection>
</Frame.ContentTransitions>
</Frame>
The contents will now be nicely animated when navigating.
Hope this helps, Stephanvs