Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

Title View's content horizontal aligment #4848

Open
bondarenkod opened this issue Dec 22, 2018 · 34 comments
Open

Title View's content horizontal aligment #4848

bondarenkod opened this issue Dec 22, 2018 · 34 comments

Comments

@bondarenkod
Copy link
Contributor

bondarenkod commented Dec 22, 2018

Description

Unable to center horizontally content in the TitleView.
I'm happy to see this feature in XF but I want to offer a few ideas based on this issue how to make this feature more useful.
We can't center the content of custom view horizontally relatively the device screen, it depends on if there is Back button or not, Right item(s) or not, etc.
the issue

Steps to Reproduce

  1. Create two pages, with identical custom Title View, add Toolbar Item at the second page
  2. Look at the Page1's Title View behaviour.
  3. Navigate to the Page2, look again.

Expected Behavior

TitleView's Content should be properly centered.
As we can see the logical horizontal center point of the TitleView's content never will be centered by phone's screen horizontal center point. So, we can't place there any image or dynamic content to create something like that (random image from the Internet):
...

Actual Behavior

TitleView's Content centered in a wrong way. We can't control that.

Basic Information

  • Version with issue: >3.1

  • Last known good version: <=3.1

  • IDE: VS2017

  • Platform Target Frameworks:

    • iOS:
  • Nuget Packages:
    Xamarin Forms 3.4.0

  • Affected Devices:
    All iOS

Screenshots

Reproduction Link

I made a short example, you can follow this link to see them in code, based on the XF's Xamarin.Forms.ControlGallery example in this branch - just clone and run the iOS app, then select Title View then BDF: TitleView Centered, combine it with the Toggle Toolbar Item

I created a few theoretical examples trying to offer a possible solution on how to solve it. In general, we need to fix the size and position of the TitleView's container to allow center its content.
...

Default Mode -

The behavior of the View Container the same as now.
Default Mode

Ghost Mode - (if it possible to implement this)

The View Container should use all the navigation bar space. We can overflow some controls by its content, but it's ok for some situations.
Ghost Mode

Unified Mode

The View Container size should take all the free space regarding the navigation bar items and pin to the bar's center.
Unified Mode

@pauldipietro pauldipietro added this to New in Triage Dec 22, 2018
@bondarenkod bondarenkod changed the title [Enhancement] Title View's content horizontal aligment Title View's content horizontal aligment Dec 25, 2018
@bondarenkod
Copy link
Contributor Author

I think it's better to change the type from [Enhancement] to issue because it's more issue than the enhancement

@beeradmoore
Copy link
Contributor

Pretty much. At the moment if you have any other items in the nav bar it makes the appearance undesirable and not worth using at all.

@ChrisTTian667
Copy link

That's a serious issue to us, we're currently waiting to be fixed.

@devlanfear
Copy link

@PureWeen Hi!
Do you have any updates on this?
It is blocker for us to update XF version for around 5 apps

Thank you!

@davidortinau
Copy link
Contributor

This continues to be an issue in Shell and one that has no reasonable workaround that I'm aware of. Negative margin hacks are prone to miscalculation. Bumping up the impact.

@developer9969
Copy link

Hi,
Amazing job all around!! However for this control "TitleView" seems to have been closed everywhere ,is this issue not longer going to be fixed? Is just not usable at all if not centered. Is it a rewrite? I have put a negative margin as a hack too but wonder if my hack work on all devices... Can somebody tell us what to do on this one. Many thanks

@programmation
Copy link

I recently butted up against this very issue in a Xamarin iOS project and there's no way to solve it using the iOS-supplied UI elements. If you use UIBarButtonItems in your UINavigationItem the OS will not tell you how wide they are after it renders them, so there's no way to set constraints or sizes on the Title view to compensate.
In the end I abandoned the whole thing and implemented my own Toolbar using a plain UIView container, and UIButtons for the toolbar buttons, with a UILabel in the middle (properly centred, because it was easy).
What we need from Xamarin Forms at this point is the ability to supply a ContentView that Forms will put into the UINavigationBar, stretched across the whole bar. It's easy to do, but don't use the UINavigationBar's Title property - Forms will have to add the ContentView as a subview of the NavigationBar itself. Then we can set our own content inside the ContentView using Buttons, Labels etc and line them up properly.

@rs-mobitech
Copy link

I'm also looking for a solution for this. It's been marked as a duplicate but the duplicate has been closed and references this issue. Is there a solution for this being considered?

@LuoyeAn
Copy link
Contributor

LuoyeAn commented Sep 18, 2019

