【Android界面实现】ZListView,一个最强大的刷新、加载、滑动删除的ListView控件(二)

【Android界面实现】ZListView,一个最强大的刷新、加载、滑动删除的ListView控件(一) 转载请注明出处:http://blog.youkuaiyun.com/zhaokaiqiang1992    两个月之前,写过一篇文章【Android界面实现】整合了刷新、加载更多、滑动删除功能的XListview ,介绍了如何整合两个开源项目实现这种功能,但是在实际的使用中,却出现了一些问题,比如说item的点击事件多次调用,item的position不准确,代码量太大太复杂等,一个哥们在自己的工作中使用了 阅读详情


    我们接着上篇的文章说,在前一篇文章中,我们学习了ZListView的使用,这一篇就开始说一些干货了,本篇文章将介绍ZListView的实现原理。

    其实说是ZListView的实现原理,不如说是ZSwipeItem的实现原理,因为ZSwipeItem才是滑动的关键所在。

    ZSwipeItem的滑动,主要是通过ViewDragHelper这个类实现的。在接触这个项目之前,我没听过,也从来碰到过这个类,ViewDragHelper是v4包中的一个帮助类,文档中是这样介绍的:

ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number of useful operations and state tracking for allowing a user to drag and reposition views within their parent ViewGroup.
    看明白的吧?ViewDragHelper就是专门用来帮助自定义我们的ViewGroup的工具类,它提供了一些非常有用的方法和状态堆栈,允许用户去拖拽并且改变ViewGroup里面的View的位置,也就是重定位。这样看来,可能还不是很明确,不着急,我们根据ZSwipeItem的实现代码一点点分析这个类的用法,一会儿你就明白了。
    下面的分析过程,请大家参照着ZListView项目的源代码来看,这样好理解一些。

    ZSwipeItem继承自Framelayout,所以我们实际上是在自定义一个ViewGroup。ZSwipeItem一共有三个构造函数,但是实际上就是一个,因为最终调用的都是下面的这个构造函数

public ZSwipeItem(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);

		mDragHelper = ViewDragHelper.create(this, mDragHelperCallback);

		TypedArray a = context.obtainStyledAttributes(attrs,
				R.styleable.ZSwipeItem);
		// 默认是右边缘检测
		int ordinal = a.getInt(R.styleable.ZSwipeItem_drag_edge,
				DragEdge.Right.ordinal());
		mDragEdge = DragEdge.values()[ordinal];
		// 默认模式是拉出
		ordinal = a.getInt(R.styleable.ZSwipeItem_show_mode,
				ShowMode.PullOut.ordinal());
		mShowMode = ShowMode.values()[ordinal];

		mHorizontalSwipeOffset = a.getDimension(
				R.styleable.ZSwipeItem_horizontalSwipeOffset, 0);
		mVerticalSwipeOffset = a.getDimension(
				R.styleable.ZSwipeItem_verticalSwipeOffset, 0);

		a.recycle();
	}

    在构造函数的第一行代码中,利用ViewDragHelper的工厂方法,创建了一个ViewDragHelper对象。注意后面的第二个参数是ViewDragHelper.Callback对象,我们需要初始化一个这个对象,然后在这里面处理对应的操作。在Callback对象里面,下面几个方法非常重要。

(1) public int clampViewPositionHorizontal(View child, int left, int dx)这个是返回被横向移动的子控件child的左坐标left,和移动距离dx,我们可以根据这些值来返回child的新的left。这个方法必须重写,要不然就不能移动了。

(2)public int clampViewPositionVertical(View child, int top, int dy) 这个和上面的方法一个意思,就是换成了垂直方向的移动和top坐标。如果有垂直移动,这个也必须重写,要不默认返回0,也不能移动了。

(3)public abstract boolean tryCaptureView(View child, int pointerId) 这个方法用来返回可以被移动的View对象,我们可以通过判断child与我们想移动的View是的相等来控制谁能移动。

(4)public int getViewVerticalDragRange(View child) 这个用来控制垂直移动的边界范围,单位是像素。

(5)public int getViewHorizontalDragRange(View child) 和上面一样,就是是横向的边界范围。

(6)public void onViewReleased(View releasedChild, float xvel, float yvel) 当releasedChild被释放的时候,xvel和yvel是x和y方向的加速度

(7)public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 这个是当changedView的位置发生变化时调用,我们可以在这里面控制View的显示位置和移动。

    我们前面虽然获取了ViewDragHelper的对象,但是现在我们还是不能接收到事件的,我们需要在onTouch()和onInterceptTouchEvent()里面,将触摸事件传入到ViewDragHelper里面,才能进行处理,就像下面这样

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

	…

	return mDragHelper.shouldInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
	
	…
	mDragHelper.processTouchEvent(event);
	return true;
}

    传递给ViewDragHelper之后,我们就可以在Callback的各个事件里面进行处理了。

    了解了这些之后,我们再看下面的代码,应该就能够明白什么意思了。

