[ windows phone 8 ] How To Implement pull-to-refresh Interaction For LongListSelector

Реализация pull-to-refresh для WP8

Начать хочется с того, что всё тоже самое справедливо и для WP7, если использовать LongListSelector for Windows Phone 7.5 1.0.1, хотя в нём и есть несколько неприятных моментов. Что касается LongListSelector из состава WPToolkit или ListBox, то тут всё совсем просто, достаточно получить ScrollViewer и грамотно воспользоваться VerticalOffset. В последнем случае все тропы уже изучены и шишки набиты, можно найти красивое готовое решение. А вот с LLS под WP8 ситуация иная.

Для начала, если вы будете пользоваться наработками других товарищей, то скорее всего в итоге получите BindingExpression path error  или, что ещё хуже, таинственный и загадочный -1073741819 (0xc0000005) ‘Access violation’. Возникает это при резком скролле из-за виртуализации данных, в различных ситуациях по-разному. Основная причина в том, что мы либо делаем binding элементов, которые уже уничтожены, либо биндим в ту область памяти, которая уже освобождена. Повинны в этом в любом случае разработчики LongListSelector, на мой взгляд, т.к. подобный вариант можно было бы и продумать или хотя бы кидать адекватный exception.

Чтобы этого избежать, надо после каждой дозагрузки списка делать _longlist.UpdateLayout();

Что касается самого pull-to-refresh, то есть полно готовых кривых реализаций, одна из них вот:

using Microsoft.Phone.Controls;
using System;
using System.Windows.Controls.Primitives;

/// <summary>
/// This class detects the pull gesture on a LongListSelector. How does it work?
/// 
///     This class listens to the change of manipulation state of the LLS, to the MouseMove event 
///     (in WP, this event is triggered when the user moves the finger through the screen)
///     and to the ItemRealized/Unrealized events.
///     
///     Listening to MouseMove, we can calculate the amount of finger movement. That is, we can 
///     detect when the user has scrolled the list.
///     
///     Then, when the ManipulationState changes from Manipulating to Animating (from user 
///     triggered movement to inertia movement), we check the viewport changes. The viewport is 
///     only constant when the user scrolls beyond the end of the list, either at the top or at the bottom.
///     If no items were added, check the direction of the scroll movement and fire the corresponding event.
/// </summary>
public class WP8PullDetector
{
    LongListSelector listbox;

    bool viewportChanged = false;
    bool isMoving = false;
    double manipulationStart = 0;
    double manipulationEnd = 0;

    public bool Bound { get; private set; }

    public void Bind(LongListSelector listbox)
    {
        Bound = true;
        this.listbox = listbox;
        listbox.ManipulationStateChanged += listbox_ManipulationStateChanged;
        listbox.MouseMove += listbox_MouseMove;
        listbox.ItemRealized += OnViewportChanged;
        listbox.ItemUnrealized += OnViewportChanged;
    }

    public void Unbind()
    {
        Bound = false;

        if (listbox != null)
        {
            listbox.ManipulationStateChanged -= listbox_ManipulationStateChanged;
            listbox.MouseMove -= listbox_MouseMove;
            listbox.ItemRealized -= OnViewportChanged;
            listbox.ItemUnrealized -= OnViewportChanged;
        }
    }

    void OnViewportChanged(object sender, Microsoft.Phone.Controls.ItemRealizationEventArgs e)
    {
        viewportChanged = true;
    }

    void listbox_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
    {
        var pos = e.GetPosition(null);

        if (!isMoving)
            manipulationStart = pos.Y;
        else
            manipulationEnd = pos.Y;

        isMoving = true;
    }

    void listbox_ManipulationStateChanged(object sender, EventArgs e)
    {
        if (listbox.ManipulationState == ManipulationState.Idle)
        {
            isMoving = false;
            viewportChanged = false;
        }
        else if (listbox.ManipulationState == ManipulationState.Manipulating)
        {
            viewportChanged = false;
        }
        else if (listbox.ManipulationState == ManipulationState.Animating)
        {
            var total = manipulationStart - manipulationEnd;

            if (!viewportChanged && Compression != null)
            {
                if (total < 0)
                    Compression(this, new CompressionEventArgs(CompressionType.Top));
                else if(total > 0) // Explicitly exclude total == 0 case
                    Compression(this, new CompressionEventArgs(CompressionType.Bottom));
            }
        }
    }

    public event OnCompression Compression;
}

public class CompressionEventArgs : EventArgs
{
    public CompressionType Type { get; protected set; }

    public CompressionEventArgs(CompressionType type)
    {
        Type = type;
    }
}

public enum CompressionType { Top, Bottom, Left, Right };

public delegate void OnCompression(object sender, CompressionEventArgs e);

 Использование:

