实现滑动的7种方法

    通过一个案例, 来看看在Android 中该如何实现滑动效果。 定义一个View, 并置于一个LinearLayout 中, 实现一个简单布局, 代码如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.yuyang.dragviewtest.DragView
        android:layout_width="100dp"
        android:layout_height="100dp" />

</LinearLayout>

 目的让这个自定义的View随着手指在屏幕上滑动而滑动初始效果如下:


一、layout方法:

        一、通过getX(),getY()来获取坐标值:

public class DragView1 extends View {

    private int lastX;
    private int lastY;

    public DragView1(Context context) {
        super(context);
        ininView();
    }

    public DragView1(Context context, AttributeSet attrs) {
        super(context, attrs);
        ininView();
    }

    public DragView1(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        ininView();
    }

    private void ininView() {
        // 给View设置背景颜色,便于观察
        setBackgroundColor(Color.BLUE);
    }

    // 视图坐标方式
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录触摸点坐标
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                // 计算偏移量
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                // 在当前left、top、right、bottom的基础上加上偏移量
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
//                        offsetLeftAndRight(offsetX);
//                        offsetTopAndBottom(offsetY);
                break;
        }
        return true;
    }
}

  二、通过getRawX(), getRawY() 来获取坐标值:

public class DragView2 extends View {

    private int lastX;
    private int lastY;

    public DragView2(Context context) {
        super(context);
        ininView();
    }

    public DragView2(Context context, AttributeSet attrs) {
        super(context, attrs);
        ininView();
    }

    public DragView2(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        ininView();
    }

    private void ininView() {
        setBackgroundColor(Color.BLUE);
    }

    // 绝对坐标方式
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int rawX = (int) (event.getRawX());
        int rawY = (int) (event.getRawY());
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录触摸点坐标
                lastX = rawX;
                lastY = rawY;
                break;
            case MotionEvent.ACTION_MOVE:
                // 计算偏移量
                int offsetX = rawX - lastX;
                int offsetY = rawY - lastY;
                // 在当前left、top、right、bottom的基础上加上偏移量
                layout(getLeft() + offsetX,
                        getTop() + offsetY,
                        getRight() + offsetX,
                        getBottom() + offsetY);
                // 重新设置初始坐标
                lastX = rawX;
                lastY = rawY;
                break;
        }
        return true;
    }
}

  使用绝对坐标, 执行MOVE 逻辑后每次都要重新设置初始坐标。

二、 offsetLeftAndRight() 和 offsetTopAndBottom():

 // 视图坐标方式
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录触摸点坐标
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                // 计算偏移量
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                // 在当前left、top、right、bottom的基础上加上偏移量
                offsetLeftAndRight(offsetX);
                offsetTopAndBottom(offsetY);
                //layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
//                        offsetLeftAndRight(offsetX);
//                        offsetTopAndBottom(offsetY);
                break;
        }
        return true;
    }

三、LayoutParams:

  LayoutParams 保存了一个View 的布局参数。通过修改参数可以动态的修改布局的位置参数,从而修改View 的位置效果, 获取偏移量后 通过setLayoutParams 来改变其LayoutParams。 
  
@Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录触摸点坐标
                lastX = (int) event.getX();
                lastY = (int) event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                // 计算偏移量
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
//                LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft() + offsetX;
                layoutParams.topMargin = getTop() + offsetY;
                setLayoutParams(layoutParams);
                break;
        }
        return true;
    }

 根据View所在父布局的类型来设置不同类型的LayoutParams, 前提是你必须需要一个父布局, 通过LayoutParams来改变View的位置通常是改变这个View的Margin属性,通常使用  ViewGroup.MarginLayoutParams 来实现这样的功能,不需要考虑父布局类型。

四、scrollTo 与 scrollBy:

  scrollTo(x,y) 表示移动到一个点, scrollBy(dx, dy) 表示移动的增量为dx, dy。直接在View使用这两个方法中拖动的是View的content而不是View, 我们想拖动View 就必须获得View的 ViewGroup, 代码如下:

((View) getParent()).scrollBy(-offsetX, -offsetY);

   要实现跟随手指移动,需要将偏移量改为负值。

五、Scroller:

  Scroller类与scrollTo, scrollBy 相似, 不同的是scrollBy是通过将一段位移通过不断切分 使其看起来是平滑的过渡, 但其实是瞬间的移动, 而Scroller类则是平滑移动, 不再是瞬间的移动。看下面个例子:

 1、 初始化Scroller, 通过构造方法创建Scroller 对象:

 private void ininView(Context context) {
        setBackgroundColor(Color.BLUE);
        // 初始化Scroller
        mScroller = new Scroller(context);
    }

 2、重写computeScroll() ,它是Scroller的核心,实现模拟滑动:

@Override
    public void computeScroll() {
        super.computeScroll();
        // 判断Scroller是否完成了整个滑动
        if (mScroller.computeScrollOffset()) {
            ((View) getParent()).scrollTo(
                    mScroller.getCurrX(),
                    mScroller.getCurrY());
            // 通过重绘来不断调用computeScroll
            invalidate();
        }
    }
    invalidate()--->draw()--->computeScroll(), 实现循环获取scrollX 和 scrollY的目的。实现平滑过渡。

3、startScroll开启模拟过程:

startScroll() 方法具有两个重载方法。

  startScroll(int startX, int startY, int dx, int dy, int duration)

  startScroll(int startX, int startY, int dx, int dy)

   它们的区别是一个可以设置过渡时长。要监听手指离开屏幕的事件, 并在该事件中通过调用startScroll() 方法完成平滑移动。

在startScroll()方法中获取子View移动的距离, 并将其偏移量设置为相反数, 从未将其子View滑动到原位置。 事件中需要添加incadate() 方法,完成屏幕刷新。

case MotionEvent.ACTION_UP:
                // 手指离开时,执行滑动过程
                View viewGroup = ((View) getParent());
                mScroller.startScroll(
                        viewGroup.getScrollX(),
                        viewGroup.getScrollY(),
                        -viewGroup.getScrollX(),
                        -viewGroup.getScrollY(),3000);
                invalidate();
                break;

  六、属性动画:

  七、ViewDargHelper:

      support库为开发者提供了DrawerLayout 和 SlodingPaneLayout 两个布局来实现侧边栏滑动的效果。 在这背后是ViewDragHelper 的实现, 通过ViewDragHelper 基本可以实现多种的滑动,拖放需求。下面通过一个案例进行ViewDrag Helper的运用:

      初始状态                               滑动展开菜单状态

1、初始化ViewDragHelper 

private void initView() {
        mViewDragHelper = ViewDragHelper.create(this, callback);
    }

    第一个参数是要监听的View, 通常是parentView, 第二个是Callback 回调, 这个回调就是ViewDragHelper 的逻辑核心。

2、拦截事件 

 然后重写事件拦截方法, 将事件传递给ViewDragHelper 进行处理: 

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递给ViewDragHelper,此操作必不可少
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

3、处理computeScroll()

   ViewDragHelper 也是通过Scroller 来进行滑动的。

 @Override
    public void computeScroll() {
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

4、处理回调Callback

  这是关键的Callback 实现:

 private ViewDragHelper.Callback callback =
            new ViewDragHelper.Callback() {

                // 何时开始检测触摸事件
                @Override
                public boolean tryCaptureView(View child, int pointerId) {
                    //如果当前触摸的child是mMainView时开始检测
                    return mMainView == child;
                }

};

   通过IDE自动填写的 tryCaptureView() 方法, 我们可以指定在创建ViewDragHelper 时, 参数parentView 中的哪一个子View 可以被移动,在这个例子中定义了两个View, 通过如下代码, 只将MainViwe 设置为可被拖动的。

 具体的滑动方法为clampViewPositionVertical() 和clampViewPositionHorizontal(), 分别对应垂直和水平方向的滑动,这是实现滑动必须实现的方法, 

// 处理垂直滑动
                @Override
                public int clampViewPositionVertical(View child, int top, int dy) {
                    return 0;  //此处返回值为0, 和没重写效果一样, 子垂直方向没有滑动
                }

                // 处理水平滑动
                @Override
                public int clampViewPositionHorizontal(View child, int left, int dx) {
                    return left;
                }

 通常情况下返回top、 left即可, 但需要更加精密的计算padding等属性时, 就需要对left 进行一些处理, 并返回合适大小。

  仅仅重写上面三个方法,就可以实现一个简单的滑动效果。 让MainView跟随手指滑动。

  接下来当手指离开后子View回到初始位置效果, 在Callback中, 提供了onViewReleased() 方法, 可以简单的实现当手纸离开屏幕后实现的操作。 其内部也是通过Scrollrt类来实现的, 

// 拖动结束后调用
                @Override
                public void onViewReleased(View releasedChild, float xvel, float yvel) {
                    super.onViewReleased(releasedChild, xvel, yvel);
                    //手指抬起后缓慢移动到指定位置
                    if (mMainView.getLeft() < 500) {
                        //关闭菜单
                        //相当于Scroller的startScroll方法
                        mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
                        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
                    } else {
                        //打开菜单
                        mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
                        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
                    }
                }
            };

 设置让MainView移动后距离左边距小于500 像素的时候, 使用 mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
