[Java, Разработка мобильных приложений, Разработка под Android, Kotlin] Android — ViewPager2 — заменяем фрагменты на лету (программно)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Вдруг вам надо листать фрагменты через ViewPager2 и при этом подменять их динамически. Например, чтобы уйти "глубже" - пользователь из фрагмента "Главные настройки" переходит во фрагмент "Выбор языка". При этом новый фрагмент должен отобразиться на месте главного фрагмента. А потом пользователь еще и захочет вернуться обратно...Дано
- ViewPager2
- список Fragment-ов (например, разделы анкеты или многостраничный раздел настроек)
- Kotlin (Java), Android собственно
ЗадачаНеобходимо заменять фрагменты динамически - на месте n-ого фрагмента должен появится другой. При этом все остальные страницы не должны терять своего состояния.Пример: Есть три фрагмента. Первый - список любимых тарелок, второй - экран с сообщениями, третий - настройки. Пользователь на первом экране выбирает тарелку -> меняем первый фрагмент на фрагмент "Информация о тарелке"Пользователь листает свайпами или выбирает через TabLayout третий экран - "Настройки", выбирает раздел "Выбор языка" -> меняем третий фрагмент на фрагмент "Выбор языка"
Теперь пользователь, не выходя из выбора языка, листает до первого фрагмента. Он должен увидеть информацию о тарелке, а не список любимых тарелок. Итак, пытаемся сдружить ArrayMap и ViewPager2.Результат Будет такой - YouTubewww.youtube.comДисклеймер (не читать, вода)Пишу, потому что найти хороших решений по этой задаче не смог, хотя вроде пытался усердно.Код местами специально упрощен - не люблю, когда в статьях добавляют десятки строк, не касающихся решения описываемой задачи, только ради соблюдения всех правил. Так что: строковые ресурсы - вынести, вью модели для фрагментов - добавить, пару уровней абстракций - ну вы понялиВесь проект - https://github.com/IDolgopolov/ViewPager2_FragmentReplacerИногда буду использовать слово "страницы" - имею ввиду позицию, на которой отображается фрагмент во ViewPager. Например, "Список любимых тарелок", "Экран сообщений" и "Настройки" - три страницы. Фрагмент "Выбор языка" заменяет третью страницу.РешениеСначала ничего интересного, просто для общего ознакомленияВерсткаmain_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager_2"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"/>
</LinearLayout>
fragment_one.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Замени меня"
android:id="@+id/b_replace_fragment_one"
android:layout_centerInParent="true"
android:layout_marginHorizontal="20dp"/>
</RelativeLayout>
fragment_deep.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Верни назад, блин"
android:id="@+id/b_back"
android:layout_margin="20dp"
/>
</RelativeLayout>
fragment_two.xml, fragment_three.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Фрагмент 3"
android:gravity="center"
xmlns:android="http://schemas.android.com/apk/res/android" />
ИнтерфейсыОпишем функции, которые нам понадобятся для смены фрагментов
interface FragmentReplacer {
fun replace(position: Int, newFragment: BaseFragment, isNotify: Boolean = true)
fun replaceDef(position: Int, isNotify: Boolean = true) : BaseFragment
}
Все фрагменты, добавляемые во ViewPagerAdapter (MyViewPager2Adapter описан ниже), будут наследоваться от BaseFragment. Определим в нем переменные для хранения:
- pageId - уникальный номер страницы. Может быть любым числом, но главное - уникальным и большим, чем у вас позиций страниц (pageId > PAGE_COUNT - 1), иначе будут баги из-за метода getItemId()
- pagePos - номер странице, на которой будет отображаться фрагмент во ViewPager (начиная с 0, естественно)
- fragmentReplacer - ссылка на ViewPagerAdapter (MyViewPager2Adapter реализует FragmentReplacer)
abstract class BaseFragment(private val layoutId: Int) : Fragment() {
val pageId = Random.nextLong(2021, 2021*3)
var pagePos = -1
protected lateinit var fragmentReplacer: FragmentReplacer
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(layoutId, container, false)
}
fun setPageInfo(pagePos: Int, fragmentReplacer: FragmentReplacer) {
this.pagePos = pagePos
this.fragmentReplacer = fragmentReplacer
}
}
Связь активити и ViewPager2
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//MyViewPager2Adapter - смотреть разбор далее
view_pager_2.adapter = MyViewPager2Adapter(this)
TabLayoutMediator(tab_layout, view_pager_2) { tab, position ->
tab.text = when(position) {
0 -> "Первый"
1 -> "Второй"
2 -> "Крайний"
else -> throw IllegalStateException()
}
}.attach()
}
}
Адаптер для ViewPager2 - всё здесь В этом классе будут реализован интерфейс FragmentReplacer и переопределены несколько классов FragmentStateAdapter. Это и позволит менять фрагменты на лету.Весь код класса, который ниже разбираю подробно
class MyViewPager2Adapter(container: FragmentActivity) : FragmentStateAdapter(container),
FragmentReplacer {
companion object {
private const val PAGE_COUNT = 3
}
private val mapOfFragment = ArrayMap<Int, BaseFragment>()
override fun replace(position: Int, newFragment: BaseFragment, isNotify: Boolean) {
mapOfFragment[position] = newFragment
if (isNotify)
notifyItemChanged(position)
}
override fun replaceDef(position: Int, isNotify: Boolean): BaseFragment {
val fragment = when (position) {
0 -> FragmentOne()
1 -> FragmentTwo()
2 -> FragmentThree()
else -> throw IllegalStateException()
}
fragment.setPageInfo(
pagePos = position,
fragmentReplacer = this
)
replace(position, fragment, isNotify)
return fragment
}
override fun createFragment(position: Int): Fragment {
return mapOfFragment[position] ?: replaceDef(position, false)
}
override fun containsItem(itemId: Long): Boolean {
var isContains = false
mapOfFragment.values.forEach {
if (it.pageId == itemId) {
isContains = true
return@forEach
}
}
return isContains
}
override fun getItemId(position: Int) =
mapOfFragment[position]?.pageId ?: super.getItemId(position)
override fun getItemCount() = PAGE_COUNT
}
В первую очередь переопределим четыре метода FragmentStateAdapter:
//ключ - позиция страницы
//значения - фрагмент, отображаемый в данный момент на этой странице
private val mapOfFragment = ArrayMap<Int, BaseFragment>()
//возвращаем количество страниц (в нашем примере - 3)
override fun getItemCount() = PAGE_COUNT
/*
эта функция вызывается, когда пользователь впервые открывает страницу
или был вызван notifyItemChanged(position)
если фрагмент еще не был создан (mapOfFragment[position] == null),
то заменяем фрагмент на этой странице фрагментов по умолчанию
*/
override fun createFragment(position: Int): Fragment {
return mapOfFragment[position] ?: replaceDef(position, false)
}
/*
у каждого нашего фрагмента есть свой id, отдаем его адаптеру
если фрагмент на этой позиции еще не был создан, можно вернуть любую чипуху,
не совпадающую со всеми существующими id
p.s. super.getItemId { return position } по умолчанию
*/
override fun getItemId(position: Int) =
mapOfFragment[position]?.pageId ?: super.getItemId(position)
override fun containsItem(itemId: Long): Boolean {
var isContains = false
mapOfFragment.values.forEach { if (it.pageId == itemId) {
isContains = true
return@forEach
} }
return isContains
}
Теперь две функции, которые позволяют подменять фрагменты
override fun replace(position: Int, newFragment: BaseFragment, isNotify: Boolean) {
//указываем фрагменту, на какой странице он отображается
newFragment.setPageInfo(
pagePos = position,
fragmentReplacer = this
)
mapOfFragment[position] = newFragment
//isNotify = true всегда, кроме вызова replace() из функции createFragment()
//иначе приложение будет крашится, так как еще ничего не отображено
if (isNotify)
notifyItemChanged(position)
}
//здесь указываем базовые фрагменты - фрагменты, отображаемые по умолчанию
//пригодится, когда захотим отобразить страницы в первый раз
//или вернуться к дефолтной странице после того, как закончим все дела на замененной
override fun replaceDef(position: Int, isNotify: Boolean): BaseFragment {
val fragment = when (position) {
0 -> FragmentOne()
1 -> FragmentTwo()
2 -> FragmentThree()
else -> throw IllegalStateException()
}
replace(position, fragment, isNotify)
return fragment
}
Пример использованияВсе готово - у каждого фрагмента есть ссылка на адаптер, значит есть возможность заменить фрагмент на любой странице, а самое главное (наиболее используемое на практике) - есть возмжность заменить самого себя.Например, фрагмент, по умолчанию, отображаемый на первой странице:FragmentOne.kt
class FragmentOne : BaseFragment(R.layout.fragment_one) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//по нажатию кнопки
b_replace_fragment_one.setOnClickListener {
//создаем фрагмент, который заменит этот фрагмент
val fragmentDeep = FragmentDeep()
//заменяем
fragmentReplacer.replace(this.pagePos, fragmentDeep)
}
}
}
FragmentDeep.kt
class FragmentDeep : BaseFragment(R.layout.fragment_deep) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
b_back.setOnClickListener {
//по нажатию кнопки заменяем текущий фрагмент
//на дефолтный: 0 -> FragmentOne()
fragmentReplacer.replaceCurrentToDef()
//или
//fragmentReplacer.replace(pagePos, FragmentOne())
//или
//fragmentReplacer.replace(0, FragmentOne())
}
}
}
Мысли в слухАккуратно, если фрагменты тяжелые, - надо задуматься об очищении mapOfFragment.Возможно, стоит хранить Class<BaseFragment> вместо BaseFragment. Но тогда придется инициализировать их каждый раз в createFragment(). Меньше памяти, больше времени. Что думаете? На правах...dendolg.ru - портфолио, контакты для сотрудничества (после переезда на GithubPages браузер иногда банит https протокол, а иногда не банит - разбираюсь)t.me/dolgopolovd - блог с нулем подписчиков, в котором рассуждаю о мобильном и промышленном дизайне и иногда отвлекаюсь от программирования
===========
Источник:
habr.com
===========
Похожие новости:
- [Java, API] ExtendScript: Работа со слоями
- [Высокая производительность, Разработка мобильных приложений, IT-инфраструктура, Разработка под Android] Как мы ускорили запуск приложения Dropbox для Android на 30 % (перевод)
- [JavaScript, Монетизация мобильных приложений, Natural Language Processing, Голосовые интерфейсы] Аккредитация разработчиков навыков для ассистентов Салют и первый аккредитованный партнёр
- [Java] 11 вопросов на собеседовании по Spring Boot, которые заставляют задуматься (перевод)
- [Программирование, Java] REST API с использованием Spring Security и JWT
- [Программирование, ReactJS] Изучение методов кэширования в React (перевод)
- [JavaScript, Программирование, HTML, TensorFlow] Отслеживание лиц в реальном времени в браузере с использованием TensorFlow.js. Часть 4 (перевод)
- [Клиентская оптимизация, Разработка мобильных приложений, Микросервисы] Платформа по работе с обращениями клиентов в «Пятёрочке»
- [Программирование, Разработка мобильных приложений, Разработка под Android, Kotlin] Влияние data-классов на вес приложения
- [Разработка мобильных приложений, Управление продуктом, IT-компании] Ничего не будет: ни приложений, ни мобильных сайтов — одни супераппы
Теги для поиска: #_java, #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_viewpager2, #_fragmet, #_kotlin, #_java, #_android, #_listat (Листать), #_swipe, #_svajpat (Свайпать), #_programmnaja_zamena (Программная замена), #_dinamicheskaja_zamena (Динамическая замена), #_java, #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_razrabotka_pod_android (
Разработка под Android
), #_kotlin
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:40
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Вдруг вам надо листать фрагменты через ViewPager2 и при этом подменять их динамически. Например, чтобы уйти "глубже" - пользователь из фрагмента "Главные настройки" переходит во фрагмент "Выбор языка". При этом новый фрагмент должен отобразиться на месте главного фрагмента. А потом пользователь еще и захочет вернуться обратно...Дано
Теперь пользователь, не выходя из выбора языка, листает до первого фрагмента. Он должен увидеть информацию о тарелке, а не список любимых тарелок. Итак, пытаемся сдружить ArrayMap и ViewPager2.Результат Будет такой - YouTubewww.youtube.comДисклеймер (не читать, вода)Пишу, потому что найти хороших решений по этой задаче не смог, хотя вроде пытался усердно.Код местами специально упрощен - не люблю, когда в статьях добавляют десятки строк, не касающихся решения описываемой задачи, только ради соблюдения всех правил. Так что: строковые ресурсы - вынести, вью модели для фрагментов - добавить, пару уровней абстракций - ну вы понялиВесь проект - https://github.com/IDolgopolov/ViewPager2_FragmentReplacerИногда буду использовать слово "страницы" - имею ввиду позицию, на которой отображается фрагмент во ViewPager. Например, "Список любимых тарелок", "Экран сообщений" и "Настройки" - три страницы. Фрагмент "Выбор языка" заменяет третью страницу.РешениеСначала ничего интересного, просто для общего ознакомленияВерсткаmain_activity.xml <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <com.google.android.material.tabs.TabLayout android:id="@+id/tab_layout" android:layout_width="match_parent" android:layout_height="wrap_content" /> <androidx.viewpager2.widget.ViewPager2 android:id="@+id/view_pager_2" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"/> </LinearLayout> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Замени меня" android:id="@+id/b_replace_fragment_one" android:layout_centerInParent="true" android:layout_marginHorizontal="20dp"/> </RelativeLayout> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Верни назад, блин" android:id="@+id/b_back" android:layout_margin="20dp" /> </RelativeLayout> <?xml version="1.0" encoding="utf-8"?>
<TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="Фрагмент 3" android:gravity="center" xmlns:android="http://schemas.android.com/apk/res/android" /> interface FragmentReplacer {
fun replace(position: Int, newFragment: BaseFragment, isNotify: Boolean = true) fun replaceDef(position: Int, isNotify: Boolean = true) : BaseFragment }
abstract class BaseFragment(private val layoutId: Int) : Fragment() {
val pageId = Random.nextLong(2021, 2021*3) var pagePos = -1 protected lateinit var fragmentReplacer: FragmentReplacer override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(layoutId, container, false) } fun setPageInfo(pagePos: Int, fragmentReplacer: FragmentReplacer) { this.pagePos = pagePos this.fragmentReplacer = fragmentReplacer } } class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //MyViewPager2Adapter - смотреть разбор далее view_pager_2.adapter = MyViewPager2Adapter(this) TabLayoutMediator(tab_layout, view_pager_2) { tab, position -> tab.text = when(position) { 0 -> "Первый" 1 -> "Второй" 2 -> "Крайний" else -> throw IllegalStateException() } }.attach() } } class MyViewPager2Adapter(container: FragmentActivity) : FragmentStateAdapter(container),
FragmentReplacer { companion object { private const val PAGE_COUNT = 3 } private val mapOfFragment = ArrayMap<Int, BaseFragment>() override fun replace(position: Int, newFragment: BaseFragment, isNotify: Boolean) { mapOfFragment[position] = newFragment if (isNotify) notifyItemChanged(position) } override fun replaceDef(position: Int, isNotify: Boolean): BaseFragment { val fragment = when (position) { 0 -> FragmentOne() 1 -> FragmentTwo() 2 -> FragmentThree() else -> throw IllegalStateException() } fragment.setPageInfo( pagePos = position, fragmentReplacer = this ) replace(position, fragment, isNotify) return fragment } override fun createFragment(position: Int): Fragment { return mapOfFragment[position] ?: replaceDef(position, false) } override fun containsItem(itemId: Long): Boolean { var isContains = false mapOfFragment.values.forEach { if (it.pageId == itemId) { isContains = true return@forEach } } return isContains } override fun getItemId(position: Int) = mapOfFragment[position]?.pageId ?: super.getItemId(position) override fun getItemCount() = PAGE_COUNT } //ключ - позиция страницы
//значения - фрагмент, отображаемый в данный момент на этой странице private val mapOfFragment = ArrayMap<Int, BaseFragment>() //возвращаем количество страниц (в нашем примере - 3) override fun getItemCount() = PAGE_COUNT /* эта функция вызывается, когда пользователь впервые открывает страницу или был вызван notifyItemChanged(position) если фрагмент еще не был создан (mapOfFragment[position] == null), то заменяем фрагмент на этой странице фрагментов по умолчанию */ override fun createFragment(position: Int): Fragment { return mapOfFragment[position] ?: replaceDef(position, false) } /* у каждого нашего фрагмента есть свой id, отдаем его адаптеру если фрагмент на этой позиции еще не был создан, можно вернуть любую чипуху, не совпадающую со всеми существующими id p.s. super.getItemId { return position } по умолчанию */ override fun getItemId(position: Int) = mapOfFragment[position]?.pageId ?: super.getItemId(position) override fun containsItem(itemId: Long): Boolean { var isContains = false mapOfFragment.values.forEach { if (it.pageId == itemId) { isContains = true return@forEach } } return isContains } override fun replace(position: Int, newFragment: BaseFragment, isNotify: Boolean) {
//указываем фрагменту, на какой странице он отображается newFragment.setPageInfo( pagePos = position, fragmentReplacer = this ) mapOfFragment[position] = newFragment //isNotify = true всегда, кроме вызова replace() из функции createFragment() //иначе приложение будет крашится, так как еще ничего не отображено if (isNotify) notifyItemChanged(position) } //здесь указываем базовые фрагменты - фрагменты, отображаемые по умолчанию //пригодится, когда захотим отобразить страницы в первый раз //или вернуться к дефолтной странице после того, как закончим все дела на замененной override fun replaceDef(position: Int, isNotify: Boolean): BaseFragment { val fragment = when (position) { 0 -> FragmentOne() 1 -> FragmentTwo() 2 -> FragmentThree() else -> throw IllegalStateException() } replace(position, fragment, isNotify) return fragment } class FragmentOne : BaseFragment(R.layout.fragment_one) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) //по нажатию кнопки b_replace_fragment_one.setOnClickListener { //создаем фрагмент, который заменит этот фрагмент val fragmentDeep = FragmentDeep() //заменяем fragmentReplacer.replace(this.pagePos, fragmentDeep) } } } class FragmentDeep : BaseFragment(R.layout.fragment_deep) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) b_back.setOnClickListener { //по нажатию кнопки заменяем текущий фрагмент //на дефолтный: 0 -> FragmentOne() fragmentReplacer.replaceCurrentToDef() //или //fragmentReplacer.replace(pagePos, FragmentOne()) //или //fragmentReplacer.replace(0, FragmentOne()) } } } =========== Источник: habr.com =========== Похожие новости:
Разработка мобильных приложений ), #_razrabotka_pod_android ( Разработка под Android ), #_kotlin |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:40
Часовой пояс: UTC + 5