Исходник игры Мозаика для Android

Исходник игры на Xamarin.Android

Игра мозаика для Android Исходник игры для Android написан на языке C# на платформе Xamarin.Android. Исходник представляет игру Мозаика, где картинки можно передвигать и таким образом составлять красивые узоры. Xamarin надстройка для среды .NET позволяющая создавать Android приложения на языке C# с использованием всех возможностей популярного языка. Xamarin.Android обеспечивает получить полный доступ к нативному (родному) Android SDK без каких либо ограничений. Программирование приложений на языке C# с помощью Xamarin интуитивно понятно и позволяет быстро перестраиваться в программировании самых различных десктоповых и веб приложений для Windows, Lunix, Android, iOS.

Интерфейс на Android.RelativeLayout

Компоновка элементов интерфейса игры Мозаика базируется на макете RelativeLayout, удобном контейнере для размещения элементов и групп элементов. Использование RelativeLayout предоставляет возможность создавать позиции элементов в точных единицах. Контейнер хоть и носит название относительный, но позволяет использовать абсолютные координаты для позиционирования элементов. В данном исходнике это и было использовано. Макет RelativeLayout создаётся в XML дизайнере, а дочерние элементы, картинки упакованные в ImageView, создаются и добавляются на поле макета программным способом. Перед показом игрового поля кратковременно демонстрируется экранная заставка.

Активные элементы на Android.ImageView

Для управления частичками Мозаики, файлы картинок упакованы в контейнер Android.ImageView. Такая оболочка для изображений имеет много полезных свойств. При помощи ImageView можно позиционировать изображение на макете по пиксельным координатам, масштабировать по горизонтали и вертикали, добавлять цветовую маску для вложенного изображения и др. Методы унаследованные от родительского класса View SetY(…), SetX(…) размещают контейнеры картинок по осям X и Y в любом месте родительского макета RelativeLayout в пиксельных единицах. Используя пространственные координаты, можно эффективно управлять положением картинок на экране смартфона или планшета. Представления ImageView создаются и добавляются в родительский контейнер программным способом. Картинки для наполнения ImageView загружаются из ресурсов Drawable и далее программно каждая в свое представление.

Получение размеров RelativeLayout

Получить программно ширину и высоту контейнера RelativeLayout во время создания или восстановления невозможно. В течение работы OnCreate(...), OnStart(..), OnResume(...) создаются только объекты визуальных классов, при этом все методы и свойства измерения на выходе выдают нулевые значения. Фактически они еще не «знают» как расположит их на экране родительский контейнер. Надежное получение ширины и высоты макета для размещения элементов ImageView гарантированно после полного создания дерева представлений. К сожалению, подобно программированию в Windows, в Android нет событий OnShow(), где можно перед показом элемента (окна) узнать его размеры. Но выход всегда есть: можно запросить размеры интересующего нас объекта RelativeLayout в отложенной задаче, которая получает доступ к интересуемым размерам после завершения подготовки пользовательского интерфейса.

// Добавляем в поток интерфейса асинхронную задачу прорисовки
 // картинок только после получения действительных 
// размеров главного макета-контейнера.
// Задача исполнится после готовности
// пользовательского интерфейса.
layoutMain.Post(() =>
    {
        ComputePos(layoutMain);      
        ShufflePositions();
        InitImages(layoutMain);  
    }
);

// -- Аналог кода на Java --
view.Post(new Runnable() {
    @Override
    public void run() {
        view.getHeight();
    }
});
//  --

// -- Аналог кода на Kotlin --
view.Post(Runnable { view.getHeight() })
// --

Подгонка размеров ImageView для заполнения экрана

Размеры представлений ImageView, а значит и картинок в них, высчитываются автоматически при визуализации MainActivity (Activity это единица экрана с пользовательским интерфейсом, Activity в приложении может быть несколько). Изображения загруженные в ImageView квадратные, а физические размеры высчитываются в пикселях. За основу расчетов, в данном исходнике игры, принята ширина главного макета в вертикальном положении. По вертикали картинки располагаются на экране до максимального заполнения. При загрузке на смартфоне, планшете или другом Android устройстве с различными размерами дисплеев визуально получается гармоничное заполнение разноцветными квадратиками.

