Главная » Разработка для Windows Phone 7 » Масштабирование и вращение Windows Phone 7

0

Продолжим работу с жестами перетягивания с участием простых геометрических фигур, но будем использовать эти жесты не для перемещения, а для масштабирования и вращения. Для следующих трех приложений помещаем Texture2D в центр экрана, и он будет оставаться в центре всегда, пользователь сможет только изменять его размер или вращать посредством касания одним пальцем.

Проект OneFingerScale (Масштабирование одним пальцем) включает на пару полей больше, чем предыдущее приложение:

Проект XNA: OneFingerScale Файл: Game1.cs (фрагмент, демонстрирующий поля)

public class Game1 : Microsoft.Xna.Framework.Game {

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

Texture2D texture;

Vector2 screenCenter;

Vector2 textureCenter;

Vector2 textureScale = Vector2.One;

public Game1() {

graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content";

// Частота кадров по умолчанию для всех устройств Windows Phone – 30

кадров/с

TargetElapsedTime = TimeSpan.FromTicks(333333); TouchPanel.EnabledGestures = GestureType.FreeDrag;

Данному приложению необходимо знать координаты центра Texture2D, потому что в нем используется развернутая версия вызова метода Draw объекта SpriteBatch, включающая центр как аргумент. Как помните, аргумент origin метода Draw – это точка объекта Texture2D, соответствующая значению аргумента position и используемая как центр масштабирования и вращения.

Обратите внимание, что в качестве значения поля textureScale задан вектор (1, 1), что обеспечивает умножение ширины и высоты на 1. Общей ошибкой является задание масштабу нулевого значения, что приводит к полному исчезновению объектов с экрана.

Все инициализированные поля задаются в перегруженном LoadContent: Проект XNA: OneFingerScale Файл: Game1.cs (фрагмент)

protected override void LoadContent() {

// Создаем новый SpriteBatch, который может использоваться для отрисовки текстур spriteBatch = new SpriteBatch(GraphicsDevice);

Viewport viewport = this.GraphicsDevice.Viewport;

screenCenter = new Vector2(viewport.Width / 2, viewport.Height / 2);

texture = this.Content.Load<Texture2D>("PetzoldTattoo"); textureCenter = new Vector2(texture.Width / 2, texture.Height / 2);

}

При обработке жеста FreeDrag в следующем перегруженном Update не делается попытки определить, действительно ли точка касания попадает в область растрового изображения. Растровое изображение располагается в центре экрана и будет масштабироваться в различной степени, поэтому такое вычисление было бы несколько сложным (хотя, безусловно, не невозможным).

Вместо этого перегруженный Update демонстрирует использование свойства Delta для определения координат предыдущей точки касания. Впоследствии эти данные используются для определения того, насколько далеко точка касания переместилась от центра текстуры (который также является центром экрана) в ходе этого конкретного отрезка жеста:

Проект XNA: OneFingerScale Файл: Game1.cs (фрагмент)

protected override void Update(GameTime gameTime) {

// Обеспечиваем возможность выхода из игры

if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit();

while (TouchPanel.IsGestureAvailable) {

GestureSample gesture = TouchPanel.ReadGesture();

if (gesture.GestureType == GestureType.FreeDrag) {

Vector2 prevPosition = gesture.Position – gesture.Delta;

float scaleX = (gesture.Position.X – screenCenter.X) / (prevPosition.X – screenCenter.X); float scaleY = (gesture.Position.Y – screenCenter.Y) /

(prevPosition.Y – screenCenter.Y);

Как видите, ничто не мешает изображению изменить пропорции, что подразумевают приведенные выше вычисления. Проводя пальцами дальше вертикальной или горизонтальной оси экрана, можно даже сжать изображение в точку!

textureScale.X *= scaleX; textureScale.Y *= scaleY;

base.Update(gameTime);

}

Например, центром экрана является точка (400, 240). Предположим, в ходе этого конкретного отрезка жеста свойство Position имеет значение (600, 200), и свойство Delta – (20, 10). Это означает, что предыдущее местоположение соответствовало точке (580, 190). В горизонтальном направлении расстояние от точки касания до центра увеличилось от 180 пикселов (580 минус 400) до 200 пикселов (600 минус 400); чтобы найти коэффициент масштабирования, делим 200 на 180, получаем 1,11. По вертикали расстояние от центра уменьшилось с 50 пикселов (240 минус 190) до 40 пикселов (240 минус 200); чтобы найти коэффициент масштабирования делим 40 на 50, получаем 0,80. Размер изображения увеличивается на 11% в горизонтальном направлении и на 20% в вертикальном.

Таким образом, умножаем составляющую X вектора масштабирования на 1,11 и составляющую Y – на 0,80. Как и ожидалось, коэффициент масштабирования применяется в перегруженном Draw:

Проект XNA: OneFingerScale Файл: Game1.cs (фрагмент)

protected override void Draw(GameTime gameTime) {

GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin();

spriteBatch.Draw(texture, screenCenter, null, Color.White, 0,

textureCenter, textureScale, SpriteEffects.None, 0); spriteBatch.End();

base.Draw(gameTime);

}

Наиболее очевидного эффекта можно добиться, если «взять» изображение за один из углов и перенести этот угол в направлении прямо к центру или от него:

В реальном приложении желательно задать минимальный коэффициент масштабирования, например 0,1 или 0,25, просто чтобы обеспечить пользователю возможность «растянуть» изображение снова.

Также скорее всего в некоторых приложениях потребуется сохранять пропорции изображения. В этом случае для масштабирования в горизонтальном и вертикальном направлениях должен использоваться один коэффициент масштабирования. Можно вычислять два коэффициента масштабирования, как в приложении OneFingerScale, и затем просто находить их среднее. Но это, безусловно, неправильно. Если пользователь знает, что приложение обеспечивает сохранение пропорций, он ожидает соответствующего поведения от изображения и будет масштабировать его, просто перетягивая в горизонтальном или вертикальном направлении.

Можно вычислять оба коэффициента масштабирования и брать наибольший из них. Но это также не вполне правильно. При выполнении OneFingerScale можно заметить, что когда точка касания располагается слишком близко к центру изображения, всего лишь небольшого движения достаточно, чтобы существенно изменить размеры изображения. Если точка касания располагается вблизи горизонтальной оси симметрии, но далеко от вертикальной оси, для равного перемещения точки касания в горизонтальном и вертикальном направлении коэффициенты масштабирования будут разными.

Вероятно, наилучшей стратегией будет проверять значение свойства Delta и определять, какая из составляющих, X или Y, имеет наибольшее значение (не учитывая знака), и затем использовать ее для вычисления коэффициента масштабирования. Такая методика реализована в проекте OneFingerUniformScale (Унифицированное масштабирование одним пальцем).

Поля такие же, как и в предыдущем приложении, за исключением того, что коэффициент масштабирования Vector2 заменен на float.

Проект XNA: OneFingerUniformScale Файл: Game1.cs (фрагмент, демонстрирующий поля)

public class Game1 : Microsoft.Xna.Framework.Game {

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

Texture2D texture; Vector2 screenCenter; Vector2 textureCenter; float textureScale = 1;

}

Перегруженный LoadContent абсолютно такой же, как и в предыдущей версии, а вот обработка жеста в перегруженном Update стала более развернутой. Метод проверяет, абсолютное значение какой из составляющих вектора Delta, горизонтальной или вертикальной, больше. Также он не выполняет никаких вычислений, если обе составляющие равны нулю, что имеет место в начале жеста, когда палец впервые касается экрана.

Проект XNA: OneFingerUniformScale Файл: Game1.cs (фрагмент)

protected override void Update(GameTime gameTime) {

// Обеспечиваем возможность выхода из игры

if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)

this.Exit();

while (TouchPanel.IsGestureAvailable) {

GestureSample gesture = TouchPanel.ReadGesture();

if (gesture.GestureType == GestureType.FreeDrag) {

Vector2 prevPosition = gesture.Position – gesture.Delta; float scale = 1;

if (Math.Abs(gesture.Delta.X) > Math.Abs(gesture.Delta.Y))

scale = (gesture.Position.X – screenCenter.X) /

(prevPosition.X – screenCenter.X);

else if (gesture.Delta.Y != 0)

scale = (gesture.Position.Y – screenCenter.Y) /

(prevPosition.Y – screenCenter.Y);

if (!float.IsInfinity(scale) && !float.IsNaN(scale))

textureScale = Math.Min(10,

Math.Max(0.25f, scale * textureScale));

}

}

base.Update(gameTime);

}

Здесь реализована еще одна мера предосторожности: проверка на то, не является ли вычисленное значение бесконечным или является ли оно числом. Это возможно, если точка касания точно совпадает с центром экрана, что приводит к делению на нуль. Также я ограничил общий коэффициент масштабирования диапазоном значений от 0,25 до 10. Значения выбраны довольно произвольно, но позволяют продемонстрировать эту важную концепцию.

Перегруженный метод Draw аналогичен используемому в предыдущем приложении, за исключением того что textureScale (Масштаб текстуры) типа float, а не Vector2:

Проект XNA: OneFingerUniformScale Файл: Game1.cs (фрагмент)

protected override void Draw(GameTime gameTime) {

GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin();

spriteBatch.Draw(texture, screenCenter, null, Color.White, 0,

textureCenter, textureScale, SpriteEffects.None, 0); spriteBatch.End();

base.Draw(gameTime);

}

Я пришел к осознанию необходимости задания допустимого максимального значения textureScale, поэкспериментировав немного с более ранней версией этого приложения. Я коснулся изображения очень близко к центру экрана, и небольшое движение привело к тому, что оно было увеличено в несколько сотен раз, так что на экране поместились лишь несколько его пикселов! Излишне говорить, что в таком масштабировании нет смысла.

Приложение можно реализовать так, чтобы оно игнорировало жесты вблизи определенной опорной точки. Я делаю это в следующем проекте.

Масштабирование посредством касания одним пальцем используется довольно редко, а вот организация вращения таким образом – очень мощная возможность, широко применяемая и в мире компьютеров, и в реальной жизни. Если на столе перед вами лежит телефон, коснитесь пальцем одного из его углов и потяните телефон к себе. Скорее всего, телефон немного повернется, а уже потом начнет перемещение в вашем направлении.

Очень часто вращение посредством касания одним пальцем используется в сочетании с обычным перетягиванием. Рассмотрим, как это реализовывается. Поля OneFingerRotation (Вращение одним пальцем) подобны используемым в предыдущих приложениях:

Проект XNA: OneFingerRotation Файл: Game1.cs (фрагмент, демонстрирующий поля)

public class Game1 : Microsoft.Xna.Framework.Game {

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

Texture2D texture; Vector2 texturePosition; Vector2 textureCenter; float textureRotation;

}

Перегруженный LoadContent также аналогичен. Поле texturePosition (Местоположение текстуры) инициализируется координатами центра экрана, но при перемещении текстуры по экрану его значение будет меняться:

Проект XNA: OneFingerRotation Файл: Game1.cs (фрагмент)

protected override void LoadContent() {

// Создаем новый SpriteBatch, который может использоваться для отрисовки текстур spriteBatch = new SpriteBatch(GraphicsDevice);

Viewport viewport = this.GraphicsDevice.Viewport;

texturePosition = new Vector2(viewport.Width / 2, viewport.Height / 2);

texture = this.Content.Load<Texture2D>("PetzoldTattoo"); textureCenter = new Vector2(texture.Width / 2, texture.Height / 2);

}

В методе Update решено сначала сравнивать координаты предыдущего касания и нового касания с координатами центра Texture2D, обозначенными в texturePosition. В Update я представляю эти два местоположения как векторы из центра текстуры в эти точки касания: oldVector (Старый вектор) и newVector (Новый вектор) (под «старый» и «новый» я подразумеваю «предыдущее» и «текущее» местоположение). Если эти два вектора располагаются под разными углами, угол textureRotation (Разворот текстуры) меняется на разницу между ними.

Теперь удалим компонент вращения из изменения этих координат. Все что осталось, должно использоваться для перетягивания текстуры. oldVector пересчитывается так, что он сохраняет исходный модуль, но теперь указывает в том же направлении, что и newVector. Вычисляется

новая разность между текущими newVector и oldVector, значение которой и используется для реализации перетягивания:

Проект XNA: OneFingerRotation Файл: Game1.cs (фрагмент)

protected override void Update(GameTime gameTime) {

// Обеспечиваем возможность выхода из игры

if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit ();

while (TouchPanel.IsGestureAvailable) {

GestureSample gesture = TouchPanel.ReadGesture();

if (gesture.GestureType == GestureType.FreeDrag) {

Vector2 delta = gesture.Delta;

Vector2 newPosition = gesture.Position;

Vector2 oldPosition = newPosition – delta;

// Находим векторы, направленные из центра растрового изображения // в точки касания

Vector2 oldVector = oldPosition – texturePosition; Vector2 newVector = newPosition – texturePosition;

// Отменяем вращение, если точка касания располагается рядом с центром

if (newVector.Length() > 25 && oldVector.Length() > 25) {

// Находим углы для векторов, проведенных из центра // растрового изображения в точки касания

float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X); float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);

// Корректируем угол поворота текстуры textureRotation += newAngle – oldAngle;

// По сути, вращаем старый вектор

oldVector = oldVector.Length() / newVector.Length() * newVector;

// Повторно вычисляем разницу delta = newVector – oldVector;

}

// Перемещаем текстуру texturePosition += delta;

}

}

base.Update(gameTime);

}

Перегруженный Draw использует этот угол поворота, но задает коэффициент масштабирования равным 1:

Проект XNA: OneFingerRotation Файл: Game1.cs (фрагмент)

protected override void Draw(GameTime gameTime) {

GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin();

spriteBatch.Draw(texture, texturePosition, null, Color.White,

textureRotation, textureCenter, 1, SpriteEffects.None, 0); spriteBatch.End();

base.Draw(gameTime);

Поэкспериментировав с этим приложением, можно заметить, что перемещения выглядят очень естественными. Изображение можно «схватить» в любой точке и «потянуть». При этом создается эффект, как будто изображение «буксируется» вашим пальцем, точно как это происходит с телефоном на столе.

Конечно, самой распространенной формой масштабирования в приложениях, поддерживающих мультисенсорный ввод, является масштабирование с участием двух пальцев жестами сведения или растяжения. Реализовать это не на много сложнее, чем то что мы уже видели. Мы уже выполняли растяжение и вращение относительно опорной точки. С участием двух пальцев все то же самое, за исключением того что опорной точкой всегда является точка касания второго пальца.

К примеру, коснитесь объекта на экране двумя пальцами. Если один палец остается неподвижным, а другой перемещается, следует ожидать, что все масштабирование и вращение будет выполняться относительно первого, неподвижного, пальца. Если этот первый палец также перемещается, все обусловливаемое им масштабирование и вращение выполняется относительно второго пальца. Если оба пальца перемещаются в одном направлении, выполняется обычное перетягивание.

Участвующие в реализации этого математические вычисления довольно сложны, но, к счастью, на помощь приходят некоторые очень мощные инструменты, предоставляемые XNA.

Источник: Чарльз Петзольд, Программируем Windows Phone 7, Microsoft Press, © 2011.

По теме:

  • Комментарии