Android - SwipeRefreshLayout with empty textview

后端 未结 15 1400
悲哀的现实
悲哀的现实 2020-12-05 02:06

I\'ve implemented SwipeRefreshLayout into my app but it can only hold one direct child which should be the listview. I\'m trying to figure out how to add an emp

相关标签:
15条回答
  • 2020-12-05 02:44

    Another option that works nicely in case that your empty view doesn't need any interaction. For example, it is a simple textview saying "No data in here."

    <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>
    

    This puts the empty view behind the SwipeToRefreshLayout which is transparent and which contains also a transparent RecyclerView.

    Then, in the code, in the place where you add the items to the recycler view adapter, you check if the adapter is empty, and if so you set the visibility of the empty view to "visible". And vice versa.

    The trick is that the view is behind the transparent recycler view, which means that the recycler view and his parent, the SwipeToRefreshLayout, will handle the scroll when there are no items in the recycler. The empty view behind won't even be touched so the touch event will be consumed by the SwipeTorefreshLayout.

    The custom RecyclerSwipeTorefreshLayout should handle the canChildScrollUp method in the following way

    @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);
    //...
    

    This will do the trick.

    UPDATE: Of course, the recycler view doesn't have to be transparent all the time. You can update the transparency to be active only when the adapter is empty.

    Cheers!

    0 讨论(0)
  • 2020-12-05 02:45

    I encountered the same problem. It is annoying that SwipeRefreshLayout can only have one AdpaterView child.

    If an empty view is set for an AdpaterView, it will be set as hidden if no data is available. However, SwipeRefreshLayout needs an AdpaterView to work. So, I extend AdpaterView to make it still shown, even if it is empty.

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

    Maybe you need to set the background of adapter view as transparent as well. But in my case, it is not needed, as the empty view is a simple TextView.

    0 讨论(0)
  • 2020-12-05 02:49

    This was a very frustrating issue for me but after a few hours with try and fail I came up with this solution.

    With this I can refresh even with empty view visible (and RecyclerView too of course)

    In my layout file I have this structure:

    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
    

    In code:

    ...
    adapter.notifyDataSetChanged()
    
    if (adapter.getItemCount() == 0) {
        recyclerView.setVisibility(View.GONE);
        emptyView.setVisibility(View.VISIBLE);
    }
    else {
        recyclerView.setVisibility(View.VISIBLE);
        emptyView.setVisibility(View.GONE);
    }
    
    0 讨论(0)
  • 2020-12-05 02:51

    Based on some answers here and the source code of SwipeRefreshLayout, I have subclassed the view to specifically handle having a RecyclerView (or ListView) and also an "empty" view inside a container which is the child.

    It expects a layout such as

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

    The code is:

    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;
        }
    }
    

    Full gist: https://gist.github.com/grennis/16cb2b0c7f798418284dd2d754499b43

    0 讨论(0)
  • 2020-12-05 02:51

    You may be just use NestedScrollView in SwipeRefreshLayout with single container. Below is the list of buggy use 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>
    
    0 讨论(0)
  • 2020-12-05 02:54

    Actually, the only think you are missing is having that empty TextView be wrapped with a scrollable container - for example ScrollView. For details, have a look at SwipeRefreshLayout.canChildScrollUp() method and its usage.

    Anyway, back to the point. Here is a successful implementation:

    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>
    

    Where your empty.xml is basically anything you wish wrapped with a 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>
    

    Now in order to get rid of the famous SwipeRefreshLayout refresh-only-when-at-the-top issue, toggle the SwipeRefreshLayout when necessary (Fragment-specific):

    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();
    }
    

    That's it! Hope it helps! ;)

    Btw, why would you use SwipeRefreshLayout with FrameLayout this way? Because this way you can do smooth transition animations, like crossfade effects, and any of your state views can be swipeable (in case you want a unified fetch/refresh/retry mechanism).

    0 讨论(0)
提交回复
热议问题