SwipeRefreshLayout進(jìn)階

SwipeRefreshLayout

SwipeRefreshLayout 是一個下拉刷新控件,幾乎可以包裹一個任何可以滾動的內(nèi)容(ListView GridView ScrollView RecyclerView),可以自動識別垂直滾動手勢。使用起來非常方便。

但是如果直接采用原生的SwipeRefreshLayout,那么它的第一個子View必須是AdapterView(可以滾動的View)。現(xiàn)在有一種情況,當(dāng)ListView沒有數(shù)據(jù)時,我們通常會用一個EmptyView來提示用戶。此時在SwipeRefreshLayout中需要有一個VIewGroup來包含ListView和一個EmptyView。

監(jiān)聽失敗

布局文件:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/id_swipe_refresh_child_test"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <ListView
                android:id="@+id/id_list_view_child_test"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
            <ImageView
                android:id="@+id/id_img_empty_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
        </FrameLayout>
    </android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>

可以看到SwipeRefreshLayout的第一個直接子View并不是ListView,這樣就會導(dǎo)致不好使用效果:ListView無法下拉,也就是當(dāng)ListView沒有在最頂部時,無法顯示上面被屏幕遮擋的數(shù)據(jù),下拉只會出發(fā)刷新。

代碼:

        mHandler = new Handler();
        mListView = (ListView) findViewById(R.id.id_list_view_child_test);
        mData = new ArrayList<>();
        for(int i = 20; i > 0; i--) {
            mData.add("This is item " + i);
        }
        mAdapter = new ArrayAdapter<String>(
                this,
                android.R.layout.simple_list_item_1,
                mData
        );
        mListView.setAdapter(mAdapter);

        mSwipeRefresh = (SwipeRefreshLayout) findViewById(R.id.id_swipe_refresh_child_test);
        mSwipeRefresh.setColorSchemeResources(
                android.R.color.holo_blue_light,
                android.R.color.holo_green_light,
                android.R.color.holo_orange_light,
                android.R.color.holo_red_light
        );
        mSwipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        double k = Math.random();
                        int index = (int) (k * 100);
                        mData.add(0, "This is item " + index);
                        mHandler.postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                mAdapter.notifyDataSetChanged();
                                mSwipeRefresh.setRefreshing(false);
                            }
                        }, 3000);
                    }
                }).start();
            }
        });

上述代碼是SwipeRefreshLayout基礎(chǔ)用法。
效果:

下拉刷新無效果.gif

自定義SwipeRefreshLayout

解決上述問題的辦法只有自定義SwipeRefreshLayout。

想法

查看文檔發(fā)現(xiàn),SwipeRefreshLayout繼承ViewGroup。所以時間攔截一定在onInterceptTouchEvent()方法中。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        ensureTarget();
        final int action = MotionEventCompat.getActionMasked(ev);
        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }
        if (!isEnabled() || mReturningToStart || canChildScrollUp()
                || mRefreshing || mNestedScrollInProgress) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }
        ...
    }

這里復(fù)制了一些關(guān)鍵代碼,可以看到首先通過ensureTarget()方法給變量mTarget賦值。

private void ensureTarget() {
        // Don't bother getting the parent height if the parent hasn't been laid
        // out yet.
        if (mTarget == null) {
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                if (!child.equals(mCircleView)) {
                    mTarget = child;
                    break;
                }
            }
        }
    }

這里默認(rèn)的認(rèn)為SwipeRefreshLayout的第一個直接子View就是需要監(jiān)聽的View,而沒有判斷到底是否屬于可滑動控件。所以有個想法直接覆寫該方法,改變默認(rèn)的方式,用自己的方法來賦值給mTarget變量。但是該方法是私有保護(hù)的,所以無法改變。再往下看onInterceptTouchEvent()方法,注意到if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing || mNestedScrollInProgress)要讓事件不被攔截,onInterceptTouchEvent必須返回false,所以這里觀察到一個很關(guān)鍵的方法canChildScrollUp()

public boolean canChildScrollUp() {
        if (android.os.Build.VERSION.SDK_INT < 14) {
            if (mTarget instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) mTarget;
                return absListView.getChildCount() > 0
                        && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
                                .getTop() < absListView.getPaddingTop());
            } else {
                return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
            }
        } else {
            return ViewCompat.canScrollVertically(mTarget, -1);
        }
    }

注意ViewCompat.canScrollVertically()就是用來判斷mTarget是否還可以垂直滾動。所以最終的方案就是重新聲明一個變量,作為自定義SwipeRefreshLayout的監(jiān)聽對象,然后創(chuàng)建該變量的setter方法,并且利用ViewCompat.canScrollVertically()覆寫canChildScrollUp()

實踐

