Android - SwipeRefreshLayout с пустым просмотром текста

Я реализовал SwipeRefreshLayout в мое приложение, но оно может содержать только одного прямого потомка, который должен быть списком. Я пытаюсь выяснить, как добавить пустое текстовое представление в следующий рабочий XML-файл:

<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/swipe_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ListView
        android:id="@+id/listViewConversation"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:dividerHeight="1dp" />

</android.support.v4.widget.SwipeRefreshLayout>

Обтекание его в линейной / относительной компоновке делает его глючным, потому что просмотр списка всегда обновляется, когда вы хотите сдвинуть назад просмотр списка. Один из способов, которым я могу придумать, - это делать это программно, но, думаю, это не лучший вариант.

Вы можете узнать, как реализовать это, используя этот учебник: Проведите пальцем, чтобы обновить РУКОВОДСТВО

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

14 ответов

Решение

Мне не понравилось ограничение на одного ребенка. Кроме того, текущая реализация SwipeRefreshLayout имеет жестко закодированную "магическую" обработку для ScrollView, ListView и GridView, которая запускается, только если представление является прямым потомком вашего собственного представления.

Тем не менее, хорошая новость заключается в том, что это открытый исходный код, поэтому вы можете либо скопировать код и приспособиться к вашим потребностям, либо сделать то, что я сделал:

Используйте два РАЗЛИЧНЫХ SwipeRefreshLayout, один для Пустого представления и один для ListView.

<FrameLayout 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"
    tools:context="com.tobrun.example.swipetorefresh.MainActivity">

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefreshLayout_listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </android.support.v4.widget.SwipeRefreshLayout>

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefreshLayout_emptyView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fillViewport="true">

            <TextView
                android:id="@+id/emptyView"
                android:text="@string/empty"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:gravity="center" />

        </ScrollView>

    </android.support.v4.widget.SwipeRefreshLayout>


</FrameLayout>

Затем скажите вашему представлению списка, что пустое представление списка - это макет обновления смахивания пустого представления.

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

Компоновка списка при обновлении смахивания не должна получать сенсорные события, поскольку список скрыт.

Удачи.

Нет необходимости в обходном пути.

Вы можете просто использовать эту иерархию представлений:

    <FrameLayout ...>

        <android.support.v4.widget.SwipeRefreshLayout ...>

            <ListView
                android:id="@android:id/list" ... />
        </android.support.v4.widget.SwipeRefreshLayout>

        <TextView
            android:id="@android:id/empty" ...
            android:text="@string/empty_list"/>
    </FrameLayout>

Затем в коде вы просто звоните:

_listView.setEmptyView(findViewById(android.R.id.empty));

Вот и все.


РЕДАКТИРОВАТЬ: Если вы хотите иметь возможность смахивания для обновления, даже когда отображается пустое представление, вам нужно как-то избежать скрытия ListView, чтобы вы могли использовать настраиваемый ListView, который имеет эту функцию внутри:

@Override
  public void setVisibility(final int visibility)
    {
    if(visibility!=View.GONE||getCount()!=0)
      super.setVisibility(visibility);
    }

Вместе с решением, которое я написал, показывается обновление, независимо от того, сколько элементов вы показываете.

Конечно, если вы действительно хотите скрыть ListView, вы должны изменить код. Может быть, добавить "setVisibilityForReal(...)":)

На самом деле, единственное, что вам не хватает - это пустое TextView быть завернутым в прокручиваемый контейнер - например ScrollView, Для получения дополнительной информации взгляните на метод SwipeRefreshLayout.canChildScrollUp() и его использование.

Во всяком случае, вернемся к делу. Вот успешная реализация:

activity_some.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/swipe_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:measureAllChildren="true">

        <WebView
            android:id="@+id/webview"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <include layout="@layout/empty" />

    </FrameLayout>

</android.support.v4.widget.SwipeRefreshLayout>

Где твой empty.xml в основном все, что вы хотите, завернутый с ScrollView,

empty.xml

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/empty"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true">

    <TextView
        android:text="Nothing here..."
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</ScrollView>

Теперь для того, чтобы избавиться от знаменитого SwipeRefreshLayout обновить только для случая, переключите SwipeRefreshLayout когда необходимо (специфично для фрагмента):

private ViewTreeObserver.OnScrollChangedListener mOnScrollChangedListener;

