Главная » Разработка для Windows Phone 7 » Жест Pinch

0

Для жеста Pinch действительны все четыре свойства GestureSample типа Vector2: Position, Delta, Position2 и Delta2. Первые два описывают местоположение и перемещение одного пальца; остальные два представляют второй палец. Это идеально для масштабирования, хотя, вероятно, математика не сразу понятна.

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

Приложение DragAndPinch (Перетягивание и сведение) обрабатывает оба жеста, FreeDrag и Pinch, с применением непропорционального масштабирования без вращения. Как обычно, эти жесты активируются в конструкторе. Новое поле, которое мы видим здесь, это объект Matrix, инициализированный как единичная матрица черех статическое свойство Matrix.Identity:

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

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

GraphicsDeviceManager graphics; SpriteBatch spriteBatch;

Texture2D texture;

Matrix matrix = Matrix.Identity;

public Game1() {

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

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

кадров/с

TargetElapsedTime = TimeSpan.FromTicks(333333);

TouchPanel.EnabledGestures = GestureType.FreeDrag | GestureType.Pinch;

}

}

Выражение

Matrix matrix = Matrix.Identity;

не аналогично выражению:

Matrix matrix = new Matrix();

Matrix – это структура, и как для всех структур, ее поля инициализируются нулевыми значениями. Объект Matrix со всеми нулями не годится ни для чего, поскольку он полностью уничтожает все, к чему применяется. В используемом по умолчанию объекте Matrix все

диагональные элементы должны быть заданы равными 1. Именно это обеспечивает свойство Matrix.Identity.

Все операции перетягивания и сведения будут применяться к полю matrix, которое затем используется в перегруженном Draw.

Метод LoadContent просто загружает Texture2D: Проект XNA: DragAndPinch Файл: Game1.cs (фрагмент)

protected override void LoadContent() {

// Создаем новый SpriteBatch, который может использоваться для отрисовки текстур

spriteBatch = new SpriteBatch(GraphicsDevice);

texture = this.Content.Load<Texture2D>("PetzoldTattoo");

}

Перегруженный Update обрабатывает жесты FreeDrag и Pinch:

Проект XNA: DragAndPinch Файл: 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();

switch (gesture.GestureType) {

case GestureType.FreeDrag:

matrix *= Matrix.CreateTranslation(gesture.Delta.X, gesture.Delta.Y,

0);

break;

case GestureType.Pinch:

Vector2 oldPoint1 = gesture.Position – gesture.Delta; Vector2 newPoint1 = gesture.Position;

Vector2 oldPoint2 = gesture.Position2 – gesture.Delta2; Vector2 newPoint2 = gesture.Position2;

matrix *= ComputeScaleMatrix(oldPoint1, oldPoint2, newPoint2); matrix *= ComputeScaleMatrix(newPoint2, oldPoint1, newPoint1); break;

}

}

base.Update(gameTime);

}

Обратите внимание, что для FreeDrag этот метод создает новый Matrix из статического метода Matrix.CreateTranslation и умножает его на существующее поле matrix. Это выражение можно заменить следующими:

matrix.M41 += gesture.Delta.X; matrix.M42 += gesture.Delta.Y;

Для жеста Pinch метод Update разбивает данные на «старые» точки и «новые» точки. Если оба пальца перемещаются относительно друг друга, можно вычислить составной коэффициент масштабирования, рассматривая касания от двух разных пальцев по отдельности. Предположим, первый палец фиксирован в точке, определённой свойством Position, и второй перемещается относительно него; а затем второй палец фиксирован в точке Position, и первый перемещается относительно него. Каждый из сценариев представляет отдельную операцию масштабирования, которые потому перемножаются. В каждом случае имеется опорная точка (фиксированный палец), старая и новая точка (перемещающийся палец).

Чтобы сделать все правильно, опорная точка первой операции масштабирования должна соответствовать старым координатам фиксированного пальца, но для второго коэффициента масштабирования опорной точкой должны быть новое местоположение фиксированного пальца. В этом причина несколько ассиметричных вызовов метода ComputeScaleMatrix (Вычисление матрицы масштабирования), показанных выше. Рассмотрим сам метод:

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

