Is there a Xamarin Mvvmcross Android Shared Element Navigation example? - xamarin

I'm trying to get this animation/transition working in my Xamarin Android application with Mvx.
I have a recyclerview with cards. When tapping on a card, I now call:
private void TimeLineAdapterOnItemClick(object sender, int position)
{
TimeLineAdapter ta = (TimeLineAdapter) sender;
var item = ta.Items[position];
int photoNum = position + 1;
Toast.MakeText(Activity, "This is photo number " + photoNum, ToastLength.Short).Show();
ViewModel.ShowDetails(item.Id);
}
I'm trying to find out how to translate this java navigation with transition to Xamarin with Mvvmcross:
ActivityOptionsCompat options =
ActivityOptionsCompat.MakeSceneTransitionAnimation(this, imageView, getString(R.string.activity_image_trans));
startActivity(intent, options.toBundle());
I know that within Mvx you can make use of custom presenters, but how do I get hold of, for example, the ImageView of the tapped Card within the RecyclerView which I would like to 'transform' to the new ImageView on the new Activity?
Thanks!
.

Is there a Xamarin Mvvmcross Android Shared Element Navigation
example?
I do not believe so.
I know that within Mvx you can make use of custom presenters, but how
do I get hold of, for example, the ImageView of the tapped Card within
the RecyclerView which I would like to 'transform' to the new
ImageView on the new Activity?
The easiest way that I can think of to achieve the sharing of control elements you want to transition is via the use of view tags and a presentation bundle when using ShowViewModel.
I would suggest making some changes to your Adapter Click handler to include the view of the ViewHolder being selected (See GitHub repo for example with EventArgs). That way you can interact with the ImageView and set a tag that can be used later to identity it.
private void TimeLineAdapterOnItemClick(object sender, View e)
{
var imageView = e.FindViewById<ImageView>(Resource.Id.imageView);
imageView.Tag = "anim_image";
ViewModel.ShowDetails(imageView.Tag.ToString());
}
Then in your ViewModel, send that tag via a presentationBundle.
public void ShowDetails(string animationTag)
{
var presentationBundle = new MvxBundle(new Dictionary<string, string>
{
["Animate_Tag"] = animationTag
});
ShowViewModel<DetailsViewModel>(presentationBundle: presentationBundle);
}
Then create a custom presenter to pickup the presentationBundle and handle the creating of new activity with the transition. The custom presenter which makes use of the tag to find the element that you want to transition and include the ActivityOptionsCompat in the starting of the new activity. This example is using a MvxFragmentsPresenter but if you are not making use of fragments and using MvxAndroidViewPresenter the solution would be almost identical (Override Show instead and no constructor required).
public class SharedElementFragmentsPresenter : MvxFragmentsPresenter
{
public SharedElementFragmentsPresenter(IEnumerable<Assembly> AndroidViewAssemblies)
: base(AndroidViewAssemblies)
{
}
protected override void ShowActivity(MvxViewModelRequest request, MvxViewModelRequest fragmentRequest = null)
{
if (InterceptPresenter(request))
return;
Show(request, fragmentRequest);
}
private bool InterceptPresenter(MvxViewModelRequest request)
{
if ((request.PresentationValues?.ContainsKey("Animate_Tag") ?? false)
&& request.PresentationValues.TryGetValue("Animate_Tag", out var controlTag))
{
var intent = CreateIntentForRequest(request);
var control = Activity.FindViewById(Android.Resource.Id.Content).FindViewWithTag(controlTag);
control.Tag = null;
var transitionName = control.GetTransitionNameSupport();
if (string.IsNullOrEmpty(transitionName))
{
Mvx.Warning($"A {nameof(transitionName)} is required in order to animate a control.");
return false;
}
var activityOptions = ActivityOptionsCompat.MakeSceneTransitionAnimation(Activity, control, transitionName);
Activity.StartActivity(intent, activityOptions.ToBundle());
return true;
}
return false;
}
}
GetTransitionNameSupport is an extension method that just does a platform API check when getting the TransitionName.
public static string GetTransitionNameSupport(this ImageView imageView)
{
if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop)
return imageView.TransitionName;
return string.Empty;
}
The final step would be to register the custom presenter in you Setup.cs
protected override IMvxAndroidViewPresenter CreateViewPresenter()
{
var mvxPresenter = new SharedElementFragmentsPresenter(AndroidViewAssemblies);
Mvx.RegisterSingleton<IMvxAndroidViewPresenter>(mvxPresenter);
return mvxPresenter;
}
You can check the repo on GitHub which demonstrates this example. The solution is designed so that the presenter does not have to care about the type of the control that is being transitioned. A control only requires a tag used to identify it. The example in the repo also allows for specifying multiple control elements that you want to transition (I did not want to include more complexity in the example above).