@Override
public void onStart() {
    super.onStart();

    mOnScrollChangedListener = new ViewTreeObserver.OnScrollChangedListener() {
        @Override
        public void onScrollChanged() {
            int scrollY = mWebView.getScrollY();
            if (scrollY == 0)
                swipeLayout.setEnabled(true);
            else
                swipeLayout.setEnabled(false);

        }
    };
    swipeLayout.getViewTreeObserver().addOnScrollChangedListener(mOnScrollChangedListener);
}

@Override
public void onStop() {
    swipeLayout.getViewTreeObserver().removeOnScrollChangedListener(mOnScrollChangedListener);
    super.onStop();
}

Это оно! Надеюсь, поможет!;)

Кстати, почему вы используете SwipeRefreshLayout с FrameLayout сюда? Потому что таким образом вы можете делать плавные переходные анимации, такие как эффекты перекрестного затухания, и любое из ваших представлений состояния может быть смахиваемым (в случае, если вам нужен унифицированный механизм извлечения / обновления / повторения).

Вот что я сделал: я отключил свайп, чтобы обновить, если мой listView не работает.

mBookedProductsListView.setOnScrollListener(new AbsListView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(AbsListView absListView, int i) {
        }
        @Override
        public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount,     int totalItemCount) {
            if (firstVisibleItem == 0)
                mSwipeRefreshLayout.setEnabled(true);
            else
                mSwipeRefreshLayout.setEnabled(false);
        }
    });

Мой Xml:

<?xml version="1.0" encoding="utf-8"?>

<android.support.v4.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/swipeRefreshLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

<RelativeLayout
        android:id="@+id/productListLL"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >

    <EditText
            android:id="@+id/search"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_alignParentTop="true"
            android:background="#a4aeb8"
            android:drawableRight="@drawable/search_icon"
            android:paddingRight="15dp"
            android:textColor="@android:color/white"
            android:paddingLeft="15dp"
            android:hint="Rechercher"
            android:textColorHint="@android:color/white"
            android:inputType="textCapWords|textNoSuggestions"
            />

    <include android:layout_width="match_parent" android:layout_height="wrap_content" layout="@layout/filter_by_categories_buttons" android:id="@+id/categoriesTree" android:layout_below="@id/search" />

    <ListView
            android:id="@+id/productListLV"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:divider="@drawable/product_listview_divider"
            android:dividerHeight="1dp"
            android:scrollbars="none"
            android:overScrollMode="never"
            android:choiceMode="multipleChoiceModal"
            android:background="#e7eaef"
            android:visibility="gone"
            android:layout_below="@id/categoriesTree"
            />

</RelativeLayout>

</android.support.v4.widget.SwipeRefreshLayout>

Работает как шарм.

Я столкнулся с той же проблемой. Раздражает, что SwipeRefreshLayout может иметь только одного дочернего элемента AdpaterView.

Если для AdpaterView установлено пустое представление, оно будет скрыто, если данные недоступны. Однако для работы SwipeRefreshLayout требуется AdpaterView. Итак, я расширяю AdpaterView, чтобы он все еще отображался, даже если он пуст.

@Override
public void setVisibility(int visibility) {
    if (visibility == View.GONE && getCount() == 0) {
        return;
    }
    super.setVisibility(visibility);
}

Может быть, вам нужно также установить прозрачность фона вида адаптера. Но в моем случае это не нужно, поскольку пустое представление - это простое TextView.

Основываясь на некоторых ответах здесь и исходном коде SwipeRefreshLayout, я разделил представление на подклассы, чтобы конкретно обрабатывать наличие RecyclerView (или ListView), а также "пустого" представления внутри контейнера, который является дочерним.

Ожидается макет, такой как

<SwipeRefreshLayoutWithEmpty ...>
  <FrameLayout ...>
    <TextView android:text="List is Empty" ...>
    <RecyclerView ...>
  </FrameLayout>
</SwipeRefreshLayoutWithEmpty>

Код является:

public class SwipeRefreshLayoutWithEmpty extends SwipeRefreshLayout {
    private ViewGroup container;

    public SwipeRefreshLayoutWithEmpty(Context context) {
        super(context);
    }

