android 滑动监听删除

=========
之前看到QQ邮箱,以及天天动听的页面滑动退出效果,总是在思考他们是怎么弄的,最主要是怎么弄,才能够比较方便。我相信弄起来很多棒法,但是最佳的办法。而且还不能影响页面的其他方面的操作,应该怎么去实现。能不能直接使用一个包的形式做。

###利用Fragment
我慢慢地带着疑问去思考了,最开始考虑到Activity受控制方面可能不如Fragment,毕竟Activity是一个应用的组件。而Fragment,完全可以当成一个View来看待,只不过这个View有着自己的生命周期。所以考虑用Fragment去实现。自然就想到构建一个抽象的BaseSlideFragment 类。然后用子类来实现这个类。将onCreateView 重新包装一遍,BaseSlideFragment自己使用一个ViewGroup,将子类生成的View 作为这个ViewGroup的子类,然后将Fragment最顶层的类监听滑动事件。

/**
 *
 */
package com.houzhi.slidefinish.fragment;

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

/**
 * @author houzhi
 *    可监听滑动删除 fragment
 *    使用该fragment 的Activity 必须实现SlideFragmentFinishListener 接口
 *
 *
 */
public abstract class BaseSlideFinishFragment extends Fragment {

    /**
     *
     * @author houzhi
     *
     */
    public static interface SlideFragmentFinishListener{
        public void onSlideFragmentFinish(Fragment fragment);
    }

    //监听 fragment 接口
    protected SlideFragmentFinishListener mOnFragmentWantFinishListener;

    protected Activity activity ;

    /**
     * 屏幕信息
     */
    protected DisplayMetrics outMetrics = new DisplayMetrics();

    @Override
    public void onAttach(Activity activity) {
        if(activity instanceof SlideFragmentFinishListener){
            mOnFragmentWantFinishListener = (SlideFragmentFinishListener)activity;
        }else{
            throw new RuntimeException("the Activity which use SlideFinishFragment" +
                    " must implements OnFragmentWantFinishLIstener!");
        }
        this.activity = activity ;

        activity.getWindowManager().getDefaultDisplay().getMetrics(outMetrics);

        super.onAttach(activity);
    }




    protected FrameLayout.LayoutParams saveParams = null;
    /**
     * the onCreateView will callback this method
     * @param inflater
     * @param container
     * @param savedInstanceState
     * @return
     */
    abstract protected View onCreateContentView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState);
}

由于上下滑动跟水平滑动的方式不一样,所以我又重新加了一层SlideHorizonalFinishFragment SlideVerticalFinishFragment。代码就不贴上了,感觉后面的实现方式更加好。结构上,对于Fragment感觉这样设计很好。

在BaseSlideFinishFragment 的子类中实现onCreateView,并且将onCreateContentView得到的View 添加到实现监听滑动时间的ViewGroup的中。将该ViewGroup作为作为Fragment 的View。这样就很方便代码的重用了。

事件监听模型

现在就是该考虑如何实现监听,并且不影响到子View的使用了。最开始我本来想直接用FrameLayout 作为一个最顶层的ViewGroup 然后设置FrameLayout的onToucheListener。但是实现后,就感觉效果不佳。当子View消费了触摸事件(MotionEvent)后, MotionEvent 压根不会传给FrameLayout的onTouchListener当中。

然后我就又重新根据android事件模型思考了一遍,也去重新温习了android事件派发等相关知识。

有些概念如果换个方向去理解可能就很容易了。

  1. 首先一次触摸事件,不要理解成从按下到松开手指。而是分成ACTION_DOWN ,ACTION_MOVE, ACTION_UP 等等。一次触摸,包括一次ACTION_DOWN ,一次ACTION_UP ,和很多次ACTION_MOVE。
  2. View树 就相当于一颗树,父节点为ViewGroup以及派生类。子节点为一个View。 事件派发从树的根节点开始,到达子节点(当然不一定会到达,中间可以截断),so,这样思考起来,任何一个触摸事件一定会经过父View,父View首先对其有处理的决定权。(不要被平时子View实现了onTouch后,父View的onTouch无效误导,而以为子View对事件处理更有优先权)。有了这个,这种实现方式就能够确定这种实现方式一定能够成功。
  3. 一次事件过程,就是从按下的那一刻到松开的那一刻。

事件派发具体的过程

一个MotionEvent 首先是经过Activity的,然后由Activity再派发给Activity中的View。然后再一层一层地传下去。传下去后,每一次都会有一个处理的,觉得是否继续往下派发,并且有个处理的反馈。反馈是从下往上的,其实就是在onTouch 等中 返回的boolean,父层根据子层反馈的boolean 来决定下一个MotionEvent(注:这里的下一个MotionEvent,是一次事件过程中的下一次MotionEvent)是否继续往下派发,还是自己直接处理掉,直接反馈给上一层。

