فهرست منبع

tv + fragments

Евгений Колесников 3 سال پیش
والد
کامیت
b34c750322
13فایلهای تغییر یافته به همراه622 افزوده شده و 4 حذف شده
  1. 309 0
      articles/android_tv.md
  2. 305 2
      articles/fragments.md
  3. BIN
      img/fragments_02.jfif
  4. BIN
      img/fragments_03.jfif
  5. BIN
      img/fragments_05.png
  6. BIN
      img/tv_01.png
  7. BIN
      img/tv_01.webp
  8. BIN
      img/tv_02.png
  9. BIN
      img/tv_03.png
  10. BIN
      img/tv_04.png
  11. BIN
      img/tv_05.png
  12. BIN
      img/tv_06.png
  13. 8 2
      readme.md

+ 309 - 0
articles/android_tv.md

@@ -0,0 +1,309 @@
+# Android TV
+
+>Содрано [отсюда](https://habr.com/ru/post/316260/), [отсюда](https://skillbox.ru/media/code/razrabotka_pod_android_tv/) и [отсюда](https://skillbox.ru/media/code/razrabotka_pod_android_tv_part2/)
+
+[Первая](https://habr.com/ru/post/316260/) статья довольно древняя (2016 год), попробуем реализовать в 2022
+
+Данная лекция познакомит вас с разработкой простого приложения для Android TV.
+
+Так как интерфейс приложений для телефонов и Android TV имеет существенные различия, то мы должны создать интерфейс приложения, подходящий для взаимодействия на TV. Например, нам следует создавать приложения с которыми можно взаимодействовать, используя только клавиши `—` `↑` `↓` `→` `←`. В реализации такого интерфейса нам может помочь библиотека **LeanbackSupport**, позволяющая вполне легко создавать UI, который будет удобен при работе с приложениями на Android TV.
+
+## Коротко о библиотеке Leanback
+
+Библиотека Leanback представляет собой набор шаблонов экранов с различными функциональными особенностями. Есть экраны для отображения списков, карточек контента, диалогов и т. д. Эти экраны обрабатывают все пользовательские переходы между элементами и анимации, а также имеют довольно обширный функционал для построения простых приложений “из коробки”. Идеология данной библиотеки заключается в том, что все приложения на ее основе должны быть похожи в плане пользования. Не нужно думать, узнает ли пользователь о том что можно прокрутить вниз? Узнает, потому что он уже пользовался сотнями однотипных приложений.
+
+## Создание проекта в Android Studio
+
+Запустив Android Studio, необходимо создать новый проект. При создании выбрать платформу TV и указать минимальную версию SDK. Android Studio предложит нам создать "Blank Activity", его и создадим (в оригинальной статье предлагают руками добавлять классы, файлы разметки и фрагменты, но это долго, проще выкинуть лишнее из рабочего проекта) 
+
+![](../img/tv_01.png)
+
+Можем запустить и увидеть что проект работает и что-то показывает:
+
+![](../img/tv_02.png)
+
+Рассмотрим структуру созданного проекта:
+
+### Манифест
+
+>Менять мы пока ничего не будем, просто ознакомимся с особенностями проекта для TV
+
+* По-умолчанию уже добавлено разрешение на работу с интернетом
+
+    ```xml
+    <uses-permission android:name="android.permission.INTERNET" />
+    ```
+
+* Заданы требования к целевому устройству
+
+    Может отсутствовать (не требуется) тачскрин:
+
+    ```xml
+    <uses-feature
+        android:name="android.hardware.touchscreen"
+        android:required="false" />
+    ```
+
+    Приложение должно запускаться только на Android TV. Если вы разрабатываете приложение не только для TV, то вам следует установить значение *false*:
+
+    ```xml
+    <uses-feature
+        android:name="android.software.leanback"
+        android:required="true" />
+    ```
+
+* При объявлении Activity мы указываем в *intent-filter* в теге *category*, что Activity должна запускаться на Android TV.
+
+    ```xml
+    <intent-filter>
+        <action android:name="android.intent.action.MAIN" />
+
+        <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
+    </intent-filter>
+    ```    
+
+### MainActivity
+
+Первым запускается как обычно **MainActivity**
+
+```kt
+class MainActivity : FragmentActivity() 
+{
+    override fun onCreate(savedInstanceState: Bundle?) 
+    {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+        if (savedInstanceState == null) {
+            getSupportFragmentManager().beginTransaction()
+                .replace(R.id.main_browse_fragment, MainFragment())
+                .commitNow()
+        }
+    }
+}
+```
+
+`activity_main.xml`:
+
+```xml
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout 
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main_browse_fragment"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity"
+    tools:deviceIds="tv"
+    tools:ignore="MergeRootFrame" />
+```    
+
+По коду мы видим, что активность наследуется от **FragmentActivity** и при запуске содержимое `main_browse_fragment` заменяется тем, что сгенерирует класс **MainFragment**. (Файла разметки `main_browse_fragment` в проекте нет, похоже он реализован в библиотеке **Leanback**)
+
+### MainFragment
+
+Класс **MainFragment** наследуется от класса `BrowseSupportFragment()`, а про его структуру хорошо расписано во [второй](https://skillbox.ru/media/code/razrabotka_pod_android_tv/) статье. 
+
+**BrowseFragment** — это фрагмент, предназначенный для создания экрана со списками элементов и заголовками. Его структура выглядит следующим образом:
+
+![](../img/tv_01.webp)
+
+Рассмотрим каждый из элементов:
+
+* **TitleView** - это контейнер с элементами. Он нужен для брендирования приложения (текст и логотип в правом верхнем углу), а также для добавления кнопки поиска. Чтобы кнопка поиска была видимой, ей необходимо установить слушателя. Это можно сделать, вызвав метод *setOnSearchClickedListener* у фрагмента.
+
+* **HeadersFragment & RowsFragment** - **HeadersFragment** — список заголовков в левой части экрана и **RowsFragment** — контейнер для контента в правой части экрана. Эти фрагменты работают в связке, и **BrowseFragment** делегирует им отрисовку элементов своего адаптера.
+
+Таким образом, в методе *onActivityCreated* класса **MainFragment** настраиваются вышеперечисленные контейнеры. Можем убедиться в этом закомментировав функции, которые что-то делают и посмотрев на результат:
+
+```kt
+override fun onActivityCreated(savedInstanceState: Bundle?) {
+    Log.i(TAG, "onCreate")
+    super.onActivityCreated(savedInstanceState)
+
+    // prepareBackgroundManager()
+    // setupUIElements()
+    // loadRows()
+    // setupEventListeners()
+}
+```
+
+![](../img/tv_03.png)
+
+#### Метод *prepareBackgroundManager*
+
+Инициализирует *lateinit* свойства класса и пока ничего не показывает
+
+#### Метод *setupUIElements*
+
+Устанавливает заголовок в правом верхнем углу и цвет для левой панели
+
+![](../img/tv_04.png)
+
+#### Метод *loadRows* 
+
+Формирует всё содержимое, разберёмся с ним подробнее:
+
+1. `val list = MovieList.list` - Получаем список фильмов
+
+    Свойство *list* класса **MovieList** возвращает массив объектов **Movie**, реализацию можно не разбирать, мы в реальном проекте всё-равно этот список будем получать динамически из АПИ.
+
+1. `val rowsAdapter = ArrayObjectAdapter(ListRowPresenter())` - Создаётся **стандартный** адаптер (массива объектов) использующий **стандартный** класс для вывода элементов в виде "строки".
+
+1. `val cardPresenter = CardPresenter()` - создание самописанного (реализующего абстрактный класс) экземпляра представления карточки фильма
+
+1. Цикл перебора категорий (`for (i in 0 until NUM_ROWS)`)
+
+    >Тут количество категорий "прибито гвоздями", в реальном проекте мы будем перебирать список имеющихся категорий
+
+    ```kt
+    if (i != 0) {
+        Collections.shuffle(list)
+    }
+    ```
+
+    Эта конструкция меняет содержимое коллекции в случайном порядке для всех строк, кроме первой. Если этого не сделать, то выведутся одинаковые строки (строки формируются из одного и того же списка фильмов):
+
+    ![](../img/tv_05.png)
+
+    Далее создаётся адаптер для строки, которая будет отображаться в RowFragment (правая часть экрана) и заполняется фильмами из списка
+
+    ```kt
+    val listRowAdapter = ArrayObjectAdapter(cardPresenter)
+    for (j in 0 until NUM_COLS) {
+        listRowAdapter.add(list[j % 5])
+    }
+    ```
+
+    В конце цикла создаётся заголовок для категории и заголовок вместе со списком фильмов добавляется в адаптер строк *rowsAdapter*
+
+    ```kt
+    val header = HeaderItem(
+        i.toLong(), 
+        MovieList.MOVIE_CATEGORY[i])
+    rowsAdapter.add(
+        ListRow(
+            header, 
+            listRowAdapter))
+    ```
+
+1. Добавление строки с настройками
+
+    ![](../img/tv_06.png)
+
+    Строка находится в общем адаптере (вертикальный список), но реализация элементов списка у неё другая:
+
+    ```kt
+    // формируем заголовок
+    val gridHeader = HeaderItem(
+        NUM_ROWS.toLong(), 
+        "PREFERENCES")
+
+    // здесь создаётся представление для элемента настроек
+    val mGridPresenter = GridItemPresenter()
+
+    // на его основе делается адаптер для строки элементов
+    val gridRowAdapter = ArrayObjectAdapter(mGridPresenter)
+
+    // и в эту строку добавляются пункты меню настроек
+    gridRowAdapter.add(
+        resources.getString(R.string.grid_view))
+    gridRowAdapter.add(
+        getString(R.string.error_fragment))
+    gridRowAdapter.add(
+        resources.getString(R.string.personal_settings))
+    ```
+
+    В итоге в основной адаптер добавляется строка с настройками
+
+    ```kt
+    rowsAdapter.add(
+        ListRow(gridHeader, gridRowAdapter))
+    ```
+
+    И готовый адаптер назначается адаптеру нашего класса (его нет в нашей реализации, он объявлен в родительском классе)
+
+    ```kt
+    adapter = rowsAdapter
+    ```
+
+#### Метод *setupEventListeners*
+
+Настраивает события, которые будет обрабатывать приложение:
+
+```kt
+setOnSearchClickedListener {
+    Toast.makeText(
+            activity!!, 
+            "Implement your own in-app search",
+            Toast.LENGTH_LONG)
+        .show()
+}
+```
+
+Назначение этого события **включает** иконку поиска. Реализации поиска пока никакой нет.
+
+
+```kt
+onItemViewClickedListener = ItemViewClickedListener()
+```
+
+*onItemViewClickedListener* - обработчик события клика по карточке фильма
+
+```kt
+onItemViewSelectedListener = ItemViewSelectedListener()
+```
+
+*onItemViewSelectedListener* - обработчик клика по элементу вертикального списка (категории)
+
+## Модификация под свои нужды
+
+### Загрузочный экран
+
+Не знаю, будет ли это на демо-экзамене, но для демонстрации работы с обычными активностями андроид добавим загрузочный экран:
+
+1. Добавьте пустую активность (в контекстном меню пакета `New -> Activity -> Empty Activity`)
+
+1. В манифесте перенесите тег `<intent-filter>` из активности *.MainActivity* в *.LaunchActivity* (я ещё скопировал атрибут `android:screenOrientation="landscape"`)
+
+1. В `drawable` ресурсы добавьте картинку для заставки и в файл разметки добавьте **ImageView** с этой картинкой
+
+1. В классе **LaunchActivity**: 
+
+    * поменяйте родителя, вместо **AppCompatActivity** оставьте просто **Activity** (**AppCompatActivity** не реализован в Android TV)
+
+    * Реализуйте таймер и переход на главное окно:
+
+        ```kt
+        class LaunchActivity : Activity() {
+            lateinit var that: Activity
+            override fun onCreate(savedInstanceState: Bundle?) {
+                super.onCreate(savedInstanceState)
+                setContentView(R.layout.activity_launch)
+                that = this
+                Timer().schedule(3000L){
+                    that.runOnUiThread {
+                        startActivity(Intent(that, MainActivity::class.java))
+                    }
+                }
+            }
+        }
+        ```
+
+        Тут мне лень было писать метод для обработки события таймера, поэтому контекст (*this*) я сохранил в отдельную переменную (внутри лямбда-функции таймера контекст другой)
+
+Всё замечательно работает!!!
+
+### Получение списка фильмов и категорий
+
+Что-бы не терять время просто так, поместим этот код в загрузочную активность (заодно проверим как тут работает класс **Application**)
+
+
+<!-- https://tv.withgoogle.com/# -->
+
+<!-- https://habr.com/ru/company/ivi/blog/351084/ -->
+
+<!-- https://corochann.com/introduction-android-tv-application-hands-on-tutorial-1-32/
+
+https://corochann.com/construction-of-browsefragment-android-tv-application-hands-on-tutorial-2-71/
+
+https://www.kodeco.com/20747024-android-tv-getting-started -->

+ 305 - 2
articles/fragments.md

@@ -58,7 +58,7 @@ Android Studio представляет готовый шаблон для до
 ></LinearLayout>
 >```
 
-Теперь мы можем поместить готовый фрагмент в основное окно. В разметку файла `activity_mail.xml` добавьте контейнер FragmentContainerView из палитры элементов
+Теперь мы можем поместить готовый фрагмент в основное окно. В разметку файла `activity_main.xml` добавьте контейнер **FragmentContainerView** из палитры элементов
 
 ![](../img/fragments_03.png)
 
@@ -66,7 +66,310 @@ Android Studio представляет готовый шаблон для до
 
 ![](../img/fragments_04.png)
 
-Не забудьте установить позицию и размер контейнера
+Не забудьте установить позицию и размер контейнера.
+
+По сути **FragmentContainerView** представляет объект **View**, который расширяет класс **FrameLayout** и предназначен специально для работы с фрагментами. Собственно кроме фрагментов он больше ничего содержать не может.
+
+Его атрибут `android:name` указывает на имя класса фрагмента, который будет использоваться.
 
 Фрагмент успешно добавлен и приложение запускается и работает, но надо учитывать, что теперь логика работы с элементами фрагмента должна быть реализована в классе фрагмента (**ContentFragment**)
 
+### Добавление фрагмента в коде
+
+Кроме определения фрагмента в xaml-файле интерфейса мы можем добавить его динамически в activity.
+
+```kt
+override fun onCreate(savedInstanceState: Bundle?) {
+    super.onCreate(savedInstanceState)
+    setContentView(R.layout.activity_main)
+
+    // добавление фрагмента
+    if (savedInstanceState == null) {
+        supportFragmentManager
+            .beginTransaction()
+            .add(R.id.fragmentContainerView,
+                SecondFragment::class.java, null)
+            .commit()
+    }
+}
+```
+
+Свойство *supportFragmentManager* является геттером для метода *getSupportFragmentManager()*, возвращающего объект **FragmentManager**, который управляет фрагментами.
+
+Объект FragmentManager с помощью метода *beginTransaction()* создает объект **FragmentTransaction**.
+
+**FragmentTransaction** выполняет два метода: *add()* и *commit()*. Метод *add()* добавляет фрагмент: `add(R.id.fragmentContainerView, SecondFragment::class.java, null)` - первым аргументом передается ресурс разметки, в который надо добавить фрагмент (это определенный в `activity_main.xml` элемент `androidx.fragment.app.FragmentContainerView`), вторым агрументом передаётся класс фрагмента, который надо добавить, третьим аргументом может передаваться *bundle* (параметры). И метод *commit()* подтвержает и завершает операцию добавления.
+
+Итоговый результат такого добавления фрагмента будет тем же, что и при явном определении фрагмента через элемент **FragmentContainerView** в разметке интерфейса.
+
+>Если вы назначили фрагмент при добавлении **FragmentContainerView**, то фрагменты наложатся друг на друга, надо в файле `activity_main.xml` убрать у элемента **FragmentContainerView** атрибут `android:name`:
+>
+>```xml
+><androidx.fragment.app.FragmentContainerView
+>    android:id="@+id/fragmentContainerView"
+>    android:name="ru.yotc.fragments.BlankFragment"
+>    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+>```
+
+## Жизненный цикл фрагментов
+
+Каждый класс фрагмента наследуется от базового класса Fragment и имеет свой жизненный цикл, состоящий из ряда этапов:
+
+![](../img/fragments_05.png)
+
+## Динамическая работа с фрагментами
+
+Основано на [этой](https://startandroid.ru/ru/uroki/vse-uroki-spiskom/175-urok-105-android-3-fragments-dinamicheskaja-rabota.html) статье.
+
+Размещать статические фрагменты мы уже умеем. Но гораздо интереснее работать с ними динамически. Система позволяет нам добавлять, удалять и заменять фрагменты друг другом. При этом мы можем сохранять все эти манипуляции в BackStack и кнопкой Назад отменять. В общем, все удобно и красиво.
+
+>Подробнее про BackStack можно почитать [тут](https://habr.com/ru/post/186434/)
+
+Создадим простое приложение с двумя фрагментами, которое будет уметь:
+
+- добавлять первый фрагмент
+- удалять первый фрагмент
+- заменять первый фрагмент вторым фрагментом
+- переключать режим сохранения в BackStack операций с фрагментами
+
+Используем приложение с фрагментами из предыдущего раздела.
+
+1. В классах фрагментов можно выкинуть все, кроме метода **onCreateView**
+
+1. Рисуем разметку *activity_main.xml*
+
+    ```xml
+    <?xml version="1.0" encoding="utf-8"?>
+    <androidx.constraintlayout.widget.ConstraintLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        tools:context=".MainActivity">
+
+        <LinearLayout
+            android:id="@+id/linearLayout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent">
+
+            <Button
+                android:id="@+id/btnAdd"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:onClick="onClick"
+                android:text="Добавить"/>
+
+            <Button
+                android:id="@+id/btnRemove"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:onClick="onClick"
+                android:text="Удалить"/>
+
+            <Button
+                android:id="@+id/btnReplace"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:onClick="onClick"
+                android:text="Заменить"/>
+
+            <CheckBox
+                android:id="@+id/chbStack"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="Добавить в Back Stack"/>
+
+        </LinearLayout>
+
+        <FrameLayout
+            android:id="@+id/frgmCont"
+            android:layout_width="match_parent"
+            android:layout_height="500dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/linearLayout"/>
+    </androidx.constraintlayout.widget.ConstraintLayout>
+    ```
+
+    Три кнопки для добавления, удаления и замены фрагментов. Чекбокс для включения использования BackStack. И FrameLayout – это контейнер, в котором будет происходить вся работа с фрагментами. Он должен быть типа ViewGroup. А элементы Fragment, которые мы использовали в предыдущем примере для размещения фрагментов, нам не нужны для динамической работы. 
+
+1. Класс MainActivity.kt:
+
+    ```kt
+    class MainActivity : AppCompatActivity() {
+
+        lateinit var frag1: Fragment1
+        lateinit var frag2: Fragment2
+
+        override fun onCreate(savedInstanceState: Bundle?) {
+            super.onCreate(savedInstanceState)
+            setContentView(R.layout.activity_main)
+
+            frag1 = Fragment1()
+            frag2 = Fragment2()
+        }
+
+        fun onClick(v: View){
+            val fTrans = supportFragmentManager.beginTransaction()
+            when(v.id){
+                R.id.btnAdd -> fTrans.add(
+                    R.id.frgmCont, frag1)
+                R.id.btnRemove -> fTrans.remove(frag1)
+                R.id.btnReplace -> fTrans.replace(
+                    R.id.frgmCont, frag2)
+            }
+            if (chbStack.isChecked) fTrans.addToBackStack(null)
+            fTrans.commit()
+        }   
+    }
+    ```
+
+    В конструкторе создаем фрагменты.
+
+    В **onClick** мы получаем менеджер фрагментов с помощью геттера *supportFragmentManager*. Этот объект является основным для работы с фрагментами. Далее, чтобы добавить/удалить/заменить фрагмент, нам необходимо использовать транзакции. Они аналогичны транзакциям в БД, где мы открываем транзакцию, производим операции с БД, выполняем *commit*. Здесь мы открываем транзакцию, производим операции с фрагментами (добавляем, удаляем, заменяем), выполняем *commit*.
+
+    Итак, мы получили **FragmentManager** и открыли транзакцию методом *beginTransaction*. Далее определяем, какая кнопка была нажата:
+
+    если **Add**, то вызываем метод add, в который передаем id контейнера (тот самый FrameLayout из activity_main.xml) и объект фрагмента. В итоге, в контейнер будет помещен Fragment1
+
+    если **Remove**, то вызываем метод remove, в который передаем объект фрагмента, который хотим убрать. В итоге, фрагмент удалится с экрана.
+
+    если **Replace**, то вызываем метод replace, в который передаем id контейнера и объект фрагмента. В итоге, из контейнера удалится его текущий фрагмент (если он там есть) и добавится фрагмент, указанный нами.
+
+    Далее проверяем чекбокс. Если он включен, то добавляем транзакцию в BackStack. Для этого используем метод addToBackStack. На вход можно подать строку-тэг. Я передаю null.
+
+    Ну и вызываем commit, транзакция завершена.
+
+
+Т.е. все достаточно просто и понятно. Скажу еще про пару интересных моментов.
+
+Я в этом примере выполнял всего одну операцию в каждой транзакции. Но, разумеется, их может быть больше.
+
+Когда мы удаляем фрагмент и не добавляем транзакцию в BackStack, то фрагмент уничтожается. Если же транзакция добавляется в BackStack, то, при удалении, фрагмент не уничтожается (onDestroy не вызывается), а останавливается (onStop).
+
+В качестве самостоятельной работы: попробуйте немного изменить приложение и добавлять в один контейнер сразу два фрагмента.
+
+## Доступ к фрагменту из Activity
+
+Разберемся, как получить доступ к фрагменту из Activity. Для этого у **FragmentManager** есть метод *findFragmentById*, который на вход принимает id компонента fragment (если фрагмент статический) или id контейнера (если динамический).
+
+В основном классе реализуем обработчик клика по кнопке btnFind:
+
+```kt
+btnFind.setOnClickListener {
+    val frag1 = supportFragmentManager.findFragmentById(R.id.fragment1)
+    frag1?.view?.findViewById<TextView>(R.id.textView)?.text = "Access to Fragment 1 from Activity"
+
+    val frag2 = supportFragmentManager.findFragmentById(R.id.frgmCont)
+    frag2?.view?.findViewById<TextView>(R.id.textView)?.text = "Access to Fragment 2 from Activity"
+}
+```
+
+Используем метод findFragmentById. В первом случае на вход передаем id компонента fragment, т.к. Fragment1 у нас размещен именно так. При поиске Fragment2 указываем id контейнера, в который этот фрагмент был помещен. В результате метод findFragmentById возвращает нам объект Fragment.
+
+Далее мы получаем доступ к его View с помощью геттера view, находим в нем TextView и меняем текст.
+
+Запускаем приложение и проверяем:
+
+![](/img/fragments_02.jfif)
+
+Тексты в фрагментах обновились. Тем самым из Activity мы достучались до фрагментов и их компонентов.
+
+На всякий случай проговорю одну вещь из разряда «Спасибо кэп!». Если посмотреть на код MainActivity, то можно заметить, что работая с frag2 в методе onCreate и с frag2 в методе onClick мы работаем с текущим фрагментом Fragment2. Это так и есть. Оба frag2 в итоге будут ссылаться на один объект. Так что, если вы динамически добавили фрагмент, то у вас уже есть ссылка на него, и искать его через findFragmentById вам уже не обязательно.
+
+## Доступ к Activity из фрагмента 
+
+Теперь попробуем из фрагмента поработать с Activity. Для этого фрагмент имеет метод getActivity (геттер *activity*).
+
+Давайте перепишем обработчик кнопки в первом фрагменте. Будем менять текст кнопки btnFind.
+
+```kt
+button.setOnClickListener {
+    //Log.d(LOG_TAG, "Button click in Fragment1")
+    activity?.findViewById<Button>(R.id.btnFind)?.text = "Access from Fragment1"
+}
+```
+
+![](/img/fragments_03.jfif)
+
+Проверяем - все работает.
+
+## Взаимодействие между фрагментами
+
+Одна activity может использовать несколько фрагментов, например, с одной стороны список, а с другой - детальное описание выбранного элемента списка. В такой конфигурации activity использует два фрагмента, которые между собой должны взаимодействовать. Рассмотрим базовые принципы взаимодействия фрагментов в приложении.
+
+>Подробнее [тут](https://metanit.com/java/android/8.5.php)
+
+## Обработка в Activity события из фрагмента 
+
+Рассмотрим следующий механизм: фрагмент генерирует некое событие и ставит Activity обработчиком.
+
+Например, в Activity есть два фрагмента. Первый – список заголовков статей. Второй – отображает содержимое статьи, выбранной в первом. Мы нажимаем на заголовок статьи в первом фрагменте и получаем содержимое во втором. В этом случае, цель первого фрагмента – передать в Activity информацию о том, что выбран заголовок. А Activity дальше уже сама решает, что делать с этой информацией. Если, например, приложение запущено на планшете в горизонтальной ориентации, то можно отобразить содержимое статьи во втором фрагменте. Если же приложение запущено на смартфоне, то экран маловат для двух фрагментов и надо запускать отдельное Activity со вторым фрагментом, чтобы отобразить статью.
+
+Фишка тут в том, что первому фрагменту неинтересны все эти терзания Activity. Фрагмент – обособленный модуль. Его дело - проинформировать, что выбрана статья такая-то. Ему не надо искать второй фрагмент и работать с ним – это дело Activity.
+
+Фрагмент должен сообщить в Activity, что выбрана статья. Для этого он будет вызывать некий метод в Activity. И лучший способ тут – это использовать интерфейс, который мы опишем в фрагменте и который затем будет реализован в Activity. Схема известная и распространенная. Давайте реализуем. В нашем приложении никаких статей нет, поэтому будем просто передавать произвольную строку из второго фрагмента в Activity. А Activity уже будет отображать эту строку в первом фрагменте.
+
+1. Описываем интерфейс FragmentEventListener, содержащий метод someEvent, который на вход получает строку (в примере этот интерфейс написан в классе Fragment2, но это не принципиально)
+
+    ```kt
+    interface FragmentEventListener {
+       fun someEvent(s: String)
+    }
+    ```
+1. Переписываем класс Fragment2 (читайте комментарии):
+
+    ```kt
+    class Fragment2: Fragment() {
+        private val LOG_TAG = "kei"
+
+        // добавляем переменную, которая будет хранить ссылку на интерфейс
+        lateinit var eventListener: FragmentEventListener
+
+        override fun onAttach(context: Context) {
+            super.onAttach(context)
+            // при присоединении к Activity проверяем, реализует ли она нужный интерфейс
+            if(activity is FragmentEventListener)
+                // запоминаем ссылку на объект
+                eventListener = activity as FragmentEventListener
+            else
+                // выбрасываем исключение
+                throw ClassCastException("${activity.toString()} must implement FragmentEventListener")
+        }
+
+        override fun onCreateView(
+            inflater: LayoutInflater,
+            container: ViewGroup?,
+            savedInstanceState: Bundle?
+        ): View? {
+            val v = inflater.inflate(R.layout.fragment2, null)
+
+            val button = v.findViewById<Button>(R.id.button)
+
+            button.setOnClickListener {
+                // при клике на кнопку вызываем метод интерфейса
+                eventListener.someEvent("Test text from Fragment 2")
+            }
+
+            return v
+        }
+    }
+    ```
+
+1. Редактируем основной класс: он должен реализовывать интерфейс *FragmentEventListener* и по событию от второго фрагмента менять текст первого:
+
+    ```kt
+    class MainActivity : AppCompatActivity(), FragmentEventListener {
+
+        override fun someEvent(s: String) {
+            val frag1 = supportFragmentManager.findFragmentById(R.id.fragment1)
+            frag1?.view?.findViewById<TextView>(R.id.textView)?.text = s
+        }
+        ...
+    ```

BIN
img/fragments_02.jfif


BIN
img/fragments_03.jfif


BIN
img/fragments_05.png


BIN
img/tv_01.png


BIN
img/tv_01.webp


BIN
img/tv_02.png


BIN
img/tv_03.png


BIN
img/tv_04.png


BIN
img/tv_05.png


BIN
img/tv_06.png


+ 8 - 2
readme.md

@@ -307,13 +307,19 @@ https://office-menu.ru/uroki-sql Уроки SQL
 
 1. [Wear OS](./articles/wear_os.md)
 
-<!-- 1. [Использование регулярных выражений для разбора данных в любом формате](./articles/regex.md) -->
-
 1. [Разбор заданий прошлых лет](./articles/f6_demo_1.md)
 
+1. [Фрагменты](./articles/fragments.md)
+
+1. [Android TV](./articles/android_tv.md)
+
+<!-- https://corochann.com/introduction-android-tv-application-hands-on-tutorial-1-32/#Android_TV_application_development_introduction -->
 
 <!-- 
 
+1. [Использование регулярных выражений для разбора данных в любом формате](./articles/regex.md)
+
+
 TODO
 
 BottomNavigation + frames