понедельник, 5 июля 2010 г.
Адаптеры в Android
Нынешняя статья будет посвящена такой важной теме, как адаптеры. Ведь каждому, кому приходится сталкиваться с ListView или Spinner или там ListActivity волей-неволей приходится с ними разбираться.
понедельник, 26 апреля 2010 г.
Багофича в LoggerAppenderDailyFile
LoggerAppenderDailyFile. И не могу понять, бага это или фича.воскресенье, 25 апреля 2010 г.
Введение в Log4php, часть 2. Использование
суббота, 24 апреля 2010 г.
Введение в Log4php, часть 1. Конфигурация
Log4php относится к тому же семейству, что и любимый мной log4net, так что многие понятия повторяются. И, кстати, у log4php имеется достаточно хорошая документация. Настолько хорошая, что в этой моей статье, в общем-то, нет ровно ничего оригинального.
пятница, 9 апреля 2010 г.
Шпаргалка по MVVM в WPF
Тема, конечно, неоднократно поднятая, но я все равно напишу шпаргалку на тот случай, если вдруг когда-нибудь забуду. А то с WCSF тоже вроде разобралась, а прошло два месяца со сдачи проекта, и уже ничего и не помню...
Сразу предупрежу, что статья предназначена для того, чтобы быстро въехать в MVVM или быстро его вспомнить. Так что описание будет предельно краткое и схематичное.
Уважаемые коллеги! Эта статья написана в 2010 году. Я примерно с 2011 пишу только на Java под андроид и не помню про WPF ничего. Что-либо у меня спрашивать — бесполезно. Извините
вторник, 30 марта 2010 г.
eval в .NET
В данном случае eval — это вовсе не Eval, который используется в дотнетовском датабиндинге. Здесь речь пойдет о том, как изобразить на .NET что-то, похожее на джаваскриптовый eval.
А дело вот в чем. Сейчас я занимаюсь одним большим вычислительным проектом. И вот, возникла у меня следующая задача. Пользователи, которым нужно проводить эти вычисления, захотели интерфейс для того, чтобы самим править некоторые используемые формулы. Вследствие этого мне пришлось озаботиться вопросом, а как вообще делать произвольные вычисления в .NET. Чтобы взял строчку типа "2 + 2.3 * x * sin(y)", сказал, что x=3 и y=34, и посчиталось.
Ниже результаты моего ресерча. Может, кому пригодится.
воскресенье, 9 августа 2009 г.
Ежедневные логи на log4net
Недавно поступил вопрос:
A есть возможность писать лог каждый день в новый файл? Чтобы частью имени файла была дата?
Сделать такое можно. Нужно только в конфигурации appender-а написать следующее:
<appender name="DebugFileAppender" type="log4net.Appender.RollingFileAppender"> <lockingModel type="log4net.Appender.FileAppender+MinimalLock" /> <file value="debug" /> <appendToFile value="true" /><rollingStyle value="Date" /> <datePattern value="yyyyMMdd" /> <staticLogFileName value="false" /><maxSizeRollBackups value="10" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%-5p [%d] [%C.%M] %m%n" /> </layout> </appender>
Что же мы тут сделали? У класса RollingFileAppender, который пишет лог в файл, есть такое свойство RollingStyle, которое определяет, когда логгер будет начинать новый файл лога. Допустимы следующие значения:
- Size — новый файл создается, как только лог достигает определенного размера (этот размер задается в свойстве
maximumFileSize). - Date — новый файл создается каждый день, и к названию файла, определенному в свойстве
file, добавляется дата в формате, заданном в свойствеdatePattern. - Composite — комбинирует свойства
DateиSize, т.е. учитываются и размер файла, и дата. Кроме того, данное значение является дефолтным для свойстваRollingStyle. - Once — лог создается один раз при запуске программы.
Итак, в нашем примере мы задали RollingStyle = Date (можно было бы и Composite) и указали формат даты. Но, пока staticLogFileName = true, все это будет игнорироваться, так что надо задать false (эта часть мне несколько непонятна, в документации для staticLogFileName написано другое, но на деле получается именно так). И теперь к имени файла с логом добавляется дата.
NB
Если в datePattern мы напишем что-нибудь вроде yyyyMMdd.log, то файлы, которые будут создаваться, будут называться debug20090808.loA.D.. Это происходит из-за того, что g также является возможным форматом даты. Логгер добросовестно применил этот формат и сообщил, что дата относится к нашей эре (кстати, тоже интересный вопрос: в MSDN написано, что формат эры — gg, а g — это general). Чтобы избежать таких ситуаций, символы, которые являются корректными форматами для дат, надо квотить:
<datePattern value="yyyyMMdd.lo\g" />
Кстати, txt это тоже касается.
среда, 22 июля 2009 г.
Дерево в выпадающем списке средствами XSLT
Задача
Допустим, у нас на сайте есть у нас нечто иерархическое. Например, категории товаров. У категорий есть подкатегории и так далее. И эта структура выражается в следующей XML-ке:
<categories> <category id="1" title="Процессоры"> <categories> <category id="2" title="Intel"> <categories> <category id="3" title="Intel Core LGA775"> <categories/> </category> <category id="4" title="Intel Core i7 LGA1366"> <categories/> </category> </categories> </category> <category id="6" title="AMD"> <categories> <category id="6" title="AMD Athlon AM2"> <categories/> </category> <category id="7" title="AMD Athlon II AM3"> <categories/> </category> </categories> </category> </categories> </category> <category id="8" title="Жесткие диски"> <categories> <category id="9" title="SATA"> <categories> <category id="10" title="Seagate"> <categories /> </category> <category id="11" title="Western Digital"> <categories /> </category> </categories> </category> <category id="12" title="IDE"> <categories> <category id="13" title="Seagate"> <categories /> </category> <category id="14" title="Western Digital"> <categories /> </category> </categories> </category> </categories> </category> </categories>
NB: На самом деле хранить строки в атрибутах не есть хорошо, их стоит делать отдельными узлами и заворачивать в CDATA. Однако, здесь title является именно атрибутом в целях экономии места. Так что прошу не брать дурной пример.
И тут нам стало нужно сформировать из этой XML-ки выпадающий список. Простейший способ это сделать — написать следующее преобразование:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html" indent="yes"/> <xsl:template match="categories"> <select> <option value="0">Корневая категория</option> <xsl:apply-templates select="//category"/> </select> </xsl:template> <xsl:template match="category"> <option value="{@id}"> <xsl:value-of select="@title"/> </option> </xsl:template> </xsl:stylesheet>
В результате мы получим вот такую картину
В общем-то, работает, но хочется, чтобы дерево категорий и выглядело, как дерево. А, так как у нас тут select, то получить это дерево можно только, расставив по нужным местам пробелы. Есть, конечно, optgroup-ы, но они в данном случае нас не спасут, потому что у категорий может быть сколь угодно большая вложенность.
Решение
Вот такой велосипед решает заданную задачу:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html" indent="yes"/> <xsl:template name="cat_tree" match="categories"> <xsl:param name="space" /> <xsl:for-each select="categories/category"> <option value="{@id}"> <xsl:value-of select="$space" /> <xsl:value-of select="@title"/> </option> <xsl:if test="./categories" > <xsl:call-template name="cat_tree"> <xsl:with-param name="space" select="concat($space, '  ')"/> </xsl:call-template> </xsl:if> </xsl:for-each> </xsl:template> <xsl:template match="/"> <select> <option value="0">Корневая категория</option> <xsl:call-template name="cat_tree"> <xsl:with-param name="space" select="'  '"/> </xsl:call-template> </select> </xsl:template> </xsl:stylesheet>
В итоге получаем следующее:
пятница, 27 марта 2009 г.
Пишем игру для Android. Часть 1. Surface
На прошлую статью про Android я получила хорошие отзывы, вдохновилась и решила продолжать. Эта серия статей будет посвящена написанию игры для Android.
Писать мы будем игру в пинг-понг. Изначально задумывался арканоид, но для мануала получалось слишком громоздко, так что я решила упростить до пинг-понга. Итак, есть прямоугольное поле, на нем две ракетки, управляемые игроками, и мячик. Мячик летает, отражаясь от ракеток и боковых стенок. Когда один игрок не успевает отбить мячик, его противнику засчитывается очко. Игра продолжается, пока один из игроков не наберет определенное число очков. Вот такую игру мы и будем писать. Одна ракетка будет управляться пользователем, другая — компьютером.
Создание проекта
Проект будем делать, как и в прошлый раз, в Eclipse. Создаём:
Получили автоматически сгенерившийся HelloWorld. На форме у нас единственный элемент управления — TextView. Но нам нужно разместить на форме компонент, который бы отрисовывал игровое поле и обрабатывал нажатия клавиш. Среди стандартных такого нет, так что придется создавать свой.
Surface
Прежде, чем создавать такой класс, рассмотрим некоторые базовые понятия и классы.
SurfaceView
SurfaceView унаследован от View и является элементом управления, предоставляющим область для рисования (Surface). Суть в том, чтобы дать отдельному потоку возможность рисовать на Surface, когда он захочет, а не только тогда, когда приложению вздумается обновить экран. Понятие Surface очень похоже на Canvas, но все же немного не то. Canvas — это область рисования на компоненте, а Surface сам является компонентом, т.е. у Surface есть Canvas.
SurfaceView является элементом управления, т.е. можно его непосредственно разместить на форме. Однако, в этом случае толку толку от него будет мало. Так что мы будем писать свой класс, унаследованный от SurfaceView, а также класс для потока, который будет на нем рисовать.
SurfaceHolder
Интерфейс, с помощью которого происходит вся непосредственная работа с областью рисования. Выглядит это примерно так:
SurfaceHolder surfaceHolder; ... Canvas canvas = surfaceHolder.lockCanvas(); // начали рисовать // рисуем surfaceHolder.unlockCanvasAndPost(canvas); // закончили рисовать
SurfaceHolder.Callback
Интерфейс содержит функции обработки изменения состояния Surface:
- surfaceCreated(SurfaceHolder holder) — первое создание Surface. Здесь можно, например, запускать поток, который будет рисовать на Surface.
- surfaceChanged(SurfaceHolder holder, int format, int width, int height) — любое изменение Surface (например, поворот экрана).
- surfaceDestroyed(SurfaceHolder holder) — уничтожение Surface. Здесь можно останавливать процесс, который рисует на Surface.
Класс для отображения игры
Итак, узнав, что такое Surface, можно двигаться дальше. Cоздаем класс GameView.java, унаследованный от SurfaceView и реализующий интерфейс SurfaceHolder.Callback. Добавим интерфейсные функции и перегрузим конструктор. Кроме того, следует завести в этом классе ссылку на SurfaceHolder. В результате получится что-то вроде того:
GameView.java
public class GameView extends SurfaceView implements SurfaceHolder.Callback { /** * Область рисования */ private SurfaceHolder mSurfaceHolder; /** * Конструктор * @param context * @param attrs */ public GameView(Context context, AttributeSet attrs) { super(context, attrs); // подписываемся на события Surface mSurfaceHolder = getHolder(); mSurfaceHolder.addCallback(this); } @Override /** * Изменение области рисования */ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override /** * Создание области рисования */ public void surfaceCreated(SurfaceHolder holder) { } @Override /** * Уничтожение области рисования */ public void surfaceDestroyed(SurfaceHolder holder) { } }
Теперь мы можем запросто писать в разметке формы такое:
main.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" > <com.android.pingpong.GameView android:id="@+id/game" android:layout_width="fill_parent" android:layout_height="fill_parent"/> </FrameLayout>
И, запустив программу, увидим пустой экран. Теперь давайте что-нибудь нарисуем.
Класс для рисования
Поставим себе первую цель: нарисовать на экране прямоугольное поле размером 300 x 250.
Как было уже ранее сказано, все рисование должно производиться из отдельного потока. Создадим класс, GameManager, унаследованный от Thread.
GameManager.java
public class GameManager extends Thread { private static final int FIELD_WIDTH = 300; private static final int FIELD_HEIGHT = 250; /** Область, на которой будем рисовать */ private SurfaceHolder mSurfaceHolder; /** Состояние потока (выполняется или нет. Нужно, чтобы было удобнее прибивать поток, когда потребуется) */ private boolean mRunning; /** Стили рисования */ private Paint mPaint; /** Прямоугольник игрового поля */ private Rect mField; /** * Конструктор * @param surfaceHolder Область рисования * @param context Контекст приложения */ public GameManager(SurfaceHolder surfaceHolder, Context context) { mSurfaceHolder = surfaceHolder; mRunning = false; mPaint = new Paint(); mPaint.setColor(Color.BLUE); mPaint.setStrokeWidth(2); mPaint.setStyle(Style.STROKE); int left = 10; int top = 50; mField = new Rect(left, top, left + FIELD_WIDTH, top + FIELD_HEIGHT); } /** * Задание состояния потока * @param running */ public void setRunning(boolean running) { mRunning = running; } @Override /** Действия, выполняемые в потоке */ public void run() { while (mRunning) { Canvas canvas = null; try { // подготовка Canvas-а canvas = mSurfaceHolder.lockCanvas(); synchronized (mSurfaceHolder) { // собственно рисование canvas.drawRect(mField, mPaint); } } catch (Exception e) { } finally { if (canvas != null) { mSurfaceHolder.unlockCanvasAndPost(canvas); } } } } }
Стоит отдельно упомянуть о классе Paint. Этот класс используется для хранения всяческих используемых при рисовании стилей — цветов, толщины и стиля линий, шрифтов (это мы рассмотрим позже) и тому подобного. В остальном код достаточно прозрачен. Собственно рисование проходит всегда одинаково — лочим Canvas, рисуем, разлочиваем.
Теперь надо запустить рисовательный поток в нашем контроле. Добавляем в класс соответствущее поле:
GameView.java
/** * Поток, рисующий в области */ private GameManager mThread;
В конструкторе GameView:
GameView.java
mThread = new GameManager(mSurfaceHolder, context);
При создании области рисования надо будет запустить наш поток:
GameView.java
public void surfaceCreated(SurfaceHolder holder) { mThread.setRunning(true); mThread.start(); }
А при удалении — прибить:
GameView.java
public void surfaceDestroyed(SurfaceHolder holder) { boolean retry = true; mThread.setRunning(false); while (retry) { try { // ожидание завершение потока mThread.join(); retry = false; } catch (InterruptedException e) { } } }
Теперь, запустив программу, видим следующее:
Поворот экрана
Как уже было упомянуто, при повороте экрана вызывается обработчик surfaceChanged. Впрочем, при создании surface он тоже вызывается. В параметрах можно получить размеры доступной части экрана, что очень приятно, потому что с помощью класса DisplayMetrics можно получить только полный размер экрана, куда еще входит верхнее поле, на котором рисовать нельзя).
Итак, в surfaceChanged мы будем пересчитывать положение нашего поля на экране. Добавим в GameManager такую функцию:
GameManager.java
/** * Инициализация положения объектов, в соответствии с размерами экрана * @param screenHeight Высота экрана * @param screenWidth Ширина экрана */ public void initPositions(int screenHeight, int screenWidth) { int left = (screenWidth - FIELD_WIDTH) / 2; int top = (screenHeight - FIELD_HEIGHT) / 2; mField.set(left, top, left + FIELD_WIDTH, top + FIELD_HEIGHT); }
Эта функция ставит наше игровое поле в центр экрана. Инициализацию положения mField в конструкторе GameManager можно вовсе убрать, оставив только:
GameManager.java
public GameManager(SurfaceHolder surfaceHolder, Context context) { mSurfaceHolder = surfaceHolder; mRunning = false; mPaint = new Paint(); mPaint.setColor(Color.BLUE); mPaint.setStrokeWidth(2); mPaint.setStyle(Style.STROKE);mField = new Rect();}
Теперь в surfaceChanged можно написать следующее:
GameView.java
@Override /** * Изменение области рисования */ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { mThread.initPositions(height, width); }
Теперь при изменении Surface (в том числе при его создании) будет пересчитываться положение поля. Так что приложение будет выглядеть так:
Или так:
Итак
Мы рассмотрели необходимые понятия, написали простое приложение, которое умеет что-то рисовать, обработали поворот экрана. Исходники, как обычно, прилагаются.
Пишем игру для Android. Часть 2. Игровые объекты
Добавим в нашу игру экшена. Как известно, в пинг-понге три действующих лица: мячик и две ракетки. Имеет смысл реализовать соответствующие классы — Ball и Racquet. А, поскольку имеются некоторые свойства, присущие обоим этим сущностям (как то: расположение, изображение, размеры и т.д.), то можно сделать базовый класс под названием GameObject. Диаграмма классов будет такая:

У всех игровых объектов есть:
mPoint— левый верхний угол прямоугольника, ограничивающего объект. С её помощью однозначно определяется положение объекта на плоскостиmImage— изображение объекта. Изображения обычно берутся из ресурсов приложения — /res/drawablemHeight,mWidth— размеры изображения объекта. Вынесены как поля класса только для того, чтобы не загромождать код всякимиmImage.getIntrinsicHeight()иmImage.getIntrinsicWidth()mSpeed— скорость перемещения объекта (фактически, на сколько перемещается объект за один шаг)
Кроме того, все игровые классы умеют рисовать себя на указанном Canvas-е (метод draw) и вычислять своё следующее состояние (update). Причем, перемещение происходит так: вычисляется следующее состояние опорной точки (updatePoint), а потом туда переносится изображение. А, поскольку мячик и ракетка перемещаются по-разному, метод updatePoint сделан абстрактным.
В класс Ball добавлено поле mAngle — угол (в градусах) к оси Ox, под которым летит мячик. В класс Racquet — поля mDirection (направление, куда движется ракетка — вправо или влево, или вообще не движется) и mScore (количество очков у игрока).
Реализации классов
GameObject
GameObject.java
public abstract class GameObject { // Константы для направлений public final int DIR_LEFT = -1; public final int DIR_RIGHT = 1; public final int DIR_NONE = 0; /** Координаты опорной точки */ protected Point mPoint; /** Высота изображения */ protected int mHeight; /** Ширина изображения */ protected int mWidth; /** Изображение */ private Drawable mImage; /** Скорость */ protected int mSpeed; /** * Конструктор * @param image Изображение, которое будет обозначать данный объект */ public GameObject(Drawable image) { mImage = image; mPoint = new Point(0, 0); mWidth = image.getIntrinsicWidth(); mHeight = image.getIntrinsicHeight(); } /** Перемещение опорной точки */ protected abstract void updatePoint(); /** Перемещение объекта */ public void update() { updatePoint(); mImage.setBounds(mPoint.x, mPoint.y, mPoint.x + mWidth, mPoint.y + mHeight); } /** Отрисовка объекта */ public void draw(Canvas canvas) { mImage.draw(canvas); } /** Задает левую границу объекта */ public void setLeft(int value) { mPoint.x = value; } /** Задает правую границу объекта */ public void setRight(int value) { mPoint.x = value - mWidth; } /** Задает верхнюю границу объекта */ public void setTop(int value) { mPoint.y = value; } /** Задает нижнюю границу объекта */ public void setBottom(int value) { mPoint.y = value - mHeight; } /** Задает абсциссу центра объекта */ public void setCenterX(int value) { mPoint.x = value - mHeight / 2; } /** Задает левую ординату центра объекта */ public void setCenterY(int value) { mPoint.y = value - mWidth / 2; } }
Ball
Ball.java
public class Ball extends GameObject { private static final int DEFAULT_SPEED = 3; private static final int PI = 180; /** Угол, который составляет направление полета шарика с осью Ox */ private int mAngle; /** * @see com.android.pingpong.objects.GameObject#GameObject(Drawable) */ public Ball(Drawable image) { super(image); mSpeed = DEFAULT_SPEED; // задали скорость по умолчанию mAngle = getRandomAngle(); // задали случайный начальный угол } /** * @see com.android.pingpong.objects.GameObject#updatePoint() */ @Override protected void updatePoint() { double angle = Math.toRadians(mAngle); mPoint.x += (int)Math.round(mSpeed * Math.cos(angle)); mPoint.y -= (int)Math.round(mSpeed * Math.sin(angle)); } /** Генерация случайного угла в промежутке [95, 110]U[275,290] */ private int getRandomAngle() { Random rnd = new Random(System.currentTimeMillis()); return rnd.nextInt(1) * PI + PI / 2 + rnd.nextInt(15) + 5; } }
Racquet
Racquet.java
public class Racquet extends GameObject { private static final int DEFAULT_SPEED = 3; /** Количество заработанных очков */ private int mScore; /** Направление движения */ private int mDirection; /** Задание направления движения*/ private void setDirection(int direction) { mDirection = direction; } /** * @see com.android.pingpong.objects.GameObject#GameObject(Drawable) */ public Racquet(Drawable image) { super(image); mDirection = DIR_NONE; // Направление по умолчанию - нет mScore = 0; // Очков пока не заработали mSpeed = DEFAULT_SPEED; // Задали скорость по умолчанию } /** * @see com.android.pingpong.objects.GameObject#updatePoint() */ @Override protected void updatePoint() { mPoint.x += mDirection * mSpeed; // двигаем ракетку по оси Ox в соответствующую сторону } }
Отображение игровых объектов
Рисуем картинки
Ну вроде классы наши готовы, можно теперь их использовать в программе. Но сначала надо нарисовать картинки, для наших объектов. Картинки должны быть в png. У меня получилось так:
![]() | Мячик |
![]() | Наша ракетка |
![]() | Pакетка противника |
Берем все это счастье, и кидаем в /res/drawable, где у нас хранятся всякие такие ресурсы.
Создаем игровые объекты
Теперь нам надо где-то создать экземпляры наших классов, чтобы они там жили, обновлялись и отображались на экране. Очевидно, что тут нам поможет GameManager. Итак, добавим в него такие поля:
GameManager.java
/** Ресурсы приложения */ private Resources mRes; /** Мячик */ private Ball mBall; /** Ракетка, управляемая игроком */ private Racquet mUs; /** Ракетка, управляемая компьютером*/ private Racquet mThem;
В конструкторе инициализируем их:
GameManager.java
public GameManager(SurfaceHolder surfaceHolder, Context context) { mSurfaceHolder = surfaceHolder; mRunning = false; // инициализация стилей рисования mPaint = new Paint(); mPaint.setColor(Color.BLUE); mPaint.setStrokeWidth(2); mPaint.setStyle(Style.STROKE);Resources res = context.getResources(); mField = new Rect(); mBall = new Ball(res.getDrawable(R.drawable.ball)); mUs = new Racquet(res.getDrawable(R.drawable.us)); mThem = new Racquet(res.getDrawable(R.drawable.them));}
Расставляем их по местам
Однако, этого мало. У всех игровых объектов опорная точка задана в начале координат, т.е. они сейчас все в куче, и надо бы их растащить по местам. И самый лучший метод, где можно это сделать — initPositions():
GameManager.java
public void initPositions(int screenHeight, int screenWidth) { int left = (screenWidth - FIELD_WIDTH) / 2; int top = (screenHeight - FIELD_HEIGHT) / 2; mField.set(left, top, left + FIELD_WIDTH, top + FIELD_HEIGHT);// мячик ставится в центр поля mBall.setCenterX(mField.centerX()); mBall.setCenterY(mField.centerY()); // ракетка игрока - снизу по центру mUs.setCenterX(mField.centerX()); mUs.setBottom(mField.bottom); // ракетка компьютера - сверху по центру mThem.setCenterX(mField.centerX()); mThem.setTop(mField.top);}
Заставляем их что-то делать
Итак, объекты мы создали, теперь надо заставить их что-то делать. Понятно, что все изменения должны происходить в цикле, который работает в методе run(). На каждом шаге цикла мы должны обновить состояния объектов и отобразить их. Добавим две функции:
GameManager.java
/** Обновление объектов на экране */ private void refreshCanvas(Canvas canvas) { // рисуем игровое поле canvas.drawRect(mField, mPaint); // рисуем игровые объекты mBall.draw(canvas); mUs.draw(canvas); mThem.draw(canvas); } /** Обновление состояния игровых объектов */ private void updateObjects() { mBall.update(); mUs.update(); mThem.update(); }
А в методе run() будут вызовы этих методов:
GameManager.java
public void run() { while (mRunning) { Canvas canvas = null; try { // подготовка Canvas-а canvas = mSurfaceHolder.lockCanvas(); synchronized (mSurfaceHolder) {updateObjects(); // обновляем объекты refreshCanvas(canvas); // обновляем экран sleep(20);} } catch (Exception e) { } finally { if (canvas != null) { mSurfaceHolder.unlockCanvasAndPost(canvas); } } } }
Ну все, вроде сделали, можно запускать. Ракетки не двигаются, потому что ими пока никто не управляет, а вот шарик вполне себе летает. Правда, оставляет при этом за собой шлейф:
Делаем фон
Это всё из-за того, что мы не очищаем экран перед очередной отрисовкой. А очищать экран это тоже не так-то просто. Ничего вроде canvas.clear() я не нашла, так что пришлось извращаться. Суть в том, что мы заводим специальный Bitmap, при инициализации Surface задать ему размеры на весь экран, а потом в refreshCanvas выводить его. При желании можно загрузить в этот Bitmap какое-нибудь изображение из ресурсов.
Итак, заводим поле:
GameManager.java
/** Фон */ private Bitmap mBackground;
Инициализируем его в initPositions
GameManager.java
public void initPositions(int screenHeight, int screenWidth) { int left = (screenWidth - FIELD_WIDTH) / 2; int top = (screenHeight - FIELD_HEIGHT) / 2; mField.set(left, top, left + FIELD_WIDTH, top + FIELD_HEIGHT);mBackground = Bitmap.createBitmap(screenWidth, screenHeight, Bitmap.Config.RGB_565);// мячик ставится в центр поля mBall.setCenterX(mField.centerX()); mBall.setCenterY(mField.centerY()); // ракетка игрока - снизу по центру mUs.setCenterX(mField.centerX()); mUs.setBottom(mField.bottom); // ракетка компьютера - сверху по центру mThem.setCenterX(mField.centerX()); mThem.setTop(mField.top); }
И отрисовываем в refreshCanvas:
GameManager.java
private void refreshCanvas(Canvas canvas) {// вывод фонового изображения canvas.drawBitmap(mBackground, 0, 0, null);// рисуем игровое поле canvas.drawRect(mField, mPaint); // рисуем игровые объекты mBall.draw(canvas); mUs.draw(canvas); mThem.draw(canvas); }
И получаем примерно вот такую картину:

Итак
Мы реализовали игровые объекты, написали код для их перемещения. Мячик у нас успешно летает, но не отражается от стенок поля, а ракетки теоретически тоже могут перемещаться, но код для их управления еще не написан. Этими вещи мы рассмотрим в следующей статье.