Related

OnAppearing different on iOS and Android

I have found that on iOS, OnAppearing is called when the page literally appears on the screen, whereas on Android, it's called when it's created.
I'm using this event to lazily construct an expensive to construct view but obviously the Android behaviour defeats this.
Is there some way of knowing on Android when a screen literally appears on the screen?
You can use the event:
this.Appearing += YourPageAppearing;
Otherwise, you should use the methods of the Application class that contains the lifecycle methods:
protected override void OnStart()
{
Debug.WriteLine ("OnStart");
}
protected override void OnSleep()
{
Debug.WriteLine ("OnSleep");
}
protected override void OnResume()
{
Debug.WriteLine ("OnResume");
}
On Android, Xamarin.Forms.Page.OnAppearing is called immediately before the page's view is shown to user (not when the page is "created" (constructed)).
If you want an initial view to appear quickly, by omitting an expensive sub-view, use a binding to make that view's IsVisible initially be "false". This will keep it out of the visual tree, avoiding most of the cost of building it. Place the (invisible) view in a grid cell, whose dimensions are constant (either in DPs or "*" - anything other than "Auto".) So that layout will be "ready" for that view, when you make it visible.
APPROACH 1:
Now you just need a binding in view model that will change IsVisible to "true".
The simplest hack is to, in OnAppearing, fire an action that will change that variable after 250 ms.
APPROACH 2:
The clean alternative is to create a custom page renderer, and override "draw".
Have draw, after calling base.draw, check an action property on your page.
If not null, invoke that action, then clear it (so only happens once).
I do this by inheriting from a custom page base class:
XAML for each of my pages (change "ContentPage" to "exodus:ExBasePage"):
<exodus:ExBasePage
xmlns:exodus="clr-namespace:Exodus;assembly=Exodus"
x:Class="YourNamespace.YourPage">
...
</exodus:ExBasePage>
xaml.cs:
using Exodus;
// After creating page, change "ContentPage" to "ExBasePage".
public partial class YourPage : ExBasePage
{
...
my custom ContentPage. NOTE: Includes code not needed for this, related to iOS Safe Area and Android hardward back button:
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
namespace Exodus
{
public abstract partial class ExBasePage : ContentPage
{
public ExBasePage()
{
// Each sub-class calls InitializeComponent(); not needed here.
ExBasePage.SetupForLightStatusBar( this );
}
// Avoids overlapping iOS status bar at top, and sets a dark background color.
public static void SetupForLightStatusBar( ContentPage page )
{
page.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUseSafeArea( true );
// iOS NOTE: Each ContentPage must set its BackgroundColor to black or other dark color (when using LightContent for status bar).
//page.BackgroundColor = Color.Black;
page.BackgroundColor = Color.FromRgb( 0.3, 0.3, 0.3 );
}
// Per-platform ExBasePageRenderer uses these.
public System.Action NextDrawAction;
/// <summary>
/// Override to do something else (or to do nothing, i.e. suppress back button).
/// </summary>
public virtual void OnHardwareBackButton()
{
// Normal content page; do normal back button behavior.
global::Exodus.Services.NavigatePopAsync();
}
}
}
renderer in Android project:
using System;
using Android.Content;
using Android.Views;
using Android.Graphics;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Exodus;
using Exodus.Android;
[assembly: ExportRenderer( typeof( ExBasePage ), typeof( ExBasePageRenderer ) )]
namespace Exodus.Android
{
public class ExBasePageRenderer : PageRenderer
{
public ExBasePageRenderer( Context context ) : base( context )
{
}
protected override void OnElementChanged( ElementChangedEventArgs<Page> e )
{
base.OnElementChanged( e );
var page = Element as ExBasePage;
if (page != null)
page.firstDraw = true;
}
public override void Draw( Canvas canvas )
{
try
{
base.Draw( canvas );
var page = Element as ExBasePage;
if (page?.NextDrawAction != null)
{
page.NextDrawAction();
page.NextDrawAction = null;
}
}
catch (Exception ex)
{
// TBD: Got Disposed exception on Android Bitmap, after rotating phone (in simulator).
// TODO: Log exception.
Console.WriteLine( "ExBasePageRenderer.Draw exception: " + ex.ToString() );
}
}
}
}
To do some action after the first time the page is drawn:
public partial class YourPage : ExBasePage
{
protected override void OnAppearing()
{
// TODO: OnPlatform code - I don't have it handy.
// On iOS, we call immediately "DeferredOnAppearing();"
// On Android, we set this field, and it is done in custom renderer.
NextDrawAction = DeferredOnAppearing;
}
void DeferredOnAppearing()
{
// Whatever you want to happen after page is drawn first time:
// ((MyViewModel)BindingContext).ExpensiveViewVisible = true;
// Where MyViewModel contains:
// public bool ExpensiveViewVisible { get; set; }
// And your XAML contains:
// <ExpensiveView IsVisible={Binding ExpensiveViewVisible}" ... />
}
}
NOTE: I do this differently on iOS, because Xamarin Forms on iOS (incorrectly - not to spec) calls OnAppearing AFTER the page is drawn.
So I have OnPlatform logic. On iOS, OnAppearing immediately calls DeferredOnAppearing. On Android, the line shown is done.
Hopefully iOS will eventually be fixed to call OnAppearing BEFORE,
for consistency between the two platforms.
If so, I would then add a similar renderer for iOS.
(The current iOS implementation means there is no way to update a view before it appears a SECOND time, due to popping the nav stack.
instead, it appears with outdated content, THEN you get a chance
to correct it. This is not good.)

Xamarin Forms Navigation without animation

I have an app where I want to show page A, from which the user can navigate to page B or C, from B back to A or to C, and from C only back to A, even if the user when through B to get to C
Currently when I'm executing the B->C transition I first PopAsync to get back to A and then I do PushAsync to get to C, so that the '
The question is: is there a civilized way to set up this navigation scheme while still relying on the built-in Navigation to keep track of navigation stack - I don't want to do that myself and use PushModalAsync.
Note that (as reflected in the image) A and C aren't the end points of the whole navigation stack, there are pages before A and after C, so the stack has to be preserved.
On iOS the NavigationRenderer has virtual methods OnPopViewAsync and OnPushAsync (similar on Android):
protected override Task<bool> OnPopViewAsync(Page page, bool animated)
{
return base.OnPopViewAsync(page, animated);
}
protected override Task<bool> OnPushAsync(Page page, bool animated)
{
return base.OnPushAsync(page, animated);
}
They call the corresponding base method with two arguments, the page and whether to animate the transition. Thus, you might be able to enable or disable the animation using the following approach:
Derive a custom navigation page.
Add an "Animated" property.
Derive a custom navigation renderer for your custom navigation page.
Override the pop and push methods calling their base methods with the "Animated" property.
Note that I haven't tried this approach, yet, since it is quite some work to do. But disabling animations on all navigation pages did work this way.
Edit: It took me several hours to actually implement my solution for my own project. Therefore, I'll share some more details. (I developed and tested on Xamarin.Forms 1.2.3-pre4.)
The custom navigation page
Besides the above-mentioned Animated property my navigation page re-implements the two transition functions and adds an optional argument animated, which is true by default. This way we'll be able to keep all existing code and only add a false where needed.
Furthermore, both method will sleep for a very short time (10 ms) after pushing/popping the page. Without this delay we'd ran into trouble with consecutive calls.
public class CustomNavigationPage: NavigationPage
{
public bool Animated { get; private set; }
public CustomNavigationPage(Page page) : base(page)
{
}
// Analysis disable once MethodOverloadWithOptionalParameter
public async Task PushAsync(Page page, bool animated = true)
{
Animated = animated;
await base.PushAsync(page);
await Task.Run(delegate {
Thread.Sleep(10);
});
}
// Analysis disable once MethodOverloadWithOptionalParameter
public async Task<Page> PopAsync(bool animated = true)
{
Animated = animated;
var task = await base.PopAsync();
await Task.Run(delegate {
Thread.Sleep(10);
});
return task;
}
}
The custom navigation renderer
The renderer for my custom navigation page overrides both transition methods and passes the Animated property to their base methods. (It's kind of ugly to inject a flag this way, but I couldn't find a better solution.)
public class CustomNavigationRenderer: NavigationRenderer
{
protected override Task<bool> OnPopViewAsync(Page page, bool animated)
{
return base.OnPopViewAsync(page, (Element as CustomNavigationPage).Animated);
}
protected override Task<bool> OnPushAsync(Page page, bool animated)
{
return base.OnPushAsync(page, (Element as CustomNavigationPage).Animated);
}
}
This is for iOS. But on Android it's almost identically.
An example application
To demonstrate the possibilities of consecutively pushing and popping pages, I wrote the following application.
The App class simply creates a new DemoPage wrapped into a CustomNavigationPage. Note that this instance must be publicly accessible for this example.
public static class App
{
public static CustomNavigationPage NavigationPage;
public static Page GetMainPage()
{
return NavigationPage = new CustomNavigationPage(new DemoPage("Root"));
}
}
The demo page contains a number of buttons that push and pop pages in different orders. You can add or remove the false option for each call to PushAsync or PopAsync.
public class DemoPage: ContentPage
{
public DemoPage(string title)
{
Title = title;
Content = new StackLayout {
Children = {
new Button {
Text = "Push",
Command = new Command(o => App.NavigationPage.PushAsync(new DemoPage("Pushed"))),
},
new Button {
Text = "Pop",
Command = new Command(o => App.NavigationPage.PopAsync()),
},
new Button {
Text = "Push + Pop",
Command = new Command(async o => {
await App.NavigationPage.PushAsync(new DemoPage("Pushed (will pop immediately)"));
await App.NavigationPage.PopAsync();
}),
},
new Button {
Text = "Pop + Push",
Command = new Command(async o => {
await App.NavigationPage.PopAsync(false);
await App.NavigationPage.PushAsync(new DemoPage("Popped and pushed immediately"));
}),
},
new Button {
Text = "Push twice",
Command = new Command(async o => {
await App.NavigationPage.PushAsync(new DemoPage("Pushed (1/2)"), false);
await App.NavigationPage.PushAsync(new DemoPage("Pushed (2/2)"));
}),
},
new Button {
Text = "Pop twice",
Command = new Command(async o => {
await App.NavigationPage.PopAsync(false);
await App.NavigationPage.PopAsync();
}),
},
},
};
}
}
Important hint: It cost me hours of debugging to find out that you need to use an instance of NavigationPage (or a derivative) rather than the ContentPage's Navigation! Otherwise the immediate call of two or more pops or pushes leads to strange behaviour and crashes.
#Falko
You now have the possibility to include a boolean parameter:
Navigation.PushAsync (new Page2Xaml (), false);
Xamarin Documentation
Currently the Xamarin Forms navigation is very spartanic and I doubt there is a nice way to achieve that. Besides Doing and extra "Pop" when necessary.
Here's a collection of snippets I whipped together along with some other niceties to improve NaviagationPage for iOS. Link to comment and code on xamarin forums.
What I would do if I were doing this is push Page C on to your NavigationStack and then take page B off of the stack. That way when you pop from page C, you would go to page A.
// Push the page you want to go to on top of the stack.
await NavigationPage.PushAsync(new CPage()));
// Remove page B from the stack, so when you want to go back next time
//you will go to page A.
Navigation.RemovePage(Navigation.NavigationStack[Navigation.NavigationStack.Count - 2] );
Alternatively, when even you pop from page C, you could remove all instances of type page B from the stack, and then pop back 1. In that case, page B would remain on the stack until you were about to move back from page C to page A.

