Xamarin Android: Calling Invalidate() inside overridden OnDraw method - xamarin

I am creating a custom renderer, that needs to display whatever I have rendered in my Vulkan engine. For this I have a VulkanSurfaceView, which inherits from MetalKit.MTKView on iOS, and from Android.Views.SurfaceView and ISurfaceHolderCallback on Android.
For iOS I can simply do this, which will draw a new frame continually, as long as the view is in focus:
public class VulkanSurfaceView : MTKView, IVulkanAppHost
{
...
public override void Draw()
{
Renderer.Tick();
base.Draw();
}
}
However, on Android I have to do this, where I call Invalidate() from within the OnDraw method, else it is only called once. I think this code smells a bit, and I am not sure, if this is the "good" way of doing it. Is my solution okay? If not, does anyone have a better idea?
public class VulkanSurfaceView : SurfaceView, ISurfaceHolderCallback, IVulkanAppHost
{
...
protected override void OnDraw(Canvas? canvas)
{
Renderer.Tick();
base.OnDraw(canvas);
Invalidate();
}
}

Did you try calling setWillNotDraw(false) in your surfaceCreated method ?
Refer the link

Thank you to #ToolmakerSteve.
I created a Timer where I call Invalidate() if a new frame has been requested (by me via a simple bool). For anyone interested I do it like so:
protected override void OnDraw(Canvas? canvas) // Just to show the updated OnDraw-method
{
Renderer.Tick();
base.OnDraw(canvas);
}
public void SurfaceCreated(ISurfaceHolder holder)
{
TickTimer = new System.Threading.Timer(state =>
{
AndroidApplication.SynchronizationContext.Send(_ => { if (NewFrameRequested) Invalidate(); }, state);
try { TickTimer.Change(0, Timeout.Infinite); } catch (ObjectDisposedException) { }
}, null, 0, Timeout.Infinite);
}
For now it is very simple, but it works and will probably grow. The reason for my initial bad framerate with this method was a misunderstanding of the "dueTime" of the Timer (see Timer Class), which I though was the framerate sought. This is actually the time between frames, which seems obvious now.
As #Bhargavi also kindly mentioned you need to set "setWillNotDraw(false)" if OnDraw is not being called when invalidating the view.

Related

How to handle back button when at the starting destination of the navigation component

I've started working with the new navigation component and I'm really digging it! I do have one issue though - How am I supposed to handle the back button when I'm at the starting destination of the graph?
This is the code I'm using now:
findNavController(this, R.id.my_nav_host_fragment)
.navigateUp()
When I'm anywhere on my graph, it's working great, it send me back, but when I'm at the start of it - the app crashes since the backstack is empty.
This all makes sense to me, I'm just not sure how to handle it.
While I can check if the current fragment's ID is the same as the one that I know to be the root of the graph, I'm looking for a more elegant solution like some bool flag of wether or not the current location in the graph is the starting location or not.
Ideas?
I had a similar scenario where I wanted to finish the activity when I was at the start destination and do a regular 'navigateUp' when I was further down the navigation graph. I solved this through a simple extension function:
fun NavController.navigateUpOrFinish(activity: AppCompatActivity): Boolean {
return if (navigateUp()) {
true
} else {
activity.finish()
true
}
}
And then call it like:
override fun onSupportNavigateUp() =
findNavController(R.id.nav_fragment).navigateUpOrFinish(this)
However I was unable to use NavigationUI as this would hide the back arrow whenever I was at the start destination. So instead of:
NavigationUI.setupActionBarWithNavController(this, controller)
I manually controlled the home icon:
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_navigation_back)
Override onBackPressed in your activity and check if the current destination is the start destination or not.
Practically it looks like this:
#Override
public void onBackPressed() {
if (Navigation.findNavController(this,R.id.nav_host_fragment)
.getCurrentDestination().getId() == R.id.your_start_destination) {
// handle back button the way you want here
return;
}
super.onBackPressed();
}
You shouldn't override "onBackPressed", you should override "onSupportNavigateUp" and put there
findNavController(this, R.id.my_nav_host_fragment)
.navigateUp()
From the official documentation:
You will also overwrite AppCompatActivity.onSupportNavigateUp() and call NavController.navigateUp
https://developer.android.com/topic/libraries/architecture/navigation/navigation-implementing
In Jetpack Navigation Component, if you want to perform some operation when fragment is poped then you need to override following functions.
Add OnBackPressedCallback in fragment to run your special operation when back present in system navigation bar at bottom is pressed .
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
//perform your operation and call navigateUp
findNavController().navigateUp()
}
}
requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback)
}
Add onOptionsItemMenu in fragment to handle back arrow press present at top left corner within the app.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
//perform your operation and call navigateUp
findNavController().navigateUp()
return true
}
return super.onOptionsItemSelected(item)
}
If there is no special code to be run when back is pressed on host fragment then use onSupportNavigateUp in Activity.
override fun onSupportNavigateUp(): Boolean {
if (navController.navigateUp() == false){
//navigateUp() returns false if there are no more fragments to pop
onBackPressed()
}
return navController.navigateUp()
}
Note that onSupportNavigateUp() is not called if the fragment contains onOptionsItemSelected()
As my back button works correctly, and using NavController.navigateUp() crashed on start destination back button. I have changed this code to something like this. Other possibility will be to just check if currentDestination == startDestination.id but I want to close Activity and go back to other Activity.
override fun onSupportNavigateUp() : Boolean {
//return findNavController(R.id.wizard_nav_host_fragment).navigateUp()
onBackPressed()
return true
}
/** in your activity **/
private boolean doubleBackToExitPressedOnce = false;
#RequiresApi(api = Build.VERSION_CODES.M)
#Override
public void onBackPressed() {
int start = Navigation.findNavController(this, R.id.nav_host_fragment).getCurrentDestination().getId();
if (start == R.id.nav_home) {
if (doubleBackToExitPressedOnce) {
super.onBackPressed();
return;
}
this.doubleBackToExitPressedOnce = true;
Toast.makeText(MainActivity.this, "Press back again to exits", Toast.LENGTH_SHORT).show();
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
doubleBackToExitPressedOnce = false;
}
}, 2000);
} else {
super.onBackPressed();
}
}
If you mean the start of your "root" navigation graph (just incase you have nested navigation graphs) then you shouldn't be showing an up button at all, at least according to the navigation principles.
Just call this in your back button Onclick
requireActivity().finish()

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.)