/**
	 * 进行拖拽的主要类
	 */
	private ViewDragHelper.Callback mDragHelperCallback = new ViewDragHelper.Callback() {

		/**
		 * 计算被横向拖动view的left
		 */
		@Override
		public int clampViewPositionHorizontal(View child, int left, int dx) {
			if (child == getSurfaceView()) {
				switch (mDragEdge) {
				case Top:
				case Bottom:
					return getPaddingLeft();
				case Left:
					if (left < getPaddingLeft())
						return getPaddingLeft();
					if (left > getPaddingLeft() + mDragDistance)
						return getPaddingLeft() + mDragDistance;
					break;
				case Right:
					if (left > getPaddingLeft())
						return getPaddingLeft();
					if (left < getPaddingLeft() - mDragDistance)
						return getPaddingLeft() - mDragDistance;
					break;
				}
			} else if (child == getBottomView()) {

				switch (mDragEdge) {
				case Top:
				case Bottom:
					return getPaddingLeft();
				case Left:
					if (mShowMode == ShowMode.PullOut) {
						if (left > getPaddingLeft())
							return getPaddingLeft();
					}
					break;
				case Right:
					if (mShowMode == ShowMode.PullOut) {
						if (left < getMeasuredWidth() - mDragDistance) {
							return getMeasuredWidth() - mDragDistance;
						}
					}
					break;
				}
			}
			return left;
		}

		/**
		 * 计算被纵向拖动的view的top
		 */
		@Override
		public int clampViewPositionVertical(View child, int top, int dy) {
			if (child == getSurfaceView()) {
				switch (mDragEdge) {
				case Left:
				case Right:
					return getPaddingTop();
				case Top:
					if (top < getPaddingTop())
						return getPaddingTop();
					if (top > getPaddingTop() + mDragDistance)
						return getPaddingTop() + mDragDistance;
					break;
				case Bottom:
					if (top < getPaddingTop() - mDragDistance) {
						return getPaddingTop() - mDragDistance;
					}
					if (top > getPaddingTop()) {
						return getPaddingTop();
					}
				}
			} else {
				switch (mDragEdge) {
				case Left:
				case Right:
					return getPaddingTop();
				case Top:
					if (mShowMode == ShowMode.PullOut) {
						if (top > getPaddingTop())
							return getPaddingTop();
					} else {
						if (getSurfaceView().getTop() + dy < getPaddingTop())
							return getPaddingTop();
						if (getSurfaceView().getTop() + dy > getPaddingTop()
								+ mDragDistance)
							return getPaddingTop() + mDragDistance;
					}
					break;
				case Bottom:
					if (mShowMode == ShowMode.PullOut) {
						if (top < getMeasuredHeight() - mDragDistance)
							return getMeasuredHeight() - mDragDistance;
					} else {
						if (getSurfaceView().getTop() + dy >= getPaddingTop())
							return getPaddingTop();
						if (getSurfaceView().getTop() + dy <= getPaddingTop()
								- mDragDistance)
							return getPaddingTop() - mDragDistance;
					}
				}
			}
			return top;
		}

		/**
		 * 确定要进行拖动的view
		 */
		@Override
		public boolean tryCaptureView(View child, int pointerId) {
			return child == getSurfaceView() || child == getBottomView();
		}

		/**
		 * 确定横向拖动边界
		 */
		@Override
		public int getViewHorizontalDragRange(View child) {
			return mDragDistance;
		}

		/**
		 * 确定纵向拖动边界
		 */
		@Override
		public int getViewVerticalDragRange(View child) {
			return mDragDistance;
		}

		/**
		 * 当子控件被释放的时候调用,可以获取加速度的数据,来判断用户意图
		 */
		@Override
		public void onViewReleased(View releasedChild, float xvel, float yvel) {
			super.onViewReleased(releasedChild, xvel, yvel);

			for (SwipeListener l : swipeListeners) {
				l.onHandRelease(ZSwipeItem.this, xvel, yvel);
			}

			if (releasedChild == getSurfaceView()) {
				processSurfaceRelease(xvel, yvel);
			} else if (releasedChild == getBottomView()) {
				if (getShowMode() == ShowMode.PullOut) {
					processBottomPullOutRelease(xvel, yvel);
				} else if (getShowMode() == ShowMode.LayDown) {
					processBottomLayDownMode(xvel, yvel);
				}
			}

			invalidate();
		}

		/**
		 * 当view的位置发生变化的时候调用,可以设置view的位置跟随手指移动
		 */
		@Override
		public void onViewPositionChanged(View changedView, int left, int top,
				int dx, int dy) {

			int evLeft = getSurfaceView().getLeft();
			int evTop = getSurfaceView().getTop();

			if (changedView == getSurfaceView()) {

				if (mShowMode == ShowMode.PullOut) {
					if (mDragEdge == DragEdge.Left
							|| mDragEdge == DragEdge.Right) {
						getBottomView().offsetLeftAndRight(dx);
					} else {
						getBottomView().offsetTopAndBottom(dy);
					}
				}

			} else if (changedView == getBottomView()) {

				if (mShowMode == ShowMode.PullOut) {
					getSurfaceView().offsetLeftAndRight(dx);
					getSurfaceView().offsetTopAndBottom(dy);
				} else {
					Rect rect = computeBottomLayDown(mDragEdge);
					getBottomView().layout(rect.left, rect.top, rect.right,
							rect.bottom);

					int newLeft = getSurfaceView().getLeft() + dx;
					int newTop = getSurfaceView().getTop() + dy;

					if (mDragEdge == DragEdge.Left
							&& newLeft < getPaddingLeft())
						newLeft = getPaddingLeft();
					else if (mDragEdge == DragEdge.Right
							&& newLeft > getPaddingLeft())
						newLeft = getPaddingLeft();
					else if (mDragEdge == DragEdge.Top
							&& newTop < getPaddingTop())
						newTop = getPaddingTop();
					else if (mDragEdge == DragEdge.Bottom
							&& newTop > getPaddingTop())
						newTop = getPaddingTop();

					getSurfaceView().layout(newLeft, newTop,
							newLeft + getMeasuredWidth(),
							newTop + getMeasuredHeight());
				}
			}

			// 及时派发滑动事件
			dispatchSwipeEvent(evLeft, evTop, dx, dy);

			invalidate();
		}
	};

    在这里面有一个变量mDragDistance,这个就是我们进行滑动的范围,这个值的初始化在onMesure()里面

