Выбор Pull To Refresh инструмента

        Недавно столкнулся с проблемой внедрения в проект механизма Pull To Refresh для обновления списков. Ввиду специфичности имеющихся списков (списки разной длины, от 0 до ~100; подгрузка элементов по требованию; набор списков располагается в самописном компоненте, аля ViewPager) это действительно оказалось проблематично. О всех моих изысканиях в данном направлении читайте под катом.

        Pull To Refresh — фишка, насколько мне известно, перекочевавшая на Android из iPhone'а. Удобный способ обновления списка.
        Рассмотрим его на примере нашего новостного приложения (в него-то собственно и возникла необходимость внедрить эту фичу): есть список новостей, который обновляется через новостной сервер. Обновление ручное, так что внизу торчит кнопочка «Обновить», которая занимает некоторое место на экране. А зачем тратить драгоценное экранное пространство на кнопочку, которая не так уж и часто используется, если можно вопользоваться приемом Pull To Refresh: находясь вверху списка потяните список вниз, а затем отпустите, чтобы список обновился. Новые новости (каламбурчик) подгрузятся и отобразятся. Выглядит это примерное так:

        Идея довольно удачная, поэтому и используется во многих приложения, включая популярные Facebook и Twitter клиенты. Так вот и мы решили внедрить в свой новостной проект такую фишку.
        Но зачем писать с нуля то, что уже есть в готовом виде? Быстрый поиск в Google, великий и могучий StackOverFlow — и вот найден самый популярный инструмент android-pulltorefresh от Йохана Нильсона. Взял последнюю версию с GitHub'а и заюзал у себя. Не тут-то было! Проект вроде бы уже почти год развивается, но… вот что Я вижу в случае малых списков:

        И такой вопрос сразу возникает: WTF? Проект watch'ат 418 человек, аж 71 человек его форкнул, а тут такое каличное некорректное поведение. А все почему? Потому что вот этот  вот «Tap to refresh...» — это header у ListView. И прячется он, в реализации от Йохана, банальным ListView.setSelection(1). А в случае коротких списков этот setSelection(1) вежливо посылает на фек не работает.
        Но потом замечаю, что у проекта оказывается есть ещё два branch'а: enhancedpull (который уже был смержен с главным брэнчем) и, отоноче, scrollfix_for_short_list :)
        Вытягиваю последнюю версию брэнча scrollfix_for_short_list, прикручиваю в проект: короткий список вроде выглядит нормально, только отчего ж у меня начал так тормозить UI? А дело вот в чем: список мой — не простой, а с подгрузкой по требованию (on-demand), т.е. саначала показываются первые 10 элементов, а если промотать до конца, то в список догружается следующая порция. А в чем же состоит fix для коротких списков по версии Йохана?
        «А давайте добавим в footer ListView пустую вьюху ровно такой высоты, чтобы setSelection(1) снова смог нормально скрыть header», — сказал Йохан и приступил к вычилениям высоты footer'а. Чтобы вычислить его высоту надо знать суммарную высоту всех элементов в списке (кроме header'а конечно). Тогда мы отнимем эту высоту от высоты ListView и получим высоту для footer'а. А чтобы узнать высоту каждого элемента списка, откуда-то было взято «гениальное» решение перебрать с помощью адаптера все элементы (с помощью getView()) и каждому сделать measure(), т.е. по сути отрисовать их (даже если они невидимы). В результате мой список думал, что его все проматывают и проматывают — и все подгружал и подгружал новые порции до тех пор, пока элементы то и не закончатся. А элементов у меня обычно за 50 в списке, и списков несколько (пролистываются наподобие недавно появившегося ViewPager'а). В общем вот такая реализация подсчета суммарной высоты элементов списка:
    private int getTotalItemHeight() {
            ListAdapter adapter = getAdapter();
            int listviewElementsheight = 0;
            for(int i = 0; i < adapter.getCount(); i++) {
                View mView  = adapter.getView(i, null, this); // wtf?
                mView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 
                                                   MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
                listviewElementsheight += mView.getMeasuredHeight();
            }
            return listviewElementsheight;
        }
    

    абсолютно не подходит для списков с подгрузкой on-demand. Да и вообще стоит избегать дергать getView() у адаптера вручную. Мало ли у кого там какая логика заложена.
        В итоге пошел Я серфить интернет в поисках более адекватного инструмента. Вот, что нашел:

         Guillermo, в лучших традиция ООП, реализовал Pull To Refresh посредством паттерна State. В результате получилось 13 классов, и возможность делать не только pull-down-to-refresh, но и pull-up-to-refresh. Уж не знаю ввиду чего, но реализация получилась не очень скоростной: ну не успевал край списка за резвым пальцем. Да и срабатывание требовало довольно резкого движения вниз, чтобы вызвать появление header'а. И анимации что-то не наблюдается… Поехали дальше.
        Tim Mahoney никого не стал обманывать а сразу в README написал: «Current Status: Buggy.» Нет уж, для сурьезного проекта не подойдет. Но посмотрел на граф версий — какой-то добрый Daniel Wang форкнул проект и пофиксил баги. Берем, прикручиваем. Удобство использования оставляет желать лучшего. Если в предыдущих реализациях, достаточно было по приходу события onRefresh обновить список и потом вызвать onRefreshComplete():
    ((PullToRefreshListView) getListView()).setOnRefreshListener(new OnRefreshListener() {
        @Override
        public void onRefresh() {
            new GetDataTask().execute();
        }
    });
    
    private class GetDataTask extends AsyncTask {
        ...
        @Override
        protected void onPostExecute(Void result) {
        ...
        ((PullToRefreshListView) getListView()).onRefreshComplete();
        }
    }
    

    то здесь надо вручную создавать header, и по приходу событий менять текст в нем, ну и список конечно обновлять:
    mRefreshHeader = new TextView(this);
    mRefreshHeader.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    mRefreshHeader.setGravity(Gravity.CENTER);
    mRefreshHeader.setText("Pull to refresh...");
    
    mContainerView = (PullRefreshContainerView) findViewById(R.id.container);
    mContainerView.setRefreshHeader(mRefreshHeader);
    
    mContainerView.setOnChangeStateListener(new OnChangeStateListener() {
        @Override
        public void onChangeState(PullRefreshContainerView container, int state) {
            switch(state) {
             case PullRefreshContainerView.STATE_IDLE:
             case PullRefreshContainerView.STATE_PULL:
                    mRefreshHeader.setText("Pull to refresh...");
                 break;
                case PullRefreshContainerView.STATE_RELEASE:
                    mRefreshHeader.setText("Release to refresh...");
                    break;
                case PullRefreshContainerView.STATE_LOADING:
                    mRefreshHeader.setText("Loading...");
                    new GetDataTask().execute(); // в onPostExecute() - mContainerView.completeRefresh();
                    break;
            }
        }
    });
    

        Не знаю, может это и более гибко, но… неудобно. К тому же оказалось, что данная реализация не очень корректно работает в Pager'е: стало возможно пролистывать список по вертикали и Pager по горизонтали одновременно.
        В итоге со слезами на глазах Я стал ставить на костыли версию Йохана из брэнча  scrollfix_for_short_list, чтобы она не заставляла списки подгружаться до бесконечности. Кое-как было сделано, но работало, мягко говоря, нестабильно. На горизонте замаячила перспектива писать компонент самому, и Я решил ещё раз пересерфить интренет. И, о чудо!, Я наткнулся на ещё одну реализацию — от Криса Бэйнса. Проект базировался на версии Йохана, но был с тех пор существенно улучшен (как пишет автор). Испытание подтвердило: данная реализация дествительно лишена всех багов, присущих версии Йохана, и выглядит более приятно (за счет дополнительной анимации).

    Так вот, к чему Я тут так распространился? А все затем чтобы выдать мораль:
    Если вы хотите использовать в своем проекте механизм Pull To Refresh — используйте реализацию от Криса Бэйнса. На мой взгляд, на данный момент это самая качественная реализация приема Pull To Refresh.

    P.S. Чуть позже обнаружил на Хабре похожую статью, также критикующую вышеописанные проекты и продвигающую свою версию реализации Pull To Refresh. Но… отчего-то при использовании этого компонента у меня просто начало падать приложение: какие-то неполадки с курсором при попытке подгрузки новой порции в список. Так что этот вариант — не для меня. Но в демке выглядит неплохо: лучше, чем у Йохана.
    Метки:
    • +19
    • 15,2k
    • 5
    Поделиться публикацией
    Комментарии 5
    • 0
      Кстати, реализация от Криса Бэйнса — единственная, которая до сих пор активно развивается (последний коммит был буквально позавчера). Остальные же либо обошлись одноразовым коммитом, либо забросили развитие проекта.
      • +1
        Спасибо, очень подробно.
        • 0
          Перезалейте картинки
          • 0
            Наткнулся на статью, при поиске реализации pull-to-refresh. Прочитал, но реализации, которые представлены тут, не очень хороши до сих пор. Советую воспользоваться вот этой. В свой проект я добавил ее очень быстро и безболезненно.
            • 0
              Ага, я смотрю вы очень внимательно прочитали статью…

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.