HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <ul><li><a>Создание проекта</a></li>
1 <ul><li><a>Создание проекта</a></li>
2 <li><a>Верстка экрана</a></li>
2 <li><a>Верстка экрана</a></li>
3 <li><a>Соединяем верстку с кодом (Binding)</a></li>
3 <li><a>Соединяем верстку с кодом (Binding)</a></li>
4 <li><a>Пишем код</a></li>
4 <li><a>Пишем код</a></li>
5 <li><a>Создаем свою вьюшку и рисуем на ней сетку</a></li>
5 <li><a>Создаем свою вьюшку и рисуем на ней сетку</a></li>
6 <li><a>Вычисляем, кто выиграл</a></li>
6 <li><a>Вычисляем, кто выиграл</a></li>
7 <li><a>Сбрасывание состояния игры</a></li>
7 <li><a>Сбрасывание состояния игры</a></li>
8 <li><a>Подводим итоги</a></li>
8 <li><a>Подводим итоги</a></li>
9 </ul><p>В этой статье, приуроченной к онлайн-курсу<a><strong>Android Developer. Basic</strong></a>, мы напишем свою собственную небольшую игру "Крестики-нолики". Для работы потребуется установить Android Studio. Какая у вас ОС - не имеет значения, т.к. эта IDE работает на Windows, Ubuntu или Mac. Язык программирования будет Kotlin, т.к. сейчас он является официальным языком для программирования на Android. Во время написания статьи использовалась версия Android Studio 4.1.3</p>
9 </ul><p>В этой статье, приуроченной к онлайн-курсу<a><strong>Android Developer. Basic</strong></a>, мы напишем свою собственную небольшую игру "Крестики-нолики". Для работы потребуется установить Android Studio. Какая у вас ОС - не имеет значения, т.к. эта IDE работает на Windows, Ubuntu или Mac. Язык программирования будет Kotlin, т.к. сейчас он является официальным языком для программирования на Android. Во время написания статьи использовалась версия Android Studio 4.1.3</p>
10 <h3>Создание проекта</h3>
10 <h3>Создание проекта</h3>
11 <p>Для начала необходимо создать новый проект. В нашем случае необходимо выбрать<strong>Fragment + ViewModel</strong>и нажать<strong>Next</strong>.</p>
11 <p>Для начала необходимо создать новый проект. В нашем случае необходимо выбрать<strong>Fragment + ViewModel</strong>и нажать<strong>Next</strong>.</p>
12 <p>Далее в поле<strong>Name</strong>вводите название приложения, в<strong>Package name</strong>можете внести свое имя или ник как доменное имя, но с зада на перед. Это нужно для уникальности вашего приложения в маркете и на телефоне. Все остальное можно оставить по умолчанию как на картинке. Далее нажимаем Finish и ждем, когда проект соберется и мы сможем работать.</p>
12 <p>Далее в поле<strong>Name</strong>вводите название приложения, в<strong>Package name</strong>можете внести свое имя или ник как доменное имя, но с зада на перед. Это нужно для уникальности вашего приложения в маркете и на телефоне. Все остальное можно оставить по умолчанию как на картинке. Далее нажимаем Finish и ждем, когда проект соберется и мы сможем работать.</p>
13 <p>Нам нужно добавить зависимости которых нам не хватает в<strong>build.gradle</strong>модуля в раздел<strong>dependencies</strong>. А так же включить<strong>viewBinding</strong>для того, чтобы было удобна делать связь верстки с кодом (смотри изображение)</p>
13 <p>Нам нужно добавить зависимости которых нам не хватает в<strong>build.gradle</strong>модуля в раздел<strong>dependencies</strong>. А так же включить<strong>viewBinding</strong>для того, чтобы было удобна делать связь верстки с кодом (смотри изображение)</p>
14 android { … buildFeatures { viewBinding true } } dependencies { … implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" implementation "androidx.gridlayout:gridlayout:1.0.0" implementation "androidx.fragment:fragment-ktx:1.3.0" }<h3>Верстка экрана</h3>
14 android { … buildFeatures { viewBinding true } } dependencies { … implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" implementation "androidx.gridlayout:gridlayout:1.0.0" implementation "androidx.fragment:fragment-ktx:1.3.0" }<h3>Верстка экрана</h3>
15 <p>Теперь можно преступить к верстанию экрана. Для начала мы добавим только несколько вьюшек. Для того, чтобы не нужно было у каждой вьюшке проставлять отступы от края экрана, воспользуемся специальным виджетом `<strong>Guideline</strong>`. Он позволяет выставить границы, за которые будут "цепляться" вьюшки. Если у вас экран открылся в режиме `<strong>Design</strong>`, то я рекомендую перейти в режим `<strong>Split</strong>`. Так удобнее мне объяснять и можно будет копировать однотипные элементы. Чтобы добавить<strong>Guideline</strong>можно внутри элемента `<strong>ConstraintLayout</strong>` написать `<strong>&lt;guideline</strong>` и студия подскажет нужный нам элемент, который мы сможем использовать. Или справа нажать на кнопку `<strong>Guidelines</strong>` и выбрать горизонтальный или вертикальный guideline. Давайте добавим вертикальный. У нас на макете сразу появится справа полоса. Продублируем элемент<strong>Gidline,</strong>скопировав его полностью и вставив 3 раза. В параметр android:id напишем @+id/ плюс название в зависимости от расположения (start, end, top, bottom). В параметре android:orientation поставим vertical/horizontal в зависимости от направления линии. И изменить параметр app:layout_constraintGuide_<strong>begin</strong>на app:layout_constraintGuide_<strong>end</strong>для элементов с id=end/bottom.</p>
15 <p>Теперь можно преступить к верстанию экрана. Для начала мы добавим только несколько вьюшек. Для того, чтобы не нужно было у каждой вьюшке проставлять отступы от края экрана, воспользуемся специальным виджетом `<strong>Guideline</strong>`. Он позволяет выставить границы, за которые будут "цепляться" вьюшки. Если у вас экран открылся в режиме `<strong>Design</strong>`, то я рекомендую перейти в режим `<strong>Split</strong>`. Так удобнее мне объяснять и можно будет копировать однотипные элементы. Чтобы добавить<strong>Guideline</strong>можно внутри элемента `<strong>ConstraintLayout</strong>` написать `<strong>&lt;guideline</strong>` и студия подскажет нужный нам элемент, который мы сможем использовать. Или справа нажать на кнопку `<strong>Guidelines</strong>` и выбрать горизонтальный или вертикальный guideline. Давайте добавим вертикальный. У нас на макете сразу появится справа полоса. Продублируем элемент<strong>Gidline,</strong>скопировав его полностью и вставив 3 раза. В параметр android:id напишем @+id/ плюс название в зависимости от расположения (start, end, top, bottom). В параметре android:orientation поставим vertical/horizontal в зависимости от направления линии. И изменить параметр app:layout_constraintGuide_<strong>begin</strong>на app:layout_constraintGuide_<strong>end</strong>для элементов с id=end/bottom.</p>
16 <p>Теперь добавим отображение того, чей сейчас ход<strong>Х</strong>или<strong>О</strong>. Для этого добавим 3<strong>ImageView</strong>. Но для начала, давайте создадим сами иконки<strong>Х</strong>и<strong>О</strong>, а так же ß для того, чтобы показывать чей ход.</p>
16 <p>Теперь добавим отображение того, чей сейчас ход<strong>Х</strong>или<strong>О</strong>. Для этого добавим 3<strong>ImageView</strong>. Но для начала, давайте создадим сами иконки<strong>Х</strong>и<strong>О</strong>, а так же ß для того, чтобы показывать чей ход.</p>
17 <p>Для этого справа в области project щелкните правой кнопкой мыши (ПКМ) выберите New → Vector Asset.</p>
17 <p>Для этого справа в области project щелкните правой кнопкой мыши (ПКМ) выберите New → Vector Asset.</p>
18 <p>В появившемся окне щелкнуть по изображению в пункте<strong>Clip Art,</strong>найти по поиску<strong>back</strong>стрелку назад и нажать<strong>OK</strong>. Я выбрал, чтобы концы были скругленными, для этого во втором выпадающем списке выбрать<strong>Round</strong>. Вы можете посмотреть, как выглядят остальные и выбрать понравившийся. Размер я задал<strong>64</strong>в пункте<strong>Size</strong>, а цвет в<strong>Color</strong>: F44336. Вы же в палитре можете выбрать тот цвет, который Вам больше всего понравился. Название в пункте<strong>Name</strong>я указал<strong>ic_arrow</strong>, чтоб проще было использовать. Для иконок, принято название начинать с<strong>ic_</strong>и в snake case, т.е. с подчеркиванием. И нажимаем<strong>Next → Finish</strong>. Теперь самостоятельно сделайте крестик и нолик. Для них я выбрал иконку<strong>close</strong>и<strong>panorama fish eye</strong>соответственно и выбрал цвет<strong>454FCE</strong>, а названия дал<strong>ic_cross</strong>и<strong>ic_circle</strong>.</p>
18 <p>В появившемся окне щелкнуть по изображению в пункте<strong>Clip Art,</strong>найти по поиску<strong>back</strong>стрелку назад и нажать<strong>OK</strong>. Я выбрал, чтобы концы были скругленными, для этого во втором выпадающем списке выбрать<strong>Round</strong>. Вы можете посмотреть, как выглядят остальные и выбрать понравившийся. Размер я задал<strong>64</strong>в пункте<strong>Size</strong>, а цвет в<strong>Color</strong>: F44336. Вы же в палитре можете выбрать тот цвет, который Вам больше всего понравился. Название в пункте<strong>Name</strong>я указал<strong>ic_arrow</strong>, чтоб проще было использовать. Для иконок, принято название начинать с<strong>ic_</strong>и в snake case, т.е. с подчеркиванием. И нажимаем<strong>Next → Finish</strong>. Теперь самостоятельно сделайте крестик и нолик. Для них я выбрал иконку<strong>close</strong>и<strong>panorama fish eye</strong>соответственно и выбрал цвет<strong>454FCE</strong>, а названия дал<strong>ic_cross</strong>и<strong>ic_circle</strong>.</p>
19 <p>После всех этих действий, в разделе<strong>res → drawable</strong>появятся новые файлы с нашими иконками. Я рекомендую значение цвета из параметра<strong>android:tint</strong>перенести в параметр<strong>android:fillColor</strong>, а сам<strong>tint</strong>удалить. На некоторых китайских телефонах, отображение ведет себя не корректно и качество иконки ухудшается.</p>
19 <p>После всех этих действий, в разделе<strong>res → drawable</strong>появятся новые файлы с нашими иконками. Я рекомендую значение цвета из параметра<strong>android:tint</strong>перенести в параметр<strong>android:fillColor</strong>, а сам<strong>tint</strong>удалить. На некоторых китайских телефонах, отображение ведет себя не корректно и качество иконки ухудшается.</p>
20 <p>Теперь давайте вернемся к верстке и добавим 3 элемента<strong>ImageView</strong>. Для этого перейдем в режим<strong>Design</strong>(это как один из способов верстания экрана), найдем<strong>ImageView</strong>и перетащим его на макет. В появившемся экране выберем иконку<strong>ic</strong><strong>_</strong><strong>cross</strong>. В поле<strong>id</strong>напишем название<strong>cross</strong>. Таким же способом перетащим еще 2 элемента для<strong>ic</strong><strong>_</strong><strong>arrow</strong>и<strong>ic</strong><strong>_</strong><strong>circle</strong>и дадим им имя id<strong>direction</strong>и<strong>circle</strong>соответственно. Теперь выделим каждый элемент поочередно и потянув за верхний круг свяжем вьюшку с верхним<strong>guideline</strong>.</p>
20 <p>Теперь давайте вернемся к верстке и добавим 3 элемента<strong>ImageView</strong>. Для этого перейдем в режим<strong>Design</strong>(это как один из способов верстания экрана), найдем<strong>ImageView</strong>и перетащим его на макет. В появившемся экране выберем иконку<strong>ic</strong><strong>_</strong><strong>cross</strong>. В поле<strong>id</strong>напишем название<strong>cross</strong>. Таким же способом перетащим еще 2 элемента для<strong>ic</strong><strong>_</strong><strong>arrow</strong>и<strong>ic</strong><strong>_</strong><strong>circle</strong>и дадим им имя id<strong>direction</strong>и<strong>circle</strong>соответственно. Теперь выделим каждый элемент поочередно и потянув за верхний круг свяжем вьюшку с верхним<strong>guideline</strong>.</p>
21 <p>Теперь сделаем из них цепочку, чтоб они были посередине и склеены друг с другом. Для этого выделяем эти 3 элемента вышкой, нажимаем<strong>ПКМ → Chains → Create Horizontal Chain</strong>.</p>
21 <p>Теперь сделаем из них цепочку, чтоб они были посередине и склеены друг с другом. Для этого выделяем эти 3 элемента вышкой, нажимаем<strong>ПКМ → Chains → Create Horizontal Chain</strong>.</p>
22 <p>Затем снова<strong>ПКМ → Chains → Horizontal Chain Style → packed</strong>. Попробуйте другие стили и посмотрите, чем они отличаются друг от друга.</p>
22 <p>Затем снова<strong>ПКМ → Chains → Horizontal Chain Style → packed</strong>. Попробуйте другие стили и посмотрите, чем они отличаются друг от друга.</p>
23 <p>Теперь они склеились вместе и находятся посередине, как я и хотел.</p>
23 <p>Теперь они склеились вместе и находятся посередине, как я и хотел.</p>
24 <p>Добавим еще один элемент<strong>Barrier</strong>, который служит для того, чтобы следующий элемент расположить после других, не зависимо от того, какого размера будет каждый. Нажмем на кнопку<strong>Guidelines → Add Horizontal Barrier</strong>.</p>
24 <p>Добавим еще один элемент<strong>Barrier</strong>, который служит для того, чтобы следующий элемент расположить после других, не зависимо от того, какого размера будет каждый. Нажмем на кнопку<strong>Guidelines → Add Horizontal Barrier</strong>.</p>
25 <p>И установим следующие значения:</p>
25 <p>И установим следующие значения:</p>
26 &lt;androidx.constraintlayout.widget.Barrier android:id="@+id/barrier" android:layout_width="wrap_content" android:layout_height="wrap_content" app:barrierDirection="bottom" app:constraint_referenced_ids="circle,cross,direction" /&gt;<p>У вас должно получиться что-то вроде этого.</p>
26 &lt;androidx.constraintlayout.widget.Barrier android:id="@+id/barrier" android:layout_width="wrap_content" android:layout_height="wrap_content" app:barrierDirection="bottom" app:constraint_referenced_ids="circle,cross,direction" /&gt;<p>У вас должно получиться что-то вроде этого.</p>
27 <p>Теперь осталось добавить последний элемент верстки это<strong>GridLayout</strong>. Но его нужно выбрать из подключенной библиотеки и выбрать<strong>androidx.gridlayout.widget.GridLayout</strong>. Его уже нужно добавить вручную.</p>
27 <p>Теперь осталось добавить последний элемент верстки это<strong>GridLayout</strong>. Но его нужно выбрать из подключенной библиотеки и выбрать<strong>androidx.gridlayout.widget.GridLayout</strong>. Его уже нужно добавить вручную.</p>
28 &lt;androidx.gridlayout.widget.GridLayout android:id="@+id/field" android:layout_width="0dp" android:layout_height="0dp" app:columnCount="3" app:layout_constraintBottom_toBottomOf="@id/bottom" app:layout_constraintDimensionRatio="1:1" app:layout_constraintEnd_toEndOf="@id/end" app:layout_constraintStart_toStartOf="@id/start" app:layout_constraintTop_toTopOf="@id/barrier" app:rowCount="3"&gt; &lt;/androidx.gridlayout.widget.GridLayout&gt;<p>Аргументы<strong> app:columnCount</strong>и<strong>app:rowCount</strong>отвечают за количество колонок и строк соответственно. Устанавливаем значение 3, т.к. поле у нас 3х3. Аргументы<strong> app:layout_constraintDimensionRatio</strong>отвечает за соотношение сторон. Для того, что бы у нас был квадрат нужно его выставить в<strong>1:1</strong>, вот из-за этого аргументаи пришлось подключать библиотеку, т.к. на версиях<strong>Android 5</strong>этот аргумент не работает. И для того, чтобы соотношение работало, необходимо установить<strong>android:layout_width</strong>и<strong>android:layout_height</strong>в<strong>0dp</strong>. Аргументы<strong> app:layout_constraintXXX_toXXX</strong>отвечают за привязку нашей вьюшки к другим элементам. Ну и запишем<strong>id="@+id/field"</strong>, а во внутрь добавим<strong>ImageView</strong>. Но так как их понадобиться 9 штук и чтобы у каждой вьюшке не прописывать одни и те же аргументы, можно создать стиль. Для этого щелкаем ПКМ по<strong>values → New → Values Resource File</strong>.</p>
28 &lt;androidx.gridlayout.widget.GridLayout android:id="@+id/field" android:layout_width="0dp" android:layout_height="0dp" app:columnCount="3" app:layout_constraintBottom_toBottomOf="@id/bottom" app:layout_constraintDimensionRatio="1:1" app:layout_constraintEnd_toEndOf="@id/end" app:layout_constraintStart_toStartOf="@id/start" app:layout_constraintTop_toTopOf="@id/barrier" app:rowCount="3"&gt; &lt;/androidx.gridlayout.widget.GridLayout&gt;<p>Аргументы<strong> app:columnCount</strong>и<strong>app:rowCount</strong>отвечают за количество колонок и строк соответственно. Устанавливаем значение 3, т.к. поле у нас 3х3. Аргументы<strong> app:layout_constraintDimensionRatio</strong>отвечает за соотношение сторон. Для того, что бы у нас был квадрат нужно его выставить в<strong>1:1</strong>, вот из-за этого аргументаи пришлось подключать библиотеку, т.к. на версиях<strong>Android 5</strong>этот аргумент не работает. И для того, чтобы соотношение работало, необходимо установить<strong>android:layout_width</strong>и<strong>android:layout_height</strong>в<strong>0dp</strong>. Аргументы<strong> app:layout_constraintXXX_toXXX</strong>отвечают за привязку нашей вьюшки к другим элементам. Ну и запишем<strong>id="@+id/field"</strong>, а во внутрь добавим<strong>ImageView</strong>. Но так как их понадобиться 9 штук и чтобы у каждой вьюшке не прописывать одни и те же аргументы, можно создать стиль. Для этого щелкаем ПКМ по<strong>values → New → Values Resource File</strong>.</p>
29 <p> В появившемся окне, в поле<strong>File name</strong>указываем<strong>styles</strong>и нажимаем<strong>OK</strong>. В созданном файле сделаем следующую запись:</p>
29 <p> В появившемся окне, в поле<strong>File name</strong>указываем<strong>styles</strong>и нажимаем<strong>OK</strong>. В созданном файле сделаем следующую запись:</p>
30 &lt;resources&gt; &lt;style name="CellStyle"&gt; &lt;item name="layout_rowWeight"&gt;1&lt;/item&gt; &lt;item name="layout_columnWeight"&gt;1&lt;/item&gt; &lt;item name="android:background"&gt;?android:attr/selectableItemBackground&lt;/item&gt; &lt;item name="android:layout_width"&gt;0dp&lt;/item&gt; &lt;item name="android:layout_height"&gt;0dp&lt;/item&gt; &lt;item name="android:adjustViewBounds"&gt;true&lt;/item&gt; &lt;/style&gt; &lt;/resources&gt;<p>Аргументы<strong>layout_columnWeight</strong>и<strong>layout_rowWeight</strong>служат для установки весов вьюшек и чтобы они равными размерами распределялись по ширине и высоте соответственно. Для того, чтобы эти веса заработали, необходимо<strong>android:layout_width</strong>и<strong>android:layout_height</strong>установить в<strong>0dp</strong>. А в<strong>android:background</strong>прописываем атрибут, который будет браться из темы и отображать нажатие на вьюшку. Теперь осталось подключить этот стиль к нашей вьюшке. Возвращаемся к нашей верстке и у созданной<strong>ImageView</strong>меняем на следующее:</p>
30 &lt;resources&gt; &lt;style name="CellStyle"&gt; &lt;item name="layout_rowWeight"&gt;1&lt;/item&gt; &lt;item name="layout_columnWeight"&gt;1&lt;/item&gt; &lt;item name="android:background"&gt;?android:attr/selectableItemBackground&lt;/item&gt; &lt;item name="android:layout_width"&gt;0dp&lt;/item&gt; &lt;item name="android:layout_height"&gt;0dp&lt;/item&gt; &lt;item name="android:adjustViewBounds"&gt;true&lt;/item&gt; &lt;/style&gt; &lt;/resources&gt;<p>Аргументы<strong>layout_columnWeight</strong>и<strong>layout_rowWeight</strong>служат для установки весов вьюшек и чтобы они равными размерами распределялись по ширине и высоте соответственно. Для того, чтобы эти веса заработали, необходимо<strong>android:layout_width</strong>и<strong>android:layout_height</strong>установить в<strong>0dp</strong>. А в<strong>android:background</strong>прописываем атрибут, который будет браться из темы и отображать нажатие на вьюшку. Теперь осталось подключить этот стиль к нашей вьюшке. Возвращаемся к нашей верстке и у созданной<strong>ImageView</strong>меняем на следующее:</p>
31 &lt;ImageView style="@style/CellStyle" tools:src="@drawable/ic_cross" /&gt;<p><strong>style="@style/CellStyle"</strong>- позволяет применить наш стиль для разных вьюшек, а<strong>tools:src="@drawable/ic_cross"</strong>позволяет отображать картинку на макете, но когда проект будет создан и установлен на телефоне, то картинки не будет. Продублируем этот элемент, чтобы в общем счете их получилось 9. В итоге должно получиться следующее:</p>
31 &lt;ImageView style="@style/CellStyle" tools:src="@drawable/ic_cross" /&gt;<p><strong>style="@style/CellStyle"</strong>- позволяет применить наш стиль для разных вьюшек, а<strong>tools:src="@drawable/ic_cross"</strong>позволяет отображать картинку на макете, но когда проект будет создан и установлен на телефоне, то картинки не будет. Продублируем этот элемент, чтобы в общем счете их получилось 9. В итоге должно получиться следующее:</p>
32 <h3>Соединяем верстку с кодом (Binding)</h3>
32 <h3>Соединяем верстку с кодом (Binding)</h3>
33 <p>Теперь нужно перейти во фрагмент<strong>MainFragment</strong>и поправить то, что было сгенерировано студией и добавить<strong>binding</strong>, который связывает верстку с кодом.</p>
33 <p>Теперь нужно перейти во фрагмент<strong>MainFragment</strong>и поправить то, что было сгенерировано студией и добавить<strong>binding</strong>, который связывает верстку с кодом.</p>
34 private lateinit var binding: MainFragmentBinding private val viewModel: MainViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = MainFragmentBinding.inflate(inflater, container, false) return binding.root }<p>А оставшийся метод<strong>onActivityCreated</strong>полностью удалите. Все элементы, которые студия подчеркнет, нужно импортировать с помощью подсказок.</p>
34 private lateinit var binding: MainFragmentBinding private val viewModel: MainViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = MainFragmentBinding.inflate(inflater, container, false) return binding.root }<p>А оставшийся метод<strong>onActivityCreated</strong>полностью удалите. Все элементы, которые студия подчеркнет, нужно импортировать с помощью подсказок.</p>
35 <h3>Пишем код</h3>
35 <h3>Пишем код</h3>
36 <p>Теперь давайте перейдем в класс<strong>MainViewModel</strong>и уберем сгенерированные<strong>TODO</strong>. Нам необходимо хранить состояния ячеек (пустая, крестик, нолик) и для этого создадим массив этих состояний потому, что в Gridlayout все ячейки хранятся списком. А для расчета того кто победил, будем переводить индекс в массиве в координаты<strong>[строка:колонка]</strong>, но об этом позже.</p>
36 <p>Теперь давайте перейдем в класс<strong>MainViewModel</strong>и уберем сгенерированные<strong>TODO</strong>. Нам необходимо хранить состояния ячеек (пустая, крестик, нолик) и для этого создадим массив этих состояний потому, что в Gridlayout все ячейки хранятся списком. А для расчета того кто победил, будем переводить индекс в массиве в координаты<strong>[строка:колонка]</strong>, но об этом позже.</p>
37 <p>Создадим<strong>enum class</strong>для хранения состояния. В нем будет храниться картинка и возможность по ней кликать, ведь когда мы уже поставили символ или игра закончилась, то уже кликать по ячейкам нельзя. Напишем следующее в этом же файле в самом низу за пределами `<strong>}`</strong>.</p>
37 <p>Создадим<strong>enum class</strong>для хранения состояния. В нем будет храниться картинка и возможность по ней кликать, ведь когда мы уже поставили символ или игра закончилась, то уже кликать по ячейкам нельзя. Напишем следующее в этом же файле в самом низу за пределами `<strong>}`</strong>.</p>
38 enum class CellState(@DrawableRes val icon: Int, val isClickable: Boolean) { None(0, true), Cross(R.drawable.ic_cross_anim, false), Circle(R.drawable.ic_circle_anim, false) }<p>Создадим поле с массивом этих состояний, я назову ее<strong>matrix</strong>для простоты. Так же добавим поле для хранения текущего состояния, т.е. чей сейчас ход и создадим метод<strong>initGame</strong>для инициализации начального состояния игры при первом запуске приложения (<strong>init</strong>- вызывается при создании класса<strong>MainViewModel</strong>) и при сбросе игры (<strong>onReloadClick</strong>).</p>
38 enum class CellState(@DrawableRes val icon: Int, val isClickable: Boolean) { None(0, true), Cross(R.drawable.ic_cross_anim, false), Circle(R.drawable.ic_circle_anim, false) }<p>Создадим поле с массивом этих состояний, я назову ее<strong>matrix</strong>для простоты. Так же добавим поле для хранения текущего состояния, т.е. чей сейчас ход и создадим метод<strong>initGame</strong>для инициализации начального состояния игры при первом запуске приложения (<strong>init</strong>- вызывается при создании класса<strong>MainViewModel</strong>) и при сбросе игры (<strong>onReloadClick</strong>).</p>
39 class MainViewModel : ViewModel() { private lateinit var matrix: Array&lt;CellState&gt; private lateinit var currentCellState: CellState init { initGame() } private fun initGame() { matrix = Array&lt;CellState&gt;(9) { CellState.None } currentCellState = CellState.Cross } fun onReloadClick() { initGame() } }<p>Создадим метод обработки кликов по ячейкам и пропишем в нем для начала смену хода и сохранение в массиве состояние ячейки. Так же создадим поле с<strong>LiveData</strong>, для того, чтобы на экране смогли отображать кто сейчас ходит.</p>
39 class MainViewModel : ViewModel() { private lateinit var matrix: Array&lt;CellState&gt; private lateinit var currentCellState: CellState init { initGame() } private fun initGame() { matrix = Array&lt;CellState&gt;(9) { CellState.None } currentCellState = CellState.Cross } fun onReloadClick() { initGame() } }<p>Создадим метод обработки кликов по ячейкам и пропишем в нем для начала смену хода и сохранение в массиве состояние ячейки. Так же создадим поле с<strong>LiveData</strong>, для того, чтобы на экране смогли отображать кто сейчас ходит.</p>
40 … private val mCurrentMove = MutableLiveData&lt;CellState&gt;() val currentMove: LiveData&lt;CellState&gt; = mCurrentMove … private fun initGame() { … mCurrentMove.value = currentCellState } fun onCellClick(index: Int) { matrix[index] = currentCellState currentCellState = if (currentCellState == CellState.Cross) CellState.Circle else CellState.Cross mCurrentMove.value = currentCellState }<p>Здесь<strong>mCurrentMove</strong>является изменяемым и он закрыт для обращения из вне класса, а<strong>currentMove</strong>является не изменяемым и он доступен снаружи класса.</p>
40 … private val mCurrentMove = MutableLiveData&lt;CellState&gt;() val currentMove: LiveData&lt;CellState&gt; = mCurrentMove … private fun initGame() { … mCurrentMove.value = currentCellState } fun onCellClick(index: Int) { matrix[index] = currentCellState currentCellState = if (currentCellState == CellState.Cross) CellState.Circle else CellState.Cross mCurrentMove.value = currentCellState }<p>Здесь<strong>mCurrentMove</strong>является изменяемым и он закрыт для обращения из вне класса, а<strong>currentMove</strong>является не изменяемым и он доступен снаружи класса.</p>
41 <p>Давайте подпишемся на изменение хода и будем отображать на экране чей сейчас ход. Для этого перейдем в<strong>MainFragment</strong>, переопределим метод<strong>onViewCreated</strong>и добавим в него следующее.</p>
41 <p>Давайте подпишемся на изменение хода и будем отображать на экране чей сейчас ход. Для этого перейдем в<strong>MainFragment</strong>, переопределим метод<strong>onViewCreated</strong>и добавим в него следующее.</p>
42 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.field.forEachIndexed { index, v -&gt; v.setOnClickListener { viewModel.onCellClick(index) } } viewModel.currentMove.observe(viewLifecycleOwner) { binding.direction.animate() .rotation(if (it == CellState.Cross) 0f else 180f) // .scaleY(if (it == Mark.Cross) 1f else -1f) } }<p>binding.field.<em>forEachIndexed</em>- к каждой вьюшке находящейся в<strong>GridLayout</strong>устанавливаем слушатель на клик.</p>
42 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.field.forEachIndexed { index, v -&gt; v.setOnClickListener { viewModel.onCellClick(index) } } viewModel.currentMove.observe(viewLifecycleOwner) { binding.direction.animate() .rotation(if (it == CellState.Cross) 0f else 180f) // .scaleY(if (it == Mark.Cross) 1f else -1f) } }<p>binding.field.<em>forEachIndexed</em>- к каждой вьюшке находящейся в<strong>GridLayout</strong>устанавливаем слушатель на клик.</p>
43 <p>binding.direction - это наша стрелочка на экране. Соберите проект и запустите его, посмотрите что происходит когда мы кликаем по ячейкам. Попробуйте убрать комментирование со строки со<strong>scaleY</strong>, а строку с<strong>rotation</strong>закомментировать или убрать и посмотреть что получилось.</p>
43 <p>binding.direction - это наша стрелочка на экране. Соберите проект и запустите его, посмотрите что происходит когда мы кликаем по ячейкам. Попробуйте убрать комментирование со строки со<strong>scaleY</strong>, а строку с<strong>rotation</strong>закомментировать или убрать и посмотреть что получилось.</p>
44 <p>Добавим отображение иконке в той ячейке, по которой мы кликнули. Чтобы не перерисовывать весь массив ячеек, а менять значение только той, по которой мы кликнули, будем передавать индекс ячейки и ее новое значение. Для этого добавим еще одно поле<strong>mCellStateByIndex</strong>, в которую будет передаваться индекс и новое состояние ячейки.</p>
44 <p>Добавим отображение иконке в той ячейке, по которой мы кликнули. Чтобы не перерисовывать весь массив ячеек, а менять значение только той, по которой мы кликнули, будем передавать индекс ячейки и ее новое значение. Для этого добавим еще одно поле<strong>mCellStateByIndex</strong>, в которую будет передаваться индекс и новое состояние ячейки.</p>
45 --- MainViewModel.kt --- private val mCellStateByIndex: MutableLiveData&lt;Pair&lt;Int, CellState&gt;&gt; = SingleLiveEvent() val cellStateByIndex: LiveData&lt;Pair&lt;Int, CellState&gt;&gt; = mCellStateByIndex fun onCellClick(index: Int) { matrix[index] = currentCellState mCellStateByIndex.value = Pair(index, currentCellState) … } --- MainFragment.kt --- override fun onViewCreated(view: View, savedInstanceState: Bundle?) { … viewModel.cellStateByIndex.observe(viewLifecycleOwner) { val (index, state) = it with(binding.field.getChildAt(index) as ImageView) { isEnabled = state.isClickable setImageResource(state.icon) } } } --- SingleLiveEvent.kt --- class SingleLiveEvent&lt;T&gt; : MutableLiveData&lt;T&gt;() { private val pending = AtomicBoolean(false) override fun observe(owner: LifecycleOwner, observer: Observer&lt;in T&gt;) { super.observe(owner, Observer { t: T -&gt; if (pending.compareAndSet(true, false)) { observer.onChanged(t) } }) } override fun setValue(value: T) { pending.set(true) super.setValue(value) } }<p>SingleLiveEvent - этот класс служит для того, чтобы при смене конфигурации (например, переворота экрана) не вызывалось снова событие на отрисовку вьюшки. Этот класс нужно создать отдельным файлом.</p>
45 --- MainViewModel.kt --- private val mCellStateByIndex: MutableLiveData&lt;Pair&lt;Int, CellState&gt;&gt; = SingleLiveEvent() val cellStateByIndex: LiveData&lt;Pair&lt;Int, CellState&gt;&gt; = mCellStateByIndex fun onCellClick(index: Int) { matrix[index] = currentCellState mCellStateByIndex.value = Pair(index, currentCellState) … } --- MainFragment.kt --- override fun onViewCreated(view: View, savedInstanceState: Bundle?) { … viewModel.cellStateByIndex.observe(viewLifecycleOwner) { val (index, state) = it with(binding.field.getChildAt(index) as ImageView) { isEnabled = state.isClickable setImageResource(state.icon) } } } --- SingleLiveEvent.kt --- class SingleLiveEvent&lt;T&gt; : MutableLiveData&lt;T&gt;() { private val pending = AtomicBoolean(false) override fun observe(owner: LifecycleOwner, observer: Observer&lt;in T&gt;) { super.observe(owner, Observer { t: T -&gt; if (pending.compareAndSet(true, false)) { observer.onChanged(t) } }) } override fun setValue(value: T) { pending.set(true) super.setValue(value) } }<p>SingleLiveEvent - этот класс служит для того, чтобы при смене конфигурации (например, переворота экрана) не вызывалось снова событие на отрисовку вьюшки. Этот класс нужно создать отдельным файлом.</p>
46 <p>Добавим еще состояние нашей игры (запущена и остановлена), чтобы нельзя было кликать по ячейкам, когда игра завершилась и<strong>LiveData</strong>, в которой будет храниться состояние игры и список состояний ячеек</p>
46 <p>Добавим еще состояние нашей игры (запущена и остановлена), чтобы нельзя было кликать по ячейкам, когда игра завершилась и<strong>LiveData</strong>, в которой будет храниться состояние игры и список состояний ячеек</p>
47 <p>Создадим enum<strong>GameStatus</strong>и напишем его ниже<strong>CellState</strong>.</p>
47 <p>Создадим enum<strong>GameStatus</strong>и напишем его ниже<strong>CellState</strong>.</p>
48 enum class GameStatus { Started, Finished }<p>Добавим поле<strong>mStates</strong>и сделаем несколько изменений.</p>
48 enum class GameStatus { Started, Finished }<p>Добавим поле<strong>mStates</strong>и сделаем несколько изменений.</p>
49 --- MainViewModel.kt --- private val mStates = MutableLiveData&lt;Pair&lt;GameStatus, Array&lt;CellState&gt;&gt;&gt;() val states: LiveData&lt;Pair&lt;GameStatus, Array&lt;CellState&gt;&gt;&gt; = mStates private fun initGame() { … mStates.value = Pair(GameStatus.Started, matrix) } --- MainFragment.kt --- override fun onViewCreated(view: View, savedInstanceState: Bundle?) { … viewModel.states.observe(viewLifecycleOwner) { val (status, matrix) = it matrix.forEachIndexed { index, state -&gt; with(binding.field.getChildAt(index) as ImageView) { setImageResource(state.icon) isEnabled = state.isClickable &amp;&amp; status == GameStatus.Started } } } }<h3>Создаем свою вьюшку и рисуем на ней сетку</h3>
49 --- MainViewModel.kt --- private val mStates = MutableLiveData&lt;Pair&lt;GameStatus, Array&lt;CellState&gt;&gt;&gt;() val states: LiveData&lt;Pair&lt;GameStatus, Array&lt;CellState&gt;&gt;&gt; = mStates private fun initGame() { … mStates.value = Pair(GameStatus.Started, matrix) } --- MainFragment.kt --- override fun onViewCreated(view: View, savedInstanceState: Bundle?) { … viewModel.states.observe(viewLifecycleOwner) { val (status, matrix) = it matrix.forEachIndexed { index, state -&gt; with(binding.field.getChildAt(index) as ImageView) { setImageResource(state.icon) isEnabled = state.isClickable &amp;&amp; status == GameStatus.Started } } } }<h3>Создаем свою вьюшку и рисуем на ней сетку</h3>
50 <p>Создадим новый файл. Для этого в правой части проекта щелкните<strong>ПКМ по ru.otus.tictactoe → New → Kotlin Class/File</strong>и введите название<strong>FieldView</strong>.</p>
50 <p>Создадим новый файл. Для этого в правой части проекта щелкните<strong>ПКМ по ru.otus.tictactoe → New → Kotlin Class/File</strong>и введите название<strong>FieldView</strong>.</p>
51 <p>И напишите следующее в этот файл:</p>
51 <p>И напишите следующее в этот файл:</p>
52 --- FieldView.kt --- import androidx.gridlayout.widget.GridLayout class FieldView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : GridLayout(context, attrs, defStyleAttr) { private var widthF: Float = 0f private var heightF: Float = 0f private var cellWidth: Float = 0f set(value) { field = value halfCellWidth = value / 2 } private var cellHeight: Float = 0f set(value) { field = value halfCellHeight = value / 2 } private var halfCellWidth: Float = 0f private var halfCellHeight: Float = 0f private val strokeSize = resources.getDimension(R.dimen.stroke_size) private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = ResourcesCompat.getColor(context.resources, R.color.penColor, null) strokeWidth = strokeSize strokeCap = Paint.Cap.ROUND } override fun onMeasure(widthSpec: Int, heightSpec: Int) { super.onMeasure(widthSpec, heightSpec) widthF = MeasureSpec.getSize(widthSpec).toFloat() heightF = MeasureSpec.getSize(heightSpec).toFloat() cellWidth = widthF / 3f cellHeight = heightF / 3f } override fun dispatchDraw(canvas: Canvas?) { super.dispatchDraw(canvas) canvas?.run { for (i in 1..2) { drawLine(cellWidth * i, strokeSize, cellWidth * i, heightF - strokeSize, paint) drawLine(strokeSize, cellHeight * i, widthF - strokeSize, cellHeight * i, paint) } } }<p>Обязательно нужно наследоваться от<strong>androidx.gridlayout.widget.GridLayout</strong>.</p>
52 --- FieldView.kt --- import androidx.gridlayout.widget.GridLayout class FieldView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : GridLayout(context, attrs, defStyleAttr) { private var widthF: Float = 0f private var heightF: Float = 0f private var cellWidth: Float = 0f set(value) { field = value halfCellWidth = value / 2 } private var cellHeight: Float = 0f set(value) { field = value halfCellHeight = value / 2 } private var halfCellWidth: Float = 0f private var halfCellHeight: Float = 0f private val strokeSize = resources.getDimension(R.dimen.stroke_size) private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = ResourcesCompat.getColor(context.resources, R.color.penColor, null) strokeWidth = strokeSize strokeCap = Paint.Cap.ROUND } override fun onMeasure(widthSpec: Int, heightSpec: Int) { super.onMeasure(widthSpec, heightSpec) widthF = MeasureSpec.getSize(widthSpec).toFloat() heightF = MeasureSpec.getSize(heightSpec).toFloat() cellWidth = widthF / 3f cellHeight = heightF / 3f } override fun dispatchDraw(canvas: Canvas?) { super.dispatchDraw(canvas) canvas?.run { for (i in 1..2) { drawLine(cellWidth * i, strokeSize, cellWidth * i, heightF - strokeSize, paint) drawLine(strokeSize, cellHeight * i, widthF - strokeSize, cellHeight * i, paint) } } }<p>Обязательно нужно наследоваться от<strong>androidx.gridlayout.widget.GridLayout</strong>.</p>
53 <p>Еще нужно добавить в ресурсы в которых будет указан цвет и размер линии. Для этого<strong>ПКМ по res → New → Android Resource File</strong>.</p>
53 <p>Еще нужно добавить в ресурсы в которых будет указан цвет и размер линии. Для этого<strong>ПКМ по res → New → Android Resource File</strong>.</p>
54 <p>В появившемся окне, в поле<strong>File</strong><strong>name:</strong>написать<strong>values</strong>и нажать<strong>OK</strong>.</p>
54 <p>В появившемся окне, в поле<strong>File</strong><strong>name:</strong>написать<strong>values</strong>и нажать<strong>OK</strong>.</p>
55 <p>Появится новый файл<strong>values.xml</strong>. откройте его и напишите следующее:</p>
55 <p>Появится новый файл<strong>values.xml</strong>. откройте его и напишите следующее:</p>
56 &lt;resources&gt; &lt;color name="penColor"&gt;#454FCE&lt;/color&gt; &lt;dimen name="stroke_size"&gt;8dp&lt;/dimen&gt; &lt;/resources&gt;<p>Теперь ошибки должны исчезнуть.</p>
56 &lt;resources&gt; &lt;color name="penColor"&gt;#454FCE&lt;/color&gt; &lt;dimen name="stroke_size"&gt;8dp&lt;/dimen&gt; &lt;/resources&gt;<p>Теперь ошибки должны исчезнуть.</p>
57 <p>Осталось поменять нашу вьюшку в верстке.</p>
57 <p>Осталось поменять нашу вьюшку в верстке.</p>
58 &lt;ru.otus.tictactoe.views.FieldView android:id="@+id/field" android:layout_width="0dp" android:layout_height="0dp" app:columnCount="3" app:layout_constraintBottom_toBottomOf="@id/bottom" app:layout_constraintDimensionRatio="1:1" app:layout_constraintEnd_toStartOf="@+id/end" app:layout_constraintStart_toStartOf="@+id/start" app:layout_constraintTop_toTopOf="@id/barrier" app:rowCount="3"&gt;<h3>Вычисляем, кто выиграл</h3>
58 &lt;ru.otus.tictactoe.views.FieldView android:id="@+id/field" android:layout_width="0dp" android:layout_height="0dp" app:columnCount="3" app:layout_constraintBottom_toBottomOf="@id/bottom" app:layout_constraintDimensionRatio="1:1" app:layout_constraintEnd_toStartOf="@+id/end" app:layout_constraintStart_toStartOf="@+id/start" app:layout_constraintTop_toTopOf="@id/barrier" app:rowCount="3"&gt;<h3>Вычисляем, кто выиграл</h3>
59 <p>Для начала создадим<strong>sealed class</strong>с возможными вариантами выигрыша (по горизонтали, по вертикали, по диагонали и нет выигрыша).</p>
59 <p>Для начала создадим<strong>sealed class</strong>с возможными вариантами выигрыша (по горизонтали, по вертикали, по диагонали и нет выигрыша).</p>
60 --- FieldView.kt --- private var mWinLineState: WinLineState = WinLineState.None private var winLineRect: RectF? = null override fun onMeasure(widthSpec: Int, heightSpec: Int) { … drawWinLine(mWinLineState) } override fun dispatchDraw(canvas: Canvas?) { super.dispatchDraw(canvas) canvas?.run { … winLineRect?.let { val line: RectF = when (mWinLineState) { is WinLineState.Horizontal -&gt; RectF(it.left, it.top, it.right, it.bottom) is WinLineState.Vertical -&gt; RectF(it.left, it.top, it.right, it.bottom) WinLineState.MainDiagonal -&gt; RectF(it.left, it.top, it.right, it.bottom) WinLineState.ReverseDiagonal -&gt; RectF(it.left, it.top, it.right, it.bottom) WinLineState.None -&gt; it } drawLine(line.left, line.top, line.right, line.bottom, paint) } } } fun drawWinLine(winLineState: WinLineState) { mWinLineState = winLineState winLineRect = when (winLineState) { WinLineState.MainDiagonal -&gt; RectF(strokeSize, strokeSize, widthF - strokeSize, heightF - strokeSize) WinLineState.ReverseDiagonal -&gt; RectF(widthF - strokeSize, strokeSize, strokeSize, heightF - strokeSize) is WinLineState.Horizontal -&gt; { val y = halfCellHeight + cellHeight * winLineState.row RectF(strokeSize, y, widthF - strokeSize, y) } is WinLineState.Vertical -&gt; { val x = halfCellWidth + cellWidth * winLineState.column RectF(x, strokeSize, x, heightF - strokeSize) } WinLineState.None -&gt; null } if (winLineRect == null) return invalidate() } sealed class WinLineState { data class Horizontal(val row: Int) : WinLineState() data class Vertical(val column: Int) : WinLineState() object MainDiagonal : WinLineState() object ReverseDiagonal : WinLineState() object None : WinLineState() }<p>Создадим методы расчета выигрышного положения.</p>
60 --- FieldView.kt --- private var mWinLineState: WinLineState = WinLineState.None private var winLineRect: RectF? = null override fun onMeasure(widthSpec: Int, heightSpec: Int) { … drawWinLine(mWinLineState) } override fun dispatchDraw(canvas: Canvas?) { super.dispatchDraw(canvas) canvas?.run { … winLineRect?.let { val line: RectF = when (mWinLineState) { is WinLineState.Horizontal -&gt; RectF(it.left, it.top, it.right, it.bottom) is WinLineState.Vertical -&gt; RectF(it.left, it.top, it.right, it.bottom) WinLineState.MainDiagonal -&gt; RectF(it.left, it.top, it.right, it.bottom) WinLineState.ReverseDiagonal -&gt; RectF(it.left, it.top, it.right, it.bottom) WinLineState.None -&gt; it } drawLine(line.left, line.top, line.right, line.bottom, paint) } } } fun drawWinLine(winLineState: WinLineState) { mWinLineState = winLineState winLineRect = when (winLineState) { WinLineState.MainDiagonal -&gt; RectF(strokeSize, strokeSize, widthF - strokeSize, heightF - strokeSize) WinLineState.ReverseDiagonal -&gt; RectF(widthF - strokeSize, strokeSize, strokeSize, heightF - strokeSize) is WinLineState.Horizontal -&gt; { val y = halfCellHeight + cellHeight * winLineState.row RectF(strokeSize, y, widthF - strokeSize, y) } is WinLineState.Vertical -&gt; { val x = halfCellWidth + cellWidth * winLineState.column RectF(x, strokeSize, x, heightF - strokeSize) } WinLineState.None -&gt; null } if (winLineRect == null) return invalidate() } sealed class WinLineState { data class Horizontal(val row: Int) : WinLineState() data class Vertical(val column: Int) : WinLineState() object MainDiagonal : WinLineState() object ReverseDiagonal : WinLineState() object None : WinLineState() }<p>Создадим методы расчета выигрышного положения.</p>
61 --- MainViewModel.kt --- private val mWinState : MutableLiveData&lt;WinLineState&gt; = MutableLiveData() val winState: LiveData&lt;WinLineState&gt; = mWinState private fun initGame() { … mWinState.value = WinLineState.None } fun onCellClick(index: Int) { matrix[index] = currentCellState mCellStateByIndex.value = Pair(index, currentCellState) val row = index / 3 val column = index % 3 val state = checkWin(row, column) if (state != WinLineState.None) { mStates.value = Pair(GameStatus.Finished, matrix) mWinState.value = state return } currentCellState = if (currentCellState == CellState.Cross) CellState.Circle else CellState.Cross mCurrentMove.value = currentCellState } private fun checkWin(row: Int, column: Int): WinLineState { //check row if (checkLine { matrix[getIndex(row, it)] == currentCellState }) return WinLineState.Horizontal(row) // check column if (checkLine { matrix[getIndex(it, column)] == currentCellState }) return WinLineState.Vertical(column) if (row == column) { // check main diagonal if (checkLine { matrix[getIndex(it, it)] == currentCellState }) return WinLineState.MainDiagonal } if (row + column == 2) { // check reverse diagonal if (checkLine { matrix[getIndex(it, 2 - it)] == currentCellState }) return WinLineState.ReverseDiagonal } return WinLineState.None } private fun checkLine(function: (Int) -&gt; Boolean): Boolean { for (i in 0..2) { if (!function(i)) return false } return true } private fun getIndex(row: Int, column: Int) = row * 3 + column --- MainFragment.kt --- override fun onViewCreated(view: View, savedInstanceState: Bundle?) { … viewModel.winState.observe(viewLifecycleOwner) { binding.field.drawWinLine(it) } }<h3>Сбрасывание состояния игры</h3>
61 --- MainViewModel.kt --- private val mWinState : MutableLiveData&lt;WinLineState&gt; = MutableLiveData() val winState: LiveData&lt;WinLineState&gt; = mWinState private fun initGame() { … mWinState.value = WinLineState.None } fun onCellClick(index: Int) { matrix[index] = currentCellState mCellStateByIndex.value = Pair(index, currentCellState) val row = index / 3 val column = index % 3 val state = checkWin(row, column) if (state != WinLineState.None) { mStates.value = Pair(GameStatus.Finished, matrix) mWinState.value = state return } currentCellState = if (currentCellState == CellState.Cross) CellState.Circle else CellState.Cross mCurrentMove.value = currentCellState } private fun checkWin(row: Int, column: Int): WinLineState { //check row if (checkLine { matrix[getIndex(row, it)] == currentCellState }) return WinLineState.Horizontal(row) // check column if (checkLine { matrix[getIndex(it, column)] == currentCellState }) return WinLineState.Vertical(column) if (row == column) { // check main diagonal if (checkLine { matrix[getIndex(it, it)] == currentCellState }) return WinLineState.MainDiagonal } if (row + column == 2) { // check reverse diagonal if (checkLine { matrix[getIndex(it, 2 - it)] == currentCellState }) return WinLineState.ReverseDiagonal } return WinLineState.None } private fun checkLine(function: (Int) -&gt; Boolean): Boolean { for (i in 0..2) { if (!function(i)) return false } return true } private fun getIndex(row: Int, column: Int) = row * 3 + column --- MainFragment.kt --- override fun onViewCreated(view: View, savedInstanceState: Bundle?) { … viewModel.winState.observe(viewLifecycleOwner) { binding.field.drawWinLine(it) } }<h3>Сбрасывание состояния игры</h3>
62 <p>Чтобы постоянно не приходилось перезапускать игру, давайте сделаем сброс к начальному состоянию игры. Для этого добавим кнопку в тулбаре.</p>
62 <p>Чтобы постоянно не приходилось перезапускать игру, давайте сделаем сброс к начальному состоянию игры. Для этого добавим кнопку в тулбаре.</p>
63 <p>Добавьте иконку<strong>cached</strong>и назовите ее<strong>ic_reload</strong>. Добавьте ресурс, как это делали с цветом и размером, только в открывшемся окне, поменяйте<strong>Resource type</strong>на<strong>Menu</strong>и в<strong>File name</strong>напишите<strong> menu_reload</strong>. А в ресурсах<strong>strings</strong>добавьте название кнопки</p>
63 <p>Добавьте иконку<strong>cached</strong>и назовите ее<strong>ic_reload</strong>. Добавьте ресурс, как это делали с цветом и размером, только в открывшемся окне, поменяйте<strong>Resource type</strong>на<strong>Menu</strong>и в<strong>File name</strong>напишите<strong> menu_reload</strong>. А в ресурсах<strong>strings</strong>добавьте название кнопки</p>
64 --- res\menu\menu_reload.xml --- &lt;menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"&gt; &lt;item android:id="@+id/action_reload" android:icon="@drawable/ic_reload" android:title="@string/reload" app:showAsAction="always"/&gt; &lt;/menu&gt; --- res\values\strings.xml --- &lt;string name="reload"&gt;reload&lt;/string&gt;<p>Теперь осталось только добавить ее и реализовать обработку этой кнопки. Для этого переходим в<strong>MainFragment</strong>и добавляем следующее.</p>
64 --- res\menu\menu_reload.xml --- &lt;menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"&gt; &lt;item android:id="@+id/action_reload" android:icon="@drawable/ic_reload" android:title="@string/reload" app:showAsAction="always"/&gt; &lt;/menu&gt; --- res\values\strings.xml --- &lt;string name="reload"&gt;reload&lt;/string&gt;<p>Теперь осталось только добавить ее и реализовать обработку этой кнопки. Для этого переходим в<strong>MainFragment</strong>и добавляем следующее.</p>
65 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) inflater.inflate(R.menu.menu_reload, menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.action_reload) { viewModel.onReloadClick() return true } return super.onOptionsItemSelected(item) }<h3>Подводим итоги</h3>
65 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) inflater.inflate(R.menu.menu_reload, menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.action_reload) { viewModel.onReloadClick() return true } return super.onOptionsItemSelected(item) }<h3>Подводим итоги</h3>
66 <p>На этом все. Можно запустить проект и посмотреть что получилось. Исходник можно взять из github (<a>https://github.com/shustreek/TicTacToe</a>). Там я добавил анимацию рисования крестика и нолика, а также линии при выигрыше.</p>
66 <p>На этом все. Можно запустить проект и посмотреть что получилось. Исходник можно взять из github (<a>https://github.com/shustreek/TicTacToe</a>). Там я добавил анимацию рисования крестика и нолика, а также линии при выигрыше.</p>
67  
67