@samhouts Hi, any plan to fix this? As far as I know, many people are paying attention to this problem.
But almost two years past, it has yet to be repaired.

@VirtualNomad00
Copy link

Sadly the issue remains even with the introduction of shell

@developer9969
Copy link

Sadly it doesn't look like it's a priority.

@bondarenkod
Copy link
Contributor Author

Celebrating 1-year for the issue. 🍾🥂 🎂

It seems like some of the default Android apps have the described behavior

dummy

@francis2
Copy link

francis2 commented Jan 2, 2020

This blows. Makes our apps look bad.

@julioas09
Copy link

This is quite annoying. Does anyone have a workaround?

@BradKwon
Copy link

BradKwon commented Jan 23, 2020

@samhouts samhouts moved this from Needs Design Review to Under consideration-High Interest in Enhancements Feb 12, 2020
@drjaydenm
Copy link

We're currently trying to center an image in the TitleView to hit some UX designs and this is causing the image to be offset to the right due to the back button being on the left, outside of the TitleView.

titleview

@programmation
Copy link

Workaround: Implement a title view that has the back button in it as well as the image, then set the navigation item's buttons to null. This should make the title view fill the whole width of the navigation bar.

@jaspervanmegroot
Copy link

Really need this..... zZz

@jaspervanmegroot
Copy link

@programmation Do you have an example for this?

@eli191
Copy link

eli191 commented May 10, 2020

@jaspervanmegroot
I know about hiding the hamburger icon

 <Shell.FlyoutIcon>
        <x:String></x:String>
    </Shell.FlyoutIcon>

But it is not perfect though, still the left margin is slightly bigger than the right one.

@programmation
Copy link

We're currently trying to center an image in the TitleView to hit some UX designs and this is causing the image to be offset to the right due to the back button being on the left, outside of the TitleView.

titleview

Alternative workaround: create a dummy, blank, do-nothing button for the opposite side of the navigation bar.

@programmation
Copy link

programmation commented May 10, 2020 via email

@eli191
Copy link

eli191 commented May 11, 2020

@programmation Thanks!

Basically it is working, but it is a bit awkward because if you put toolbar items, then the Shell.TitleView is pushed also to the left, so either you don't use those, either your title is navigating from one page to the other right to left and vice versa.

@programmation
Copy link

@eli191 I agree it's really not ideal, but that was the best workaround I had while still using the XF-provided NavigationPage. Overall however I'm not a fan of the customisations that have been applied to the underlying UINavigationController, which are partly responsible for how hard it is to do what you're trying to do. I'm almost thinking it would be simpler to make a NavigationBar object with platform renderers (might not actually need any though), and then use a ControlTemplate to apply it to pages that want it, while removing the "official" navigation bar from the enclosing NavigationPage. It would certainly be simpler when it comes to things like applying custom colours to the navigation background, and it would also allow full control over right and left button stacks.

@jaspervanmegroot
Copy link

@programmation Do you have an example project on github?

@MaxFmi
Copy link

MaxFmi commented Aug 26, 2020

Still facing this issue in August 2020. We soon will hit 2 years for this bug. Is there a plan to fix it?

@conor-codes
Copy link

We now how excellent features like drag & drop and swipe view but i'm still struggling to centre the title.

@Hackavist
Copy link

Is there any possibility for this to be fixed any soon ? maybe in XF 5

@julioas09
Copy link

julioas09 commented Jan 3, 2021