Triggering event in Activity using MvvmCross

I have an MvxFragmentActivity which loads a google map and places markers on the map. The code to create the map and markers is very Droid specific so it is in the Activity. The markers are created based on objects in the ViewModel which each contain lat/long coordinates. This worked fine as long as I loaded the objects in my Init method. I have since moved the load objects method to a service and call it on a different thread. This way the UI is responsive. However, how do I call the method in the Activity when the load is completed?
Here is my current code in the Activity (this code shouldn't change, just how it is called):
private void InitMapFragment()
{
foreach (var item in viewModel.Items)
{
var icon = BitmapDescriptorFactory.FromResource(Resource.Drawable.place_img);
var markerOptions = new MarkerOptions()
.SetPosition(new LatLng(item.Latitude, item.Longitude))
.InvokeIcon(icon)
.SetSnippet(item.DistanceText)
.SetTitle(item.Name);
var marker = _map.AddMarker(markerOptions);
_markerIds.Add(marker.Id, item.Id);
}
}
Code in my viewModel:
private void BeginLoadItems()
{
_loadItemsService.Load();
}
// This is triggered by a message
private void OnLoadItemsComplete(LoadCompleteMessage message)
{
Items = message.Items;
}
Code in my Service:
public void Load()
{
ThreadPool.QueueUserWorkItem(state =>
{
var results = _repository.Retrieve();
_messenger.Publish(new LoadCompleteMessage(this, results));
});
}
You're already triggering an event when you set:
Items = message.Items;
This triggers PropertyChanged with a property name of "Items"
For more on map binding, see Using MvvmCross how can I bind a list of annotations to a MapView? - although with Droid you'll need to use markers instead of annotations.

How to dismiss a Alert Dialog in Mono for android correctly?

In my application i have a Custom AlertView, which works quite good so far. I can open it the first time, do, what i want to do, and then close it. If i want to open it again, i'll get
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first
so, here some code:
public Class ReadingTab
{
...
private AlertDialog AD;
...
protected override void OnCreate(Bundle bundle)
{
btnAdd.Click += delegate
{
if (IsNewTask)
{
...
AlertDialog.Builer adb = new AlertDialog.Builer(this);
...
View view = LayoutInflater.Inflate(Resource.Layout.AlertDView15ET15TVvert, null);
adb.setView(view)
}
AD = adb.show();
}
}
}
that would be the rough look of my code.
Inside of btnAdd are two more buttons, and within one of them (btnSafe) i do AD.Dismiss() to close the Alert dialoge, adb.dispose() hasn't done anything.
the first time works fine, but when i call it the secon time, the debugger holds at AD = adb.show(); with the Exception mentioned above.
So what do i have to do, to remove the Dialoge from the parent? i can't find removeView() anywhere.
If you are setting up an AlertView once and then using it in multiple places (especially if you are using the same AlertView across different Activities) then you should consider creating a static AlertDialog class that you can then call from all over the place, passing in the current context as a parameter each time you want to show it. Then when a button is clicked you can simply dismiss the dialog and set the instance to null. Here is a basic example:
internal static class CustomAlertDialog
{
private static AlertDialog _instance;
private const string CANCEL = #"Cancel";
private const string OK = #"OK";
private static EventHandler _handler;
// Static method that creates your dialog instance with the given title, message, and context
public static void Show(string title,
string message,
Context context)
{
if (_instance != null)
{
throw new Exception(#"Cannot have more than one confirmation dialog at once.");
}
var builder = new AlertDialog.Builder(context);
builder.SetTitle(title);
builder.SetMessage(message);
// Set buttons and handle clicks
builder.SetPositiveButton(OK, delegate { /* some action here */ });
builder.SetNegativeButton(CANCEL, delegate { /* some action here */});
// Create a dialog from the builder and show it
_instance = builder.Create();
_instance.SetCancelable(false);
_instance.Show();
}
}
And from your Activity you would call your CustomAlertDialog like this:
CustomAlertDialog.Show(#"This is my title", #"This is my message", this);

lwuit change UI language

I use codenameone to develop my mobile application. In this application I implement some classes and codes manually for instance create all forms by hard coding not using codenameone designer for some reason.
By the way I wanted to navigate in forms like what codenameone use, so I use one variable from type of Form called it prevForm and when I want to open a form I set it to current form and then I show new form.
Ok, that is main scenario. In this application I wanna implement internationalization too, so I create my own hashtable (Farsi and English) for this application.
This is my problem:
How can I set or change language and apply it to forms that I opened?
Is my method for navigate between forms are good?
Here is my code:
public class BaseForm extends Form implements ActionListener {
public BaseForm(){
this.setLayout(new BoxLayout(BoxLayout.Y_AXIS));
}
Command exit, ok, back;
Form prevForm;
protected void initForm(){
}
protected void showForm(){
}
protected void showForm(final Form prevForm){
//String name = this.getName();
//if("Reminder".equals(name) || "3Transaction".equals(name))
{
this.prevForm = prevForm;
Form f = this;
back = new Command("Back");
//ok = new Command("Ok");
//delete = new Command("Delete");;
Button button = new Button("Button");
f.addCommand(back);
//f.addCommand(ok);
//f.addCommand(delete);
//f.addComponent(button);
f.addCommandListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
if (ae.getCommand().equals(back)) {
//Do Exit command code
System.out.println("Back pressed");
prevForm.showBack();
} else if (ae.getCommand().equals(ok)) {
//Do Start command code
System.out.println("Ok pressed");
}
}
});
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
//Do button code
System.out.println("Action performed");
}
});
}
showForm();
}}
for open nested form I use this code:
LanguageUI lang = new LanguageUI();
lang.showForm(this);
change language [form]:
protected boolean onBtnSave() {
if(isRbFarsiSelected()){
UIManager.getInstance().setResourceBundle(new CommonSettings().getFarsi());
}
else {
UIManager.getInstance().setResourceBundle(new CommonSettings().getEnglish());
}
return false;
}
I also hard code my UI on lwuit, and i have a variable parentForm on every class so i can easily show previous form. For language change i know there is Localization in the resource editor that you can make use of. Below is how you can access it. I guess the trick is how to set the content of the L10N in the res file in code? On the other hand you can create your own helper classes that mirror the methods below.
Resources theme = Resources.open("/theme.res");
theme.getL10N(id, locale);
theme.getL10NResourceNames();
theme.isL10N(name);
theme.listL10NLocales(id)

Resources