Статьи

Исправление Pinch Увеличить в Silverlight для Windows Phone

Масштабирование — это одна из тех вещей, которые выглядят невероятно простыми, пока вы не попытаетесь их реализовать. В этот момент вы понимаете, что он скрывает целый ряд тонкостей, которые затрудняют его правильное понимание. Если вы пытались реализовать масштабирование пинчером в Silverlight для Windows Phone 7, вы, вероятно, знаете, о чем я говорю.

Что это значит сделать это правильно ?

Адриан Цай уже дал отличное объяснение , поэтому я не буду повторять его слова. Тест очень прост: выберите две точки на изображении (например, два глаза) и увеличьте их пальцами. Если в конце увеличения две точки все еще находятся под вашими пальцами, вы поняли это правильно, иначе вы ошиблись.

Мультитач Поведение

Лоран Бюньон, Дэвид Зордан и Дэвид Келли — люди, стоящие за  Multitouch Behavior для SL и WPF . Это впечатляющий проект с открытым исходным кодом, и вы должны проверить его. В дополнение к пинч-масштабированию он дает вам вращение, инерцию, режим отладки и многое другое. С ним очень легко работать, так как вам нужна пара строк XAML. Единственным недостатком является то, что во время написания кажется, что нет способа прочитать текущее состояние масштабирования, что затрудняет полную поддержку захоронения. Если вам это не нужно, возьмите Multitouch Behavior и перестаньте читать: это, вероятно, будет работать лучше, и вы сэкономите время.

XAML

Это XAML, с которого мы начинаем. Обратите внимание, что наша DIY-реализация опирается на InputGesture набора инструментов Silverlight . Если вы еще не используете его, пожалуйста, установите инструментарий и добавьте ссылку на Microsoft.Phone.Controls.Toolkit в свой проект.

xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"

 

<Image x:Name="ImgZoom"
        Source="sample.jpg"
        Stretch="UniformToFill"
        RenderTransformOrigin="0.5,0.5">
    <toolkit:GestureService.GestureListener>
        <toolkit:GestureListener
                PinchStarted="OnPinchStarted"
                PinchDelta="OnPinchDelta"/>
    </toolkit:GestureService.GestureListener>
    <Image.RenderTransform>
        <CompositeTransform
                ScaleX="1" ScaleY="1"
                TranslateX="0" TranslateY="0"/>
    </Image.RenderTransform>
</Image>

Неправильный путь

Я видел этот пример несколько раз, полагаю, вы тоже видели его где-то на Interwebs ™:

double initialScale = 1d;

private void OnPinchStarted(object s, PinchStartedGestureEventArgs e)
{
    initialScale = ((CompositeTransform)ImgZoom.RenderTransform).ScaleX;
}

private void OnPinchDelta(object s, PinchGestureEventArgs e)
{
    var transform = (CompositeTransform)ImgZoom.RenderTransform;
    transform.ScaleX = initialScale * e.DistanceRatio;
    transform.ScaleY = transform.ScaleX;
}

Очень простой и красивый. Я люблю простые решения и готов поспорить, что и вы тоже, но, как кто-то однажды сказал: «Все должно быть как можно проще, но не проще». И, к сожалению, это проще, чем возможно (это даже предложение?). Проблема в том, что масштабирование всегда центрируется по центру изображения, поэтому это решение не пройдет тест «тыкаешь двумя пальцами в глаза».

Лучше, но все же неправильно

Реакция коленного рефлекса заключается в перемещении центра масштабирования между нашими пальцами при выполнении масштабирования:

double initialScale = 1d;

private void OnPinchStarted(object s, PinchStartedGestureEventArgs e)
{
    initialScale = ((CompositeTransform)ImgZoom.RenderTransform).ScaleX;
}

private void OnPinchDelta(object s, PinchGestureEventArgs e)
{
    var finger1 = e.GetPosition(ImgZoom, 0);
    var finger2 = e.GetPosition(ImgZoom, 1);

    var center = new Point(
        (finger2.X + finger1.X) / 2 / ImgZoom.ActualWidth,
        (finger2.Y + finger1.Y) / 2 / ImgZoom.ActualHeight);

    ImgZoom.RenderTransformOrigin = center;

    var transform = (CompositeTransform)ImgZoom.RenderTransform;
    transform.ScaleX = initialScale * e.DistanceRatio;
    transform.ScaleY = transform.ScaleX;
}

Это лучше. В первый раз это действительно хорошо работает, но как только вы зажимаете изображение во второй раз, вы понимаете, что изображение перемещается. Причина: состояние масштабирования является суммой всех операций масштабирования(каждый имеет свой центр) и перемещая центр каждый раз, когда вы эффективно удаляете информацию из предыдущих шагов. Чтобы решить эту проблему, мы могли бы заменить CompositeTransform на TransformGroup, а затем добавить новый ScaleTransform (с новым центром) в каждую группу событий PinchStart + PinchDelta. Это, вероятно, сработает: каждое масштабирование будет держать свой центр, и все хорошо. За исключением того, что ваш телефон может загореться и взорваться из-за количества преобразуемых вами преобразований. У моей команды есть название для такого рода решений, и оно не очень удачное (к счастью, для этого нет английского перевода).