Yep, still struggling with this in 2021 :(
iOS works flawlessly, Android has that anoying shift to the right.

@julioas09
Copy link

Used this dirty workaround to solve it for now:

<NavigationPage.TitleView>

        <StackLayout Orientation="Horizontal" VerticalOptions="Center" HorizontalOptions="Center">
            <StackLayout.Margin>
                <OnPlatform x:TypeArguments="Thickness">
                    <On Platform="Android" Value="0, 0, 25, 0" />
                </OnPlatform>
            </StackLayout.Margin>
            <Image Source="icon_low_no_bg.png" 
                   HeightRequest="35"
                   WidthRequest="120"/>
        </StackLayout>

    </NavigationPage.TitleView>

@xamiell
Copy link

xamiell commented Jul 7, 2022

Unfortunately this does not seem to be a priority event when it was reported in 2018 but still in 2022.

@kasimier-vireq
Copy link

kasimier-vireq commented Oct 19, 2022

This is what we use as a workaround (is it a workaround?).

The relevant stuff is in the method ApplyToolbarAdjustments.
We have a dedicated view (PageTitleView) which is a grid and needs to span the entire width of the screen in order to satisfy our UX/UI department.

iOS:

EDIT (2022-01-28): I had to adjust the computation for iOS >= 16.
(Also removed some non-related code parts)

class CustomNavigationRenderer : NavigationRenderer
    {
        private readonly Info _info = new Info();

        protected override void OnElementChanged(VisualElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            if (e.OldElement is NavigationPage oldNavigationPage)
            {
                oldNavigationPage.PropertyChanged -= OnNavigationPagePropertyChanged;
            }

            if (e.NewElement is NavigationPage navigationPage)
            {
                SetInfo(navigationPage);
                navigationPage.PropertyChanged += OnNavigationPagePropertyChanged;
            }
            else
            {
                SetInfo(null);
            }
        }

        private void OnNavigationPagePropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == nameof(NavigationPage.CurrentPage))
            {
                SetInfo(sender as NavigationPage);
            }
        }

        public override void ViewDidLayoutSubviews()
        {
            base.ViewDidLayoutSubviews();

            ApplyToolbarAdjustments();
        }

        private void ApplyToolbarAdjustments()
        {
            if (_info.TitleView != null && NavigationBar != null)
            {
                UIView containerView = FindContainerViewInSubviews(NavigationBar);
                if (containerView != null)
                {
                    UIView uiBarNavigationContentView = containerView.Superview;

                    // Set negative horizontal padding of the PageTitleView (which is a PyjamaGrid)
                    // in order to compensate for the built-in padding of the navigation bar.
                    // Note that we can't use PageTitleView.Margin here because changes to the margin
                    // are applied when the page is already visible (I don't know why),
                    // which results in a jumping of the title visuals.

                    double horizontalOffset;

                    if (DeviceInfo.Version.Major < 16)
                    {
                        // Example (Simulator: iOS 15, iPhone 13):
                        // uiBarNavigationContentView (superview):
                        //     x:0 y:0 w:390 h:44
                        // containerView:
                        //     x:50 y:0 w:331 h:44
                        horizontalOffset = containerView.Frame.X;
                    }
                    else
                    {
                        // Example (Simulator: iOS 16.0, iPhone 14):
                        // uiBarNavigationContentView (superview):
                        //     x:50 y:0 w:323 h:44
                        // containerView:
                        //     x:0 y:0 w:323 h:44
                        horizontalOffset = uiBarNavigationContentView.Frame.X;
                    }

                    var mainDisplayInfo = DeviceDisplay.MainDisplayInfo;
                    var screenWidth = mainDisplayInfo.Width / mainDisplayInfo.Density;

                    var padding = new Thickness(
                        -horizontalOffset,
                        0,
                        -(screenWidth - containerView.Frame.Width - horizontalOffset),                       
                        0);

                    _info.TitleView.Padding = padding;
                }
            }
        }

        /// <summary>
        /// Finds the Container instance anywhere in the descendants of the UINavigationBar.
        /// Xamarin uses the Container as a container for e.g. the TitleView.
        /// </summary>
        private UIView FindContainerViewInSubviews(UIView view)
        {
            foreach (var subView in view.Subviews)
            {
                // The Container is a private class of NavigationRenderer.
                if (subView.GetType().FullName == "Xamarin.Forms.Platform.iOS.NavigationRenderer+Container")
                {
                    return subView;
                }
                else
                {
                    var descendantView = FindContainerViewInSubviews(subView);
                    if (descendantView != null)
                    {
                        return descendantView;
                    }
                }
            }

            return null;
        }

        private void SetInfo(NavigationPage navigationPage)
        {
            _info.NavigationPage = navigationPage;
            _info.CurrentContentPage = navigationPage?.CurrentPage as AppContentPage;
            _info.TitleView = navigationPage?.CurrentPage?.GetValue(NavigationPage.TitleViewProperty) as PageTitleView;
        }

        private sealed class Info
        {
            public NavigationPage NavigationPage { get; set; }
            public PageTitleView TitleView { get; set; }
            public AppContentPage CurrentContentPage { get; set; }
        }
    }

Android

