项目中需要有listview侧拉出菜单,点击可以置顶,撤销,暂停,删除的功能 在此记录一下。
需求是侧拉可以拉出菜单,然后点击侧拉菜单的每个条目有相应的点击事件处理,而且再次侧拉的图标需要改变,再加上上拉加载更多,下拉刷新。安卓原生的listview不像苹果的tableview一样自带侧拉接口,所有肯定需要自定义控件了,所以刚开始试了3种实现方式
先看效果图(这是3的效果图)
[demo](https://github.com/PangHaHa12138/EasyMenuListViewDemo)
1.第一种就是把listview的item布局设置成paddingleft或者paddingright是负的,然后继承listview 重写onTouchEvent方法,然在move时候判断x坐标变化,以此来判断左滑还是右滑,然后滑出来的点击事件在adapter里操作,因为我侧拉置顶或者别的需要点击事件继续请求服务器,实际操作中很麻烦而且换不了侧拉后的图标,所以planA放弃了
package com.hxzh.uniwill.lingjian.View; /** * Created by pang on 2017/3/30. */ import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.widget.AdapterView; import android.widget.ListView; import android.widget.Scroller; /** * 侧向滑出菜单的ListView * 使用请注意与ListView的Item的布局配合, * 该效果的实现是基于在Item的布局中通过设置PaddingLeft和PaddingRight来隐藏左右菜单的, * 所以使用此ListView时,请务必在布局Item时使用PaddingLeft和PaddingRight; * 或者自己改写此ListView,已达到想要的实现方式 * */ public class SlideListView2 extends ListView { /**禁止侧滑模式*/ public static int MOD_FORBID = 0; /**从左向右滑出菜单模式*/ public static int MOD_LEFT = 1; /**从右向左滑出菜单模式*/ public static int MOD_RIGHT = 2; /**左右均可以滑出菜单模式*/ public static int MOD_BOTH = 3; /**当前的模式*/ private int mode = MOD_FORBID; /**左侧菜单的长度*/ private int leftLength = 0; /**右侧菜单的长度*/ private int rightLength = 0; /** * 当前滑动的ListView position */ private int slidePosition; /** * 手指按下X的坐标 */ private int downY; /** * 手指按下Y的坐标 */ private int downX; /** * ListView的item */ private View itemView; /** * 滑动类 */ private Scroller scroller; /** * 认为是用户滑动的最小距离 */ private int mTouchSlop; /** * 判断是否可以侧向滑动 */ private boolean canMove = false; /** * 标示是否完成侧滑 */ private boolean isSlided = false; /**后加 * 速度追踪对象 */ private VelocityTracker velocityTracker; private static final int SNAP_VELOCITY = 600; /**后加 * 是否响应滑动,默认为不响应 */ private boolean isSlide = false; private OnDeleteListener listener; public SlideListView2(Context context) { this(context, null); } public SlideListView2(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SlideListView2(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); scroller = new Scroller(context); mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } public void setOnDeleteListener(OnDeleteListener l) { listener = l; } /** * 初始化菜单的滑出模式 * @param mode */ public void initSlideMode(int mode){ this.mode = mode; } /** * 后加+ * 分发事件,主要做的是判断点击的是那个item, 以及通过postDelayed来设置响应左右滑动事件 */ @Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { System.out.println("dispatch-->" + "down"); addVelocityTracker(event); // 假如scroller滚动还没有结束,我们直接返回 if (!scroller.isFinished()) { return false; } downX = (int) event.getX(); downY = (int) event.getY(); slidePosition = pointToPosition(downX, downY); // 无效的position, 不做任何处理 if (slidePosition == AdapterView.INVALID_POSITION) { return super.dispatchTouchEvent(event); } // 获取我们点击的item view itemView = getChildAt(slidePosition - getFirstVisiblePosition()); break; } case MotionEvent.ACTION_MOVE: { System.out.println("dispatch-->" + "move"); if (Math.abs(getScrollVelocity()) > SNAP_VELOCITY || (Math.abs(event.getX() - downX) > mTouchSlop && Math .abs(event.getY() - downY) < mTouchSlop)) { isSlide = true; } break; } case MotionEvent.ACTION_UP: recycleVelocityTracker(); break; } return super.dispatchTouchEvent(event); } /** * 处理我们拖动ListView item的逻辑 */ @Override public boolean onTouchEvent(MotionEvent ev) { final int action = ev.getAction(); int lastX = (int) ev.getX(); switch (action) { case MotionEvent.ACTION_DOWN: System.out.println("touch-->" + "down"); /*当前模式不允许滑动,则直接返回,交给ListView自身去处理*/ if(this.mode == MOD_FORBID){ return super.onTouchEvent(ev); } // 如果处于侧滑完成状态,侧滑回去,并直接返回 if (isSlided) { scrollBack(); return false; } // 假如scroller滚动还没有结束,我们直接返回 if (!scroller.isFinished()) { return false; } downX = (int) ev.getX(); downY = (int) ev.getY(); slidePosition = pointToPosition(downX, downY); // 无效的position, 不做任何处理 if (slidePosition == AdapterView.INVALID_POSITION) { return super.onTouchEvent(ev); } // 获取我们点击的item view itemView = getChildAt(slidePosition - getFirstVisiblePosition()); /*此处根据设置的滑动模式,自动获取左侧或右侧菜单的长度*/ if(this.mode == MOD_BOTH){ this.leftLength = -itemView.getPaddingLeft(); this.rightLength = -itemView.getPaddingRight(); }else if(this.mode == MOD_LEFT){ this.leftLength = -itemView.getPaddingLeft(); }else if(this.mode == MOD_RIGHT){ this.rightLength = -itemView.getPaddingRight(); } break; case MotionEvent.ACTION_MOVE: System.out.println("touch-->" + "move"); if (!canMove && slidePosition != AdapterView.INVALID_POSITION && (Math.abs(ev.getX() - downX) > mTouchSlop && Math.abs(ev .getY() - downY) < mTouchSlop)) { int offsetX = downX - lastX; if(offsetX > 0 && (this.mode == MOD_BOTH || this.mode == MOD_RIGHT)){ /*从右向左滑*/ canMove = true; }else if(offsetX < 0 && (this.mode == MOD_BOTH || this.mode == MOD_LEFT)){ /*从左向右滑*/ canMove = true; }else{ canMove = false; } /*此段代码是为了避免我们在侧向滑动时同时触发ListView的OnItemClickListener时间*/ MotionEvent cancelEvent = MotionEvent.obtain(ev); cancelEvent .setAction(MotionEvent.ACTION_CANCEL | (ev.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); onTouchEvent(cancelEvent); } if (canMove) { /*设置此属性,可以在侧向滑动时,保持ListView不会上下滚动*/ requestDisallowInterceptTouchEvent(true); // 手指拖动itemView滚动, deltaX大于0向左滚动,小于0向右滚 int deltaX = downX - lastX; if(deltaX < 0 && (this.mode == MOD_BOTH || this.mode == MOD_LEFT)){ /*向左滑*/ itemView.scrollTo(deltaX, 0); }else if(deltaX > 0 && (this.mode == MOD_BOTH || this.mode == MOD_RIGHT)){ /*向右滑*/ itemView.scrollTo(deltaX, 0); }else{ itemView.scrollTo(0, 0); } return true; // 拖动的时候ListView不滚动 } case MotionEvent.ACTION_UP: System.out.println("touch-->" + "up"); if (canMove){ canMove = false; scrollByDistanceX(); } break; } // 否则直接交给ListView来处理onTouchEvent事件 return super.onTouchEvent(ev); } /** * 根据手指滚动itemView的距离来判断是滚动到开始位置还是向左或者向右滚动 */ private void scrollByDistanceX() { /*当前模式不允许滑动,则直接返回*/ if(this.mode == MOD_FORBID){ return; } if(itemView.getScrollX() > 0 && (this.mode == MOD_BOTH || this.mode == MOD_RIGHT)){ /*从右向左滑*/ if (itemView.getScrollX() >= rightLength / 2) { scrollLeft(); } else { // 滚回到原始位置 scrollBack(); } }else if(itemView.getScrollX() < 0 && (this.mode == MOD_BOTH || this.mode == MOD_LEFT)){ /*从左向右滑*/ if (itemView.getScrollX() <= -leftLength / 2) { scrollRight(); } else { // 滚回到原始位置 scrollBack(); } }else{ // 滚回到原始位置 scrollBack(); } } /** 后加+ * 添加用户的速度跟踪器 * * @param event */ private void addVelocityTracker(MotionEvent event) { if (velocityTracker == null) { velocityTracker = VelocityTracker.obtain(); } velocityTracker.addMovement(event); } /**后加+ * 移除用户速度跟踪器 */ private void recycleVelocityTracker() { if (velocityTracker != null) { velocityTracker.recycle(); velocityTracker = null; } } /**后加+ * 获取X方向的滑动速度,大于0向右滑动,反之向左 * * @return */ private int getScrollVelocity() { velocityTracker.computeCurrentVelocity(1000); int velocity = (int) velocityTracker.getXVelocity(); return velocity; } /** * 往右滑动,getScrollX()返回的是左边缘的距离,就是以View左边缘为原点到开始滑动的距离,所以向右边滑动为负值 */ private void scrollRight() { isSlided = true; final int delta = (leftLength + itemView.getScrollX()); // 调用startScroll方法来设置一些滚动的参数,我们在computeScroll()方法中调用scrollTo来滚动item scroller.startScroll(itemView.getScrollX(), 0, -delta, 0, Math.abs(delta)); postInvalidate(); // 刷新itemView } /** * 向左滑动,根据上面我们知道向左滑动为正值 */ private void scrollLeft() { isSlided = true; final int delta = (rightLength - itemView.getScrollX()); // 调用startScroll方法来设置一些滚动的参数,我们在computeScroll()方法中调用scrollTo来滚动item scroller.startScroll(itemView.getScrollX(), 0, delta, 0, Math.abs(delta)); postInvalidate(); // 刷新itemView } /** * 滑动会原来的位置 */ private void scrollBack() { isSlided = false; scroller.startScroll(itemView.getScrollX(), 0, -itemView.getScrollX(), 0, Math.abs(itemView.getScrollX())); postInvalidate(); // 刷新itemView } @Override public void computeScroll() { // 调用startScroll的时候scroller.computeScrollOffset()返回true, if (scroller.computeScrollOffset()) { // 让ListView item根据当前的滚动偏移量进行滚动 itemView.scrollTo(scroller.getCurrX(), scroller.getCurrY()); postInvalidate(); } } /** * 提供给外部调用,用以将侧滑出来的滑回去 */ public void slideBack() { this.scrollBack(); } public interface OnDeleteListener { void onDelete(int index); void onTop(int index); } }
2.还是从item角度考虑,把item当作一个viewgroup,耦合性最低,没有冲突,滑动体验非常的棒,类似与ios侧拉的阻塞式体验,也就是侧拉时候禁止上拉下拉,但是实际操作中并不能动态更换图标,所有也放弃了planB(控件没有问题,可能我自己没解决好)
[这里是开源控件的地址](https://github.com/mcxtzhang/SwipeDelMenuLayout)
3.然后就选择了开源的控件使用
[感谢](https://github.com/baoyongzhang/SwipeMenuListView)
3.1
compile 'com.baoyz.swipemenulistview:library:1.3.0'
布局
<android.support.v4.widget.SwipeRefreshLayout android:id="@+id/swiperefreshlayout" android:layout_below="@+id/titlefragment" android:layout_width="match_parent" android:layout_height="match_parent"> <com.baoyz.swipemenulistview.SwipeMenuListView android:id="@+id/swiplistView" android:cacheColorHint="#00000000" android:listSelector="#00000000" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v4.widget.SwipeRefreshLayout>下拉刷新用的是v4包自带的swiperefreshlayout
3.2:然后先看adapter
//这里根据置顶字段来区别position @Override public int getItemViewType(int position) { int type = -1; data = list2.get(position); iszhiding = data.getStick();//置顶字段 switch (iszhiding) { case 0: type = 0; break; case 1: type = 1; break; } return type; } //返回条目类型,侧拉置顶,侧拉取消置顶,-1 @Override public int getViewTypeCount() { return 3; }
就是正常的写法 ,这里是加了两个方法,就是实现多条目布局,getitemviewtype返回条目类型,getviewtypecount返回条目类型个数,然后侧拉出来置顶,和再次侧拉出来取消置顶,两个显示图标其实就是两种条目类型,根据服务器字段显示置顶或是取消置顶的图标,我这里只写了一种菜单,比如侧拉菜单要是多个,比如三个的话,就是6种条目类型,然后根据返回字段判断对应显示的图标,
3.3:然后用代码构造侧拉的菜单,可以设置图标,还有宽度
//侧拉swipmenulistview 构建侧拉菜单 SwipeMenuCreator creator1 = new SwipeMenuCreator() { @Override public void create(SwipeMenu menu) { switch (menu.getViewType()) { case 0: createMenu1(menu); break; case 1: createMenu2(menu); break; } } private void createMenu1(SwipeMenu menu) { //创建置顶项 SwipeMenuItem zditem1 = new SwipeMenuItem(MainActivity.this); //设置item width zditem1.setWidth(dp2px(60)); //设置背景 zditem1.setBackground(R.color.listview_item_right_bg); //图标 zditem1.setIcon(R.drawable.list_slide_but_stick2x); // 添加到菜单 menu.addMenuItem(zditem1); } private void createMenu2(SwipeMenu menu) { //创建置顶项 SwipeMenuItem zditem2 = new SwipeMenuItem(MainActivity.this); //设置item width zditem2.setWidth(dp2px(60)); //设置背景 zditem2.setBackground(R.color.listview_item_right_bg); //图标 zditem2.setIcon(R.drawable.list_slide_but_canstick2x); // 添加到菜单 menu.addMenuItem(zditem2); } };然后设置侧拉菜单的点击事件
//设置创建者 swiplistView.setMenuCreator(creator1); //step 2. listener item click event swiplistView.setOnMenuItemClickListener(new SwipeMenuListView.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(int position, SwipeMenu menu, int index) { data1 = list1.get(position); taskid = data1.getTaskid(); iszhiding1 = data1.getStick();//置顶 //显示置顶 if (iszhiding1 == 0) { try { OkHttpUtils.post(zhiding) .params("userid", userid) .params("taskid", taskid) .execute(new StringCallback() { @Override public void onSuccess(String s, Call call, Response response) { Data_uploadBack_tag back_tag = JsonUtil.parseJsonToBean(s, Data_uploadBack_tag.class); if (back_tag != null) { // ToastUtil.showToast("置顶"); Toast.makeText(MainActivity.this,"置顶",Toast.LENGTH_SHORT).show(); showmore();//置顶服务器排序后从新获取数据刷新页面 } else { // ToastUtil.showToast("置顶失败"); Toast.makeText(MainActivity.this,"置顶失败",Toast.LENGTH_SHORT).show(); } } @Override public void onError(Call call, Response response, Exception e) { super.onError(call, response, e); // ToastUtil.showToast("置顶失败,头皮发麻"); Toast.makeText(MainActivity.this,"置顶失败,头皮发麻",Toast.LENGTH_SHORT).show(); } }); } catch (Exception e) { e.printStackTrace(); } } else if (iszhiding1 == 1) { try { OkHttpUtils.post(quxiaozd) .params("userid", userid) .params("taskid", taskid) .execute(new StringCallback() { @Override public void onSuccess(String s, Call call, Response response) { Data_uploadBack_tag back_tag = JsonUtil.parseJsonToBean(s, Data_uploadBack_tag.class); if (back_tag != null) { // ToastUtil.showToast("取消置顶"); Toast.makeText(MainActivity.this,"取消置顶",Toast.LENGTH_SHORT).show(); showmore(); } else { // ToastUtil.showToast("取消失败"); Toast.makeText(MainActivity.this,"取消失败",Toast.LENGTH_SHORT).show(); } } @Override public void onError(Call call, Response response, Exception e) { super.onError(call, response, e); Toast.makeText(MainActivity.this,"联网失败",Toast.LENGTH_SHORT).show(); } }); } catch (Exception e) { e.printStackTrace(); } } return false; } });上面代码大体就是根据点位置响应点击事件,然后联网,告诉服务器是那个条目被置顶或者别的操作,服务器会重新排序,这是后根据回调,然后重新加载页面,刷新,这样置顶就变成了取消置顶,同时图标也更换了,而且条目位置也会有所改变(这里服务器排序,并非手动置顶)
手动:可以通过修改对应集合数据源,根据侧拉的potion,添加到第一个索引,然后移除当前位置条目,然后刷新adapter,删除的话就直接集合rmove掉对应索引数据然后刷新
这里还有一点需要注意,通过隐藏下拉控件解决侧拉和上拉的冲突
//解决listview与SwipeRefreshLayout滑动冲突问题 swiplistView.setOnMenuStateChangeListener(new SwipeMenuListView.OnMenuStateChangeListener() { @Override public void onMenuOpen(int position) { swipeRefreshLayout.setEnabled(false); } @Override public void onMenuClose(int position) { } });
3.4:然后就是上拉加载更多和下拉刷新
swipeRefreshLayout.setColorSchemeResources(android.R.color.holo_blue_light, android.R.color.holo_green_light, android.R.color.holo_orange_light); //下拉刷新 try { swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { swipeRefreshLayout.setRefreshing(true); Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { showmore(); Toast.makeText(MainActivity.this,"刷新完成",Toast.LENGTH_SHORT).show(); swipeRefreshLayout.setRefreshing(false); } },2000); } }); } catch (Exception e) { e.printStackTrace(); }
联网操作
//下拉刷新 private void showmore() { //联网刷新listview try { OkHttpUtils.get(Sever) .params("pn", 1) .params("size", sizeup) .params("userid", userid) .params("status", 3) .params("sort", 1) .params("type",1) .execute(new StringCallback() { @Override public void onSuccess(String s, Call call, Response response) { shoudaorenwu = JsonUtil.parseJsonToBean(s, Data_chaxunliebiao_shoudaorenwu.class); if (shoudaorenwu != null) { if (list1 != null) {//还有数据 list1.clear(); } list1 = shoudaorenwu.getList(); slideAdapter1 = new SlideAdapter1(MainActivity.this, list1); swiplistView.setAdapter(slideAdapter1); slideAdapter1.notifyDataSetChanged(); }else { Toast.makeText(MainActivity.this,"对不起,没有最新数据了",Toast.LENGTH_SHORT).show(); } } }); } catch (Exception e) { e.printStackTrace(); }上拉就是添加脚布局
开始设成负的,拉的时候判断是否是底部,是就显示脚布局,然后分页加载,加载完,隐藏脚布局
footview = View.inflate(MainActivity.this, R.layout.listview_footer, null); footview.measure(0, 0); footheight = footview.getMeasuredHeight(); // bootm = dp2px(45); footview.setPadding(0, -footheight, 0, 0); swiplistView.addFooterView(footview);
swiplistView.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (scrollState == SCROLL_STATE_IDLE) { int lastposition = swiplistView.getLastVisiblePosition();//最后一个item的位置 if (lastposition == swiplistView.getCount() - 1) { footview.setPadding(0, 0, 0, footheight); Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { onLoadMore(); footview.setPadding(0, -footheight, 0, 0); // Toast.makeText(MainActivity.this,"加载完成",Toast.LENGTH_SHORT).show(); } }, 2000); } } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { boolean enable = false; //这里做判断,只有滑到顶部才出来下拉进度条 if (swiplistView != null && swiplistView.getChildCount() > 0) { // check if the first item of the list is visible boolean firstItemVisible = swiplistView.getFirstVisiblePosition() == 0; // check if the top of the first item is visible boolean topOfFirstItemVisible = swiplistView.getChildAt(0).getTop() == 0; // enabling or disabling the refresh layout enable = firstItemVisible && topOfFirstItemVisible; } swipeRefreshLayout.setEnabled(enable); } });
/**上拉加载更多 * * 分页加载主要逻辑 * 两个集合,一个用来接收最新的数据,他是小集合,因为每次联网对集合长度做了限制,最多10条, * 然后服务器有单独一个字段是服务器总共有多少数据,每次把小集合addll到大集合里, * 再判断大集合的长度是否小于服务器返回总条目数,小于就继续添加,等于或大于,就提示没有更多数据了 * * * */ int m = 1; private void onLoadMore() { //联网刷新listview m++; try { OkHttpUtils.get(Sever) .params("pn", m) .params("size", sizedown) .params("userid", userid) .params("status", 3) .params("sort", 1) .params("type",1) .execute(new StringCallback() { @Override public void onSuccess(String s, Call call, Response response) { shoudaorenwu = JsonUtil.parseJsonToBean(s, Data_chaxunliebiao_shoudaorenwu.class); if (shoudaorenwu.getList()!= null){ list2.clear(); } list2 = shoudaorenwu.getList(); int totle = shoudaorenwu.getTotal();//服务器返回总数 int totle2 = list1.size();//加载的数据 if (totle2 < totle) { list1.addAll(list2); slideAdapter1.notifyDataSetChanged(); } else { m = 1; // Toast.makeText(MainActivity.this,"没有更多数据了",Toast.LENGTH_SHORT).show(); } } }); } catch (Exception e) { e.printStackTrace(); }最后 感谢阅读
[demo地址](https://github.com/PangHaHa12138/EasyMenuListViewDemo)