• 开源库AndroidSwipeLayout分析(二),SwipeLayout源码探究



    如需转载请注明博客出处: http://www.cnblogs.com/wondertwo/p/5525670.html


    开源库AndroidSwipeLayout地址请戳: https://github.com/daimajia/AndroidSwipeLayout 开源库作者 @代码家


    从上面的开源库目录可以看出,SwipeLayout是整个swipe效果实现的基石,我们就从源码着手,SwipeLayout.java文件的源码较为复杂庞大,有1600+行代码,所以阅读源码的正确的打开姿势是这样的:分清主线,只抓重点!所以我们重点去看SwipeLayout在TouchEvent事件处理方面是怎样实现的。本篇博客分三个部分来写:

    • SurfaceView和BottomView绘制();
    • TouchEvent事件传递(多层嵌套不破坏事件传递是怎样做到的?);
    • Listener监听与回调(设置隐藏百分比、过渡动画效果);

    第一部分 SurfaceView和BottomView绘制

    SwipeLayout从本质上来说,可以看作一个自定义的View控件,继承自帧布局FrameLayout。通过我的前一篇博客,相信同学们已经了解了,它的布局其实是由上层的SurfaceViews和下层的BottomViews叠加在一起的。先来看构造方法,SwipeLayout有3个重载的构造方法,主要做一些初始化工作。我们需要关注一下mDragEdges这个成员变量,mDragEdges是一个记录滑动方向DragEdge的哈希表,一共记录Left,Right,Top,Bottom四个方向值。

    有过自定义View经验的同学都很清楚要处理好两件事情:View绘制和事件处理。ViewGroup需要先遍历View树中所有子View,通过onMeasure()方法对其大小进行测量,调用onLayout()方法把它显示出来。按照这个思路去看源码,果然,SwipeLayout重写了onLayout()方法,由于Android原生android.view.View.OnLayoutChangeListener is added in API 11,为了兼容到api 8,作者做了OnLayout接口抽象,一共涉及到一个接口和四个成员方法,把这部分代码单独拿出来如下:

    /**
     * {@link android.view.View.OnLayoutChangeListener} added in API 11. I need
     * to support it from API 8.
     */
    public interface OnLayout {
        void onLayout(SwipeLayout v);
    }
    
    private List<OnLayout> mOnLayoutListeners;
    
    public void addOnLayoutListener(OnLayout l) {
        if (mOnLayoutListeners == null) mOnLayoutListeners = new ArrayList<OnLayout>();
        mOnLayoutListeners.add(l);
    }
    
    public void removeOnLayoutListener(OnLayout l) {
        if (mOnLayoutListeners != null) mOnLayoutListeners.remove(l);
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        updateBottomViews();
    
        if (mOnLayoutListeners != null) for (int i = 0; i < mOnLayoutListeners.size(); i++) {
            mOnLayoutListeners.get(i).onLayout(this);
        }
    }
    
    private void updateBottomViews() {
        View currentBottomView = getCurrentBottomView();
        if (currentBottomView != null) {
            if (mCurrentDragEdge == DragEdge.Left || mCurrentDragEdge == DragEdge.Right) {
                mDragDistance = currentBottomView.getMeasuredWidth() - dp2px(getCurrentOffset());
            } else {
                mDragDistance = currentBottomView.getMeasuredHeight() - dp2px(getCurrentOffset());
            }
        }
    
        if (mShowMode == ShowMode.PullOut) {
            layoutPullOut();
        } else if (mShowMode == ShowMode.LayDown) {
            layoutLayDown();
        }
    
        safeBottomView();
    }
    
    1. addOnLayoutListener(OnLayout l)和removeOnLayoutListener(OnLayout l)这两个方法是添加和移除布局监听;
    2. 在重写的父类onLayout()方法中,我们看到先调用了updateBottomViews(),那么updateBottomViews()的作用字面上理解就是更新底层View,当CurrentBottomView非空,根据拖动方向(左右方向为一组,上下方向为一组)来计算拖动距离mDragDistance,接着根据两种不同的模式执行对应的方法把表层View移开,
    3. 在onLayout()方法的最后,调用safeBottomView()来拦截掉底层View的所有事件;safeBottomView()方法在第二部分会出现;
    4. 到这里我们很熟悉,遍历所有子View,回调onLayout(),将其布局显示在SwipeLayout这个父控件中;

    第二部分 TouchEvent传递(多层嵌套不破坏事件传递是怎样做到的?)

    其实自定义View说到底就是在View绘制和事件处理这两块大做文章!在第一部分分析了SwipeLayout的View绘制过程后,我们接着来看它的事件传递过程,哈哈,原作者提到它的特性:“可以嵌套在任何地方而不破坏触摸事件传递(这是最难的地方)”,我想说这也是这个开源库最精华的地方,以前自己也写过View控件,在触摸事件处理上遇到了各种问题,SwipeLayout在这方面确实值得借鉴。

    所以很快的我们就会想到要去SwipeLayout中找三个很重要很重要的重写方法:dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent()!他们都只接收一个MotionEvent对象,分别负责事件分发、事件拦截、事件消耗。把这部分相关的代码单独拿出来如下:

    private int mEventCounter = 0;
    
    protected void dispatchSwipeEvent(int surfaceLeft, int surfaceTop, int dx, int dy) {
        DragEdge edge = getDragEdge();
        boolean open = true;
        if (edge == DragEdge.Left) {
            if (dx < 0) open = false;
        } else if (edge == DragEdge.Right) {
            if (dx > 0) open = false;
        } else if (edge == DragEdge.Top) {
            if (dy < 0) open = false;
        } else if (edge == DragEdge.Bottom) {
            if (dy > 0) open = false;
        }
    
        dispatchSwipeEvent(surfaceLeft, surfaceTop, open);
    }
    
    protected void dispatchSwipeEvent(int surfaceLeft, int surfaceTop, boolean open) {
        safeBottomView();
        Status status = getOpenStatus();
    
        if (!mSwipeListeners.isEmpty()) {
            mEventCounter++;
            for (SwipeListener l : mSwipeListeners) {
                if (mEventCounter == 1) {
                    if (open) {
                        l.onStartOpen(this);
                    } else {
                        l.onStartClose(this);
                    }
                }
                l.onUpdate(SwipeLayout.this, surfaceLeft - getPaddingLeft(), surfaceTop - getPaddingTop());
            }
    
            if (status == Status.Close) {
                for (SwipeListener l : mSwipeListeners) {
                    l.onClose(SwipeLayout.this);
                }
                mEventCounter = 0;
            }
    
            if (status == Status.Open) {
                View currentBottomView = getCurrentBottomView();
                if (currentBottomView != null) {
                    currentBottomView.setEnabled(true);
                }
                for (SwipeListener l : mSwipeListeners) {
                    l.onOpen(SwipeLayout.this);
                }
                mEventCounter = 0;
            }
        }
    }
    
    /**
     * prevent bottom view get any touch event. Especially in LayDown mode.
     */
    private void safeBottomView() {
        Status status = getOpenStatus();
        List<View> bottoms = getBottomViews();
    
        if (status == Status.Close) {
            for (View bottom : bottoms) {
                if (bottom != null && bottom.getVisibility() != INVISIBLE) {
                    bottom.setVisibility(INVISIBLE);
                }
            }
        } else {
            View currentBottomView = getCurrentBottomView();
            if (currentBottomView != null && currentBottomView.getVisibility() != VISIBLE) {
                currentBottomView.setVisibility(VISIBLE);
            }
        }
    }
    
    protected void dispatchRevealEvent(final int surfaceLeft, final int surfaceTop, final int surfaceRight, final int surfaceBottom) {
        if (mRevealListeners.isEmpty()) return;
        for (Map.Entry<View, ArrayList<OnRevealListener>> entry : mRevealListeners.entrySet()) {
            View child = entry.getKey();
            Rect rect = getRelativePosition(child);
            if (isViewShowing(child, rect, mCurrentDragEdge, surfaceLeft, surfaceTop,
                    surfaceRight, surfaceBottom)) {
                mShowEntirely.put(child, false);
                int distance = 0;
                float fraction = 0f;
                if (getShowMode() == ShowMode.LayDown) {
                    switch (mCurrentDragEdge) {
                        case Left:
                            distance = rect.left - surfaceLeft;
                            fraction = distance / (float) child.getWidth();
                            break;
                        case Right:
                            distance = rect.right - surfaceRight;
                            fraction = distance / (float) child.getWidth();
                            break;
                        case Top:
                            distance = rect.top - surfaceTop;
                            fraction = distance / (float) child.getHeight();
                            break;
                        case Bottom:
                            distance = rect.bottom - surfaceBottom;
                            fraction = distance / (float) child.getHeight();
                            break;
                    }
                } else if (getShowMode() == ShowMode.PullOut) {
                    switch (mCurrentDragEdge) {
                        case Left:
                            distance = rect.right - getPaddingLeft();
                            fraction = distance / (float) child.getWidth();
                            break;
                        case Right:
                            distance = rect.left - getWidth();
                            fraction = distance / (float) child.getWidth();
                            break;
                        case Top:
                            distance = rect.bottom - getPaddingTop();
                            fraction = distance / (float) child.getHeight();
                            break;
                        case Bottom:
                            distance = rect.top - getHeight();
                            fraction = distance / (float) child.getHeight();
                            break;
                    }
                }
    
                for (OnRevealListener l : entry.getValue()) {
                    l.onReveal(child, mCurrentDragEdge, Math.abs(fraction), distance);
                    if (Math.abs(fraction) == 1) {
                        mShowEntirely.put(child, true);
                    }
                }
            }
    
            if (isViewTotallyFirstShowed(child, rect, mCurrentDragEdge, surfaceLeft, surfaceTop,
                    surfaceRight, surfaceBottom)) {
                mShowEntirely.put(child, true);
                for (OnRevealListener l : entry.getValue()) {
                    if (mCurrentDragEdge == DragEdge.Left
                            || mCurrentDragEdge == DragEdge.Right)
                        l.onReveal(child, mCurrentDragEdge, 1, child.getWidth());
                    else
                        l.onReveal(child, mCurrentDragEdge, 1, child.getHeight());
                }
            }
    
        }
    }
    
    1. mEventCounter,顾名思义,事件计数器,只要SwipeListener监听到有事件触发,mEventCounter++;
    2. 第一个重载的dispatchSwipeEvent()方法,屏蔽掉了一些无效的触摸事件,也就是屏蔽掉了SwipeLayout(也就是ItemView)边界以外触发的所有事件,当事件无效,则把布尔型标记位open变量置为false,这个标记位直接控制着是否回调事件的监听;
    3. 第二个重载的dispatchSwipeEvent()方法,则是真正的事件分发逻辑所在。safeBottomView(),在第一部分出现过,拦截底层View的所有事件;然后通过getOpenStatus()拿到表层View的打开状态status,表层View有三种状态:Middle,Open,Close表示半打开状态,完全打开(此时只能看底层View),关闭(此时只能看到表层View)。
    4. 循环遍历mSwipeListeners,拿到每一个触摸事件,根据标记位open判断是否执行事件的监听回调;
    5. 根据表层View的打开状态status,通过回调方法执行关闭或者打开操作;并把mEventCounter重新置零。到此整个触摸事件的分发,似乎已经完成;但是你发现没有?上面的这些事件分发都是一些SwipeListener的方法回调,其具体逻辑是交由用户去实现的;那么问题来了,我们的ItemView总得响应用户的触摸事件,进行相应的UI更新吧,比如表层View的Open和Close,你会发现这些事件是不能交给用户去处理的,所以这里涉及到了另外一个很重要的接口OnRevealListener(关于接口的监听回调,会在第三部分细讲),负责重绘ItenView以更新用户界面显示的UI。这是整个开源库的精髓所在;
    6. 此时看到最后还有一个dispatchRevealEvent(),那么这个方法是干嘛的呢?dispatchRevealEvent()就负责分发
      UI更新相关事件。逻辑也很简单,通过getShowMode() == ShowMode.LayDown | ShowMode.PullOut判断用户的滑动操作模式是LayDown模式还是PullOut模式,根据不同的模式计算两个不同的参数distance和fraction,这两个参数就是直接控制着表层View的滑动行为;
    7. 最后,遍历所有的OnRevealListener接口对象,回调onReveal()更新用户界面UI。

    上面说完了SwipeLayout的事件分发,那么SwipeLayout事件是如何拦截以及消耗的呢?把相关的代码单独拿出来如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!isSwipeEnabled()) {
            return false;
        }
        if (mClickToClose && getOpenStatus() == Status.Open && isTouchOnSurface(ev)) {
            return true;
        }
        for (SwipeDenier denier : mSwipeDeniers) {
            if (denier != null && denier.shouldDenySwipe(ev)) {
                return false;
            }
        }
    
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDragHelper.processTouchEvent(ev);
                mIsBeingDragged = false;
                sX = ev.getRawX();
                sY = ev.getRawY();
                //if the swipe is in middle state(scrolling), should intercept the touch
                if (getOpenStatus() == Status.Middle) {
                    mIsBeingDragged = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                boolean beforeCheck = mIsBeingDragged;
                checkCanDrag(ev);
                if (mIsBeingDragged) {
                    ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                if (!beforeCheck && mIsBeingDragged) {
                    //let children has one chance to catch the touch, and request the swipe not intercept
                    //useful when swipeLayout wrap a swipeLayout or other gestural layout
                    return false;
                }
                break;
    
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mIsBeingDragged = false;
                mDragHelper.processTouchEvent(ev);
                break;
            default://handle other action, such as ACTION_POINTER_DOWN/UP
                mDragHelper.processTouchEvent(ev);
        }
        return mIsBeingDragged;
    }
    
    private float sX = -1, sY = -1;
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!isSwipeEnabled()) return super.onTouchEvent(event);
    
        int action = event.getActionMasked();
        gestureDetector.onTouchEvent(event);
    
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mDragHelper.processTouchEvent(event);
                sX = event.getRawX();
                sY = event.getRawY();
    
    
            case MotionEvent.ACTION_MOVE: {
                //the drag state and the direction are already judged at onInterceptTouchEvent
                checkCanDrag(event);
                if (mIsBeingDragged) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                    mDragHelper.processTouchEvent(event);
                }
                break;
            }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                mDragHelper.processTouchEvent(event);
                break;
    
            default://handle other action, such as ACTION_POINTER_DOWN/UP
                mDragHelper.processTouchEvent(event);
        }
    
        return super.onTouchEvent(event) || mIsBeingDragged || action == MotionEvent.ACTION_DOWN;
    }
    

    上面这段代码很简单,重写父类的onInterceptTouchEvent()方法和onTouchEvent(),和我们预想的一模一样。那么什么情况下,SwipeLayout拦截掉了子View的触摸事件?当SurfaceView处于打开状态,想要点击关闭,却点击在SurfaceView上时,SwipeLayout会拦截掉SurfaceView的触摸事件。至于事件拦截和处理的具体逻辑,就在上面这段代码中了。


    第三部分 Listener监听与回调(设置隐藏百分比、过渡动画效果)

    第三部分我们来分析Listener监听与回调,包括怎样设置子View显示隐藏百分比,以及更新UI的过渡动画效果。前文提到了两个监听接口OnLayout和OnRevealListener,前者的毁掉方法,主要响应一些用户的自定义操作,比如手机QQ聊天列表的ItenView左滑呼出删除、置顶、标为未读等操作功能;后者在第二部分已经提及,主要处理更新UI等功能,而这些功能已经在SwpieLayout中封装好了,不需要用户关心;当然,作为一个小巧但并不影响它强大存在的开源库,用户的可定制性还是非常高的,比如可以实现一些ItemView呼出或者关闭的动画。

    关于SwipeLayout开源库的使用方法,点击阅读我的上一篇博客:开源库AndroidSwipeLayout分析(一),炫酷ItemView滑动呼出效果

  • 相关阅读:
    关于binary log一点总结[转]
    使用mysql索引技巧及注意事项
    优化php性能的一点总结
    html静态页面实现微信分享思路
    MySql字符串函数使用技巧
    Oracle计算时间差函数
    oracle10g获取Date类型字段无时分秒解决办法!
    Oracle常用函数
    COALESCE操作符
    关于null的操作
  • 原文地址:https://www.cnblogs.com/wondertwo/p/5525670.html
Copyright © 2020-2023  润新知