Русский
Русский
English
Статистика
Реклама

Программная замена

Android ViewPager2 заменяем фрагменты на лету (программно)

05.03.2021 16:08:13 | Автор: admin

Вдруг вам надо листать фрагменты через ViewPager2 и при этом подменять их динамически. Например, чтобы уйти "глубже" - пользователь из фрагмента "Главные настройки" переходит во фрагмент "Выбор языка". При этом новый фрагмент должен отобразиться на месте главного фрагмента. А потом пользователь еще и захочет вернуться обратно...

Дано

  • ViewPager2

  • список Fragment-ов (например, разделы анкеты или многостраничный раздел настроек)

  • Kotlin (Java), Android собственно

Задача

Необходимо заменять фрагменты динамически - на месте n-ого фрагмента должен появится другой. При этом все остальные страницы не должны терять своего состояния.

Пример:

Есть три фрагмента. Первый - список любимых тарелок, второй - экран с сообщениями, третий - настройки.

Пользователь на первом экране выбирает тарелку -> меняем первый фрагмент на фрагмент "Информация о тарелке"

Пользователь листает свайпами или выбирает через TabLayout третий экран - "Настройки", выбирает раздел "Выбор языка" -> меняем третий фрагмент на фрагмент "Выбор языка"

Теперь пользователь, не выходя из выбора языка, листает до первого фрагмента. Он должен увидеть информацию о тарелке, а не список любимых тарелок.

Итак, пытаемся сдружить ArrayMap и ViewPager2.

Результат

Будет такой

Дисклеймер (не читать, вода)

Пишу, потому что найти хороших решений по этой задаче не смог, хотя вроде пытался усердно.

Код местами специально упрощен - не люблю, когда в статьях добавляют десятки строк, не касающихся решения описываемой задачи, только ради соблюдения всех правил. Так что: строковые ресурсы - вынести, вью модели для фрагментов - добавить, пару уровней абстракций - ну вы поняли

Весь проект - https://github.com/IDolgopolov/ViewPager2_FragmentReplacer

Иногда буду использовать слово "страницы" - имею ввиду позицию, на которой отображается фрагмент во ViewPager. Например, "Список любимых тарелок", "Экран сообщений" и "Настройки" - три страницы. Фрагмент "Выбор языка" заменяет третью страницу.

Решение

Сначала ничего интересного, просто для общего ознакомления

Верстка

main_activity.xml
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:tools="http://personeltest.ru/away/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://personeltest.ru/away/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://personeltest.ru/away/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://personeltest.ru/away/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 - блог с нулем подписчиков, в котором рассуждаю о мобильном и промышленном дизайне и иногда отвлекаюсь от программирования

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru