1.1 高仿QQ对话列表左滑删除的功能(作者不详,发现于公司年代久远的项目,拿来学习一下,看看思路),滑动item时显示操作菜单,只要划出来了,你想干嘛就好办了。。
其实思路还是挺简单的,就是每个item多了几个操作菜单,这个ItemView其实就是两部分构成的,一个是本来的ContentView,一个是显示菜单的MenuView。我们要做的只是,找个Framelayout把两个view放在一起。然后通过手势去更新两个view的位置就OK了。
我们从简单的搞起
第一步,先搞划出的菜单:
4.1
BaseMenuItem.java 这就是单个菜单的实体类。设置菜单icon和文字信息的。别忘了set和get方法。
public class BaseMenuItem {
private int id;
private Context mContext;
private String title;
private Drawable icon;
private Drawable background;
private int titleColor;
private int titleSize;
private int width;
public BaseMenuItem(Context context) {
mContext = context;
}
}
4.2
BaseMenuGroups.java 这个类就是BaseMenuItem的集合,这么没什么说的。同样,别忘了set和get方法。
public class BaseMenuGroups {
private Context mContext;
private List mItems;
private int mViewType;
public BaseMenuGroups(Context context) {
mContext = context;
mItems = new ArrayList();
}
}
4.3
BaseMenuLayout.java 现在就来说这个装多个菜单的容器了。这部分就是划出可见的那部分,我们继承Linearlayout并实现OnClickListener接口,毕竟有菜单要能点击。从BaseMenuLayout的构造方法传入需要的菜单BaseMenuGroups。
a:先准备文字和图标的工具
//显示icon的View
private ImageView createIcon(BaseMenuItem item) {
ImageView iv = new ImageView(getContext());
iv.setImageDrawable(item.getIcon());
return iv;
}
//显示文字的view
private TextView createTitle(BaseMenuItem item) {
TextView tv = new TextView(getContext());
tv.setText(item.getTitle());
tv.setGravity(Gravity.CENTER);
tv.setTextSize(item.getTitleSize());
tv.setTextColor(item.getTitleColor());
return tv;
}
b:创建我们单个菜单的view,这里用一个LinearLayout来装我们的图标和文字,至于是上下还是左右,就是看你喜好了。我们这里设置图标上,文字下。
这个参数id,当然是指BaseMenuLayout中的第几个菜单,不然我们怎么点啊。
private void addItem(BaseMenuItem item, int id) {
LayoutParams params = new LayoutParams(item.getWidth(),
LayoutParams.MATCH_PARENT);
LinearLayout parent = new LinearLayout(getContext());
parent.setId(id);
parent.setGravity(Gravity.CENTER);
parent.setOrientation(LinearLayout.VERTICAL);
parent.setLayoutParams(params);
parent.setBackgroundDrawable(item.getBackground());
parent.setOnClickListener(this);
//把菜单添加到我们的BaseMenuLayout中去。
addView(parent);
//添加图标
if (item.getIcon() != null) {
parent.addView(createIcon(item));
}
//添加标题
if (!TextUtils.isEmpty(item.getTitle())) {
parent.addView(createTitle(item));
}
}
这里还需要一个位置信息。不然我怎么知道是点的Listview的哪一行的哪一个菜单啊,so,来自于Listview的item的position。
private int position;
public int getPosition() {
return position;
}
public void setPosition(int position) {
this.position = position;
}
光这样还不行,总得把单个小菜单的位置搞出去吧。
private OnSwipeItemClickListener onItemClickListener;
public static interface OnSwipeItemClickListener {
void onItemClick(BaseMenuLayout view, BaseMenuGroups menu, int index);
}
其实这里还有其他的判断,就是要在我们的菜单处于显示的时候才能点击菜单,后面会上传demo。
再来说一说构造方法中的代码:把传入的BaseMenuGroups 中的全部菜单都添加到BaseMenuLayout中,按照顺序设置菜单的id
public BaseMenuLayout(BaseMenuGroups menu) {
super(menu.getContext());
mMenu = menu;
List items = menu.getMenuItems();
int id = 0;
for (BaseMenuItem item : items) {
addItem(item, id++);
}
}
4.4
接下来构建一个接口,BaseMenuCreator.java,只有一个实现方法,便于我们在外部设置菜单。怎么用这个,后面会讲到。
void create(BaseMenuGroups menu);
4.5
现在该做一个组装我们最先提到的ContentView和显示菜单的MenuView的BaseViewAdapter.java了,这个BaseViewAdapter并不是ListView的数据Adapter。在这个adapter里面做的事情有,组装MenuView,并且把MenuView和ContentView传入BaseItemViewLayout(这里就比较复杂了,接下来会提到),由BaseItemViewLayout来组装完整的Listview的item。
这里我们只说getView方法:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
BaseItemViewLayout layout = null;
if (convertView == null) {
View contentView = mAdapter.getView(position, convertView, parent);
BaseMenuGroups menu = new BaseMenuGroups(mContext);
menu.setViewType(mAdapter.getItemViewType(position));
createMenu(menu);
BaseMenuLayout menuView = new BaseMenuLayout(menu);
menuView.setOnSwipeItemClickListener(this);
//但这里都是在组装菜单的数据,接下来就是把contentView 和menuView 传到BaseItemViewLayout去了。剩下的就是处理view的复用了
DelListView listView = (DelListView) parent;
layout = new BaseItemViewLayout(contentView, menuView,
listView.getCloseInterpolator(),
listView.getOpenInterpolator());
layout.setPosition(position);
} else {
layout = (BaseItemViewLayout) convertView;
layout.closeMenu();
layout.setPosition(position);
View view = mAdapter.getView(position, layout.getContentView(),parent);
/**
* 可能有一些小伙伴觉得view都没使用,就觉得这样代码没什么用,其实这里是为了复用,关键在于layout.getContentView()。
*/
}
return layout;
}
4.6
下面一起来看看这个BaseItemViewLayout.java了。感觉这就是精华之一所在了。
首先说一下这里面的布局情况:
LayoutParams contentParams = new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT);
mContentView.setLayoutParams(contentParams);
mMenuView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT));
addView(mContentView);
addView(mMenuView);
很显然,感觉我们的menuView被顶到屏幕外面去了。但是,光感觉怎么行呢,看下面的代码就知道,肯定在屏幕外,至少我们看不到。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mMenuView.measure(MeasureSpec.makeMeasureSpec(0,
MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(
getMeasuredHeight(), MeasureSpec.EXACTLY));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mContentView.layout(0, 0, getMeasuredWidth(),
mContentView.getMeasuredHeight());
mMenuView.layout(getMeasuredWidth(), 0,
getMeasuredWidth() + mMenuView.getMeasuredWidth(),
mContentView.getMeasuredHeight());
}
其实处理手势是最麻烦的了。因为有几种情况:
第一种,当快速滑动打开、快速滑动关闭、打开状态,点击一下关闭时,动作不跟手:
//关闭,动作不跟手
public void smoothCloseMenu() {
state = STATE_CLOSE;
mBaseX = -mContentView.getLeft();
mCloseScroller.startScroll(0, 0, mBaseX, 0, 350);//这里五个参数的意思是begin(x,y) end(x,y) duration;
postInvalidate();
}
//打开,动作不跟手
public void smoothOpenMenu() {
state = STATE_OPEN;
mOpenScroller.startScroll(-mContentView.getLeft(), 0,
mMenuView.getWidth(), 0, 350);
postInvalidate();
}
第二种:动作跟手时打开与关闭:这里的这个dis是根据手指滑动的距离算出来的
private void swipe(int dis) {
if (dis > mMenuView.getWidth()) {
dis = mMenuView.getWidth();
}
if (dis < 0) {
dis = 0;
}
mContentView.layout(-dis, mContentView.getTop(),
mContentView.getWidth() - dis, getMeasuredHeight());
mMenuView.layout(mContentView.getWidth() - dis, mMenuView.getTop(),
mContentView.getWidth() + mMenuView.getWidth() - dis,
mMenuView.getBottom());
}
除开这两个处理滑动的方法之外,还有一个很重要的方法computeScroll(),这个方法会在Scroller.startScroll()的时候自动执行,这个方法的作用是计算滑动量的。没有这个方法,活动的时候,会显得卡顿。
@Override
public void computeScroll() {
if (state == STATE_OPEN) {
if (mOpenScroller.computeScrollOffset()) {
swipe(mOpenScroller.getCurrX());
postInvalidate();
}
} else {
if (mCloseScroller.computeScrollOffset()) {
swipe(mBaseX - mCloseScroller.getCurrX());
postInvalidate();
}
}
}
然后就是对手势的监听了:这里的触摸事件肯定来自于最外层的ListView的触摸事件:所以:写一个方法等着MotionEvent传进来处理就好:这里就判断了是快速滑动打开,还是跟手滑动打开了,代码很简单。
public boolean onSwipe(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = (int) event.getX();
isFling = false;
break;
case MotionEvent.ACTION_MOVE:
int dis = (int) (mDownX - event.getX());
if (state == STATE_OPEN) {
dis += mMenuView.getWidth();
}
Log.d("DIS>>>>>>>>>>", dis+"");
swipe(dis);
break;
case MotionEvent.ACTION_UP:
if (isFling || (mDownX - event.getX()) > (mMenuView.getWidth() / 2)) {
// open
smoothOpenMenu();
} else {
// close
smoothCloseMenu();
return false;
}
break;
}
return true;
}
4.7
现在就是精华之二所在了。我们的核心,DelListView.java了
这里面有两个重要的部分了:
第一个就是:我们的BaseMenuCreator终于排上用场了:
重写Listview的SetAdapter方法,把我们set进来的数据adapter传出我们的BaseViewAdapter,组建合成最终的itemView;并且重写BaseViewAdapter的createMenu方法,实现我们的BaseMenuCreator接口。这个接口实例从外部传进来;然后在重写BaseViewAdapter的onItemClick方法,把item的position菜单的position回调回来。
@Override
public void setAdapter(ListAdapter adapter) {
adapters = adapter;
super.setAdapter(new BaseViewAdapter(getContext(), adapter) {
@Override
public void createMenu(BaseMenuGroups menu) {
if (mMenuCreator != null) {
mMenuCreator.create(menu);
}
}
@Override
public void onItemClick(BaseMenuLayout view, BaseMenuGroups menu,
int index) {
if (mOnMenuItemClickListener != null) {
mOnMenuItemClickListener.onMenuItemClick(
view.getPosition(), menu, index);
}
if (mTouchView != null) {
mTouchView.smoothCloseMenu();
}
}
});
}
第二个就是:处理手势,处理完成后传入BaseItemViewLayout.onSwipe(MotionEvent event)方法:
这里要处理的就比BaseItemViewLayout多得多了。这里要判断点击的位置,菜单是否打开,滑动的方向。
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() != MotionEvent.ACTION_DOWN && mTouchView == null)//有父类处理事件
return super.onTouchEvent(ev);
int action = MotionEventCompat.getActionMasked(ev);
action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
int oldPos = mTouchPosition;
mDownX = ev.getX();
mDownY = ev.getY();
mTouchState = TOUCH_STATE_NONE;
//通过点击的位置来判断点在listview的那个位置上。
mTouchPosition = pointToPosition((int) ev.getX(), (int) ev.getY());
if (mTouchPosition == oldPos && mTouchView != null
&& mTouchView.isOpen()) {
mTouchState = TOUCH_STATE_X;
mTouchView.onSwipe(ev);
return true;
}
View view = getChildAt(mTouchPosition - getFirstVisiblePosition());
;
if (mTouchView != null && mTouchView.isOpen()) {
mTouchView.smoothCloseMenu();
mTouchView = null;
return super.onTouchEvent(ev);
}
if (view instanceof BaseItemViewLayout) {
mTouchView = (BaseItemViewLayout) view;
/**
* 可以通过item中某一个子view的显示还是隐藏来控制该item能不能别左滑出菜单
* 假如现在item中的tv_value_gone控件处于显示的状态,那么该item则不能被左滑出菜单
*/
if(signViewId!=-1){
isshow = mTouchView.findViewById(signViewId).isShown();
}
}
if (!isshow) {
if (mTouchView != null) {
mTouchView.onSwipe(ev);
}
}
break;
case MotionEvent.ACTION_MOVE:
float dy = Math.abs((ev.getY() - mDownY));
float dx = Math.abs((ev.getX() - mDownX));
if (mTouchState == TOUCH_STATE_X) {
if (!isshow) {
if (mTouchView != null) {
mTouchView.onSwipe(ev);
}
}
getSelector().setState(new int[] { 0 });
ev.setAction(MotionEvent.ACTION_CANCEL);
super.onTouchEvent(ev);
return true;
} else if (mTouchState == TOUCH_STATE_NONE) {
if (Math.abs(dy) > MAX_Y) {
mTouchState = TOUCH_STATE_Y;
} else if (dx > MAX_X) {
mTouchState = TOUCH_STATE_X;
if (mOnSwipeListener != null) {
mOnSwipeListener.onSwipeStart(mTouchPosition);
}
}
}
break;
case MotionEvent.ACTION_UP:
if (mTouchState == TOUCH_STATE_X) {
if (!isshow) {
if (mTouchView != null) {
mTouchView.onSwipe(ev);
if (!mTouchView.isOpen()) {
mTouchPosition = -1;
mTouchView = null;
}
}
}
if (mOnSwipeListener != null) {
mOnSwipeListener.onSwipeEnd(mTouchPosition);
}
ev.setAction(MotionEvent.ACTION_CANCEL);
super.onTouchEvent(ev);
return true;
}
break;
}
return super.onTouchEvent(ev);
}
剩下的就是接口回调参数了。这部分代码请看demo吧。
使用方法很简单。大家看demo吧
demo下载地址:http://download.csdn.net/detail/u010886975/9718243
估计有人可能对有些地方有疑问,请自行查看:
1、setViewType:https://www.pocketdigi.com/20130731/1155.html
2、computeScroll():http://www.linuxidc.com/Linux/2016-01/127276.htm