class CustomNavigationPageRenderer : NavigationPageRenderer
    {
        private readonly Info _info = new Info();

        public CustomNavigationPageRenderer(Context context)
            : base(context)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<NavigationPage> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement is NavigationPage oldNavigationPage)
            {
                oldNavigationPage.PropertyChanged -= OnNavigationPagePropertyChanged;
            }

            if (e.NewElement is NavigationPage navigationPage)
            {
                // NOTE: @_Info.Toolbar is set in OnViewAdded which is called before OnElementChanged.
                SetInfo(navigationPage);
                navigationPage.PropertyChanged += OnNavigationPagePropertyChanged;
            }
            else
            {
                SetInfo(null);
            }
        }

        private void OnNavigationPagePropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == nameof(NavigationPage.CurrentPage))
            {
                SetInfo(sender as NavigationPage);
            }
        }

        protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
        {
            base.OnLayout(changed, left, top, right, bottom);

            // Since OnLayout is called many times without the NaviationPage itself being
            // changed in position/size: apply adjustments only if IsToolbarAdjustmentPending
            // (which is set when a new CurrentPage arrives).
            if (changed || _info.IsToolbarAdjustmentPending)
            {
                _info.IsToolbarAdjustmentPending = false;
                ApplyToolbarAdjustments();
            }
        }

        private void ApplyToolbarAdjustments()
        {
            if (_info.Toolbar != null)
            {           
                    _info.Toolbar.SetPadding(0, 0, 0, 0);
                    _info.Toolbar.SetTitleMargin(0, 0, 0, 0);

                    if (_info.TitleView != null)
                    {
                        // Apply negative padding-left on the title view (which is a PyjamaGrid)
                        //   in order to compensate for the navigation inset (e.g. the back/hamburger icon).
                        //   We can't use the Margin because it's a PyjamaGrid.
                        _info.TitleView.Padding = new Thickness(
                            -(_info.Toolbar.ContentInsetStartWithNavigation / DeviceDisplay.MainDisplayInfo.Density),
                            0, 0, 0);
                    }
            }
        }

        public override void OnViewAdded(Android.Views.View child)
        {
            base.OnViewAdded(child);

            // NOTE: OnViewAdded is called before OnElementChanged.

            // We can't use "Context.GetActivity().FindViewById(Resource.Id.toolbar) as AndroidX.AppCompat.Widget.Toolbar"
            //   because the NavigationPageRenderer uses an own Toolbar instance.
            //   Plus that Toolbar is not exposed via a public member,
            //   thus we have to fish for it here.
            if (child is AndroidX.AppCompat.Widget.Toolbar toolbar)
            {
                _info.Toolbar = toolbar;
            }
        }

        private void SetInfo(NavigationPage navigationPage)
        {
            _info.NavigationPage = navigationPage;

            var currentContentPage = navigationPage?.CurrentPage as AppContentPage;

            // Since OnLayout is called many times without the NaviationPage itself being
            // changed in position/size: apply adjustments only if IsToolbarAdjustmentPending
            // (which is set when a new CurrentPage arrives).
            _info.IsToolbarAdjustmentPending = _info.CurrentContentPage != currentContentPage;

            _info.CurrentContentPage = currentContentPage;

            _info.TitleView = navigationPage?.CurrentPage?.GetValue(NavigationPage.TitleViewProperty) as PageTitleView;
        }

        private sealed class Info
        {
            public NavigationPage NavigationPage { get; set; }
            public AndroidX.AppCompat.Widget.Toolbar Toolbar { get; set; }
            public PageTitleView TitleView { get; set; }
            public AppContentPage CurrentContentPage { get; set; }
            public bool IsToolbarAdjustmentPending { get; set; }
        }
    }

@xamiell
Copy link

xamiell commented Dec 13, 2022

9 days left to 4 years 🥲

Celebrating 1-year for the issue. 🍾🥂 🎂

It seems like some of the default Android apps have the described behavior

dummy

@bondarenkod
Copy link
Contributor Author

bondarenkod commented Feb 5, 2023

Hi guys, check this out and ping me with code examples if something is still wrong.
This version of XF also contains fixes for #15680
In the sources you can find a small demo project called titleview.iOS.

This is XF project adopted to use with VS2022 for Windows and the latest VS2022 for Mac. You need to run the .Xamarin.Forms.vs2022.iOS.slnf file and deploy the titleview.iOS project.

Here is all the changes I've made bondarenkod/repro-xf-titleimage-not-centered@66b04fb

You can build nuget using this sources https://github.com/bondarenkod/repro-xf-titleimage-not-centered
compiled nuget - Xamarin.Forms.5.0.0.2549.zip

@xamiell, @beeradmoore, @ChrisTTian667, @devlanfear, @developer9969, @programmation, @rs-mobitech, @VirtualNomad00 , @francis2, @julioas09 , @BradKwon , @drjaydenm , @jaspervanmegroot, @eli191, @MaxFmi , @Hackavist, @julioas09, @kasimier-vireq

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
Enhancements
  
Under consideration-High Interest
Development

No branches or pull requests