    public SwipeRefreshLayoutWithEmpty(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean canChildScrollUp() {
        // The swipe refresh layout has 2 children; the circle refresh indicator
        // and the view container. The container is needed here
        ViewGroup container = getContainer();
        if (container == null) {
            return false;
        }

        // The container has 2 children; the empty view and the scrollable view.
        // Use whichever one is visible and test that it can scroll
        View view = container.getChildAt(0);
        if (view.getVisibility() != View.VISIBLE) {
            view = container.getChildAt(1);
        }

        return ViewCompat.canScrollVertically(view, -1);
    }

    private ViewGroup getContainer() {
        // Cache this view
        if (container != null) {
            return container;
        }

        // The container may not be the first view. Need to iterate to find it
        for (int i=0; i<getChildCount(); i++) {
            if (getChildAt(i) instanceof ViewGroup) {
                container = (ViewGroup) getChildAt(i);

                if (container.getChildCount() != 2) {
                    throw new RuntimeException("Container must have an empty view and content view");
                }

                break;
            }
        }

        if (container == null) {
            throw new RuntimeException("Container view not found");
        }

        return container;
    }
}

Полный текст: https://gist.github.com/grennis/16cb2b0c7f798418284dd2d754499b43

Вы можете просто использовать NestedScrollView в SwipeRefreshLayout с одним контейнером. Ниже приведен список использования багги mRecyclerView.setNestedScrollingEnabled(false);

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <!-- some toolbar -->

    <android.support.v4.widget.SwipeRefreshLayout
            android:id="@+id/conversationFragment_swipeRefresh"
            android:layout_alignParentBottom="true"
            android:layout_below="@+id/some_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <android.support.v4.widget.NestedScrollView
                android:layout_width="match_parent"
                android:layout_height="match_parent">

            <FrameLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent">

                <TextView
                        android:id="@+id/conversationFragment_noResultText"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:text="@string/FragmentConversations_empty"
                        android:layout_centerHorizontal="true" />

                <android.support.v7.widget.RecyclerView
                        android:id="@+id/conversationFragment_recyclerView"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent" />

            </FrameLayout>

        </android.support.v4.widget.NestedScrollView>

    </android.support.v4.widget.SwipeRefreshLayout>

</RelativeLayout>

Возможно, поздно, но если вы все еще сталкиваетесь с этой проблемой, вот чистое решение! Нет необходимости создавать другой SwipeRefreshLayout,

заворачивать empty view в ScrollView, Придерживаться обоих AdapterView и ScrollView в ViewGroup и положить его в SwipeRefreshLayout

Образец:

<android.support.v4.widget.SwipeRefreshLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:layout_width="250dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:gravity="center" />

        </ScrollView>

    </FrameLayout>

</android.support.v4.widget.SwipeRefreshLayout>

РЕДАКТИРОВАТЬ

Здесь гораздо более простой способ.

проверьте, если ваш список пуст. если да, то сделай его невидимым. Образец:

if(lst.size() > 0) {
 mRecyclerView.setVisibility(View.VISIBLE);
//set adapter
}else
{
  mRecyclerView.setVisibility(View.INVISIBLE);
 findViewById(R.id.txtNodata).setVisibility(View.VISIBLE);
}

это сделает mRecyclerView все еще там и все свайпы будут работать нормально, теперь даже нет данных!

Я попробовал следующую вещь. Как в пустом представлении, так и в случае списка, свайп обновит данные, а также fastscroll работает нормально без каких-либо изменений кода, требуемых в действии. Я поставил emptyview перед списком и отметил его видимость исчезнувшим. и listview ставится после этого внутри блока SwipeToRefresh. Образец кода -

<FrameLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <TextView
        android:id="@+id/tv_empty_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="32dp"
        android:gravity="center_horizontal"
        android:text="No item found"
        android:visibility="gone"/>


    <android.support.v4.widget.SwipeRefreshLayout 
        android:id="@+id/swipe"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/lv_product"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </android.support.v4.widget.SwipeRefreshLayout>


</FrameLayout>

Почему бы не обернуть все внутри SwipeRefreshLayout

<android.support.v4.widget.SwipeRefreshLayout 
    android:id="@+id/swipe"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal"/>