// Вычисление позиций и размеров квадратиков
void ComputePos(RelativeLayout layoutMain)
{
    int widthLayout = layoutMain.Width;
    int heightLayout = layoutMain.Height;

    // Размер картинок высчитывается точно для горизонтали, 
    // чтобы гармонично смотрелось по ширине.
    int widthRect = widthLayout / NumberRectHorizontal;

    // Картинка квадратная.
     int heightRect = widthRect;

    // Количество строк до заполнения контейнера по высоте.
     int numberRectVertical = heightLayout / heightRect;

    // Все позиции в пространстве.
    RectPositionImages = new RectF[NumberRectHorizontal * numberRectVertical];

    // Расчет позиций в пространстве.
    int countPosY = 0;
    int countPosX = 0;
    for (int i = 0; i < RectPositionImages.Length; i++)
    {
        var rect = new RectF
        {
            Left = widthRect * countPosX,
            Top = widthRect * countPosY,
        };
        rect.Right = rect.Left + widthRect;
        rect.Bottom = rect.Top + heightRect;

        RectPositionImages[i] = rect;

        if (countPosX > 0 && countPosX % (NumberRectHorizontal - 1) == 0)
        {
            countPosX = -1;
            countPosY++;
        }
        countPosX++;
    }
}

Пространственные координаты

Координаты позиций и размерность элементов ImageView вычисляются после создания дерева представлений MainActivity и хранятся в массиве прямоугольников RectF[] RectPositionImages. Хранение координат отдельно от контейнеров картинок позволяет контролировать их положения на экране дисплея и корректировать в случае необходимости. Пространственными координаты названы, потому что они не привязаны к элементам макета и только виртуально делят пространство главного контейнера на ячейки. Пространственные координаты вычисляются на основе количества ячеек по ширине экрана. Элементы ImageView размещаются на позициях в контейнере RelativeLayout исчисляемых в пикселях. При создании приложения создается игровое поле с размерами ячеек высчитанных соответственно размеру экрана данного Android устройства.

// Количество позиций в пространстве
RectPositionImages = new RectF[NumberRectHorizontal * numberRectVertical];

// Расчет позиций в пространстве.
int countPosY = 0;
int countPosX = 0;
for (int i = 0; i < RectPositionImages.Length; i++)
{
    var rect = new RectF
    {
        Left = widthRect * countPosX,
        Top = widthRect * countPosY,
    };
    rect.Right = rect.Left + widthRect;
    rect.Bottom = rect.Top + heightRect;

    RectPositionImages[i] = rect;

    if (countPosX > 0 && countPosX % (NumberRectHorizontal - 1) == 0)
    {
        countPosX = -1;
        countPosY++;
    }
    countPosX++;
}

Событие прикосновения Touch

Для взаимодействия пользователя с игрой Мозаика применяется событие Touch(...). Это событие представляет собой реагирование на прикосновение пальцем или стилусом к сенсорному экрану. Прикосновение к объекту ImageView вызывает видимое изменение размеров и прозрачности картинки. Такая полезная обратная связь даёт информацию игроку Мозаики о том, что он действительно выбрал необходимый квадратик и может его перемещать в желаемое место для создания задуманного узора. В процессе перемещения также есть обратный сигнал о том, что квадратик находится над нужным местом и его можно отпустить. При этом цвет перемещаемой картинки изменяется если она находится на допустимом расстоянии от целевого места размещения. После отпускания картинки (подсобытие MotionEventActions.Up) активная картинка обменивается координатами позиции с нижележащей картинкой.