Matrix ComputeScaleMatrix(Vector2 refPoint, Vector2 oldPoint, Vector2 newPoint) {

float scaleX = (newPoint.X – refPoint.X) / (oldPoint.X – refPoint.X); float scaleY = (newPoint.Y – refPoint.Y) / (oldPoint.Y – refPoint.Y);

if (float.IsNaN(scaleX) || float.IsInfinity(scaleX) || float.IsNaN(scaleY) || float.IsInfinity(scaleY) || scaleX <= 0 || scaleY <= 0)

{

return Matrix.Identity;

}

scaleX = Math.Min(1.1f, Math.Max(0.9f, scaleX)); scaleY = Math.Min(1.1f, Math.Max(0.9f, scaleY));

Matrix matrix = Matrix.CreateTranslation(-refPoint.X, -refPoint.Y, 0);

matrix *= Matrix.CreateScale(scaleX, scaleY, 1);

matrix *= Matrix.CreateTranslation(refPoint.X, refPoint.Y, 0);

return matrix;

}

Эта опорная точка всегда выполняет здесь двоякую роль: она используется для измерения изменения координат перемещающейся точки касания и также для разграничения вызовов Matrix.CreateScale (Создать масштабирование для матрицы) в конце описания структуры для задания масштабирования относительно центральной точки. Эти три вызова в конце можно заменить следующим образом:

Matrix matrix = Matrix.Identity; matrix.M41 -= refPoint.X; matrix.M4 2 -= refPoint.Y;

matrix *= Matrix.CreateScale(scaleX, scaleY, 1);

matrix.M41 += refPoint.X; matrix.M4 2 += refPoint.Y;

Суммарная составная матрица просто передается как последний аргумент вызова метода Begin объекта spriteBatch в перегруженном Draw:

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

protected override void Draw(GameTime gameTime) {

GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, matrix);

spriteBatch.Draw(texture, Vector2.Zero, Color.White); spriteBatch.End();

base.Draw(gameTime);

}

Если вы предпочитаете использовать более простую форму вызова Begin, можно извлечь из объекта Matrix данные о масштабировании и местоположении и использовать их в вызове Draw:

Vector2 scale = new Vector2(matrix.M11, matrix.M22); Vector2 position = new Vector2(matrix.M41, matrix.M42);

spriteBatch.Begin();

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

Vector2.Zero, scale, SpriteEffects.None, 0); spriteBatch.End();

Структура Matrix также поддерживает метод Decompose (Разложить), который обеспечивает извлечение составляющих масштабирования, вращения и перемещения. Составляющая вращения представлена в форме Quaternion (Кватернион). Это очень широко применяемый инструмент для объемного вращения, но никогда (по моим сведениям) не используемый в двухмерной графике. Заменим вычисления scale и position следующим:

Vector3 scale3; Quaternion quaternion; Vector3 translation3;

matrix.Decompose(out scale3, out quaternion, out translation3);

Vector2 scale = new Vector2(scale3.X, scale3.Y);

Vector2 position = new Vector2(translation3.X, translation3.Y);

Добавим в DragAndPinch поддержку вращения посредством одного и двух пальцев и назовем это приложение DragPinchRotate. Все осталось неизменным, за исключением перегруженного Update.

Проект XNA: DragPinchRotate Файл: 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();

switch (gesture.GestureType) {

case GestureType.FreeDrag:

Vector2 newPoint = gesture.Position;

Vector2 oldPoint = newPoint – gesture.Delta;

Vector2 textureCenter = new Vector2(texture.Width / 2,

texture.Height / 2);

Vector2 refPoint = Vector2.Transform(textureCenter, matrix);

matrix *= ComputeRotateAndTranslateMatrix(refPoint, oldPoint,

newPoint);

break;

case GestureType.Pinch:

Vector2 oldPoint1 = gesture.Position – gesture.Delta; Vector2 newPoint1 = gesture.Position;

Vector2 oldPoint2 = gesture.Position2 – gesture.Delta2; Vector2 newPoint2 = gesture.Position2;

matrix *= ComputeScaleAndRotateMatrix(oldPoint1, oldPoint2,

newPoint2);

matrix *= ComputeScaleAndRotateMatrix(newPoint2, oldPoint1,

newPoint1);

break;

}

}

base.Update(gameTime);

}

В приложении, демонстрирующем вращение посредством касания одним пальцем, которое рассматривалось ранее, позиционирование Texture2D всегда осуществлялось относительно его центра, поэтому опорную точку вращения всегда было легко найти. Теперь позиционирование Texture2D осуществляется в ходе вызова метода Draw объекта SpriteBatch в верхнем левом углу экрана, но его фактическое местоположение определяется на основании объекта Matrix.

