Disappearing SearchBar for Xamarin.Forms

Many apps with lists now allow the search bar to disappear while scrolling down the list, then reappear whenever the user starts to scroll back up.  That isn’t possible out of the box with Xamarin.Forms.  Instead we can either:

  • Insert the SearchBar in the ListView.Header so it disappears when scrolling down the list, but it won’t reappear until the user scrolls back to the start of the list.
  • Put the SearchBar above the ListView so the user doesn’t have to scroll back to the top to see it, but that means it’s always taking up space on the screen.

Neither is exactly what we want.

Supporting that in Xamarin.Forms requires a bit of work, but luckily we build most of what we need in a post on Observing ListView Scrolling.

With that as a starting point, there are two steps:

Create view model property for SearchBar visibility

For this example, I’m taking the easy way out and creating a property on the view model. The behavior I want is to make the SearchBar visible if the ListView is at the top or the scroll direction is going towards the start. This composite condition isn’t directly exposed based on existing infrastructure I built, so I’m going to define a new view model property.

This design isn’t as elegant as it could be, as it puts a lot of control of the view into the view model; a cleaner approach might involve a value converter or a trigger, depending on the situation.  With that in mind, I’m going to charge ahead.

So all I need to do is this:

public bool ShouldShowSearchBar
{
    get {
        return AtStartOfList || LastScrollDirection == ScrollToStart;
    }
}

However that doesn’t work well in practice.  People’s fingers aren’t always steady when interacting with a list, and this is very responsive to subtle changes in direction. The result, if implemented this way, ends up being very jittery, with the ScrollBar appearing and disappearing repeatedly, very quickly.

Note that it’s not so bad on the desktop with a mouse, as that ends up being fairly steady.

Anyway, we need a better way.  What I went with is to make an arbitrary decision that the state shouldn’t change more often than every 250ms.  There are a few ways to implement this, but this is pretty straightforward, not relying on Rx or anything too fancy:

        private bool? _lastShowValue;
        private DateTime _nextShowChange;
        public bool ShouldShowSearchBar
        {
            get {
                var newValue = AtStartOfList
                       || LastScrollDirection == ScrollToStart;
                var shouldChange = (!_lastShowValue.HasValue)
                       || (_lastShowValue.Value != newValue && _nextShowChange < DateTime.Now);

                if (shouldChange)
                {
                    _lastShowValue = newValue;
                    _nextShowChange = DateTime.Now.AddMilliseconds(250);
                }

                return shouldChange ? newValue : _lastShowValue.Value;
            }
        }

Why 250ms?  Any less and there was a lot of disconcerting jitter if you scroll to the end of the list iOS.  With 250ms, the jitter disappears in all but a few circumstances, and even then it’s minimal.  I may try to eliminate it entirely, but will probably require more work on the iOS custom renderer.  That being said, 250ms still feels plenty responsive for making the SearchBar appear and disappear when wanted.

The last thing that needs to be done is to make sure that our new property has the appropriate PropertyChanged events fired on it.  Since the value of ShouldShowSearchBar is based entirely on two other properties, we need to fire that event in the setters for those:

        public string LastScrollDirection
        {
            get { return (string)GetValue(LastScrollDirectionProperty); }
            set {
                SetValue(LastScrollDirectionProperty, value);
                OnPropertyChanged(nameof(ShouldShowSearchBar));
            }
        }

This only shows one, the pattern for updating the other is identical. See the full source on GitHub for the complete example.

Add SearchBar to page with ListView

If the SearchBar is in the ListView.Header, it will scroll off with the top of the list.  We don’t want that, so we insert the SearchBar above the ListView in the layout.  We just need to bind its IsVisible property to the view model property we just defined:

<SearchBar IsVisible="{Binding ShouldShowSearchBar}"></SearchBar>

In this example, the SearchBar doesn’t actually do anything except appear and disappear. In the real project, it’s hooked up to filtering the element in the ListView, as you’d expect.

The way the SearchBar appears and disappears is slightly more sudden than I’d like.  A project for another day is to make that smoother.

Wrapping up

The GitHub repo from the previously ListView blog entry has been updated to include this non-functional but appearing & disappearing SearchBar.  The code is here: https://github.com/david-js/XFListViewInfo

 

Author

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.