然后根据上面的过程。描述一下ViewGroup中的几个函数的意义。

  • dispatchTouchEvent: 派发MotionEvent给子View。如果返回为true ,则将事件给onTouchEvent,如果为false,则将事件给 onIterceptTouchEvent 来决定是否终端事件的派发。
  • onInterceptTouchEvent :终止事件派发的函数。如果返回true 则事件不会继续往下传,将事件给onTouchEvent处理。如果false 则继续往下传
  • onTouchEvent : View自己处理事件的函数。如果子View的ontouch 返回true 则表示此次事件已经消费。父view 的ontouchEvent将不会运行。如果为false ,将会奖事件传到父view 的ontouch。如果一直返回false,则事件就会消失。下次事件也不会传递下来了。

上面的参考[1]

SlideFrameLayout

依据上面的知识,就可以实现一个SlideFrameLayout,来判断是否滑动。我们滑动分成四个方向,每次如果超出一定范围后,就调用监听滑动(SlideFinishListener)通知已经滑动到底了。具体代码如下:

public class SlideFrameLayout extends FrameLayout {

    public SlideFrameLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    public SlideFrameLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public SlideFrameLayout(Context context) {
        super(context);
        init();
    }

    public static enum SlideDirection {
        LEFT, RIGHT, HORIZONAL, TOP, BOTTOM, VERTICAL,NO
    };

    /**
     * listen to the slide
     *
     * @author houzhi
     *
     */
    public static interface SlideListener {
        /**
         *
         */
        public void onSlideFinish();
    }

    private SlideListener mSlideListener;

    protected FrameLayout.LayoutParams saveParams = null;

    public SlideListener getSlideListener() {
        return mSlideListener;
    }

    public void setSlideListener(SlideListener mSlideListener) {
        this.mSlideListener = mSlideListener;
    }

    private void init() {  //为了防止设置params.margin 无效
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
        params.gravity = Gravity.TOP | Gravity.LEFT;
        setLayoutParams(params);
    }

    private boolean slideHorizonalFinish(View v, MotionEvent event) {

        float moveDis = 0;

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:

            break;
        case MotionEvent.ACTION_MOVE:
            if (!isMoveFragment) {
                // doesn't move
                break;
            }
            moveDis = event.getRawX() - beginX;

            switch (direction) {
            case RIGHT:
                if (moveDis <= 0)
                    moveDis = 0;
                break;
            case LEFT:
                if (moveDis >= 0)
                    moveDis = 0;
                break;
            case HORIZONAL:
                break;
            default:
                break;
            }


            FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) v
                    .getLayoutParams();

            params.leftMargin = (int) moveDis;
            params.rightMargin = (int) -moveDis;
            params.gravity = Gravity.LEFT | Gravity.TOP;
            Log.i("", "touch ACTION_MOVE---" + moveDis + "--" + event.getRawX());
            v.setLayoutParams(params);
            return true;
        case MotionEvent.ACTION_UP:
            if (!isMoveFragment)
                break;

            moveDis = event.getRawX() - beginX;
            switch (direction) {
            case RIGHT:
                if (moveDis <= 0)
                    moveDis = 0;
                break;
            case LEFT:
                if (moveDis >= 0)
                    moveDis = 0;
                break;
            case HORIZONAL:
                break;
            default:
                break;
            }

