在Android系统中,“上拉刷新/下拉加载更多”和“侧滑菜单”都是非常常用的操作界面,二者都比较容易,网上也有许多牛人做好的库可以直接使用。可是很少有讲解如何让两者并存的方法,前不久在一个项目中需要在已有侧滑菜单的应用中,对其中一个菜单项加入上拉/下拉菜单。由于都要捕捉触摸事件,这两者之间可能会产生一些冲突。这里记录一下我的解决方案和步骤,也希望能够为遇到同样问题的朋友们提供一些思路。
单独加入侧滑菜单还是比较容易的,这里我是参照了网上一个牛人写的一个Demo,源代码可以点击下载。
其中SlidingLayout.java这个是侧滑的布局文件,注释很详细,大部分的代码都不需要修改。需要注意的是在xml文件中,SlidingLayout里只能有两个子元素,左侧为菜单(如ListView),右侧为界面。然后将需要监听侧滑事件的控件通过slidingLayout.setScrollEvent(View view)函数设置好就OK。
注:这里实际上是通过view的触摸监听器onTouchListener()实现的
对于上拉/下拉界面,网上比较流传的版本是pull_to_refresh这个库,源代码点击下载。同样,注释非常详细,用法也很简单。需要注意在xml中,RefreshableView标签只能有一个ListView。也就是用于下拉刷新的listview,然后在通过RefreshableView.setOnRefreshListener(PullToRefresh Listener listener, int id)方法设置需要下拉刷新的布局即可完成。
**注:同样,在内部这里也是通过控件的触摸监听器完成的。
以上两种界面分开做都有很多简单易用的库,但是当合在一起的时候会发现容易有冲突。主要原因是两者实现原理都是通过监听控件的触摸事件完成,而大多数时候我们需要下拉的和侧滑的都是同样一个控件,这样就会导致同一个控件被设置了两次setOnTouchListener(),结果后一次的会覆盖掉前一次的,这也就是为什么我们会发现二者无法兼容。我所想到的解决方案有以下几种:
既然同一个布局只能设置一次触摸监听器,那么只有让下拉刷新和侧滑分别对不同的控件进行监听。这里很明显下拉刷新肯定是要对listview进行操作的,那么我们需要修改的就是侧滑的监听事件。可以将侧滑的setScrollEvent参数设置为listview的父布局,然后在父布局中判断用户的触摸行为。如果判定用户动作为上下滑动,则将触摸事件传递给子布局处理,即下拉刷新。反之如果判定为左右滑动,则在父布局中直接拦截事件,并在父布局中处理事件,即侧滑菜单。具体操作如下:
1. 新建一个自定义布局,作为下拉的listview父布局。并通过setScrollEvent对父控件加入侧滑监听。
2. 在父布局中覆盖onInterceptTouchEvent方法。用于拦截触摸事件。
3. 接着覆盖onTouch方法,当事件被拦截时,调用本类中的onTouch处理触摸事件。
4. 通过setOnRefreshListener对listview加入下拉刷新功能。
其中需要了解onInterceptTouchEvent的功能。主要用于拦截事件,当控件被触摸的时此方法第一个被调用,返回true则父布局拦截,事件不会传入子布局(即listview)。而在本布局的onTouch方法中处理。若返回false,则事件被传入子布局处理。
这样在父布局中判断用户行为,即可将侧滑和下拉分开处理。
这个方法虽然可以实现功能,但感觉不太灵活,而且本人真机测试后会有明显卡顿现象,最后没有使用方法一,而是使用下面的方法。
谷歌官方在android-support-v4支持包中加入了下拉刷新类库SwipeRefreshLayout。查看官方源码后发现底层并非简单的监听onTouch事件完成,可以完美的解决冲突问题。用法也很简单。
导入v4支持包之后,将要下拉的控件(如listview)布局外再套一个SwipeRefreshLayout布局即可,然后通过refreshableView.setOnRefreshListener()方法设置一个监听内部类即可:
refreshableView.setOnRefreshListener(new OnRefreshListener()
{
@Override
public void onRefresh()
{
//tbd
}
});
官方的支持包中只有下拉刷新功能,如果需要上拉加载更多,需要对官方包进行扩展。方法如下:
写一个自定义布局继承自SwipeRefreshLayout(直接使用官方的下拉)。然后再里面加入上拉加载的代码,如下:
/** * 继承自SwipeRefreshLayout,从而实现滑动到底部时上拉加载更多的功能. */
public class RefreshLayout extends SwipeRefreshLayout implements OnScrollListener {
/** * 滑动到最下面时的上拉操作 */
private int mTouchSlop;
/** * listview实例 */
private ListView mListView;
/** * 上拉监听器, 到了最底部的上拉加载操作 */
private OnLoadListener mOnLoadListener;
/** * ListView的加载中footer */
private View mListViewFooter;
/** * 按下时的y坐标 */
private int mYDown;
/** * 抬起时的y坐标, 与mYDown一起用于滑动到底部时判断是上拉还是下拉 */
private int mLastY;
/** * 是否在加载中 ( 上拉加载更多 ) */
private boolean isLoading = false;
/** * @param context */
public RefreshLayout(Context context)
{
this(context, null);
}
public RefreshLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mListViewFooter = LayoutInflater.from(context).inflate(
R.layout.pull_up_refresh, null, false);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom)
{
super.onLayout(changed, left, top, right, bottom);
// 初始化ListView对象
if (mListView == null)
{
getListView();
}
}
/** * 获取ListView对象 */
private void getListView()
{
int childs = getChildCount();
if (childs > 0)
{
View childView = getChildAt(0);
if (childView instanceof ListView)
{
mListView = (ListView) childView;
// 设置滚动监听器给ListView, 使得滚动的情况下也可以自动加载
mListView.setOnScrollListener(this);
Log.d(VIEW_LOG_TAG, "### 找到listview");
}
}
}
/* * (non-Javadoc) * * @see android.view.ViewGroup#dispatchTouchEvent(android.view.MotionEvent) */
@Override
public boolean dispatchTouchEvent(MotionEvent event)
{
final int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
// 按下
mYDown = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
// 移动
mLastY = (int) event.getRawY();
break;
case MotionEvent.ACTION_UP:
// 抬起
if (canLoad())
{
loadData();
}
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
/** * 是否可以加载更多, 条件是到了最底部, listview不在加载中, 且为上拉操作. * * @return */
private boolean canLoad()
{
return isBottom() && !isLoading && isPullUp();
}
/** * 判断是否到了最底部 */
private boolean isBottom()
{
if (mListView != null && mListView.getAdapter() != null)
{
return mListView.getLastVisiblePosition() == (mListView
.getAdapter().getCount() - 1);
}
return false;
}
/** * 是否是上拉操作 * * @return */
private boolean isPullUp()
{
return (mYDown - mLastY) >= mTouchSlop;
}
/** * 如果到了最底部,而且是上拉操作.那么执行onLoad方法 */
private void loadData()
{
if (mOnLoadListener != null)
{
// 设置状态
setLoading(true);
//
mOnLoadListener.onLoad();
}
}
/** * @param loading */
public void setLoading(boolean loading)
{
isLoading = loading;
if (isLoading)
{
mListView.addFooterView(mListViewFooter);
}
else
{
mListView.removeFooterView(mListViewFooter);
mYDown = 0;
mLastY = 0;
}
}
/** * @param loadListener */
public void setOnLoadListener(OnLoadListener loadListener)
{
mOnLoadListener = loadListener;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState)
{
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount)
{
// 滚动时到了最底部也可以加载更多
if (canLoad())
{
loadData();
}
}
/** * 加载更多的监听器 */
public static interface OnLoadListener {
public void onLoad();
}
}
主要是判断下滑过程中是否到了最底部来实现加载更多。最后在通过setOnLoadListener()设置回调监听类即可完成:
refreshableView.setOnLoadListener(new OnLoadListener()
{
@Override
public void onLoad()
{
currentPage++;
new HttpThread(FragmentAbstract.this, handlerLoadMore).start();
}
});
效果还是挺不错的。
到此,侧滑菜单以及上拉/下拉二者的兼容问题可以得到很好的解决,如果各位朋友有更好的解决方案,欢迎给我留言,相互讨论,共同进步!
谢谢!!
——超低空