<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Android Developer</title><generator>teletype.in</generator><description><![CDATA[Мои личные заметки и решения для тех или иных задач на Android/Java]]></description><link>https://teletype.in/@android_developer?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=android_developer</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/android_developer?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/android_developer?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Tue, 21 Apr 2026 13:21:02 GMT</pubDate><lastBuildDate>Tue, 21 Apr 2026 13:21:02 GMT</lastBuildDate><item><guid isPermaLink="true">https://teletype.in/@android_developer/8K9QCkH1Nrr</guid><link>https://teletype.in/@android_developer/8K9QCkH1Nrr?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=android_developer</link><comments>https://teletype.in/@android_developer/8K9QCkH1Nrr?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=android_developer#comments</comments><dc:creator>android_developer</dc:creator><title>Эффективная работа с BottomSheet</title><pubDate>Wed, 03 Nov 2021 18:41:41 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/56/ce/56cec900-59d0-4e0c-a1e5-68833c5402e6.png"></media:content><description><![CDATA[<img src="https://lh3.googleusercontent.com/yCWRSDd6LjJJYovpQ-8ixmJnHa56UXWxlVEkHskpypfmo4XW2a0EczckNLbUVs--IAh8NLLsgfPxZUsHZuFMvyYclZ_QXuoFuQnN=w1064-v0"></img>В данной заметке пойдёт речь об организации эффективной работы с BottomSheet  компонентами на уровне Activity. ]]></description><content:encoded><![CDATA[
  <p id="P3oA">В данной заметке пойдёт речь об организации эффективной работы с <code>BottomSheet </code> компонентами на уровне <code>Activity</code>. </p>
  <p id="dvWO">Заметка представляет собой квинтэссенцию личного опыта, боли и пройденных шагов для поиска наиболее эффективного решения различных задач. </p>
  <h2 id="cJph">Sheets: Bottom</h2>
  <p id="1X03">Согласно Material Design, <code>BottomSheet</code> представляет собой компоненты с дополнительным контентом, привязанные к нижней части экрана.</p>
  <figure id="6NKc" class="m_original">
    <img src="https://lh3.googleusercontent.com/yCWRSDd6LjJJYovpQ-8ixmJnHa56UXWxlVEkHskpypfmo4XW2a0EczckNLbUVs--IAh8NLLsgfPxZUsHZuFMvyYclZ_QXuoFuQnN=w1064-v0" width="1064" />
    <figcaption>Источник: https://material.io/components/sheets-bottom</figcaption>
  </figure>
  <p id="viW3">В общем случае работа с <code>BottomSheet</code> не представляет собой ничего сложного. Этой теме посвящено огромное количество статей. Ознакомиться с тем, как подключить и использовать данные компоненты, вы можете, например, <a href="https://habr.com/ru/post/309200/" target="_blank">тут</a>. </p>
  <h2 id="azJo">Проблема</h2>
  <p id="HSJH">Во время работы над проектом передо мной возникли следующие вопросы:</p>
  <ul id="1AGG">
    <li id="M942">Как эффективно организовать работу с различными <code>BottomSheet</code> фрагментами для всех <code>Activity</code> проекта;</li>
    <li id="Qr90">Инкапсулировать шаблонный код работы с <code>BottomSheet</code> в одном месте;</li>
    <li id="nJID">Упростить отслеживание состояний <code>BottomSheet</code>.</li>
  </ul>
  <p id="u8KK">Это является проблемой, когда имеется множество <code>Activity</code>, которые должны одинаково работать с компонентами <code>BottomSheet</code>, обрабатывать различные их состояния, реагировать на нажатие кнопки Back, показывать/скрывать дополнительный контент, иметь некоторое начальное состояние для всех <code>BottomSheet</code>, объявленных в <code>Activity</code>.</p>
  <h2 id="gzrG">BottomSheetActivity</h2>
  <p id="yTpO">Первым делом было принято решение наследовать все <code>Activity</code>, которые будут иметь <code>BottomSheetBehaviour</code>, от специального класса, который инкапсулирует весь обслуживающий код для <code>BottomSheet</code> в себе.</p>
  <pre id="noDI" data-lang="java">public abstract class BottomSheetActivity 
    extends NavigateActivity implements Dependence, BottomSheet {
     
    private BottomSheetBehavior bottomSheetBehavior;
    //Активный, открытый в текущий момент bottomSheet fragment
    private Fragment activeFragment;
    //Список bottomSheet fragments, которые должны быть заблокироаны для смахивания жестом
    private HashMap&lt;Integer, Boolean&gt; lockedViewsForDragAndDrop;
}</pre>
  <p id="FoLr">Класс <code>BottomSheetActivity</code> является базовым для <code>Activity</code>, который содержит в <code>BottomSheet</code> компоненты.</p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="RADv">Пояснения extends классов и интерфейсов:</p>
    <p id="OC7v">NavigateActivity - класс, инкапсулирующий навигационный код для всех Activity и некоторые общие для всех Activity компоненты.</p>
    <p id="2sMm">Dependence - интерфейс для работы с подписками/отписками на события различных компонентов.</p>
    <p id="ypdX">BottomSheet - интерфейс с методами регистрации BottomSheet фрагментов.</p>
  </section>
  <p id="slM6">Интерфейс <code>Dependence</code> обязывает класс реализовать следующие методы:</p>
  <pre id="Zlko" data-lang="java">    @Override @CallSuper
    public void createDependencies() {
        bottomSheetBehavior.addBottomSheetCallback(onBottomSheetCallback);
        registerFragments();
    }

    @Override @CallSuper
    public void deleteDependencies() {
        bottomSheetBehavior.removeBottomSheetCallback(onBottomSheetCallback);
    }</pre>
  <p id="zCYg"><code>onBottomSheetCallback</code> является реализацией <code>BottomSheetBehavior.BottomSheetCallback()</code> и выполняет некоторые действия по отображению/сокрытию, а также работе с swipe по <code>BottomSheet</code> фрагменту. Реализация <code>onBottomSheetCallback</code> приведена ниже.</p>
  <pre id="X4QG" data-lang="java">    protected final BottomSheetBehavior.BottomSheetCallback onBottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() {
        @Override
        public void onStateChanged(@NonNull View view, int i) {
            switch (i) {
                case BottomSheetBehavior.STATE_COLLAPSED:
                case BottomSheetBehavior.STATE_HIDDEN:
                    hideFragment(activeFragment);
                    break;
                case BottomSheetBehavior.STATE_EXPANDED:
                    break;
                case BottomSheetBehavior.STATE_DRAGGING:
                    Boolean value = lockedViewsForDragAndDrop.get(view.getId());
                    if (value != null &amp;&amp; value) {
                        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
                    }
                    break;
                case BottomSheetBehavior.STATE_SETTLING:
                    break;
            }
        }</pre>
  <p id="ANtK">Чтобы в момент загрузки <code>Activity</code>, <code>BottomSheet</code> компонент был скрыт и не отображался на экране, в методе <code>onCreate у Activity</code>, которая будет наследовать данный класс, вызывается метод <code>hideBottomSheetView</code>, которому передается <code>View</code> с <code>BottomSheetBehavior</code>.</p>
  <pre id="uF1U" data-lang="java">    protected void hideBottomSheetView(View bottomSheetView) {
        bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetView);
        bottomSheetBehavior.setHideable(true);
        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
    }</pre>
  <p id="zu0H">Главная и единственная задача этого метода - спрятать <code>bottomSheetView</code>. Если их несколько, этот метод вызывается для каждого <code>bottomSheetView</code>.</p>
  <h2 id="zNOL">Регистрация фрагментов в FragmentManager</h2>
  <p id="75ix">Используемые в <code>Activity</code> фрагменты необходимо зарегистрировать в <code>FragmentManager</code> для их скрытия и отображения. Интерфейс <code>BottomSheet</code>, который упоминался выше, обязывает <code>Activity</code> реализовать метод <code>registerFragments</code>. Реализация метода приведена ниже.</p>
  <pre id="7W45" data-lang="java">@Override
    public void registerFragments() {
        fragment = new ImplFragment();

        addFragmentToFragmentManager(binder.activityFragment.getRoot(), fragment, false);
    }</pre>
  <p id="oQ7Y">Метод <code>addFragmentToFragmentManager</code> реализован в классе <code>BottomSheetActivity</code>:</p>
  <pre id="5abf" data-lang="java">    protected void addFragmentToFragmentManager(View rootView, Fragment fragment, boolean isLockedForDragAndDrop) {
        getSupportFragmentManager().beginTransaction()
                .add(rootView.getId(), fragment)
                .hide(fragment)
                .commit();

        lockedViewsForDragAndDrop.put(rootView.getId(), isLockedForDragAndDrop);
    }</pre>
  <p id="a5oq"><code>HashMap&lt;Integer, Boolean&gt; lockedViewsForDragAndDrop</code> выполняет функцию регистрации фрагментов, которым разрешены/запрещены операции <code>swipe</code>.</p>
  <h2 id="XfG0">Сокрытие и отображение BottomSheet фрагментов</h2>
  <p id="8O7W">Для управления отображением <code>BottomSheet</code> фрагментов в <code>BottomSheetActivity</code> имеется вспомогательный метод <code>showHideFragment</code>.</p>
  <p id="H682">Входными параметрами являются <code>Fragment</code> и <code>View</code>, которые требуется показать или скрыть. Решение о том, что требуется сделать, принимается внутри.</p>
  <pre id="Eugi" data-lang="java">    protected void showHideFragment(Fragment target, View view) {
        if (bottomSheetBehavior.getState() != BottomSheetBehavior.STATE_HIDDEN) {
            bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
        } else {
            showFragment(target, view);
        }
    }</pre>
  <p id="Ewhi">Если <code>Fragment target (и его view)</code> открыт в данный момент, его необходимо закрыть. Если закрыт - отобразить.</p>
  <p id="0mFE">Реализация метода <code>showFragment</code>:</p>
  <pre id="8cpm" data-lang="java">    private void showFragment(Fragment target, View view) {
        getSupportFragmentManager().beginTransaction()
                .show(target)
                .commit();

        bottomSheetBehavior = BottomSheetBehavior.from(view);
        bottomSheetBehavior.setHideable(true);
        bottomSheetBehavior.setSkipCollapsed(true);
        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
        activeFragment = target;

        //Хак для решения проблем изменяющегося размера контента внутри bottomSheetBehavior (например listView загружает данные).
        activeFragment.getView().getViewTreeObserver().removeOnGlobalLayoutListener(onGlobalLayoutListener);
        activeFragment.getView().getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
    }</pre>
  <p id="uE0P">Метод <code>hideFragment</code>, который вызывается при событии <code>onBottomSheetCallback.onStateChanged - BottomSheetBehavior.STATE_HIDDEN</code>, которое описано выше.</p>
  <pre id="wzeS" data-lang="java">    private void hideFragment(Fragment target) {
        getSupportFragmentManager().beginTransaction()
                .hide(activeFragment)
                .commit();

        if (activeFragment == target) { activeFragment = null; }
    }</pre>
  <h3 id="qOMr">Решение проблемы для случая, когда BottomSheet имеет динамический размер (данные)</h3>
  <p id="BQ7r">Вы могли заметить выше комментарий, указывающий на решение данной проблемы. Она состоит в следующем:</p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="iHp4">В момент вызова onCreateView у Fragment, он может изменить размер (требование данных), что приведет к тому, что Fragment не будет полностью развернут на экране.</p>
  </section>
  <p id="2bTb">Решение состоит в дополнительной установке высоты <code>Fragment</code>. Эту задачу решает <code>onGlobalLayoutListener</code>, на который <code>Fragment</code> подписывается в момент вызова <code>showFragment</code>, исполняется и затем сразу же отписывается от него.</p>
  <pre id="kk1G" data-lang="java">    private ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            if(activeFragment == null || activeFragment.getView() == null) {
                return;
            }
            int h = activeFragment.getView().getHeight();
            bottomSheetBehavior.setPeekHeight(h);
            bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
            activeFragment.getView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }
    };</pre>
  <p id="YIi0">Обработка закрытия <code>BottomSheet Fragment</code> при событии <code>Back</code>.</p>
  <p id="lCdM">Когда пользователь нажимает кнопку <code>Back</code>, и <code>BottomSheet Fragment</code> открыт, он будет закрыт, при этом событие <code>Back</code> не должно привести к выходу из экрана. Событие <code>Back</code> проверяет, открыт ли <code>Fragment</code> (если открыт, закрывает его) и если <code>Fragment</code> не открыт, то позволяет уйти из <code>Activity</code>.</p>
  <p id="7hig">Метод <code>backClickHideBottomSheetView</code>, определенный в <code>BottomSheetActivity</code> и вызываемый по <code>callback onBackPressed</code>, выполняет эту функцию.</p>
  <pre id="Ko3s" data-lang="java">    protected boolean backClickHideBottomSheetView() {
        if(bottomSheetBehavior.getState() != BottomSheetBehavior.STATE_HIDDEN) {
            bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
            return true;
        }

        return false;
    }</pre>
  <p id="CnBh">Вызов метода <code>backClickHideBottomSheetView</code> в <code>onBackPressed Activity</code>.</p>
  <pre id="suVk" data-lang="java">   @Override
    public void onBackPressed() {
        if(backClickHideBottomSheetView()) return;

        getNavigationManager().back(this);
    }</pre>
  <h2 id="sYTO">Заключение</h2>
  <p id="B2tc">Реализация класса BottomSheetActivity и инкапсуляция логики работы с BottomSheetBehavior и множеством фрагментов позволила переместить шаблонный код в одно место и использовать его на множестве Activity. </p>
  <p id="qwmU">При дальнейшем усложнении логики работы с BottomSheet фрагментами необходимо модифицировать только этот класс.</p>
  <p id="n2SK">В Activity, которая будет наследоваться от BottomSheetActivity, требуется только несколько действий:</p>
  <ul id="SBUc">
    <li id="GDwo">Первичное сокрытие BottomSheetView&#x27;s, которые присутствуют на Activity;</li>
    <li id="1Muq">Реализация метода registerFragments для регистрации фрагментов в FragmentManager;</li>
    <li id="BeCY">Проверка необходимости закрыть BottomSheet фрагмент перед выходом с Activity.</li>
    <li id="kL06">Вызов метода showHideFragment, когда требуется отобразить или скрыть Fragment.</li>
  </ul>
  <p id="X7e3">Полный код класса BottomSheetActivity приведён ниже. </p>
  <p id="eEsy">Спасибо, что прочли заметку.</p>
  <pre id="PObu" data-lang="java">public abstract class BottomSheetActivity extends NavigateActivity implements Dependence, BottomSheet {

    private BottomSheetBehavior bottomSheetBehavior;
    private Fragment activeFragment;
    private HashMap&lt;Integer, Boolean&gt; lockedViewsForDragAndDrop;

    public BottomSheetActivity() {
        lockedViewsForDragAndDrop = new HashMap&lt;&gt;();
    }

    @Override @CallSuper
    public void createDependencies() {
        bottomSheetBehavior.addBottomSheetCallback(onBottomSheetCallback);
        registerFragments();
    }

    @Override @CallSuper
    public void deleteDependencies() {
        bottomSheetBehavior.removeBottomSheetCallback(onBottomSheetCallback);
    }

    protected void hideBottomSheetView(View bottomSheetView) {
        bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetView);
        bottomSheetBehavior.setHideable(true);
        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
    }

    protected void addFragmentToFragmentManager(View rootView, Fragment fragment, boolean isLockedForDragAndDrop) {
        getSupportFragmentManager().beginTransaction()
                .add(rootView.getId(), fragment)
                .hide(fragment)
                .commit();

        lockedViewsForDragAndDrop.put(rootView.getId(), isLockedForDragAndDrop);
    }

    protected void showHideFragment(Fragment target, View view) {
        if (bottomSheetBehavior.getState() != BottomSheetBehavior.STATE_HIDDEN) {
            bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
        } else {
            showFragment(target, view);
        }
    }

    protected boolean backClickHideBottomSheetView() {
        if(bottomSheetBehavior.getState() != BottomSheetBehavior.STATE_HIDDEN) {
            bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
            return true;
        }

        return false;
    }

    protected final BottomSheetBehavior.BottomSheetCallback onBottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() {
        @Override
        public void onStateChanged(@NonNull View view, int i) {
            switch (i) {
                case BottomSheetBehavior.STATE_COLLAPSED:
                case BottomSheetBehavior.STATE_HIDDEN:
                    hideFragment(activeFragment);
                    break;
                case BottomSheetBehavior.STATE_EXPANDED:
                    break;
                case BottomSheetBehavior.STATE_DRAGGING:
                    Boolean value = lockedViewsForDragAndDrop.get(view.getId());
                    if (value != null &amp;&amp; value) {
                        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
                    }
                    break;
                case BottomSheetBehavior.STATE_SETTLING:
                    break;
            }
        }

        @Override
        public void onSlide(@NonNull View view, float v) {

        }
    };

    private void hideFragment(Fragment target) {
        getSupportFragmentManager().beginTransaction()
                .hide(activeFragment)
                .commit();

        if (activeFragment == target) { activeFragment = null; }
    }

    private void showFragment(Fragment target, View view) {
        getSupportFragmentManager().beginTransaction()
                .show(target)
                .commit();

        bottomSheetBehavior = BottomSheetBehavior.from(view);
        bottomSheetBehavior.setHideable(true);
        bottomSheetBehavior.setSkipCollapsed(true);
        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
        activeFragment = target;

        //Хак для решения проблем изменяющегося размера контента внутри bottomSheetBehavior (например listView загружает данные).
        activeFragment.getView().getViewTreeObserver().removeOnGlobalLayoutListener(onGlobalLayoutListener);
        activeFragment.getView().getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
    }

    private ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            if(activeFragment == null || activeFragment.getView() == null) {
                return;
           }
            int h = activeFragment.getView().getHeight();
            bottomSheetBehavior.setPeekHeight(h);
            bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
            activeFragment.getView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }
    };
}</pre>

]]></content:encoded></item></channel></rss>