Force overlapping View to render on top of other Views

ShimmyWeitzhandlerShimmyWeitzhandler ✭✭✭USMember ✭✭✭

Cross posted here.

I'm creating a suggestion box, and using a RelativeLayout I managed to make a ListView stick out from the SearchBar.

Everything works like a charm, except the ListView appears to be under the elements overlapping it.
In the image below (taken from the tablet oriented page in the UWP), the areas in the ListView that are not opaque, receive user input when touched/clicked.

What I want is to force the ListView's 'ZIndex' to be the top element, so it's:

  • White background
  • Sole element to receive user input (i.e. click/touch)

As you can also see in my code, after setting the background the ListView still looks like its transparent, because it's rendered under the rest of the controls overlapping it.

Here's how I'm setting the ListView in the RelativeLayout:

public class AutoCompleteView : ContentView
{
  readonly RelativeLayout _Container = new RelativeLayout();
  readonly ListView _ListView = new ListView();
  readonly SearchBar _SearchBar = new SearchBar();

  public AutoCompleteView()
  {
    _SearchBar.SearchButtonPressed += (sender, e) => OnSearch();
    _SearchBar.TextChanged += SearchBarTextChanged;
    _SearchBar.Focused += SearchBar_Focused;
    _SearchBar.Unfocused += (sender, e) => _ListView.IsVisible = false;

    MeasureInvalidated += (sender, e) => InvalidateSearchBar();
    _SearchBar.SizeChanged += (sender, e) => InvalidateSearchBar();

    _ListView.IsVisible = false;
    _ListView.ItemSelected += _ListView_ItemSelected;

    var heightR = HeightRequest;
    _Container.BackgroundColor = Color.White;
    _ListView.BackgroundColor = Color.White;
    _ListView.InputTransparent = false;

    _Container.Children.Add(_SearchBar, Constraint.Constant(0), Constraint.Constant(0));
    _Container.Children.Add(_ListView, 
      yConstraint: Constraint.RelativeToParent(rl => rl.Height), 
      widthConstraint: Constraint.RelativeToParent(rl=> rl.Width));
    Content = _Container;
  }
}

Best Answer

  • ShimmyWeitzhandlerShimmyWeitzhandler ✭✭✭ US ✭✭✭
    edited July 2017 Accepted Answer

    OK I got it to work on the UWP

    Here's what I did:

    In the PCL:

    public interface IZindex
    {                                 
      void Set(View view, int zindex);
      void MoveToTop(View view);
      void MoveToBack(View view);     
    }
    

    In the UWP:

    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Media;
    using Xamarin.Forms;
    
    [assembly: Dependency(typeof(ZIndex))]
    namespace Xamarin.Forms
    {
      public class ZIndex : IZindex
      {
        const int MaxZindex = 1000000;
    
        public void Set(View view, int zindex)
        {
          var renderer = Xamarin.Forms.Platform.UWP.Platform.GetRenderer(view);
          var element = renderer.ContainerElement;
    
          SetZindex(element, zindex);
        }
    
        public void MoveToTop(View view) =>
          Set(view, MaxZindex);
    
        public void MoveToBack(View view) =>
          Set(view, int.MinValue);
    
        void SetZindex(UIElement child, int value)
        {
          var parent = VisualTreeHelper.GetParent(child);
          Canvas.SetZIndex(child, value);
          if (parent is UIElement parentElement)
            SetZindex(parentElement, value);
        }
      }
    }  
    

    In the ACV's constructor:

    public AutoCompleteView()
    {
      /* omitted for brevity */
    
      void ListView_ItemAppearing(object sender, ItemVisibilityEventArgs e)
      {
        var lv = (ListView)sender;
        lv.ItemAppearing -= ListView_ItemAppearing;
        var zindex = DependencyService.Get<IZindex>();
        zindex.MoveToTop(lv);
      };
      _ListView.ItemAppearing += ListView_ItemAppearing;
    }
    

    I'm currently focusing on the UWP, but I believe Android and iOS offer an answer for this, so implementing this interface should be possible on both.