    <RelativeLayout
        android:id="@android:id/empty"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        ...
    </RelativeLayout>
</LinearLayout>
</android.support.v4.widget.SwipeRefreshLayout>

Поместите SwipeRefreshLayout в FrameLayout и другие представления позади него.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <LinearLayout
        android:id="@+id/your_message_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >

            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="No result"/>
        </LinearLayout>

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <ListView
            android:id="@+id/your_list_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
        </ListView>
    </android.support.v4.widget.SwipeRefreshLayout>
</FrameLayout>

Это была очень неприятная проблема для меня, но после нескольких часов попыток и неудач я нашел это решение.

Благодаря этому я могу обновиться даже с видимым пустым представлением (и, конечно, с RecyclerView)

В моем файле макета у меня есть эта структура:

SwipeRefreshLayout
    FrameLayout
        RecyclerView
        my_empty_layout // Doesnt have to be ScrollView it can be whatever ViewGroup you want, I used LinearLayout with a single TextView child

В коде:

...
adapter.notifyDataSetChanged()

if (adapter.getItemCount() == 0) {
    recyclerView.setVisibility(View.GONE);
    emptyView.setVisibility(View.VISIBLE);
}
else {
    recyclerView.setVisibility(View.VISIBLE);
    emptyView.setVisibility(View.GONE);
}

Это сработало для меня

<FrameLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/lighter_white">

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipeContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/list"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:background="@color/silver"
            android:layout_marginTop="10dp"
            android:divider="@android:color/transparent"
            android:dividerHeight="8dp"
            android:padding="4dp"/>

    </android.support.v4.widget.SwipeRefreshLayout>

    <TextView
        android:id="@+id/empty"
        android:text="@string/strNoRecordsFound"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:textColor="@android:color/black"
        android:alpha="0.5"
        android:gravity="center">
    </TextView>
</FrameLayout>

Еще один вариант, который хорошо работает, если ваше пустое представление не нуждается во взаимодействии. Например, это простое текстовое представление "Нет данных здесь".

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>

<TextView
    android:id="@+id/emptyView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:visibility="visible"
    android:layout_centerInParent="true"
    android:text="@string/no_earnable_available"
    android:textSize="18dp"
    android:textColor="@color/black"
    android:background="@color/white"
    />

<some.app.RecyclerViewSwipeToRefreshLayout
    android:id="@+id/swipe_refresh_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="4dp"
    android:background="@android:color/transparent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    >
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"
        />

</some.app.RecyclerViewSwipeToRefreshLayout>
</RelativeLayout>

Это ставит пустой вид за SwipeToRefreshLayout, который является прозрачным и который также содержит прозрачный RecyclerView.

Затем в коде, в месте, где вы добавляете элементы в адаптер представления переработчика, вы проверяете, является ли адаптер пустым, и если да, то вы устанавливаете видимость пустого представления как "видимая". И наоборот.

Хитрость заключается в том, что представление находится за прозрачным представлением рециркулятора, что означает, что представление рециркулятора и его родительский объект, SwipeToRefreshLayout, будут обрабатывать прокрутку, когда в рециркуляторе нет элементов. Пустой вид сзади даже не будет затронут, поэтому событие касания будет использовано SwipeTorefreshLayout.

Пользовательский RecyclerSwipeTorefreshLayout должен обрабатывать метод canChildScrollUp следующим образом

@Override
public boolean canChildScrollUp() {
    if (recycler == null) throw new IllegalArgumentException("recycler not set!");
    else if (recycler.getAdapter().getItemCount() == 0){ // this check could be done in a more optimised way by setting a flag from the same place where you change the visibility of the empty view
        return super.canChildScrollUp();
    } else {
        RecyclerView.LayoutManager layoutManager = recycler.getLayoutManager();

        if (layoutManager instanceof LinearLayoutManager) {
            return ((LinearLayoutManager) recycler.getLayoutManager()).findFirstVisibleItemPosition() != 0 ||
                    (((LinearLayoutManager) recycler.getLayoutManager()).findFirstVisibleItemPosition() == 0
                            && recycler.getChildAt(0) != null && recycler.getChildAt(0).getY() < 0);
//...

Это сделает свое дело.

ОБНОВЛЕНИЕ: Конечно, представление переработчика не должно быть прозрачным все время. Вы можете обновить прозрачность, чтобы она была активной только тогда, когда адаптер пуст.

Ура!

Как я уже упоминал, я сделал это для RecyclerView:

я использовал clickable="true" как ниже с пустым RecyclerView:

mRecyclerView.setVisibility(View.VISIBLE);
mRecyclerView.setClickable(true);
myText.setVisibility(View.VISIBLE);

XML-макет:

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefreshKartvizitKisilerim"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </android.support.v4.widget.SwipeRefreshLayout>


    <TextView
        android:id="@+id/myText"
        android:visibility="gone"
        android:text="@string/noListItem"
        android:textSize="18sp"
        android:layout_centerInParent="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

RecyclerView имеет высоту match_parent а также SwipeRefresh имеет wrap_content, Когда есть элемент в списке, не забудьте сделать текст gone,

Другие вопросы по тегам