И ничего толком не находим. Хотя не совсем, в Windows Phone 8 есть PinchManipulation, но его тоже придется вручную оборачивать в контрол, то есть не безнадежно. Но если вы хороший разработчик, и делаете приложение одновременно на Windows Phone 7 и 8, то это не вариант, т.к. на первой платформе такого свойства нет. В Windows Phone 7 нам предлагали воспользоваться GestureService, входящим в состав WP Toolkit, но народ в этой самой сети постоянно жалуется на его нестабильную работу, а Windows Phone 8 он вообще официально уже не поддерживается. Придется как-то выкручиваться. Первое на что вы, возможно, набредете – это так называемые Manipulation Events, которые достались Windows Phone в наследство от WPF. Это три события, которые есть в любом контроле, унаследованном от UIElement, то есть да, в любом.
Секрет успеха предлагается следующий: нашему Image присваиваем RenderTransform в обличии CompositeTransform, далее хватаем событие ManipultionDelta и практически не прилагая никаких усилий, увеличиваем/уменьшаем и двигаем наш Image на дельту, которая в вышеупомянутом событии магическим образом возникает.<Grid x:Name="LayoutRoot"> <Image x:Name="Image" Source="/Assets/Venice.jpg" ManipulationDelta="Image_OnManipulationDelta"> <Image.RenderTransform> <CompositeTransform x:Name="CompositeTransform"/> </Image.RenderTransform> </Image> </Grid>
private void Image_OnManipulationDelta(object sender, ManipulationDeltaEventArgs e) { // Scale if (e.DeltaManipulation.Scale.X != 0) CompositeTransform.ScaleX *= e.DeltaManipulation.Scale.X; if (e.DeltaManipulation.Scale.Y != 0) CompositeTransform.ScaleY *= e.DeltaManipulation.Scale.Y; // Translate CompositeTransform.TranslateX += e.DeltaManipulation.Translation.X; CompositeTransform.TranslateY += e.DeltaManipulation.Translation.Y; e.Handled = true; }
- Scale изменяет размер Image непропорционально (ожидаемо) и слишком резко, картинка практически мгновенно “улетает” из-под пальцев.
- Translate работает адекватно только в изначальном масштабе (то бишь, единичном). Затем изображение двигается либо слишком медленно, либо слишком быстро.
- Естественно, scale и translate ничем не ограничены, поэтому картинка рискует улететь за пределы экрана и не вернуться :-(
Проблему с translate решить еще проще: нужно перемножить дельта-координаты с уже действующими значениями CompositeTransform.ScaleX и CompositeTransform.ScaleY. Manipulation Events ведь не знают, что мы там уже намасштабировали. И вот, что получится:
private void Image_OnManipulationDelta(object sender, ManipulationDeltaEventArgs e) { const double epsilon = 0.01; var uniformScale = Math.Sqrt(Math.Pow(e.DeltaManipulation.Scale.X, 2) + Math.Pow(e.DeltaManipulation.Scale.Y, 2)); if (Math.Abs(uniformScale) > epsilon) { // Scale var scaleFactor = uniformScale / Math.Sqrt(2); CompositeTransform.ScaleX *= scaleFactor; CompositeTransform.ScaleY *= scaleFactor; } // Translate CompositeTransform.TranslateX += e.DeltaManipulation.Translation.X * CompositeTransform.ScaleX; CompositeTransform.TranslateY += e.DeltaManipulation.Translation.Y * CompositeTransform.ScaleY; e.Handled = true; }
Дело в том, что мы масштабируем картинку относительно координат 0,0, в то время как классический Pinch To Zoom работает иначе: он масштабирует относительно точки, которая находится ровно между пальцами. Иначе говоря, пиксель между двумя пальцами должен всегда находиться на своем месте. К счастью, для нас уже предусмотрели e.ManipulationOrigin, которым мы и воспользуемся. Если взять в руки бумажку и ручку, провести некоторые инженерные расчеты (нарисовать два квадратика), то можно прийти к следующей формуле: сразу после масштабирования картинку нужно сдвинуть на разницу между первоначальной точкой e.ManipulationOrigin и ней же, только умноженной на scaleFactor. И не забыть все это перемножить на текущие CompositeTransform.ScaleX и CompositeTransform.ScaleY, ведь Manipulation Events их не запоминают. В результате, у нас получится следующий код:
private void Image_OnManipulationDelta(object sender, ManipulationDeltaEventArgs e) { const double epsilon = 0.01; var uniformScale = Math.Sqrt(Math.Pow(e.DeltaManipulation.Scale.X, 2) + Math.Pow(e.DeltaManipulation.Scale.Y, 2)); if (Math.Abs(uniformScale) > epsilon) { // Scale var scaleFactor = uniformScale / Math.Sqrt(2); CompositeTransform.ScaleX *= scaleFactor; CompositeTransform.ScaleY *= scaleFactor; // Consider scale origin var originX = e.ManipulationOrigin.X; var originY = e.ManipulationOrigin.Y; var dx = originX - originX * scaleFactor; var dy = originY - originY * scaleFactor; CompositeTransform.TranslateX += dx * CompositeTransform.ScaleX; CompositeTransform.TranslateY += dy * CompositeTransform.ScaleY; } // Translate CompositeTransform.TranslateX += e.DeltaManipulation.Translation.X * CompositeTransform.ScaleX; CompositeTransform.TranslateY += e.DeltaManipulation.Translation.Y * CompositeTransform.ScaleY; e.Handled = true; }
И еще один важный момент, о котором часто забывают. Настоятельно рекомендую в XAML к нашему дорогому Image добавить атрибут CacheMode="BitmapCache", который тесно подружит нашу картинку с GPU. Задержки станут меньше, картинка станет двигаться шустрее.
А теперь, как и обещал, о плохом. Если кто использовал Manipulation Events на Windows Phone, то наверное заметил, что ведут они себя “слегка” неадекватно. А именно: e.DeltaManipulation.Scale очень часто выдает рандомные значения. Чтобы окончательно его свести с ума, достаточно взять два пальца и (что бы вы могли подумать) очертить ими окружность по экрану телефона. А иногда достаточно просто свести пальцы близко друг к другу, как этот самый Scale начинает выдавать то отрицательные, то положительные значения, никак не связанные друг с другом. Вот пример (спасибо Debug.WriteLine-у):
X: 0,00, Y: 0,00
X: -2,01, Y: -0,05
X: 1,03, Y: 0,80
X: 1,02, Y: 0,50
X: 1,02, Y: 0,25
X: 1,05, Y: -22,00
X: 1,03, Y: 2,05
X: 1,03, Y: 1,42
Буквально за один шаг, то есть доли секунды, масштаб становится то в 2 раза больше, то в минус 22 раза меньше. Откуда эти цифры берутся – для меня загадка. Если их фильтровать (хотя как, это еще вопрос), то масштабирование получается рваным. Пробовал интерполировать, картинка опять “отлипает” от пальцев, и получается не так плавно, как в галерее (даже в браузере, и то лучше). В общем, для меня стало очевидным (как говорится, необъяснимо но факт), что сама Microsoft не использует эти события ни в одном из своих приложений. Я уверен, что не используют их и сторонние (платные) компоненты. И я не буду. И вы не используйте :-)
Но не все так плохо! И большая часть кода выше еще пригодится. Дело в том, что мы можем опуститься на уровень ниже и получать экранные координаты пальцев прямо с сенсора. Не, не ассемблер. Touch.FrameReported!
Забегая вперед скажу, что работает он отлично.
Учитывая, что событие это – статическое, то главное не забыть от него в нужный момент отписаться. Идеальным для этого местом станет другое событие – Unloaded, которое присутствует в любом FrameworkElement, коим, кончено, и является наш с вами Image. Обработчик самого FrameReported выглядит следующим образом:
private void Touch_OnFrameReported(object sender, TouchFrameEventArgs e) { var points = e.GetTouchPoints(Image); if (points.Count == 1) { // Translate... } else if (points.Count == 2) { // Zoom... } }
public static double Distance(Point p1, Point p2) { var dx = p1.X - p2.X; var dy = p1.Y - p2.Y; return Math.Sqrt(dx * dx + dy * dy); } public static Point Lerp(Point p1, Point p2, double blend = 0.5) { return new Point(p1.X + blend * (p2.X - p1.X), p1.Y + blend * (p2.Y - p1.Y)); }
Для большей наглядности я оформил все вышесказанное в приложение на Windows Phone 7 (которое невозбранно переносится и на WP8). В нем можно воочию оценить ту необъятную пропасть между Manipulaton Events и Touch.FrameReported, исходный код прилагается для всех желающих. Фотографию Венеции любезно предоставил сайт стоковых фотографий :-)
Надеюсь, мой опыт кому-нибудь пригодится. Тем более, что в Сети я не нашел ни одного примера с правильным зумом (везде какие-то косяки). Удачного кодинга!
Комментариев нет:
Отправить комментарий