public class SwipeRefreshLayout extends android.support.v4.widget.SwipeRefreshLayout{
    private View mView;
    public SwipeRefreshLayout(Context context) {
        super(context);
    }

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


    /**
     * 設(shè)定監(jiān)聽View,必須是AdapterView
     * @param view
     */
    public void setTarget(View view) {
        mView = view;
    }
    @Override
    public boolean canChildScrollUp() {
        //判斷監(jiān)聽的View是否是可滑動View,
        //如果為true,那么根據(jù)ViewCompat.canScrollVertically返回的值來決定是否攔截時間
        if(mView instanceof AbsListView)
            return canChildScrollUp(mView);
        //否則返回true,攔截事件,開啟刷新動畫
        else
            return true;
    }

    /**
    * 判斷垂直方向是否能滾動
    **/
    public boolean canChildScrollUp(View view) {
        return ViewCompat.canScrollVertically(view, -1);
    }
}

布局文件和上面一樣,只不過用了自定義的SwipeRefreshLayout控件。在Activity.java中,除了SwipeRefreshLayout的基礎(chǔ)用法外,還要調(diào)用定義的setter方法,給自定義的SwipeRefreshLayout設(shè)置監(jiān)聽對象。

mRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.id_swipe_refresh_custom);
    mRefreshLayout.setColorSchemeResources(
            android.R.color.holo_blue_light,
            android.R.color.holo_green_light,
            android.R.color.holo_orange_light,
            android.R.color.holo_red_light
    );
    mListView = (ListView) findViewById(R.id.id_list_view_custom);
    mRefreshLayout.setTarget(mListView);

效果:

SwipeRefreshLayout自定義動畫效果消失.gif

問題

從上面的效果看到,當(dāng)ListView的Adapter沒有數(shù)據(jù)時,正常顯示了布局文件中的EmptyView。但是當(dāng)下拉刷新時,SwipeRefreshLayout的動畫效果非常不好,貌似被隱藏了一樣。沒辦法只有通過谷歌來解決。
發(fā)現(xiàn)一個帖子Android - SwipeRefreshLayout with empty textview

上面回答者講到,SwipeRefreshLayout必須有一個AdapterView才可以正常工作。這就聯(lián)想到了AdapterView.setEmptyView()方法當(dāng)給定的參數(shù)不是null時,會把自己Visibility屬性設(shè)置為gone

setEmptyView()

public void setEmptyView(View emptyView) {
        mEmptyView = emptyView;
        // If not explicitly specified this view is important for accessibility.
        if (emptyView != null
                && emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
            emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
        }
        final T adapter = getAdapter();
        final boolean empty = ((adapter == null) || adapter.isEmpty());
        updateEmptyStatus(empty);
    }

可以看到?jīng)Q定ListView的Visibility屬性有兩個關(guān)鍵條件,一個是adapter不能為null,另外adapter不能沒有數(shù)據(jù)源。
updateEmptyStatus

private void updateEmptyStatus(boolean empty) {
        if (isInFilterMode()) {
            empty = false;
        }
        if (empty) {
            if (mEmptyView != null) {
                mEmptyView.setVisibility(View.VISIBLE);
                setVisibility(View.GONE);
            } else {
                // If the caller just removed our empty view, make sure the list view is visible
                setVisibility(View.VISIBLE);
            }

updateEmptyStatus()方法根據(jù)setEmptyView()給定的參數(shù)empty來設(shè)置ListView的Visibility屬性。

由此可以推斷,解決該現(xiàn)象的方法就是在調(diào)用setEmptyView()方法后設(shè)定ListView的Visibility屬性為View.VISIBLE?;蛘邔⒖諗?shù)據(jù)源的adapter設(shè)置給ListView時也初始化ListView的Visibility屬性為View.VISIBLE。

最終效果.gif

INVISIBLE和GONE區(qū)別

大部分控件都有visibility這個屬性,其屬性有3個分別為“visible ”、“invisible”、“gone”。主要用來設(shè)置控制控件的顯示和隱藏。

  • visible,設(shè)置View可見
  • invisible,設(shè)置View不可見
  • gone,隱藏View

而INVISIBLE和GONE的主要區(qū)別是:當(dāng)控件visibility屬性為INVISIBLE時,界面保留了view控件所占有的空間;而控件屬性為GONE時,界面則不保留view控件所占有的空間。也就是說當(dāng)一個ViewGroup的ChildView的visibility被設(shè)置成gone時,該ChildView不在ViewGroup的ViewTree中。

參考

SwipeRefreshLayout與RecyclerView的巧奪天工

SwipeRefreshLayout的學(xué)習(xí)

分析SwipeRefreshLayout源碼

SwipeRefreshLayout

解決SwipeRefreshLayout結(jié)合ListView EmptyView使用不起作用的問題

Android中visibility屬性VISIBLE、INVISIBLE、GONE的區(qū)別

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容