How to use CXCallObserver in Xamarin?

I need to subscribe an event to handle incoming phone call. Since iOS version 11.0 CTCallCenter is deprecated we have to use CXCallObserver. I successfully implemented solution for CTCallCenter, but I am not able to subscribe event for CXCallObserver. Does anyone have working solution for CXCallObserver?
Here is my code to subscribe event for CTCallCenter..
_callCenter = new CTCallCenter();
_callCenter.CallEventHandler += CallEvent;
private void CallEvent(CTCall call)
{
CoreFoundation.DispatchQueue.MainQueue.DispatchSync(() =>
{
if(call.CallState.Equals(call.StateIncoming))
//Do something
});
}
Implement the delegate for CXCallObserver:
public class MyCXCallObserverDelegate : CXCallObserverDelegate
{
public override void CallChanged(CXCallObserver callObserver, CXCall call)
{
Console.WriteLine(call);
}
}
Then in your code, create a instance of CXCallObserver (maintain a strong reference to this) and then assign the delegate:
cXCallObserver = new CXCallObserver();
cXCallObserver.SetDelegate(new MyCXCallObserverDelegate(), null);

Xamarin "Sorry, this video cannot be played"

How can i handle this error?
please help me out of this situation.
private void previewVideo(){
try{
var path = Android.Net.Uri.Parse(App._file.AbsolutePath);
preview.SetVideoURI (path);
preview.Start ();
}
catch(Exception e){
e.GetBaseException ();
}
}
Your'e lucky that I was following your previous question. Please try to have your questions as detailed as possible so it's easier for us to analyze and possible replicate the error.
To be able to set an error listener on the VideoView, the VideoView needs an object that implements the Android.Media.MediaPlayer.IOnErrorListener interface.
You can accomblish that by letting your Activity implement the previous mentioned interface, and setting the Activity as the ErrorListener for the VideoView
public class MainActivity : Activity, Android.Media.MediaPlayer.IOnErrorListener
{
...
protected override void OnCreate(Bundle bundle)
{
...
preview = FindViewById<VideoView> (Resource.Id.SampleVideoView);
preview.SetOnErrorListener(this); // <- Set the error listener
...
}
...
//The implementation of MediaPlayer.IOnErrorListener
public bool OnError(MediaPlayer player, MediaError error, int extra)
{
// Do Something here because error happened
}
...
}
By doing this, when error occurs in the VideoView the VideoView will call the public OnError method.
From the Android Docs of OnErrorListener you can see what the OnError method should return.
Returns:
True if the method handled the error, false if it didn't. Returning false, or not having an OnErrorListener at all, will cause the OnCompletionListener to be called.

Windows Phone Back button and page instance creation

I need to recreate new page instance on every page load (also when user pressed Back button).
So I overrided OnBackKeyPress method:
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
if (NavigationService.CanGoBack) {
e.Cancel = true;
var j = NavigationService.RemoveBackEntry();
NavigationService.Navigate(j.Source);
NavigationService.RemoveBackEntry();
}
}
The problem is that I can't handle case when user press back button to close CustomMessageBox dialog. How can I check it? Or is there any way to force recreation of page instance when going back through history state?
Why do you need to recreate the page instance? If you are simply trying to re-read the data to be displayed, why not put the data loading logic into OnNavigatedTo()?
Assuming that is what you are actually trying to achieve, try something like this...
public partial class MainPage : PhoneApplicationPage
{
// Constructor
public MainPage()
{
InitializeComponent();
// don't do your data loading here. This will only be called on page creation.
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
LoadData();
base.OnNavigatedTo(e);
}
MyViewModel model;
async void LoadData()
{
model = new MyViewModel();
await model.LoadDataAsync();
}
}
If you also have specific logic that you need to run on first construction of the page vs. on a back key navigation, check the NavigationMode property of the NavigationEventArgs object that gets passed to OnNavigatedTo.
if(e.NavigationMode == NavigationMode.New)
{
//do what you need to do specifically for a new page instance
}
if (e.NavigationMode == NavigationMode.Back)
{
// do anything specific for back navigation here.
}
Ha, in the near thread, i have opposite question :)
What about MessageBox - it depends, which one are you using. It can be custom message box, for example. Anyway, try to check MessageBox.IsOpened (or alternative for your MessageBox) in your OnBackKeyPress().
Another solution is to use OnNavigatedTo() of the page you want to be new each time.
Third solution: in case you works with Mvvm Light, add some unique id in ViewModel getter, like
public MyViewModel MyViewModel
{
get
{
return ServiceLocator.Current.GetInstance<MyViewModel>((++Uid).ToString());
}
}
This would force to recreate new ViewModel each time, so you'd have different instance of VM, so you would have another data on the View.

Resources