private void ImageView_Touch(object sender, View.TouchEventArgs e)
{
    // Координаты курсора
    MotionEvent motionEvent = e.Event;

    // Принимаем абсолютные координаты курсора.
    float cursorX = motionEvent.RawX;
    float cursorY = motionEvent.RawY;


    // Главный макет для доступа ко всем картинкам.
    RelativeLayout layoutMain = 
        this.FindViewById(Resource.Id.LayoutMain);

    // Активная картинка
    ImageView ivSender = (ImageView)sender;

    // Позиции активной картинки в пространстве экрана
    Positions posSender = (Positions)ivSender.Tag;

    // Центр картинки сделаем посередине, чтобы её 
    // было видно из-под пальца.
    ivSender.PivotX = ivSender.Width / 2;
    ivSender.PivotY = ivSender.Height / 2;


    // Прикасаемся, т.е. нажимаем.
    if (motionEvent.Action == MotionEventActions.Down)
    {
        // Увеличиваем активную картинку для удобного
        // вождения пальцем.
        ivSender.ScaleX = 2.0f;
        ivSender.ScaleY = 2.0f;

        // Делаем картинку полупрозрачной, чтобы было видно 
        // картинки под ней.
        ivSender.Alpha = 0.5f;

        // Поднимаем над всеми.
        ivSender.BringToFront();

        // Запоминаем данную позицию активной картинки.
        posSender.Position = new RectF
        {
            Left = ivSender.GetX(),
            Top = ivSender.GetY(),
            Right = ivSender.GetX() + ivSender.Width,
            Bottom = ivSender.GetY() + ivSender.Height
        };

        // Постоянная дельта на время вождения картинки.
        // Дельта это разница между координатным положением 
        // картинки по данной оси и местом соприкосновения пальца.
        // Измеряется в абсолютных единицах.
        // Расстояние от места прикосновения до начала координат активной картинки.
        DeltaX = cursorX - ivSender.GetX();
        DeltaY = cursorY - ivSender.GetY();
    }

    // Перемещение активной картинки в поисках места для создания узора.
    if (motionEvent.Action == MotionEventActions.Move)
    {
        // Из координат курсора, во время перемещения, вычитаем расстояние от места прикосновения
        // до начала координат выбранной картинки.
        // Благодаря этому картинка относительно пальца будет неподвижна,
        // и будет двигаться точно под пальцем.
        ivSender.SetX(cursorX - DeltaX);
        ivSender.SetY(cursorY - DeltaY);

        // Отлавливаем место возможного отпускания картинки.
        for (int i = 0; i < RectPositionImages.Length; i++)
        {
            if (ivSender.GetX() < (RectPositionImages[i].Left + 20) &&
            ivSender.GetX() > (RectPositionImages[i].Left - 20) &&
            ivSender.GetY() < (RectPositionImages[i].Top + 20) &&
            ivSender.GetY() > (RectPositionImages[i].Top - 20))
            {
                // Если место найдено просигналим изменением цвета 
                // передвигаемой картинки.
                ivSender.SetColorFilter(Color.DarkViolet);

                 // Устанавливаем флаг место найдено.
                 posSender.isFoundPos = true;

                 // Запоминаем новую позицию для активной картинки.
                 posSender.newPos = RectPositionImages[i];

                 break;
            }

            // Если отдалились от места возможного приземления
            // снимаем цветовую сигнализацию перемещаемой картикни.
            ivSender.ClearColorFilter();

            // Снимаем флаг обнаружения места приземления.
            posSender.isFoundPos = false;
        }
    }


    // Поднимаем палец. Отпускаем картинку.
    if (motionEvent.Action == MotionEventActions.Up)
    {

        // Возвращаем картинке нормальный масштаб.
        ivSender.ScaleX = 1;
        ivSender.ScaleY = 1;

        // Восстанавливаем непрозрачность.
        ivSender.Alpha = 1.0f;

        // Отпуская курсор принимаем новые координаты для данной картинки.
        // А картинка которая была уже на этой позиции отправляется на прежнее
        // место активной картинки.
        if (posSender.isFoundPos == true)
        {

            // Извлекаем информацию о нижележащей картинке под перемещаемой.
            for (int img = 0; img < layoutMain.ChildCount; img++)
            {
                ImageView imageView = (ImageView)layoutMain.GetChildAt(img);

                if (imageView.GetX() == posSender.newPos.Left && imageView.GetY() == posSender.newPos.Top)
                {
                    // --- Если картинку нашли ---

                    // Поднимаем её над всеми картинками.
                    imageView.BringToFront();

                   // Перемещаем с анимацией картинку на старое место передвигаемой картинки.
                   imageView.Animate().TranslationX(posSender.Position.Left);
                   imageView.Animate().TranslationY(posSender.Position.Top);

                    break;
                }
            }

            // Активная картинка устанавливается на новое место.
            ivSender.Animate().TranslationX(posSender.newPos.Left);
            ivSender.Animate().TranslationY(posSender.newPos.Top);

            // Сбрасываем флаг найденного места.
            posSender.isFoundPos = false;

            // Восстанавливаем настоящий цвет.
            ivSender.ClearColorFilter();
        }
        else
        {
            // Если нижележащая картинка не найдена,
            // возвращаем активную картинку на прежнее место.
            ivSender.Animate().TranslationX(posSender.Position.Left);
             ivSender.Animate().TranslationY(posSender.Position.Top);

            // Восстанавливаем настоящий цвет.
            ivSender.ClearColorFilter();
        }
    }
}


Использование свойства View.Tag

Свойство Tag класса Android.View предназначено для хранения пользовательской информации непосредственно в объекте класса. ImageView является наследником класса View и соответственно тоже имеет данное свойство. Свойство удобно тем, что в нем можно хранить любой объект. В исходнике игры Мозаика ImageView.Tag используется для хранения информации о текущей и новой позиции владельца свойства при исполнении события Touch(...). Для этого создан класс Positions в котором храниться координатная информация для необходимых перемещений цветных квадратиков.

// Собственный класс обязательно наследуем от Java.Lang.Object
// иначе свойству элементов Tag не сможем
// присвоить объект класса.
class Positions : Java.Lang.Object
{
    // Текущая позиция
    public RectF Position;

    // Новая позиция
    public RectF newPos;

    // Флаг обнаружения места приземления.
    public bool isFoundPos;
}

Прикрепленные файлы исходника

К статье прикреплен архив исходника игры Мозаика для скачивания. Исходник написан на языке C# на платформе Xamarin.Android. Инструмент программирования MS Visual Studio 2019. Целевая платформа API 26 (Android 8.0).

Скачать исходник