Поэтому в логике FreeDrag центр Texture2D выражается относительно верхнего левого угла как значение Vector2. Затем посредством трансформации текущей матрицы получаем координаты refPoint (Опорная точка) относительно экрана.

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

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

Matrix ComputeRotateAndTranslateMatrix(Vector2 refPoint, Vector2 oldPoint, Vector2

newPoint) {

Matrix matrix = Matrix.Identity; Vector2 delta = newPoint – oldPoint; Vector2 oldVector = oldPoint – refPoint; Vector2 newVector = newPoint – refPoint;

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

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);

// Вычисляем матрицу вращения float angle = newAngle – oldAngle;

matrix *= Matrix.CreateTranslation(-refPoint.X, -refPoint.Y, 0); matrix *= Matrix.CreateRotationZ(angle);

matrix *= Matrix.CreateTranslation(refPoint.X, refPoint.Y, 0);

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

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

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

}

// Включаем перемещение

matrix *= Matrix.CreateTranslation(delta.X, delta.Y, 0); return matrix;

}

Обратите внимание, что вызов Matrix.CreateRotationZ (Создать матрицу вращения относительно оси Z) располагается между двумя вызовами Matrix.CreateTranslation (Создать матрицу перемещения) для осуществления вращения относительно опорной точки, в роли которой выступает перенесенный центр Texture2D. В конце описания структуры еще один вызов Matrix.CreateTranslation обрабатывает составляющую жеста, которая обеспечивает перемещение, после извлечения из него составляющей вращения.

Новый метод ComputeScaleAndRotateMatrix получен на основании метода ComputeScaleMatrix из предыдущего проекта, в который была добавлена подобная логика. Метод ComputeScaleAndRotateMatrix вызывается дважды для любого жеста Pinch:

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

Matrix ComputeScaleAndRotateMatrix(Vector2 refPoint, Vector2 oldPoint, Vector2

newPoint) {

Matrix matrix = Matrix.Identity; Vector2 oldVector = oldPoint – refPoint; Vector2 newVector = newPoint – refPoint;

// Находим углы для векторов, проведенных из опорной точки в точки касания float oldAngle = (float)Math.Atan2(oldVector.Y, oldVector.X); float newAngle = (float)Math.Atan2(newVector.Y, newVector.X);

// Вычисляем матрицу вращения float angle = newAngle – oldAngle;

matrix *= Matrix.CreateTranslation(-refPoint.X, -refPoint.Y, 0); matrix *= Matrix.CreateRotationZ(angle);

matrix *= Matrix.CreateTranslation(refPoint.X, refPoint.Y, 0); // По сути, вращаем старый вектор

oldVector = oldVector.Length() / newVector.Length() * newVector; float scale = 1;

// Определяем коэффициент масштабирования из большей разницы

if (Math.Abs(newVector.X – oldVector.X) > Math.Abs(newVector.Y – oldVector.Y)) scale = newVector.X / oldVector.X;

else

scale = newVector.Y / oldVector.Y; // Вычисляем матрицу масштабирования

if (!float.IsNaN(scale) && Ifloat.IsInfinity(scale) && scale > 0) {

scale = Math.Min(1.1f, Math.Max(0.9f, scale));

matrix *= Matrix.CreateTranslation(-refPoint.X, -refPoint.Y, 0); matrix *= Matrix.CreateScale(scale, scale, 1);

matrix *= Matrix.CreateTranslation(refPoint.X, refPoint.Y, 0);

}

return matrix;

Для обеспечения пропорционального масштабирования метод определяет, в каком направлении, по горизонтали или по вертикали, произошло наибольшее перемещение относительно опорной точки. Для этого производится сравнение абсолютных значений разностей newVector и oldVector (после исключения составляющей вращения). Также обратите внимание, что Matrix.CreateScale располагается между двумя вызовами Matrix.CreateTranslation, в которых используются координаты опорной точки.

Теперь все готово, чтобы выполнить перемещение и вращение посредством касания одним пальцем и пропорциональное масштабирование и вращение посредством касания двумя пальцами:

Это явно не описано, но перемещать изображение можно также двумя пальцами, если двигать ими в одном направлении.

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

По теме:

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