Answers

  • ChaseFlorellChaseFlorell mod CAInsider, University mod

    Shameless plug: Here's an autocomplete that I wrote.
    https://www.nuget.org/packages/Xfx.Controls/

  • ShimmyWeitzhandlerShimmyWeitzhandler ✭✭✭ USMember ✭✭✭
    edited July 2017

    @ChaseFlorell

    1. I was asking a general question with a specific example. My AutoComplete works perfectly except of the overlapping issue. I was rather looking for a solution to the overlapping problem rather than for a new AutoComplete example.
    2. I was clearly stating in my question that I'm on UWP and tablet layout.
    3. Thanks for your effort
  • ChaseFlorellChaseFlorell mod CAInsider, University mod

    Right, I understand. (excepting for the fact that I missed the UWP bit). I would still however recommend going the custom renderer route since you are going to run into more issues using a SearchBar and ListView (IE: if you need to put it inside a scrollview). In fact, I'd love a contribution to UWP with this.

    To your point however, order definitely matters when adding views to a parent view, and I don't actually see the code you're using to add your AutoComplete to your page. You've added the ListView to the ContentView (blow the SearchBar) correctly, but how have you added the AutoComplete to your page?

    Also what does the code look like when setting the background color? In other words, are you setting myAutoComplete.BackgroundColor=Color.Foo or are you actually setting the background color of the ListView inside the RelativeLayout inside the AutoComplete?

  • ShimmyWeitzhandlerShimmyWeitzhandler ✭✭✭ USMember ✭✭✭
    edited July 2017

    I add the ACV directly into the XAML of my Page I can't share it here because it's a rather complex page of some 400 lines.
    Here's the source code for the AutoCompleteView.

    Anyway, the ACV works and behaves perfect.
    Setting ListView or any of the controls' background doesn't solve the issue because the ListView remains on the bottom of the controls overlapping it.

    Is there an event or a virtual method that's called when the element is ready for render?

    I tried to create this recursive function to bring the view to top:

    protected override void OnParentSet() =>
      BringToTop();
    void BringToTop(Element element = null)
    {
      if (element == null) element = this;
      var parent = element.Parent;
      if (parent == null) return;
    
      var sp = (IViewContainer<View>)new StackLayout();
      if (parent is Layout layout)
        if (parent is IViewContainer<View> viewContainer)
          foreach (var view in viewContainer.Children)
            if (view == element)
              layout.RaiseChild(view);
            else
            {
              try
              {
                layout.LowerChild(view);
              }
              catch { /* in case elements are still adding */ }
            }
    
      BringToTop(parent);
    }
    

    But the OnParentSet is called during adding children, an in turn, if I call LowerChild I get the ObservableCollection's exception that I'm trying to alter the collection while it's still adding. So what I would want to do is call BringToTop when element is ready for render (i.e. after all its parents have been set entirely).

    XF is so limited. Why isn't there a BringToTop method on each view?

    Please also see the discussion in the cross posted question.

  • Amar_BaitAmar_Bait ✭✭✭✭✭ DZMember ✭✭✭✭✭

    Use a Grid.

  • ShimmyWeitzhandlerShimmyWeitzhandler ✭✭✭ USMember ✭✭✭

    @nadjib Can you please try to be more specific? A Grid where? Instead of what? Have you read my code?

  • ChaseFlorellChaseFlorell mod CAInsider, University mod

    He means use a grid in place of RelativeLayout. @TheRealJasonSmith says that RelativeLayout should be used as a last resort, and also when you use a grid, first in automatically goes to the bottom.

  • ShimmyWeitzhandlerShimmyWeitzhandler ✭✭✭ USMember ✭✭✭

    @ChaseFlorell

    He means use a grid in place of RelativeLayout

    That's obviously not an option, since I need the overlapping here. I just need the ACV and/or at least the ListView be on top. I'm really shocked this is apparently so hard to implement.

    Did you have a chance to look at the my full code?

    I think I might be closer to a solution if I know about an event/method that's raised when view is ready for render or after the visual tree has finished setting up, do you know about such an event?

  • ShimmyWeitzhandlerShimmyWeitzhandler ✭✭✭ USMember ✭✭✭
    edited July 2017

    Look what a huge pile of events MS provides.

    I'm sure there are many events and options that are also common to UWP, Droid and iOS.
    For someone coming from WPF/UWP, XF feels very empty and green.

    @ChaseFlorell
    Can you please think of a few events/virtuals that I can try to mess with? Anything that's happening after OnParentSet that's available in ContentView or any event of ListView.

  • ShimmyWeitzhandlerShimmyWeitzhandler ✭✭✭ USMember ✭✭✭
    edited July 2017

    OK. I managed to make the MoveToTop work (had to call ToArray in the foreach so the iterator doesn't operates on a copied array), but it moved the ListView to the end of the form hahaha WTF!!

    RaiseChild and LowerChild actually simply moves the view's order within parent. I'm starting to believe Xamarin.Forms has no ZIndex at all, so any View appearing after another one will be on top of the first one when overlapping, without any option to change that. Is that correct @TheRealJasonSmith?

  • ShimmyWeitzhandlerShimmyWeitzhandler ✭✭✭ USMember ✭✭✭
    edited July 2017 Accepted Answer

    OK I got it to work on the UWP

    Here's what I did:

    In the PCL:

    public interface IZindex
    {                                 
      void Set(View view, int zindex);
      void MoveToTop(View view);
      void MoveToBack(View view);     
    }
    

    In the UWP:

    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Media;
    using Xamarin.Forms;
    
    [assembly: Dependency(typeof(ZIndex))]
    namespace Xamarin.Forms
    {
      public class ZIndex : IZindex
      {
        const int MaxZindex = 1000000;
    
        public void Set(View view, int zindex)
        {
          var renderer = Xamarin.Forms.Platform.UWP.Platform.GetRenderer(view);
          var element = renderer.ContainerElement;
    
          SetZindex(element, zindex);
        }
    
        public void MoveToTop(View view) =>
          Set(view, MaxZindex);
    
        public void MoveToBack(View view) =>
          Set(view, int.MinValue);
    
        void SetZindex(UIElement child, int value)
        {
          var parent = VisualTreeHelper.GetParent(child);
          Canvas.SetZIndex(child, value);
          if (parent is UIElement parentElement)
            SetZindex(parentElement, value);
        }
      }
    }  
    

    In the ACV's constructor:

    public AutoCompleteView()
    {
      /* omitted for brevity */
    
      void ListView_ItemAppearing(object sender, ItemVisibilityEventArgs e)
      {
        var lv = (ListView)sender;
        lv.ItemAppearing -= ListView_ItemAppearing;
        var zindex = DependencyService.Get<IZindex>();
        zindex.MoveToTop(lv);
      };
      _ListView.ItemAppearing += ListView_ItemAppearing;
    }
    

    I'm currently focusing on the UWP, but I believe Android and iOS offer an answer for this, so implementing this interface should be possible on both.

Sign In or Register to comment.