@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		// 初始化移动距离
		if (mDragEdge == DragEdge.Left || mDragEdge == DragEdge.Right)
			mDragDistance = getBottomView().getMeasuredWidth()
					- dp2px(mHorizontalSwipeOffset);
		else {
			mDragDistance = getBottomView().getMeasuredHeight()
					- dp2px(mVerticalSwipeOffset);
		}
	}
    因为没必要对onMeasure()进行自定义,所以调用了父类的onMeasure(),然后,根据后面布局的宽度或者是高度,对滑动范围进行了初始化。

    getBottomView()返回的就是后面的布局ViewGroup对象,而getSurfaceView()返回的则是前面的布局ViewGroup对象。下面是获取方法,这也就解释了为什么在item的布局文件里面,必须嵌套两个ViewGroup对象了。

public ViewGroup getSurfaceView() {
		return (ViewGroup) getChildAt(1);
	}

	public ViewGroup getBottomView() {
		return (ViewGroup) getChildAt(0);
	}
    其实,在onLayout()里面也保证了必须这样做,要不然就崩掉了!
@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		int childCount = getChildCount();
		if (childCount != 2) {
			throw new IllegalStateException("You need 2  views in SwipeLayout");
		}
		if (!(getChildAt(0) instanceof ViewGroup)
				|| !(getChildAt(1) instanceof ViewGroup)) {
			throw new IllegalArgumentException(
					"The 2 children in SwipeLayout must be an instance of ViewGroup");
		}

		if (mShowMode == ShowMode.PullOut) {
			layoutPullOut();
		} else if (mShowMode == ShowMode.LayDown) {
			layoutLayDown();
		}

		safeBottomView();

		if (mOnLayoutListeners != null)
			for (int i = 0; i < mOnLayoutListeners.size(); i++) {
				mOnLayoutListeners.get(i).onLayout(this);
			}

	}

    我们再回过头来仔细看一下Callback里面的一些回调方法。

    下面的办法返回是这样的,意思就是说,前面布局和后面布局都是可以移动的。

@Override
		public boolean tryCaptureView(View child, int pointerId) {
			return child == getSurfaceView() || child == getBottomView();
		}

    而在onViewPositionChange()中,如果位置发生变化的是前面的布局,并且展示方式是PullOut,那么就通过View.offsetLeftAndRight()、View.offsetTopAndBottom()来控制位置,这样,就能够实现前面布局随着手指移动的效果了!

if (changedView == getSurfaceView()) {

				if (mShowMode == ShowMode.PullOut) {
					if (mDragEdge == DragEdge.Left
							|| mDragEdge == DragEdge.Right) {
						getBottomView().offsetLeftAndRight(dx);
					} else {
						getBottomView().offsetTopAndBottom(dy);
					}
				}

			}
    但是如果移动的View是后面的布局的话,就复杂一点了。如果是PullOut,操作是一样的。但是如果是LayDown模式,需要通过layout()来控制显示的位置,具体的计算就不详细说了。