Правильный путь

К настоящему времени ясно, что простая установка масштабного коэффициента и перемещение центра не приведут нас далеко. Поскольку мы настоящие DIYourselfers, мы сделаем это с помощью комбинации масштабирования и перевода. В уже упомянутой статье Адриан Цай использует эту технику в XNA, и мы будем применять ту же концепцию в Silverlight. Если изображение стоит миллион, строка кода, вероятно, стоит даже больше, поэтому я позволю c # говорить.

// these two fully define the zoom state:
private double TotalImageScale = 1d;
private Point ImagePosition = new Point(0, 0);

private Point _oldFinger1;
private Point _oldFinger2;
private double _oldScaleFactor;

private void OnPinchStarted(object s, PinchStartedGestureEventArgs e)
{
    _oldFinger1 = e.GetPosition(ImgZoom, 0);
    _oldFinger2 = e.GetPosition(ImgZoom, 1);
    _oldScaleFactor = 1;
}

private void OnPinchDelta(object s, PinchGestureEventArgs e)
{
    var scaleFactor = e.DistanceRatio / _oldScaleFactor;

    var currentFinger1 = e.GetPosition(ImgZoom, 0);
    var currentFinger2 = e.GetPosition(ImgZoom, 1);

    var translationDelta = GetTranslationDelta(
        currentFinger1,
        currentFinger2,
        _oldFinger1,
        _oldFinger2,
        ImagePosition,
        scaleFactor);

    _oldFinger1 = currentFinger1;
    _oldFinger2 = currentFinger2;
    _oldScaleFactor = e.DistanceRatio;

    UpdateImage(scaleFactor, translationDelta);
}

private void UpdateImage(double scaleFactor, Point delta)
{
    TotalImageScale *= scaleFactor;
    ImagePosition = new Point(ImagePosition.X + delta.X, ImagePosition.Y + delta.Y);

    var transform = (CompositeTransform)ImgZoom.RenderTransform;
    transform.ScaleX = TotalImageScale;
    transform.ScaleY = TotalImageScale;
    transform.TranslateX = ImagePosition.X;
    transform.TranslateY = ImagePosition.Y;
}

private Point GetTranslationDelta(
    Point currentFinger1, Point currentFinger2,
    Point oldFinger1, Point oldFinger2,
    Point currentPosition, double scaleFactor)
{
    var newPos1 = new Point(
        currentFinger1.X + (currentPosition.X - oldFinger1.X) * scaleFactor,
        currentFinger1.Y + (currentPosition.Y - oldFinger1.Y) * scaleFactor);

    var newPos2 = new Point(
        currentFinger2.X + (currentPosition.X - oldFinger2.X) * scaleFactor,
        currentFinger2.Y + (currentPosition.Y - oldFinger2.Y) * scaleFactor);

    var newPos = new Point(
        (newPos1.X + newPos2.X) / 2,
        (newPos1.Y + newPos2.Y) / 2);

    return new Point(
        newPos.X - currentPosition.X,
        newPos.Y - currentPosition.Y);
}

Также обратите внимание, что в XAML мы должны установить RenderTransformOrigin равным 0,0.
Это, наконец, проходит тест «пальцы в глазах»! Теперь мы можем добавить некоторые навороты, такие как управление перетаскиванием, блокировка уменьшения масштаба, когда изображение находится на полном экране, и предотвращение перетаскивания изображения за пределы видимой области. Для этих дополнительных деталей, пожалуйста, смотрите образец решения в конце статьи.

Как насчет MVVM?

Вы используете MVVM-light для своего приложения WP7, не так ли? Мы все согласны с тем, что мой код ужасен и не очень дружелюбен к MVVM, я не буду оправдываться. Однако все это строго UI-код, поэтому не так уж и плохо иметь его в коде. Что вы, вероятно, будете делать, это связать TotalImageScale и ImagePosition с вашей ViewModel. Эти два значения полностью определяют состояние масштабирования, поэтому, если вы сохраните и перезагрузите их в ViewModel, вам будет хорошо.

Скачать

Вот полный пример проекта, чтобы вы могли поиграть с кодом, не выходя из своей Visual Studio (моя дочь изображена на рисунке, пожалуйста, относитесь к ней с уважением :-)).
Не стесняйтесь использовать код в вашем проекте. Как всегда, любые отзывы очень важны!


Источник: http://www.frenk.com/2011/03/windows-phone-7-correct-pinch-zoom-in-silverlight/