            boolean disappear = false;
            float from = 0,
            to = -moveDis;
            if (Math.abs(moveDis) > v.getWidth() / 2) {
                // 移动超出 消失此Fragment
                to = v.getWidth() * (Math.abs(moveDis) / moveDis);
                disappear = true;
            }
            long duration = 500;
            Log.i("", "touch dis---" + disappear + "=---=" + (moveDis) + "---"
                    + v.getWidth());
            startEndHorizonalAnimation(v, duration, from, to, disappear);
            return true;
        }
        return false;

    }

    private SlideDirection direction = SlideDirection.RIGHT;

    public SlideDirection getDirection() {
        return direction;
    }

    public void setDirection(SlideDirection direction) {
        this.direction = direction;
    }

    private boolean slideVerticalFinish(View v, MotionEvent event) {

        float moveDis = 0;

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            break;
        case MotionEvent.ACTION_MOVE:
            if (!isMoveFragment) {
                // not move
                break;
            }
            moveDis = event.getRawY() - beginY;

            //judge whether  distance of moving over or not
            switch (direction) {
            case BOTTOM:
                if (moveDis <= 0)
                    moveDis = 0;
                break;
            case TOP:
                if (moveDis >= 0)
                    moveDis = 0;
                break;
            case VERTICAL:
                break;
            default:
                break;
            }

            FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) v
                    .getLayoutParams();

            params.topMargin = (int) moveDis;
            params.bottomMargin = (int) -moveDis;
            params.gravity = Gravity.LEFT | Gravity.TOP;
            Log.i("", "touch ACTION_MOVE---" + moveDis + "--" + event.getRawY());
            v.setLayoutParams(params);
            return true;
        case MotionEvent.ACTION_UP:
            if (!isMoveFragment)
                break;

            moveDis = event.getRawY() - beginY;
            //judge whether  distance of moving over or not
            switch (direction) {
            case BOTTOM:
                if (moveDis <= 0)
                    moveDis = 0;
                break;
            case TOP:
                if (moveDis >= 0)
                    moveDis = 0;
                break;
            case VERTICAL:
                break;
            default:
                break;
            }

            boolean disappear = false;
            float from = 0,
            to = -moveDis;
            if (Math.abs(moveDis) > v.getHeight() / 2) {
                // 移动超出 消失此Fragment
                to = v.getHeight() * (Math.abs(moveDis) / moveDis);
                disappear = true;
            }
            long duration = 500;
            Log.i("", "touch dis---" + disappear + "=---=" + (moveDis) + "---"
                    + v.getWidth());
            startEndVerticalAnimation(v, duration, from, to, disappear);
            return true;
        }

        return false;

    }

    // move horizonal animation
    private void startEndHorizonalAnimation(final View view, long duration,
            float from, float to, final boolean disappear) {
        AnimationSet animationSet = new AnimationSet(true);

        if (disappear) {
            AlphaAnimation alphaAnimation = new AlphaAnimation(1, 0);
            animationSet.addAnimation(alphaAnimation);
        }
        TranslateAnimation translateAnimation = new TranslateAnimation(from,
                to, 0, 0);
        animationSet.addAnimation(translateAnimation);
        animationSet.setDuration(duration);
        animationSet.setFillAfter(true);
        animationSet.setFillEnabled(true);
        animationSet.setAnimationListener(new Animation.AnimationListener() {

            @Override
            public void onAnimationEnd(Animation arg0) {
                if (disappear) {
                    if (mSlideListener != null) {
                        mSlideListener.onSlideFinish();
                    }
                } else {
                    // 恢复 一定要清除动画,否则动画效果仍然在,params 效果 跟Animation 效果同时作用着
                    view.clearAnimation();
                    view.setLayoutParams(saveParams);

                }
            }

            @Override
            public void onAnimationRepeat(Animation arg0) {}

            @Override
            public void onAnimationStart(Animation arg0) {}
        });

        view.startAnimation(animationSet);

    }

    // move vertical animation
    private void startEndVerticalAnimation(final View view, long duration,
            float from, float to, final boolean disappear) {
        AnimationSet animationSet = new AnimationSet(true);

        if (disappear) {
            AlphaAnimation alphaAnimation = new AlphaAnimation(1, 0);
            animationSet.addAnimation(alphaAnimation);
        }
        TranslateAnimation translateAnimation = new TranslateAnimation(0, 0,
                from, to);
        animationSet.addAnimation(translateAnimation);
        animationSet.setDuration(duration);
        animationSet.setFillAfter(true);
        animationSet.setFillEnabled(true);
        animationSet.setAnimationListener(new Animation.AnimationListener() {

            @Override
            public void onAnimationEnd(Animation arg0) {
                if (disappear) {
                    if (mSlideListener != null) {
                        mSlideListener.onSlideFinish();
                    }
                } else {
                    // 恢复 一定要清除动画,否则动画效果仍然在,params 效果 跟Animation 效果同时作用着
                    view.clearAnimation();
                    view.setLayoutParams(saveParams);

                }
            }

            @Override
            public void onAnimationRepeat(Animation arg0) {}

            @Override
            public void onAnimationStart(Animation arg0) {}
        });

        view.startAnimation(animationSet);

    }


    float beginX = 0, beginY = 0;

    boolean isMoveFragment = false;
    boolean isFirstMoveEvent = false;

    private boolean judgeVertical(View v, MotionEvent event) {

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            beginY = event.getRawY();
            beginX = event.getRawX();
            isFirstMoveEvent = true;
            isMoveFragment = false;
            Log.i("", "touch ACTION_DOWN beginX---" + beginY);

            saveParams = new FrameLayout.LayoutParams(
                    (ViewGroup.MarginLayoutParams) v.getLayoutParams());
            return false;
        case MotionEvent.ACTION_MOVE:


            if (isFirstMoveEvent) {
                isFirstMoveEvent = false;
                isMoveFragment = true ;
                float moveDis = event.getRawY() - beginY;
                float moveDisH = Math.abs(event.getRawX() - beginX) ;
                if(moveDisH > 0){
                    //  move horizonal
                    isMoveFragment = false;
                    break;
                }

                switch (direction) {
                case BOTTOM:
                    if (moveDis <= 0)
                        isMoveFragment = false;
                    break;
                case TOP:
                    if (moveDis >= 0)
                        isMoveFragment = false;
                    break;
                case VERTICAL:
                    if (moveDis == 0)
                        isMoveFragment = false;
                    break;
                default:
                    break;
                }
            }

            if (!isMoveFragment) {
                //
                break;
            }

            return true;
        case MotionEvent.ACTION_UP:
            if (!isMoveFragment)
                break;

            return true;
        }

        return false;
    }

    private boolean judgeHorizonal(View v, MotionEvent event) {

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            beginX = event.getRawX();
            beginY = event.getRawY();
            isFirstMoveEvent = true;
            isMoveFragment = false;
            Log.i("", "touch ACTION_DOWN beginX---" + beginX);

            saveParams = new FrameLayout.LayoutParams(
                    (ViewGroup.MarginLayoutParams) v.getLayoutParams());
            return false;
        case MotionEvent.ACTION_MOVE:

            if (isFirstMoveEvent) {
                float moveDis = event.getRawX() - beginX;
                float  moveDisV =Math.abs( event.getRawY() - beginY );
                isFirstMoveEvent = false;
                isMoveFragment = true ;
                if(moveDisV > 0){
                    // if has move vertical
                    isMoveFragment = false;
                    break;
                }
                switch (direction) {
                case RIGHT:
                    if (moveDis <= 0 )
                        isMoveFragment = false;
                    break;
                case LEFT:
                    if (moveDis >= 0)
                        isMoveFragment = false;
                    break;
                case HORIZONAL:
                    if (moveDis == 0)
                        isMoveFragment = false;
                    break;
                default:
                    break;
                }

            }

            if (!isMoveFragment) {
                break;
            }

            return true;
        case MotionEvent.ACTION_UP:
            if (!isMoveFragment)
                break;
            return true;
        }

        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean res = false;
        switch (direction) {
        case LEFT:
        case RIGHT:
        case HORIZONAL:
            res = slideHorizonalFinish(this, event);
            break;
        case TOP:
        case BOTTOM:
        case VERTICAL:
            res = slideVerticalFinish(this, event);
            break;
            default:
        }
        return res;
    }

    private static final String TAG = "SLIDEFRAMELAYOUT";

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean judgeRes = false;
        switch (direction) {
        case LEFT:
        case RIGHT:
        case HORIZONAL:
            judgeRes = judgeHorizonal(this, ev);
            break;
        case TOP:
        case BOTTOM:
        case VERTICAL:
            judgeRes = judgeVertical(this, ev);
            break;
        case NO:
            judgeRes = false;
            break;
        }
        Log.i(TAG, "onInterceptTouchEvent--"+judgeRes);
        return judgeRes;
    }
}

由于将上下左右滑动整合到一起去了,还有点混乱。主要的思路就是在onIterceptTouchEvent中判断是否已经有了指定方向上的滑动,如果滑动了,则截断事件的派发,转而去滑动页面(onTouchEvent)。

代码demo在:https://github.com/xxxzhi/SlideListener

是一个library,可以引用后使用。

在Activity中 使用的 demo如下:

View view = getLayoutInflater().inflate(R.layout.activity_test, null);
SlideFrameLayout slideFrameLayout = new SlideFrameLayout(this);
slideFrameLayout.addView(view);
slideFrameLayout.setDirection(SlideDirection.BOTTOM);
setContentView(slideFrameLayout);


slideFrameLayout.setSlideListener(new SlideFrameLayout.SlideListener() {

    @Override
    public void onSlideFinish() {
        finish();
    }
});

参考

[1] http://mobile.51cto.com/abased-374715.htm


多一点思考,多一点想法,多一点实践,多一份美妙

==============================

刚还在看鲁迅的杂文,特别赞同蔡元培先生的评论:

杂文与短评……他的感想之丰富,观察之深刻,意境之隽永,字句之正确,他人所苦思力索而不易得当的,他就很自然地写出来,这是何等天才!又是何等学力!