Как мы учили кнопку плавать

    Сразу, наверное, стоит предупредить, что мы сделали плавающую кнопку немножко по-своему и собственно до релиза библиотеки, реализующей материальный дизайн как таковой. Но обо всем по порядку.

    В нашей концепции (приложений, сайта surfingbird.ru) активно используется такое понятие как «сёрф». Сервис генерирует для пользователя набор релевантных его интересам статей и переход к следующей, рекомендуемой статье мы называем «сёрфом». После очередного редизайна мы кнопку сёрф потеряли (раньше она была сверху, в акшенбаре, и до неё было не очень удобно тянуться), чем пользователи были очень возмущены. После просмотра ролика с превью материального дизайна мы загорелись идеей реализовать нечто подобное в приложении.

    С одной стороны, нам очень понравилась сама концепция крупной кнопки — на которую хочется нажать, с другой — нас сильно смущало то, что кнопка заслоняет контент и как бы отвлекает, что ли во время чтения. Тогда наш дизайнер предложил научить её «плавать».

    image


    Перехватываем событие скроллинга

    Стандартный компонент ListView не очень-то горит желанием делиться подробной информацией о том, на сколько пикселей и в каком направлении юзер только что проскролил. В RecyclerView с этим получше, но и ListView можно без труда научить «делиться», тем более что нужная функция в нем есть. Расширяем класс:

    /**
     * Created by recoilme on 29.08.14.
     */
    public class ScrollDetectingListView extends ListView {
        public ScrollDetectingListView(Context context) {
            super(context);
        }
    
        public ScrollDetectingListView(Context context, AttributeSet attrs) {
            super(context,attrs);
        }
    
        public ScrollDetectingListView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    
        //we need this protected method for scroll detection
        public int getVerticalScrollOffset() {
            return computeVerticalScrollOffset();
        }
    }
    


    Теперь, в активити мы можем определить в какую сторону смеcтился скролл:

            listView = (ScrollDetectingListView) aq.id(R.id.detailmain_listview).getListView();
    
            listView.setOnScrollListener(new AbsListView.OnScrollListener() {
    
                private int mInitialScroll = 0;
    
                @Override
                public void onScrollStateChanged(AbsListView view, int scrollState) {
    
                }
    
                @Override
                public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                    int scrolledOffset = listView.getVerticalScrollOffset();
                    if (scrolledOffset!=mInitialScroll) {
                        //if scroll position changed
                        boolean scrollUp = (scrolledOffset - mInitialScroll) < 0;
                        mInitialScroll = scrolledOffset;
                        if (scrollUp) {
                            if (isNetworkAvailable && !animationProcess && aq.id(R.id.surfButton).getView().getVisibility() == View.GONE) {
                                actionBar.show();
                                aq.id(R.id.surfButton).animate(animFadeIn);
                            }
                        }
                        else {
                            if (isNetworkAvailable && !animationProcess && aq.id(R.id.surfButton).getView().getVisibility() == View.VISIBLE) {
                                actionBar.hide();
                                aq.id(R.id.surfButton).animate(animFadeOut);
                            }
                        }
                    }
                }
            });
    


    Когда пользователь скроллит вверх — запускается анимация появления кнопки и экшенбара, когда вниз — кнопка скрывается. При нажатии — кнопка скрывается, при загрузке следующего сёрфа — появляется. Всё просто.

    Собственно анимации:

            isNetworkAvailable = UtilsApi.isOnline(activity);
            if (isNetworkAvailable) {
                aq.id(R.id.surfButton).visible();
            }
            else {
                aq.id(R.id.surfButton).gone();
            }
            animFadeIn = AnimationUtils.loadAnimation(getApplicationContext(),
                    R.xml.grow_from_top);
            animFadeIn.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {
                    animationProcess = true;
                    aq.id(R.id.surfButton).visible();
                }
    
                @Override
                public void onAnimationEnd(Animation animation) {
                    animationProcess = false;
                    aq.id(R.id.surfButton).visible();
                }
    
                @Override
                public void onAnimationRepeat(Animation animation) {}
            });
            animFadeOut = AnimationUtils.loadAnimation(getApplicationContext(),
                    R.xml.shrink_from_top);
            animFadeOut.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {
                    animationProcess = true;
                    aq.id(R.id.surfButton).visible();
                }
    
                @Override
                public void onAnimationEnd(Animation animation) {
                    animationProcess = false;
                    aq.id(R.id.surfButton).gone();
                }
    
                @Override
                public void onAnimationRepeat(Animation animation) {}
            });
    
    //grow
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android">
    	<scale
    		android:fromXScale="0.3" android:toXScale="1.0"
    		android:fromYScale="0.3" android:toYScale="1.0"
    		android:pivotX="50%" android:pivotY="0%"
    		android:duration="@android:integer/config_shortAnimTime"
    	/>
    	<alpha
    		android:interpolator="@android:anim/decelerate_interpolator"
    		android:fromAlpha="0.0" android:toAlpha="1.0"
    		android:duration="@android:integer/config_shortAnimTime"
    	/>
    </set>
    //shrink
    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android">
    	<scale
    		android:fromXScale="1.0" android:toXScale="0.3"
    		android:fromYScale="1.0" android:toYScale="0.3"
    		android:pivotX="50%" android:pivotY="100%"
    		android:duration="@android:integer/config_shortAnimTime"
    	/>
    	<alpha
    		android:interpolator="@android:anim/accelerate_interpolator"
    		android:fromAlpha="1.0" android:toAlpha="0.0"
    		android:duration="@android:integer/config_shortAnimTime"
    	/>
    </set>
    


    Вот и всё. Надеемся кому нибудь пригодится. Приятного сёрфа!
    Surfingbird
    Company
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 14

      +1
      Реализация для RecyclerView, вы забыли для FAB тени добавить и почему вы решили использовать scale анимацию, вместо появление сниху / съезд вниз?
        +2
        Мы не забыли, мы попробовали с тенями и без, с разными анимациями и выбрали тот вариант, который как нам больше понравился)
          0
          Без тени выглядит немного скучновато (мое мнение), я думал вы будете придерживаться Material Design. Кстати почему вы не хотите использовать иконки от гугла?

          Извините что слишком много критики с моей стороны :) Хочется сделать ваш продукт качественее
            +1
            Иконки от гугла, кстати. С тенью было слишком «весело» на мой взгляд, я попросил дизайнера попробовать вариант без тени и нам понравилось. Тем более что у нас везде нет теней. Нет, мы не собираемся сделать образцово показательное приложение по канонам гугл. Но мы стараемся следовать духу андроид, простите если звучит пафосно. Лично меня, например, жутко раздражают заслоняющие контент кнопки в новых приложениях от гугл (inbox, gmail, maps), мне кажется что если бы они скрывались — выглядело бы чище, но конечно же кого то будет раздражать, например отсутствие тени у кнопки в нашем приложении. Я думаю это нормально.
        +1
        кстати, как её скрыть, когда скроллить некуда?
          +1
          У нас под каждой статьёй генерируется блок похожих статей, поэтому такая проблема маловероятна. А вообще никак.
            +1
            угу. в общем, напарывался я на такие кнопки — и везде ответ «никак». в итоге она прекрасно перекрывает хвост полезной информации и убрать никак :)
              0
              а у вас есть предложения, как их можно скрыть в обычном состоянии и как обратно показать?
                +4
                Есть два решения — 1) «Пустой свайп». то есть кнопка скрывается не при скролле, а при свайпе — сооветственно, даже если нечего скроллить — она будет скрываться-появляться.

                2) Паддинг. То есть неважно какой размер контента — паддинг пустого есть всегда.
                  +2
                  Можно вычислять высоту контента и сравнивать с высотой экрана, если высота контента меньше- добавлять пустое вью, чтобы появился небольшой скролл
                    +1
                    Можно по двойному тапу на элементы без действия скрывать/показывать. Как скрываются/показываются элементы при просмотре фотографий в галерее при полноэкранном режиме (по крайней мере на айфоне так).
                      0
                      Мне кажется у вас есть что «скролить» всегда. И это никак не зависит от содержимого ListView! Вы всегда можете проскролить содержимое на высоту ActionBar.

                      Если использовать не «Пустой свайп» (как упоменалось выше), а скролл который сначала будет прятать ActionBar и кнопку — а только после определенных условий (когда они уже спрятались) — пускать ивент глубже, чтобы ListView воспринимал его как собственный.
                0
                Очень интересно Вас библиотека AQuery полностью устраивает? Есть какие-нибудь нарекания?
                  +2
                  Устраивает полностью, нареканий нет. При том что обычно у меня вагон и маленькая тележка нареканий.

                Only users with full accounts can post comments. Log in, please.