自定义ListView实现仿QQ消息列表滑动item出现删除按钮

来源:互联网 发布:js undefined判断 编辑:程序博客网 时间:2024/06/05 15:54

最近项目组中要出线下版产品,其中一个界面需要实现一个右滑Listview中item,然后可以删除和重命名该item。正好我最近在看自定义控件,所以自荐做这个页面的交互设计,也算练手。刚开始我想的太简单了,我以为添加一个滑动的action,然后item去响应这个action就好了,至于滑动多少距离,或者说item滚动多少距离,不同的滚动会有怎样的效果,好多实际的问题没有考虑进去,当初自己写出来,也是各种crash。所以上网去看别人写的,后来看了一个,模仿写了一个只能左滑,但是项目需求右滑,我就觉得这简单嘛,结果反过来后滑是滑了,item设置的一些控件没有显示出来,也是醉。最终找到一个比较合适的,然后根据那个自己重写了一下ListView,和一个适配器Adapter。

首先写一个类继承ListView,我定义为CustomListView:

public class CustomListView extends ListView {/**禁止侧滑模式*/public static int MOD_FORBID = 0;/**从左向右滑出菜单模式*/public static int MOD_LEFT = 1;/**当前的模式*/private int mode = MOD_FORBID;/**左侧菜单的长度*/private int leftLength = 0;/** * 当前滑动的ListView position */private int slidePosition;/** * 手指按下X的坐标 */private int downX;/** * 手指按下Y的坐标 */private int downY;/** * ListView的item */private View itemView;/** * 滑动类 */private Scroller scroller;/** * 认为是用户滑动的最小距离 */private int mTouchSlop;/** * 判断是否可以侧向滑动 */private boolean canMove = false;/** * 标示是否完成侧滑 */private boolean isSlided = false;public CustomListView(Context context) {this(context,null);}public CustomListView(Context context, AttributeSet attrs) {this(context, attrs,0);}public CustomListView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);scroller = new Scroller(context);mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();}//初始化菜单的划出模式,供外部调用public void initSlideMode(int mode){this.mode = mode;}/** * 处理我们拖动ListView item的逻辑 */@Overridepublic boolean onTouchEvent(MotionEvent ev) {final int action = ev.getAction();int lastX = (int) ev.getX();switch (action) {case MotionEvent.ACTION_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 viewitemView = getChildAt(slidePosition - getFirstVisiblePosition());/*此处根据设置的滑动模式,自动获取左侧菜单的长度*/if(this.mode == MOD_LEFT){this.leftLength = -itemView.getPaddingLeft();}break;case MotionEvent.ACTION_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_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_LEFT){/*向左滑*/itemView.scrollTo(deltaX, 0);}else{itemView.scrollTo(0, 0);}return true; // 拖动的时候ListView不滚动}case MotionEvent.ACTION_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_LEFT){/*从左向右滑*/if (itemView.getScrollX() <= -leftLength / 2) {scrollRight();} else {// 滚回到原始位置scrollBack();}}else{// 滚回到原始位置scrollBack(); }}/** * 往右滑动,getScrollX()返回的是左边缘的距离,就是以View左边缘为原点到开始滑动的距离,所以向右边滑动为负值 */private void scrollRight() {isSlided = true;final int delta = (leftLength + itemView.getScrollX());// 调用startScroll方法来设置一些滚动的参数,我们在computeScroll()方法中调用scrollTo来滚动itemscroller.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}@Overridepublic void computeScroll() {// 调用startScroll的时候scroller.computeScrollOffset()返回true,if (scroller.computeScrollOffset()) {// 让ListView item根据当前的滚动偏移量进行滚动itemView.scrollTo(scroller.getCurrX(), scroller.getCurrY());postInvalidate();}}/** * 提供给外部调用,用以将侧滑出来的滑回去 */public void slideBack() {this.scrollBack();}}
这里定义了几个常量,负责控制item是否可以滑动,这样的好处是方便管理,和控制。然后用到了Scroller类,这个类负责item的滚动位置。这里注意,因为继承的ListView,所以在解析xml文件时,调用的构造函数是一个参数的构造函数,而我们需要的是三个参数的,所以要一步一步的调用到三个参数的构造函数。这里面做些初始化工作。所有滑动的逻辑都是响应了touch事件,所以实现onTouchEvent方法,里面就是一些坐标的计算,当计算完后,会调用实现的scroller类里面的方法,去完成滑动。这里使用的padding实现的,我也看过几个margin实现的,但是觉得没有这个好。