else if (changedView == getBottomView()) {

				if (mShowMode == ShowMode.PullOut) {
					getSurfaceView().offsetLeftAndRight(dx);
					getSurfaceView().offsetTopAndBottom(dy);
				} else {
					Rect rect = computeBottomLayDown(mDragEdge);
					getBottomView().layout(rect.left, rect.top, rect.right,
							rect.bottom);

					int newLeft = getSurfaceView().getLeft() + dx;
					int newTop = getSurfaceView().getTop() + dy;

					if (mDragEdge == DragEdge.Left
							&& newLeft < getPaddingLeft())
						newLeft = getPaddingLeft();
					else if (mDragEdge == DragEdge.Right
							&& newLeft > getPaddingLeft())
						newLeft = getPaddingLeft();
					else if (mDragEdge == DragEdge.Top
							&& newTop < getPaddingTop())
						newTop = getPaddingTop();
					else if (mDragEdge == DragEdge.Bottom
							&& newTop > getPaddingTop())
						newTop = getPaddingTop();

					getSurfaceView().layout(newLeft, newTop,
							newLeft + getMeasuredWidth(),
							newTop + getMeasuredHeight());
				}
			}
    在这个方法的最后,调用了dispatchSwipeEvent(), dispatchSwipeEvent()有两个重载的方法,第一个方法根据滑动边缘方向和移动距离,来判断是否是想要打开,然后就调用了另外一个dispatchSwipeEvent(),代码如下所示。

    在这个方法里面,完成了对swipeListener的各种方法的调用。这里大家可能有个疑问啊,其实每一个item并不是只有一个SwipeListener的,而是两个,一个是我们自己添加的,另外一个是在适配器中,自动添加的,因此这里我们需要遍历取得每一个SwipeListener,然后进行调用。之前的BUG也是因为这个地方出现了问题。

	protected void dispatchSwipeEvent(int surfaceLeft, int surfaceTop,
			boolean open) {

		safeBottomView();
		Status status = getOpenStatus();

		if (!swipeListeners.isEmpty()) {

			mEventCounter++;

			if (mEventCounter == 1) {
				if (open) {
					swipeListeners.get(0).onStartOpen(ZSwipeItem.this);
					swipeListeners.get(swipeListeners.size() - 1).onStartOpen(
							ZSwipeItem.this);
				} else {
					swipeListeners.get(0).onStartClose(ZSwipeItem.this);
					swipeListeners.get(swipeListeners.size() - 1).onStartClose(
							ZSwipeItem.this);
				}
			}

			for (SwipeListener l : swipeListeners) {
				l.onUpdate(ZSwipeItem.this, surfaceLeft - getPaddingLeft(),
						surfaceTop - getPaddingTop());
			}

			if (status == Status.Close) {
				swipeListeners.get(0).onClose(ZSwipeItem.this);
				swipeListeners.get(swipeListeners.size() - 1).onClose(
						ZSwipeItem.this);
				mEventCounter = 0;
			} else if (status == Status.Open) {
				getBottomView().setEnabled(true);
				swipeListeners.get(0).onOpen(ZSwipeItem.this);
				swipeListeners.get(swipeListeners.size() - 1).onOpen(
						ZSwipeItem.this);
				mEventCounter = 0;
			}
		}
	}

    分析了上面这些之后,我们明白了为什么手指在item上滑动的时候,前面布局为什么能跟随手指移动了,下面我们分析为什么放手之后,前面布局能够很优雅的会弹回去。
    如果要了解这个的实现原理,那么我们就需要分析onViewReleased()。这个方法就是在用户的手指离开屏幕的时候触发的,下面是ZSwipeItem中的代码实现

/**
		 * 当子控件被释放的时候调用,可以获取加速度的数据,来判断用户意图
		 */
		@Override
		public void onViewReleased(View releasedChild, float xvel, float yvel) {
			super.onViewReleased(releasedChild, xvel, yvel);

			for (SwipeListener l : swipeListeners) {
				l.onHandRelease(ZSwipeItem.this, xvel, yvel);
			}

			if (releasedChild == getSurfaceView()) {
				processSurfaceRelease(xvel, yvel);
			} else if (releasedChild == getBottomView()) {
				if (getShowMode() == ShowMode.PullOut) {
					processBottomPullOutRelease(xvel, yvel);
				} else if (getShowMode() == ShowMode.LayDown) {
					processBottomLayDownMode(xvel, yvel);
				}
			}

			invalidate();
		}
    在这里面,首先调用了SwipeListener的onHandRelease(),然后根据用户所触摸的布局的不同,分配给了其他的方法,我们以processSurfaceRelease(xvel, yvel)为例,说明到底做了什么。代码如下:
/**
	 * 执行前布局的释放过程
	 * 
	 * @param xvel
	 * @param yvel
	 */
	private void processSurfaceRelease(float xvel, float yvel) {
		if (xvel == 0 && getOpenStatus() == Status.Middle)
			close();

		if (mDragEdge == DragEdge.Left || mDragEdge == DragEdge.Right) {
			if (xvel > 0) {
				if (mDragEdge == DragEdge.Left)
					open();
				else
					close();
			}
			if (xvel < 0) {
				if (mDragEdge == DragEdge.Left)
					close();
				else
					open();
			}
		} else {
			if (yvel > 0) {
				if (mDragEdge == DragEdge.Top)
					open();
				else
					close();
			}
			if (yvel < 0) {
				if (mDragEdge == DragEdge.Top)
					close();
				else
					open();
			}
		}
	}
    在这个方法里面,根据用户拖动方向和x或者是y加速度的大小,调用了open()和close()方法,那么我们在进入这些方法看看。
public void open() {
		open(true, true);
	}

	public void open(boolean smooth) {
		open(smooth, true);
	}

	public void open(boolean smooth, boolean notify) {
		ViewGroup surface = getSurfaceView(), bottom = getBottomView();
		int dx, dy;
		Rect rect = computeSurfaceLayoutArea(true);
		if (smooth) {
			mDragHelper
					.smoothSlideViewTo(getSurfaceView(), rect.left, rect.top);
		} else {
			dx = rect.left - surface.getLeft();
			dy = rect.top - surface.getTop();
			surface.layout(rect.left, rect.top, rect.right, rect.bottom);
			if (getShowMode() == ShowMode.PullOut) {
				Rect bRect = computeBottomLayoutAreaViaSurface(
						ShowMode.PullOut, rect);
				bottom.layout(bRect.left, bRect.top, bRect.right, bRect.bottom);
			}
			if (notify) {
				dispatchSwipeEvent(rect.left, rect.top, dx, dy);
			} else {
				safeBottomView();
			}
		}
		invalidate();
	}
    上面的3个open()实现了方法重载,最后调用的是最后一个。smooth代表是否是平滑移动的,如果是的话,就调用了ViewDragHelper.smoothSlideViewTo()。其实在ViewDragHelper里面有一个Scroller,这个方法就是通过Scroller类来实现的,但是只这样写还不行,我们还需要重写computeScroll(),然后用下面的代码,让滚动一直持续下去,否则View是不会滚动起来的。如果不是smooth的话,就直接layout(),把View的位置定位过去了。
	@Override
	public void computeScroll() {
		super.computeScroll();
		// 让滚动一直进行下去
		if (mDragHelper.continueSettling(true)) {
			ViewCompat.postInvalidateOnAnimation(this);
		}
	}

    如果我们只做好这些,那么SwipeItem的侧滑功能已经差不多了,但是会有一个问题,因为我们重写了onTouch()和onInterceptTouchEvent(),所以如果我们给ListView设置onItemClickListener,是没有反应的,也就是说item的点击事件被我们破坏了!

    怎么办呢?我们可以通过GestureDetector,来模拟item的点击事件,这也就是为什么在ZSwipeItem中需要存在一个手势监听器了。

private GestureDetector gestureDetector = new GestureDetector(getContext(),
			new SwipeDetector());

	/**
	 * 手势监听器,通过调用performItemClick、performItemLongClick,来解决item的点击问题,
	 * 
	 * @class: com.socks.zlistview.SwipeDetector
	 * @author zhaokaiqiang
	 * @date 2015-1-7 下午3:44:09
	 * 
	 */
	private class SwipeDetector extends GestureDetector.SimpleOnGestureListener {
		@Override
		public boolean onDown(MotionEvent e) {
			return true;
		}

		@Override
		public boolean onSingleTapUp(MotionEvent e) {
			// 当用户单击之后,手指抬起的时候调用,如果没有双击监听器,就直接调用
			performAdapterViewItemClick(e);
			return true;
		}

		@Override
		public boolean onSingleTapConfirmed(MotionEvent e) {
			// 这个方法只有在确认用户不会发生双击事件的时候调用
			return false;
		}

		@Override
		public void onLongPress(MotionEvent e) {
			// 长按事件
			performLongClick();
		}

		@Override
		public boolean onDoubleTap(MotionEvent e) {
			return false;
		}
	}
    在上面的代码里面,我们通过手势监听,然后手动的去调用item的点击事件和长按事件,如果需要双击事件,也可以添加。
private void performAdapterViewItemClick(MotionEvent e) {
		ViewParent t = getParent();

		Log.d(TAG, "performAdapterViewItemClick()");

		while (t != null) {
			if (t instanceof AdapterView) {
				@SuppressWarnings("rawtypes")
				AdapterView view = (AdapterView) t;
				int p = view.getPositionForView(ZSwipeItem.this);
				if (p != AdapterView.INVALID_POSITION
						&& view.performItemClick(
								view.getChildAt(p
										- view.getFirstVisiblePosition()), p,
								view.getAdapter().getItemId(p)))
					return;
			} else {
				if (t instanceof View && ((View) t).performClick())
					return;
			}

			t = t.getParent();
		}
	}
    
    其实ZSwipeItem代码里面还有好多需要介绍的东西,但是确实篇幅有限,大家还是去自己去看吧!

    下面再简单的介绍下BaseSwipeAdapter的实现。

    我们直接从getView()开始看

@Override
	public final View getView(int position, View convertView, ViewGroup parent) {

		if (convertView == null) {
			convertView = generateView(position, parent);
			initialize(convertView, position);
		} else {
			updateConvertView(convertView, position);
		}
		fillValues(position, convertView);
		return convertView;

	}
    在这里面,我们实现了convertView的复用,减少布局初始化的消耗。initialize()是用来初始化布局的,下面是代码实现。在这里面添加了一个SwipeMemory,它是一个SwipeListener的实现类,这也就是为什么在ZSwipeItem里面不只有一个监听器了。同时,还添加了一个onLayoutListener,然后封装到一个ValueBox里面,添加了tag上。
public void initialize(View target, int position) {

		int resId = getSwipeLayoutResourceId(position);

		OnLayoutListener onLayoutListener = new OnLayoutListener(position);
		ZSwipeItem swipeLayout = (ZSwipeItem) target.findViewById(resId);
		if (swipeLayout == null)
			throw new IllegalStateException(
					"can not find SwipeLayout in target view");

		SwipeMemory swipeMemory = new SwipeMemory(position);
		// 添加滑动监听器
		swipeLayout.addSwipeListener(swipeMemory);
		// 添加布局监听器
		swipeLayout.addOnLayoutListener(onLayoutListener);
		swipeLayout.setTag(resId, new ValueBox(position, swipeMemory,
				onLayoutListener));

		mShownLayouts.add(swipeLayout);

	}
    而在复用的问题上,则是通过下面的代码完成的,主要是更新了position属性。
public void updateConvertView(View target, int position) {

		int resId = getSwipeLayoutResourceId(position);

		ZSwipeItem swipeLayout = (ZSwipeItem) target.findViewById(resId);
		if (swipeLayout == null)
			throw new IllegalStateException(
					"can not find SwipeLayout in target view");

		ValueBox valueBox = (ValueBox) swipeLayout.getTag(resId);
		valueBox.swipeMemory.setPosition(position);
		valueBox.onLayoutListener.setPosition(position);
		valueBox.position = position;

		Log.d(TAG, "updateConvertView=" + position);

	}
    SwipeMerory是干了什么呢?从下面的代码中,我们根据不同的模式,维护着当前打开的item的position属性。为什么维护position呢?这是因为getView()的复用会导致我们刚打开的item,如果离开当前屏幕然后再返回的话,就会恢复原状,因此我们需要自己维护这个状态。
class SwipeMemory extends SimpleSwipeListener {

		private int position;

		SwipeMemory(int position) {
			this.position = position;
		}

		@Override
		public void onClose(ZSwipeItem layout) {

			if (mode == Mode.Multiple) {
				openPositions.remove(position);
			} else {
				openPosition = INVALID_POSITION;
			}
		}

		@Override
		public void onStartOpen(ZSwipeItem layout) {

			if (mode == Mode.Single) {
				closeAllExcept(layout);
			}
		}

		@Override
		public void onOpen(ZSwipeItem layout) {

			if (mode == Mode.Multiple)
				openPositions.add(position);
			else {
				closeAllExcept(layout);
				openPosition = position;
			}
		}

		public void setPosition(int position) {
			this.position = position;
		}
	}
    既然维护了position,我们就要用啊!在什么地方用呢?我们看下下面的代码

class OnLayoutListener implements OnSwipeLayoutListener {

		private int position;

		OnLayoutListener(int position) {
			this.position = position;
		}

		public void setPosition(int position) {
			this.position = position;
		}

		@Override
		public void onLayout(ZSwipeItem v) {

			if (isOpen(position)) {
				v.open(false, false);
			} else {
				v.close(false, false);
			}
		}

	}
    还记得ZSwipeItem里面的onLayout()吗?在那里面执行了onLayout的onLayout方法,因此,当我们的item被滑动的时候,就会不断的调用这个onLayout方法,我们判断当前打开的position,然后恢复现场即可!

    说了这么多,不知道大家有没有学到新的东西,看SwipeLayout的源码看了三天,我才搞懂了这么些东西,感觉很有收获,所以想分享给大家,如果你有什么疑问或者是建议,请留言或者是私信。

    写的很累,我要休息去了,拜拜~


    项目的Github地址:https://github.com/ZhaoKaiQiang/ZListVIew

Android实现自定义listview上拉刷新下拉加载以及侧滑编辑、删除 今天上午我出了一篇 Android 史上最简单的自定义listview上拉刷新下拉加载,有位网友询问我说如果在这个基础上在实现滑动编辑和删除可以么。我回答说,这个是可以的。下面我们一起来看一下具体怎么实现的吧 如果不需要侧滑删除和修改的童鞋可以看我的这篇文章 Android 史上最简单的自定义listview上拉刷新下拉加载 文章地址:https://blog.youkuaiyun.com/k571039... 阅读详情

相关推荐

Android 自定义TextView去除paddingTop和paddingBottom

主要介绍了Android 自定义TextView去除paddingTop和paddingBottom的相关资料,这里提供实例来帮助大家实现这样的功能,需要的朋友可以参考下

getPadding、getMargin与getTop