将MainView 还原到初始状态。否则移动到(300,0), 即显示MenuView. 接着我们继续完善案例, 实现类似QQ侧滑栏菜单的效果, 自定义一个ViewGroup, 在onFinishInflate() 方法中, 按顺序将子View 分别定义成MeunView 和 MainView, 并在onSizeChanged()方法中获得View的宽度。 如果需要根据View的宽带处理滑动后的效果, 就可以使用这个值来进行判断。

@Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMenuView = getChildAt(0);
        mMainView = getChildAt(1);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = mMenuView.getMeasuredWidth();
    }

  完整代码如下:

public class DragViewGroup extends FrameLayout {

    private ViewDragHelper mViewDragHelper;
    private View mMenuView, mMainView;
    private int mWidth;

    public DragViewGroup(Context context) {
        super(context);
        initView();
    }

    public DragViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public DragViewGroup(Context context,
                         AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMenuView = getChildAt(0);
        mMainView = getChildAt(1);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = mMenuView.getMeasuredWidth();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递给ViewDragHelper,此操作必不可少
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

    private void initView() {
        mViewDragHelper = ViewDragHelper.create(this, callback);
    }

    private ViewDragHelper.Callback callback =
            new ViewDragHelper.Callback() {

                // 何时开始检测触摸事件
                @Override
                public boolean tryCaptureView(View child, int pointerId) {
                    //如果当前触摸的child是mMainView时开始检测
                    return mMainView == child;
                }

                // 触摸到View后回调
                @Override
                public void onViewCaptured(View capturedChild,
                                           int activePointerId) {
                    super.onViewCaptured(capturedChild, activePointerId);
                }

                // 当拖拽状态改变,比如idle,dragging
                @Override
                public void onViewDragStateChanged(int state) {
                    super.onViewDragStateChanged(state);
                }

                // 当位置改变的时候调用,常用与滑动时更改scale等
                @Override
                public void onViewPositionChanged(View changedView,
                                                  int left, int top, int dx, int dy) {
                    super.onViewPositionChanged(changedView, left, top, dx, dy);
                }

                // 处理垂直滑动
                @Override
                public int clampViewPositionVertical(View child, int top, int dy) {
                    return 0;
                }

                // 处理水平滑动
                @Override
                public int clampViewPositionHorizontal(View child, int left, int dx) {
                    return left;
                }

                // 拖动结束后调用
                @Override
                public void onViewReleased(View releasedChild, float xvel, float yvel) {
                    super.onViewReleased(releasedChild, xvel, yvel);
                    //手指抬起后缓慢移动到指定位置
                    if (mMainView.getLeft() < 500) {
                        //关闭菜单
                        //相当于Scroller的startScroll方法
                        mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
                        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
                    } else {
                        //打开菜单
                        mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
                        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
                    }
                }
            };

    @Override
    public void computeScroll() {
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

 在ViewDragHelper.Callback中, 系统定义了大量的监听事件来帮助我们处理各种事件,去处理程序中的滑动效果。


 总结:

   一、scrollTo/ scrollBy: 操作简单, 适合对View 内容的滑动。

  二、动画: 操作简单, 主要适合用于没有交互的View, 和实现复杂的动画效果

  三、改变参数布局: 操作稍复杂, 适用于有交互的View 

--------------------------------------------------------------


阅读更多
换一批

没有更多推荐了,返回首页