public Page1()
{
    InitializeComponent();
    var objWP8PullDetector = new WP8PullDetector();
    objWP8PullDetector.Bind(objLongListSelector);
    //objWP8PullDetector.Unbind(); To unbind from compression detection
    objWP8PullDetector.Compression += objWP8PullDetector_Compression;
}

void objWP8PullDetector_Compression(object sender, CompressionEventArgs e)
{
    //TODO: Your logic here
}

Срабатывать это чудо научной жизни будет не при завершении (или начале) списка, а каждый раз при прокрутке вверх или вниз в пределах одного viewport

Посему, сразу хотелось бы обратить внимание на немного более здравую задумку

И в сумме получаем что-то вроде этого:

public class WP8PullDetector
{
    LongListSelector listbox;

    bool viewportChanged = false;
    bool isMoving = false;
    double manipulationStart = 0;
    double manipulationEnd = 0;

    public bool Bound { get; private set; }
	private ViewportControl _viewport = null;

    public void Bind(LongListSelector listbox)
    {
        Bound = true;
        this.listbox = listbox;
        listbox.ManipulationStateChanged += listbox_ManipulationStateChanged;
        listbox.MouseMove += listbox_MouseMove;
        listbox.ItemRealized += OnViewportChanged;
        listbox.ItemUnrealized += OnViewportChanged;
		_viewport = FindVisualChild<ViewportControl>(listbox);
    }

    public void Unbind()
    {
        Bound = false;

        if (listbox != null)
        {
            listbox.ManipulationStateChanged -= listbox_ManipulationStateChanged;
            listbox.MouseMove -= listbox_MouseMove;
            listbox.ItemRealized -= OnViewportChanged;
            listbox.ItemUnrealized -= OnViewportChanged;
			_viewport=null;
        }
    }

	public static T FindVisualChild<T>(DependencyObject depObj) where T : DependencyObject
	{
		if (depObj != null)
		{
			for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
			{
				DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
				if (child != null && child is T)
				{
					return (T)child;
				}

				T childItem = FindVisualChild<T>(child);
				if (childItem != null) return childItem;
			}
		}
		return null;
	}

    void OnViewportChanged(object sender, Microsoft.Phone.Controls.ItemRealizationEventArgs e)
    {
        viewportChanged = true;
    }

    void listbox_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
    {
        var pos = e.GetPosition(null);

        if (!isMoving)
            manipulationStart = pos.Y;
        else
            manipulationEnd = pos.Y;

        isMoving = true;
    }

    void listbox_ManipulationStateChanged(object sender, EventArgs e)
    {
        if (listbox.ManipulationState == ManipulationState.Idle)
        {
            isMoving = false;
            viewportChanged = false;
        }
        else if (listbox.ManipulationState == ManipulationState.Manipulating)
        {
            viewportChanged = false;
        }
        else if (listbox.ManipulationState == ManipulationState.Animating)
        {
            var total = manipulationStart - manipulationEnd;

            if (!viewportChanged && Compression != null)
            {
                if (total < 0 && (_viewport.Viewport.Top == _viewport.Bounds.Top))
                    Compression(this, new CompressionEventArgs(CompressionType.Top));
                else if(total > 0 && (_viewport.Bounds.Bottom == _viewport.Viewport.Bottom)) 
                    Compression(this, new CompressionEventArgs(CompressionType.Bottom));
            }
        }
    }

    public event OnCompression Compression;
}

public class CompressionEventArgs : EventArgs
{
    public CompressionType Type { get; protected set; }

    public CompressionEventArgs(CompressionType type)
    {
        Type = type;
    }
}

public enum CompressionType { Bottom, Top };

public delegate void OnCompression(object sender, CompressionEventArgs e);

Работоспособность в боевых условиях кода выше я лично не проверял, т.к. под себя переписывал полностью, но как идеи, этого вполне достаточно. В данном случае мы точно не будем лишний раз сигналить о приближении к концу (или к началу), именно с этой целью мы и получали _viewport, чтобы затем сравнить _viewport.Bounds.Bottom == _viewport.Viewport.Bottom, к слову, второе сравнение (total > 0) можно и вовсе убрать в данном случае, как и всё что с ним связано.

А можно и не убирать, к примеру, если мы будем делать так:

                if(total > 0 && ((_viewport.Bounds.Bottom - (_viewport.Viewport.Height + _viewport.Viewport.Bottom))) 
                    Compression(this, new CompressionEventArgs(CompressionType.Bottom));

То событие будет происходить ещё до того, как мы потянем вниз, это очень удобно, если пользователь не догадывается, что теоретически можно тянуть, а так же и просто удобнее, чем тянуть. Но тогда стоит добавить дополнительный флаг, что мы уже отправляли event, и не забывать его сбрасывать, а так же, отправлять событие и при ManipulationState.Idle

Впрочем, это уже совсем другая история…

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *