转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992
我们接着上篇的文章说,在前一篇文章中,我们学习了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的实现代码一点点分析这个类的用法,一会儿你就明白了。
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(); }
(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; }
了解了这些之后,我们再看下面的代码,应该就能够明白什么意思了。
/** * 进行拖拽的主要类 */ 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(); } };
@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); } }
下面的办法返回是这样的,意思就是说,前面布局和后面布局都是可以移动的。
@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; } } }
/** * 当子控件被释放的时候调用,可以获取加速度的数据,来判断用户意图 */ @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); } }
怎么办呢?我们可以通过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(); } }
下面再简单的介绍下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,然后恢复现场即可!
写的很累,我要休息去了,拜拜~
项目的Github地址:https://github.com/ZhaoKaiQiang/ZListVIew
【Android界面实现】ZListView,一个最强大的刷新、加载、滑动删除的ListView控件(二)
原文地址:http://blog.csdn.net/zhaokaiqiang1992/article/details/42524957