我写的item的布局文件如下:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="80dp"    android:paddingLeft="-160dp"    android:paddingRight="0dp"    android:background="#ffffff"    android:orientation="horizontal" >    <RelativeLayout         android:layout_width="160dp"        android:layout_height="80dp"        >         <LinearLayout         android:id="@+id/option"        android:layout_width="160dp"        android:layout_height="match_parent"        android:background="#ffffff"        android:orientation="horizontal"        >        <RelativeLayout             android:id="@+id/delete"            android:layout_width="80dp"            android:layout_height="match_parent"            android:clickable="true">            <TextView                 android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_centerInParent="true"                android:textSize="15sp"                android:text="删除"/>        </RelativeLayout>        <View android:layout_width="1dp"            android:layout_height="50dp"            android:layout_gravity="center_vertical"            android:background="#ededed"/>        <RelativeLayout             android:id="@+id/rename"            android:layout_width="80dp"            android:layout_height="match_parent"            android:clickable="true">            <TextView                 android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_centerInParent="true"                android:textSize="15sp"                android:text="重命名"/>        </RelativeLayout>    </LinearLayout>    <View android:layout_width="match_parent"            android:layout_height="1dp"            android:layout_alignParentBottom="true"            android:background="#ededed"/>    </RelativeLayout>    <View android:layout_width="1dp"            android:layout_height="70dp"            android:layout_gravity="center_vertical"            android:background="#ededed"/>    <RelativeLayout         android:layout_width="match_parent"        android:layout_height="80dp"        >        <RelativeLayout android:layout_width="match_parent"            android:layout_height="match_parent"            >            <TextView                 android:id="@+id/script_name"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_centerVertical="true"                android:layout_marginLeft="10dp"                android:textColor="#000000"                android:textSize="20sp"                />        </RelativeLayout>        <View android:layout_width="match_parent"            android:layout_height="1dp"            android:layout_alignParentBottom="true"            android:background="#ededed"/>    </RelativeLayout></LinearLayout>
在主布局文件中直接写入自定义的ListView就ok了。

之后重写了一个Adapter,去适配这个listview,及它的item,如下:

public class SlideAdapter extends BaseAdapter {private List<String> data;private LayoutInflater mInflate;private Context context;private CustomListView listView;private onDeleteListener deleteListener;private onRenameListener renameListener;public interface onDeleteListener {public void deleteScript(int position);}public interface onRenameListener {public void renameScript(int position);}public SlideAdapter(List<String> data, Context context, CustomListView listView) {this.data = data;this.context = context;this.mInflate = LayoutInflater.from(context);this.listView = listView;}public SlideAdapter(List<String> data, Context context, CustomListView listView, onDeleteListener deleteListener,onRenameListener renameListener) {this.data = data;this.context = context;this.mInflate = LayoutInflater.from(context);this.listView = listView;this.deleteListener = deleteListener;this.renameListener = renameListener;}@Overridepublic int getCount() {return data.size();}@Overridepublic Object getItem(int position) {return data.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(final int position, View convertView, ViewGroup parent) {ViewHolder holder = new ViewHolder();if (convertView == null) {convertView = mInflate.inflate(R.layout.script_list_item, null);holder.title = (TextView) convertView.findViewById(R.id.script_name);holder.delete = (RelativeLayout) convertView.findViewById(R.id.delete);holder.rename = (RelativeLayout) convertView.findViewById(R.id.rename);convertView.setTag(holder);} else {holder = (ViewHolder) convertView.getTag();}String name = data.get(position);holder.title.setText(name);holder.delete.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {// 刪除操作if(deleteListener!=null){deleteListener.deleteScript(position);listView.slideBack();}}});holder.rename.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {//重命名if(renameListener!=null){renameListener.renameScript(position);listView.slideBack();}}});return convertView;}private static class ViewHolder {public TextView title;public RelativeLayout delete;public RelativeLayout rename;}}

这里想的是,如果我确定了listview中item的类型,我就完全封装这个adapter,至于右滑出现的按钮的响应事件,使用回调函数在使用这个自定义Listview的Activity自己去实现,那样就会很方便开发人员。ListView并没有进行什么优化,这方面我还得再找时间学习。

最后在主Activity中使用该Listview就好了,

public class MainActivity extends Activity implements onDeleteListener,onRenameListener{private CustomListView mCustomListView;SlideAdapter mAdapter;private List<String> mData;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mData = new ArrayList<String>();mData.add("第一个测试用例");mData.add("第二个测试用例");mData.add("第三个测试用例");mCustomListView = (CustomListView) findViewById(R.id.custom_list);mCustomListView.initSlideMode(CustomListView.MOD_LEFT);mAdapter = new SlideAdapter(mData, getApplicationContext(), mCustomListView,this,this);mCustomListView.setAdapter(mAdapter);}@Overridepublic void renameScript(int position) {mData.set(position, "更改后的名称");mAdapter.notifyDataSetChanged();}@Overridepublic void deleteScript(int position) {mData.remove(position);mAdapter.notifyDataSetChanged();}}

这里我很简单的添加了几个数据便于测试。

运行效果如下(我依然不会改成gif……T_T):


参考了这位大神的博客:http://www.2cto.com/kf/201407/319941.html

自定义控件的路还有好长。



0 0
原创粉丝点击