SwipeRefreshLayout
SwipeRefreshLayout 是一个下拉刷新控件,几乎可以包裹一个任何可以滚动的内容(ListView GridView ScrollView RecyclerView),可以自动识别垂直滚动手势。使用起来非常方便。
但是如果直接采用原生的SwipeRefreshLayout,那么它的第一个子View必须是AdapterView(可以滚动的View)。现在有一种情况,当ListView没有数据时,我们通常会用一个EmptyView来提示用户。此时在SwipeRefreshLayout中需要有一个VIewGroup来包含ListView和一个EmptyView。
监听失败
布局文件:
可以看到SwipeRefreshLayout的第一个直接子View并不是ListView,这样就会导致不好使用效果:ListView无法下拉,也就是当ListView没有在最顶部时,无法显示上面被屏幕遮挡的数据,下拉只会出发刷新。
代码:
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(
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基础用法。
效果:
自定义SwipeRefreshLayout
解决上述问题的办法只有自定义SwipeRefreshLayout。
想法
查看文档发现,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;
}
...
}
这里复制了一些关键代码,可以看到首先通过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;
}
}
}
}
这里默认的认为SwipeRefreshLayout的第一个直接子View就是需要监听的View,而没有判断到底是否属于可滑动控件。所以有个想法直接覆写该方法,改变默认的方式,用自己的方法来赋值给mTarget变量。但是该方法是私有保护的,所以无法改变。再往下看onInterceptTouchEvent()方法,注意到if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing || mNestedScrollInProgress)
要让事件不被拦截,onInterceptTouchEvent必须返回false,所以这里观察到一个很关键的方法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的监听对象,然后创建该变量的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);
}
/**
* 设定监听View,必须是AdapterView
* @param view
*/
public void setTarget(View view) {
mView = view;
}
@Override
public boolean canChildScrollUp() {
//判断监听的View是否是可滑动View,
//如果为true,那么根据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的基础用法外,还要调用定义的setter方法,给自定义的SwipeRefreshLayout设置监听对象。
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);
效果:
问题
从上面的效果看到,当ListView的Adapter没有数据时,正常显示了布局文件中的EmptyView。但是当下拉刷新时,SwipeRefreshLayout的动画效果非常不好,貌似被隐藏了一样。没办法只有通过谷歌来解决。
发现一个帖子Android - SwipeRefreshLayout with empty textview
上面回答者讲到,SwipeRefreshLayout必须有一个AdapterView才可以正常工作。这就联想到了AdapterView.setEmptyView()方法当给定的参数不是null时,会把自己Visibility属性设置为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);
}
可以看到决定ListView的Visibility属性有两个关键条件,一个是adapter不能为null,另外adapter不能没有数据源。
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()方法根据setEmptyView()给定的参数empty来设置ListView的Visibility属性。
由此可以推断,解决该现象的方法就是在调用setEmptyView()方法后设定ListView的Visibility属性为View.VISIBLE。或者将空数据源的adapter设置给ListView时也初始化ListView的Visibility属性为View.VISIBLE。
INVISIBLE和GONE区别
大部分控件都有visibility这个属性,其属性有3个分别为“visible ”、“invisible”、“gone”。主要用来设置控制控件的显示和隐藏。
- visible,设置View可见
- invisible,设置View不可见
- gone,隐藏View
而INVISIBLE和GONE的主要区别是:当控件visibility属性为INVISIBLE时,界面保留了view控件所占有的空间;而控件属性为GONE时,界面则不保留view控件所占有的空间。也就是说当一个ViewGroup的ChildView的visibility被设置成gone时,该ChildView不在ViewGroup的ViewTree中。
参考
SwipeRefreshLayout与RecyclerView的巧夺天工
SwipeRefreshLayout的学习
分析SwipeRefreshLayout源码
SwipeRefreshLayout
解决SwipeRefreshLayout结合ListView EmptyView使用不起作用的问题
Android中visibility属性VISIBLE、INVISIBLE、GONE的区别