padding是自身填充内容与自身的边距,如textview或button中字体与边界距离 margin是组件与其他组件的间距,可以是父组件也可是同一级的子组件 getTop之类的是与父组件的间距,如一个linearlayout中包含3个按钮垂直分布,中间的按钮B与父组件的上间距是 A.getTop+A.getHeight+B.marginTop

吾娱生活的专栏 1543

西门子S7-1200与S7-200SMART的PROFINET通讯实战:从GSD文件生成到数据监控

本文详细解析了西门子S7-1200与S7-200SMART之间实现PROFINET通讯的完整实战流程。核心内容包括将S7-200SMART配置为智能IO设备、生成并安装GSD文件、在TIA Portal中进行网络组态与地址映射,最终实现双向数据交换与监控。该方案为整合新旧设备、构建灵活高效的工业自动化系统提供了可靠路径。

weixin_29271263的博客 248

android自定义View学习二

2.Margin属性不需要你定义,padding属性需要自己再onDraw()方法里面把padding考虑进去 @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int paddingLeft = getPaddingLeft(); int paddi...

南风子的博客 337

第三方系统(异构系统)基于token身份认证方式访问Ecology9的Restful接口Ecology9服务器配置部分

通过Token的认证方式调用Ecology9系统Restful业务接口服务器配置部分

svsou的专栏 2177

AnolisOS网卡驱动源码编译与安装实战指南

本文提供了一份详尽的AnolisOS网卡驱动源码编译与安装实战指南。针对系统自带通用驱动不兼容导致网络无法使用的问题,文章从环境准备、源码获取、编译安装到新旧驱动替换与持久化配置,一步步解析了如何通过源码编译安装官方驱动,解决硬件兼容性问题,并确保驱动在系统重启后自动生效。

weixin_28358083的博客 307

Android SwipeLayout实现界面滑动布局

github地址https://github.com/daimajia/AndroidSwipeLayout/通过对daimajia 的SwipeLayout 中Library的修改和封装,特意多加了一个滑动监听器,让layout在滑动时对外传出相对屏幕顶部和左侧的滑动距离,方便制作其他动态UI封装好的SwipeLayout.java import android.content.Context...

0.0 6029

糖尿病预测(深度学习pytorch实现-最新)

本次做的项目是糖尿病的预测,也是来自kaggle上面的一个小项目。根据一个人的怀孕次数、血糖、血压、皮肤厚度、胰岛素指标、身体质量指数、糖尿病谱系指数、年龄等身体参数来预测一个人是否患有糖尿病。

NangoGreen的博客 5360

SwipeRefreshLayout完美添加及完善上拉加载功能

关于Google推出的下拉刷新控件SwipeRefreshLayout的相关使用方法,

u012036813的专栏 2万+

2025年06月中国电子学会青少年软件编程(图形化)等级考试试卷(三级)答案 + 解析

2025年06月中国电子学会青少年软件编程(图形化)等级考试试卷(三级)答案 + 解析

伶俐角少儿编程 344

YOLO26-seg分割优化:轻量化卷积魔改 | 新的partial convolution(PConv)结合C3k2 | CVPR2023

​​​​​摘要:本研究对Ultralytics YOLO26进行了全面分析,重点阐述了其关键架构改进及其在实时边缘目标检测中的性能基准测试。YOLO26于2025年9月发布,是YOLO系列最新、最先进的成员,专为在边缘及低功耗设备上实现高效、精确且易于部署的目标而构建。本文依次详述了YOLO26的架构创新,包括:移除了分布焦点损失(DFL);采用端到端的无NMS推理;集成了渐进损失(ProgLoss)与小目标感知标签分配(STAL);以及引入了用于稳定收敛的MuSGD优化器。

①答疑群聊服务;②YOLO大模型知识问答系统;③计算机视觉论文生成智能体; 153

[ Android界面实现 ] ZListView,一个最强大的刷新、加载、滑动删除的 ListView 控件(一)

        两个月之前,写过一篇文章【Android界面实现】整合了刷新、加载更多、滑动删除功能的XListview ,介绍了如何整合两个开源项目实现这种功能,但是在实际的使用中,却出现了一些问题,比如说item的点击事件多次调用,item的position不准确,代码量太大太复杂等,一个哥们在自己的工作中使用了这个项目,这些BUG给他造成了困扰。所以,花费了三天时间

codepython的专栏 550

边缘计算与产业生态系统

目录 文章目录目录边缘计算产业参与者边缘计算产业生态系统 边缘计算产业参与者 边缘计算领域参与者众多,主流公司以打造生态优势为目标,基于自身服务层级向产业链上下两端进行拓展,目前市场主要参与者有: 系统集成商:在产业头部,具有广泛的能力,可以集成不同生态系统公司的产品,具有一定的全球性属性,在边缘计算领域大多由云服务商、电信运营商担任。 云服务商:具有领先的云技术优势与应用生态体系,同时拥有全球性资源布局与市场资源生态,并渴望成为边缘计算主导者。 电信运营商:具有边缘基础设施优势,以及不可替代的

