I'm using Xamarin.Forms to create a cross platform application, all of my ContentPages are situated within the PCL.
I'm looking for a way to set and lock the orientation of a single ContentPage to Landscape, preferably without having to create another activity in each of the platform specific projects.
Since my ContentPage.Content is set to a ScrollView, I've tried setting the ScrollOrientation to Horizontal, however this did not work.
I've also tried using a RelativeLayout, but I can't see an Orientation property on this.
public class PlanningBoardView : ContentPage //Container Class.
{
public PlanningBoardView()
{
scroller = new ScrollView ();
Board = new PlanningBoard();
scroller.Orientation = ScrollOrientation.Horizontal;
scroller.WidthRequest = Board.BoardWidth;
scroller.Content = Board;
Content = scroller;
}
}
The last thing I tried was using Xamarin Studio's version of Intellisense and the Xamarin Forms API Doc's to look through the different Layouts available to me, none of which had a Orientation property.
I fear the only way to do this is by creating a second platform specific Activity just for this one ContentPage and setting the orientation to landscape.
Although this method would work, it makes the Navigation between screens a lot more complex.
This is currently being tested in Android.
Hate to say this but this can only be done using custom renderers or a platform-specific code
In android, you can set the RequestedOrientation property of the MainActivity to ScreenOrientation.Landscape.
In iOS, you can override GetSupportedInterfaceOrientations in the AppDelegate class to return one of the UIInterfaceOrientationMask values when Xamarin.Forms.Application.Current.MainPage is the ContentPage that you are intereted in.
Android
[assembly: Xamarin.Forms.ExportRenderer(typeof(MyCustomContentPage), typeof(CustomContentPageRenderer))]
public class CustomContentPageRenderer : Xamarin.Forms.Platform.Android.PageRenderer
{
private ScreenOrientation _previousOrientation = ScreenOrientation.Unspecified;
protected override void OnWindowVisibilityChanged(ViewStates visibility)
{
base.OnWindowVisibilityChanged(visibility);
var activity = (Activity)Context;
if (visibility == ViewStates.Gone)
{
// Revert to previous orientation
activity.RequestedOrientation = _previousOrientation == ScreenOrientation.Unspecified ? ScreenOrientation.Portrait : _previousOrientation;
}
else if (visibility == ViewStates.Visible)
{
if (_previousOrientation == ScreenOrientation.Unspecified)
{
_previousOrientation = activity.RequestedOrientation;
}
activity.RequestedOrientation = ScreenOrientation.Landscape;
}
}
}
iOS
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow forWindow)
{
if (Xamarin.Forms.Application.Current == null || Xamarin.Forms.Application.Current.MainPage == null)
{
return UIInterfaceOrientationMask.Portrait;
}
var mainPage = Xamarin.Forms.Application.Current.MainPage;
if (mainPage is MyCustomContentPage ||
(mainPage is NavigationPage && ((NavigationPage)mainPage).CurrentPage is MyCustomContentPage) ||
(mainPage.Navigation != null && mainPage.Navigation.ModalStack.LastOrDefault() is MyCustomContentPage))
{
return UIInterfaceOrientationMask.Landscape;
}
return UIInterfaceOrientationMask.Portrait;
}
}
This can also be done by sending the message from Form project to host project using MessagingCenter class. without using the custom renderer or dependency service as follows,
public partial class ThirdPage : ContentPage
{
protected override void OnAppearing()
{
base.OnAppearing();
MessagingCenter.Send(this, "allowLandScapePortrait");
}
//during page close setting back to portrait
protected override void OnDisappearing()
{
base.OnDisappearing();
MessagingCenter.Send(this, "preventLandScape");
}
}
Change in mainactivity to receive the message and set RequestedOrientation
[Activity(Label = "Main", ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation,ScreenOrientation = ScreenOrientation.Portrait)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsApplicationActivity
{
//allowing the device to change the screen orientation based on the rotation
MessagingCenter.Subscribe<ThirdPage>(this, "allowLandScapePortrait", sender =>
{
RequestedOrientation = ScreenOrientation.Unspecified;
});
//during page close setting back to portrait
MessagingCenter.Subscribe<ThirdPage>(this, "preventLandScape", sender =>
{
RequestedOrientation = ScreenOrientation.Portrait;
});
}
Check for more in my blog post : http://www.appliedcodelog.com/2017/05/force-landscape-or-portrait-for-single.html
If you are also running into issue on Android where device rotation returns you back to prompt for user email, you can follow up progress of fixes for both ADAL and MSAL here:
https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/issues/1622 https://github.com/xamarin/xamarin-android/issues/3326
Dealdiane's code works well for me with minor change:
protected override void OnWindowVisibilityChanged(ViewStates visibility) {
base.OnWindowVisibilityChanged( visibility );
IRotationLock page = Element as IRotationLock;
if ( page == null )
return;
var activity = (Activity) Context;
if ( visibility == ViewStates.Gone ) {
// Revert to previous orientation
activity.RequestedOrientation = _previousOrientation;
} else if ( visibility == ViewStates.Visible ) {
if ( _previousOrientation == ScreenOrientation.Unspecified ) {
_previousOrientation = activity.RequestedOrientation;
}
switch ( page.AllowRotation() ) {
case RotationLock.Landscape:
activity.RequestedOrientation = ScreenOrientation.SensorLandscape;
break;
case RotationLock.Portrait:
activity.RequestedOrientation = ScreenOrientation.SensorPortrait;
break;
}
}
}
Related
Description
Hello,
Rendering of Layouts are not updated if they contains controls (ContentView) with some rendering Task !
I created a simple ContentView which contains three Image (the FireControl in the screenshot).
In the constructor, I use a simple async method, with Task.Factory.StartNew, which will play on the visibility of the Face images with a small Task.Delay between them.
In the MainPage, I load this control in a StackLayout, using a button : "Load control".
It is impossible for me to add a second control (if I press again "Load control" )!
It's like if the rendering of the StackLayout in my MainPage was not updated ...
If I dont call my task method: no rendering problem!
If I dont touch controls in my task method: no rendering problem !
If I spam-click "Load control" and "Remove control", sometimes loaded controls appears...
Is there a way to tell my StackLayout to update its rendering after adding a control?
Is there a better way than Task.Factory.StartNew to perform frame animation in a control, so as not to block the rendering?
(In this example the desired animation has been simplified)
Steps to Reproduce
My Test solution :
Test Solution
TestControl
public class TestControl : ContentView
{
protected Image FaceNormal;
protected Image FaceLoose;
public TestControl()
{
var principalImage = new Image { Source = "fire_principal.png" }; // The body
FaceNormal = new Image { Source = "fire_facenormal.png" }; // Opened eyes
FaceLoose = new Image { Source = "fire_faceloose.png" }; // Closed eyes
Content = new Grid { Children = { principalImage, FaceNormal, FaceLoose } }; // Loaded in the Content
// /!\ Causes rendering errors in the StackLayout
Task.Factory.StartNew(() => StartFaceAnimation());
}
public async Task StartFaceAnimation()
{
Dispatcher.Dispatch(() =>
{
FaceNormal.IsVisible = true;
FaceLoose.IsVisible = false;
});
await Task.Delay(2000);
Dispatcher.Dispatch(() =>
{
FaceNormal.IsVisible = false;
FaceLoose.IsVisible = true;
});
}
}
MainPage
public class MainPage : ContentPage
{
public MainPage()
{
var verticalStackLayout = new VerticalStackLayout();
var loadTestControlButton = new Button { Text = "Load control", Margin = 2 };
loadTestControlButton.Clicked += (o, e) => verticalStackLayout.Children.Add(new TestControl());
verticalStackLayout.Children.Add(loadTestControlButton);
var removeTestControlButton = new Button { Text = "Remove control", Margin = 2 };
removeTestControlButton.Clicked += (o, e) => verticalStackLayout.Children.Remove(verticalStackLayout.Children.Last());
verticalStackLayout.Children.Add(removeTestControlButton);
Content = verticalStackLayout;
}
}
The trick is to call Arrange after IsVisible, see the below modifications:
public async Task StartFaceAnimation()
{
Dispatcher.Dispatch(() =>
{
FaceNormal.IsVisible = true;
FaceLoose.IsVisible = false;
Arrange(new Rect());
});
await Task.Delay(2000);
Dispatcher.Dispatch(() =>
{
FaceNormal.IsVisible = false;
FaceLoose.IsVisible = true;
Arrange(new Rect());
});
}
It redraws the controls!
I have a project that use Android to display Unity Player. So I export Untiy project as Android module which implemented by Android Application.
I create buttons in Android Activity which contains UnityPlayer, And when I click button, it send a message to Unity Player to invoke C# function, just like this:
findViewById(R.id.btnChange).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mUnityPlayer.UnitySendMessage("ScriptHolder", "ChangeSkin", "");
}
});
And the function named "ChangeSkin" is just to change some GameObjects' active. Just like this:
void ChangeSkin()
{
int prefab;
if (_currentPrefab == PREFAB_DEFAULT)
{
prefab = PREFAB_PRINCESS;
}
else
{
prefab = PREFAB_DEFAULT;
}
ShowSkin(prefab);
}
private void ShowSkin(int prefab)
{
_currentPrefab = prefab;
foreach (var item in _defaultDressList)
{
item.SetActive(prefab == PREFAB_DEFAULT);
}
foreach (var item in _princessDressList)
{
item.SetActive(prefab == PREFAB_PRINCESS);
}
}
And something weird happening: when I click button to change the person's cloth in Unity, the GameObjects which called SetActive(true) show at the position above the right position for a frame and become normal, it looks like they flash. Here is the gif of the project demo:
It looks like the position offset is equal to the height of status bar. If I create a button on Unity Scene and call "ChangeSkin" function, everything will be OK.
I tried all I can to fix this but not succeed. So I hope you will help me, thx.
You should check your Unity layout. If you have a layout group that has content size fitter component, and the child objects also have it (content size fitter), this could cause these glitches.
I fixed this problem by using a flag (or trigger). Just like this:
// set value true to trigger ChangeSkin() in Update(),
// instead of invoke ChangeSkin() directly
private bool _changingSkinTrigger = false;
void ChangeSkin()
{
if (_currentPrefab == PREFAB_DEFAULT)
{
_currentPrefab = PREFAB_PRINCESS;
}
else
{
_currentPrefab = PREFAB_DEFAULT;
}
_changingSkinTrigger = true;
}
void Update()
{
if (_changingSkinTrigger)
{
_changingSkinTrigger = false;
ShowSkin();
}
}
private void ShowSkin()
{
foreach (var item in _defaultDressList)
{
item.SetActive(_currentPrefab == PREFAB_DEFAULT);
}
foreach (var item in _princessDressList)
{
item.SetActive(_currentPrefab == PREFAB_PRINCESS);
}
}
Thank #AndriyMarshalek for enlightening me.
I have a custom Entry that takes text as input, I've noticed that if I go on the GIF keybord on my Xiaomi, when adding a GIF a Toast comes out saying "GIF are not supported in this text box" but it stills put the link to the GIF in the Entry. This does not happen in other phone but I still want to figure out why, or maybe a way to disable the GIF button.
When approacing to this problem I have tried different solution to disable the GIF keyboard such as this or this but they does not seems to work in my case.
Tried to implement this method like reported in the documentation but, in debug mode, it seems it does not even call it
public override IInputConnection OnCreateInputConnection(EditorInfo outAttrs)
{
IInputConnection ic = base.OnCreateInputConnection(outAttrs);
EditorInfoCompat.SetContentMimeTypes(outAttrs, new string[] { "image/*", "image/png", "image/gif", "image/jpeg" });
return InputConnectionCompat.CreateWrapper(ic, outAttrs, null);
}
Tried to add private ImeOptions but nothing
Control.PrivateImeOptions = "disableSticker=true;disableGifKeyboard=true";
Any idea ?
You could disable the emojis with custom renderer.
[assembly: ExportRenderer(typeof(Entry), typeof(MyEntryRenderer))]
namespace App10.Droid
{
public class MyEntryRenderer : EntryRenderer
{
public MyEntryRenderer(Android.Content.Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (Control != null)
{
Control.ImeOptions = Android.Views.InputMethods.ImeAction.Done;
Control.InputType = Android.Text.InputTypes.ClassText | Android.Text.InputTypes.TextVariationVisiblePassword | Android.Text.InputTypes.TextFlagMultiLine;
Control.SetTypeface(Typeface.Default, TypefaceStyle.Normal);
}
}
}
}
The problem occurs on Android.
I have implemented a custom renderer for a WebView to get the capability of resizing the height request base on its content.
I took this from a xamarin forum post.
[assembly: ExportRenderer(typeof(AutoHeightWebView), typeof(AutoHeightWebViewRenderer))]
namespace MyProject.Droid.Renderers
{
public class AutoHeightWebViewRenderer : WebViewRenderer
{
public AutoHeightWebViewRenderer(Context context): base(context) {}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if (e.NewElement is AutoHeightWebView webViewControl)
{
if (e.OldElement == null)
{
Control.SetWebViewClient(new ExtendedWebViewClient(webViewControl));
}
}
}
class ExtendedWebViewClient : Android.Webkit.WebViewClient
{
private readonly AutoHeightWebView _control;
public ExtendedWebViewClient(AutoHeightWebView control)
{
_control = control;
}
public override async void OnPageFinished(Android.Webkit.WebView view, string url)
{
if (_control != null)
{
int i = 10;
while (view.ContentHeight == 0 && i-- > 0) // wait here till content is rendered
{
await System.Threading.Tasks.Task.Delay(100);
}
_control.HeightRequest = view.ContentHeight;
}
base.OnPageFinished(view, url);
}
}
}
}
Based on a certain logic, I change the source of the WebView and use the custom renderer to resize the view.
This works when the size is increased but not when the content size is smaller than the one before...
The be clearer, if I set the source of the WebView to a html file that is 200px height and change it to a html file that is 1000px, it works fine and I can see all the content. BUT, if I try to go back to my 200px html file, I get a 800px blank space underneath since the content doesn't change on the view.ContentHeight and keep the value of 1000px.
I followed this issue/thread and didn't find a solution to resolve this problem : https://github.com/xamarin/Xamarin.Forms/issues/1711
I have seen a lot of topics on Android saying that we need to recreate the webview. Is there any other solution?
I found a way to do it based on this thread.
I first tried with a JS command that returns the body height : document.body.scrollHeight.
The problem was the same, it always returned the largest size, but never decreased the size.
As the JS command have no problem to increase the size, I had to set the height to 0, set a 100ms delay (arbitrary) and then get the height of the HTML with the JS command above and set the HeightRequest of the view.
With this, the HTML body height will not decrease... It will always start from 0 and increase to the HTML body size.
Final result :
public class AutoHeightWebViewRenderer : WebViewRenderer
{
static AutoHeightWebView _xwebView = null;
public AutoHeightWebViewRenderer(Android.Content.Context context) : base(context)
{
}
class DynamicSizeWebViewClient : WebViewClient
{
public async override void OnPageFinished(Android.Webkit.WebView view, string url)
{
try
{
if (_xwebView != null)
{
view.Settings.JavaScriptEnabled = true;
view.Settings.DomStorageEnabled = true;
_xwebView.HeightRequest = 0d;
await Task.Delay(100);
string result = await _xwebView.EvaluateJavaScriptAsync("(function(){return document.body.scrollHeight;})()");
_xwebView.HeightRequest = Convert.ToDouble(result);
}
base.OnPageFinished(view, url);
}
catch (Exception ex)
{
Console.WriteLine($"{ex.Message}");
}
}
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
_xwebView = e.NewElement as AutoHeightWebView;
if (e.OldElement == null)
{
Control.SetWebViewClient(new DynamicSizeWebViewClient());
}
}
}
I'm trying to determine when a screen is rotated (in Android) using the XLabs method detailed here How to handle screen rotation/orientation in Xamarin Forms? and I'm having trouble with it.
I override the OnConfigurationChanged method in MainActivity
public override void OnConfigurationChanged (Android.Content.Res.Configuration newConfig)
{
base.OnConfigurationChanged (newConfig);
var xapp = Resolver.Resolve<IXFormsApp> ();
if (xapp == null)
return;
switch (newConfig.Orientation) {
case Android.Content.Res.Orientation.Landscape:
xapp.Orientation = XLabs.Enums.Orientation.Landscape;
break;
case Android.Content.Res.Orientation.Portrait:
//xapp.Orientation = XLabs.Enums.Orientation.Portrait;
break;
default:
break;
}
}
I'm having trouble with the Orientation variable in IXFormsApp i.e. xapp.Orientation. The XLabs documentation lists this as 'protected set', as does the compiler:
MainActivity.cs(109,5,109,21): error CS0200: Property or indexer 'XLabs.Platform.Mvvm.IXFormsApp.Orientation' cannot be assigned to -- it is read only
and it doesn't get set automagically (when I check where it is used, it is always set to 'None'), so I was wondering how to make use of it, and indeed, how to use the XLabs/IXFormsApp to determine rotation?
On a related note, I was also trying to set the Rotation handler (not sure why, but I thought I'd give it a go) with unusual results.
xapp.Rotation += (sender, args) =>
{
switch (args.Value)
{
case XLabs.Enums.Orientation.Landscape:
//xapp.Orientation = XLabs.Enums.Orientation.Landscape;
...
break;
case XLabs.Enums.Orientation.Portrait:
...
break;
default:
break;
}
};
If I try in the Android code I get the following error:
MainActivity.cs(60,4,60,22): error CS0019: Operator '+=' cannot be applied to operands of type 'System.EventHandler<XLabs.EventArgs<XLabs.Enums.Orientation>>' and 'lambda expression'
however, if I set it in the Forms code (where the results are used), it is fine (altho the handler never seems to actually be called). Does anyone know whay this would be the case?
There are two different solutions I have used in the past.
The first is by making a PageBase class which all my pages inherit from, and PageBase inherits from a regular Page.
My PageBase has two abstract methods (so the children of it have to fill it in), which are UpdateLandscape and UpdatePortait. Children will fill these methods in for how to layout the page depending on whether it is being laid out in landscape or portrait mode.
Pages have a method OnSizeAllocated, as Daniel said. I made PageBase override it and make it call UpdateLandscape and UpdatePortait accordingly.
If, as you said, you are only looking to check when it has rotated, the above works just fine, as OnSizeAllocated gets called for a page whenever you rotate your phone.
If you are checking for landscape vs portait because you want your code to be able to check at any time, then the second solution below works too.
The second way I solved it is to use dependency services to fill in an IDeviceInfo interface, and write all dynamic things by checking if DeviceInfo.IsPortait() is true or false (and this way I also let DeviceInfo have a Width and Height, so I can request the screen dimensions at any point).
On Android, I filled in my Android code as so:
[assembly: Dependency (typeof(Namespace.DeviceInfoProvider))]
namespace Namespace
{
public class DeviceInfoProvider : IDeviceInfoProvider
{
public bool IsPortait () { return DeviceInfoManager.Width < DeviceInfoManager.Height; }
public int GetWidth () { return DeviceInfoManager.Width; }
public int GetHeight () { return DeviceInfoManager.Height; }
}
public static class DeviceInfoManager
{
public static MainActivity MainActivity { get; set; }
public static int Width { get { return MainActivity.GetWidth (); } }
public static int Height { get { return MainActivity.GetHeight (); } }
}
}
Then in MainActivity I gave it these methods:
public int GetWidth() {
return (int)(Resources.DisplayMetrics.WidthPixels / Resources.DisplayMetrics.Density);
}
public int GetHeight() {
return (int)(Resources.DisplayMetrics.HeightPixels / Resources.DisplayMetrics.Density);
}
And on the iOS side, I filled it in as so:
[assembly: Dependency (typeof(Namespace.DeviceInfoProvider))]
namespace Namespace {
public class DeviceInfoProvider : IDeviceInfoProvider {
public bool IsPortait() { return UIScreen.MainScreen.Bounds.Width < UIScreen.MainScreen.Bounds.Height; }
public int GetWidth() { return (int)UIScreen.MainScreen.Bounds.Width; }
public int GetHeight() { return (int)UIScreen.MainScreen.Bounds.Height; }
}
}
Personally, I am more of a fan of writing it the second way and making it check "if we are in portait mode, here are the differences". That way those things that are not different between portait and landscape only have to be written once, only the differences are written twice.
You can use OnSizeAllocated method override to detect orientation change;
double previousWidth;
double previousHeight;
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
if (previousWidth != width || previousHeight != height)
{
previousWidth = width;
previousHeight = height;
if (width > height)
{
// landscape mode
}
else
{
// portrait mode
}
}
}