烟云的计算 2104

AndroidSwipeLayout刷卡布局(仿微信删除条目)遇到的大坑

AndroidSwipeLayout坑1item布局获取不到点击的位置position AndroidSwipeLayout坑2滑动当前item布局出来后其他item不能自动恢复 解决方案 item布局使用SwipeLayoutsurfaceview和buttonview不变 摒弃文档中建议的包含子类在内的BaseSwipeAdapter使用BaseQuickAdapter 附件1Recycl

The blog of 优快云 in 杨天福 8668

《C#零基础入门之百识百例》(七)算数运算符 -- 圆的面积

运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C# 有丰富的内置运算符,本文带你学习C# 中算数运算符。

陈言必行 -- Unity游戏开发领域优质博主 3615

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

Android好奇宝宝_11_SwipeRefreshLayout原理浅析

SwipeRefreshLayout的简单与原理分析及一个官方Bug的发现。

Android Blog Factory 4464

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

【布线神器】3DMAX平滑布尔插件超级布尔工具SmoothBoolean直接提升10倍建模速度

3DMAX平滑布尔插件超级布尔工具SmoothBoolean

沐风老师 1946

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

android进度显示按钮,Android自定义控件制作显示进度的Button

最近看到一些应用在下载文件的时候,并没有额外弹出进度条,而是很炫的使用启动下载任务的Button直接显示文件的下载进度,通过改变其背景色,从左向右推进,直到填满整个Button时,意味着下载任务的完成。除了这种效果,还看到某酷的视频客户端,在观看过的视频对应的按钮上,会给该按钮添加一个描边效果,4条边,每条边代表25%的进度,由上沿开始,顺时针最终到左边沿,则代表100%的进度,这种效果也很不错。...

weixin_34207787的博客 684

android 画布控件,Android Canvas控件View控件绘制

2015年收尾制作了如下几种控件 其中2篇代码被小伙伴放到了优快云上当然,以上代码由于硬盘格式化了,全没了,看下面的吧 publicclassTabBarViewextendsViewimplementsRunnable{//画笔privatePaintmSolidPaint;//尺寸privatefloatmHeight;privatefloatmWidth;//...

weixin_42499444的博客 339

2022年11月AI技术拐点:MaaS、多模态对齐与推理优化实战解析

大模型时代的技术演进已从‘能跑通’迈入‘可计量、可落地、可盈利’新阶段。模型即服务(MaaS)正推动AI成本结构化下降,token级计费倒逼工程团队重构输入预处理与资源调度逻辑;多模态对齐技术突破弱监督瓶颈,使中小企业得以用伪标签+小样本实现工业级实用效果。其底层支撑在于推理优化从黑盒加速走向白盒可控——FlashAttention-2、量化部署、KV Cache管理等技术不再仅属框架层,而成为开发者必修的系统级能力。本文聚焦2022年11月这一关键时间切片,基于真实产线压测数据,解析MaaS商业化验证、开

abc20899的专栏 643

Android—— ListView 的简单用法及定制ListView界面

一、ListView的简单用法 2. 训练目标 1) 掌握 ListView 控件的使用 2) 掌握 Adapter 桥梁的作用 二、定制ListView界面 1.训练目标 1) 掌握 ListView 控件的使用 2) 掌握如何自定义 Adapter 的使用

小小小森屿的博客 16万+

android应用开发全程实录-你有多熟悉listview?

今天给大家带来《android应用开发全程实录》中关于listview和adatper中的部分。包括listview的基本使用,listview的优化等。 我们经常会在应用程序中使用列表的形式来展现一些内容,所以学好ListView是非常必需的。ListView也是Android中比较难以使用的控件,这节内容就将详细解读ListView的用法。 一个ListView通常有两个职责。 (1)将...

weixin_30367873的博客 2020

Android(定制的ListView的界面)

1、运行时效果图: 2、Fruit.java(实体类)//定义了一个水果的实类 public class Fruit { private int imageView; private String textView; public Fruit(int imageView, String textView) { this.imageView = image

只想离想象更近一点 5万+

自定义控件(13)---View绘制的Padding、Margin

activity_main.xml--图片包裹内容 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:backgro

全干工程师 3513

Android自定义View中的常用方法(距离、位置、点击事件)

View介绍的官网地址: https://developer.android.com/reference/android/view/View.html屏幕的左上角为坐标的原点,屏幕上边缘往右为X轴正方向,屏幕左边缘往下为Y轴正方向.view自身坐标:getLeft(),getTop(),getRight(),getBottom() view自身宽高: getWidth(),getMeasured

小马快跑的博客 3457
上一篇: [ Android界面实现 ] ZListView,一个最强大的刷新、加载、滑动删除的 ListView 控件(一)
下一篇: 整合了刷新、加载更多、滑动删除功能的XListview
codepython
博客等级 码龄20年 125粉丝 6原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值