Android里的View滑动

我们平时会经常遇到View的滑动,不管自定义View,还是动画,都需要这个东西,但是往往最熟悉的最陌生。

View滑动可以分为三大类

1、自身的ScrollTo和ScrollBy

2、通过动画给View平移效果

3、改变View的LayoutParams进行修改View的位置

很传统的三大分类。但是我们要思考为什么要分出这三类,各自的实现意义。

一、ScrollTo和ScrollBy。

首先看源码:

 public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

 public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

  postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt);

其实都是本质都是ScrollTo方法。我们看一下mScrollX和mScrollY的含义是什么?
View里的内容在横向的偏移像素量。记住View和View里的内容。
就好比:view = 盒子 内容 = 盒子里的东西,我们改变的是盒子里的东西的位置,而不是这个盒子的位置。所以我们在运用这个方法的时候,一定是移动View里的子View。而不是这个View本身。

  /**
     * The offset, in pixels, by which the content of this view is scrolled
     * horizontally.
     * {@hide}
     */
    @ViewDebug.ExportedProperty(category = "scrolling")
    protected int mScrollX;

我们来看一下具体实现,x,y就是我们要scroll的位置,首先要把之前scroll的值赋为oldX,oldY,也就是之前的偏移量,那么如果我们要到达x,y,就需要计算差值,也就是:

desX – oldX = 移动的值 =dx。

如果我们之前的偏移量为100,100,ScrollTo(200,200),那么移动的值就是 200-100 = 100;需要在移动100,而不必从原始位置再到目的位置了,而是从现有位置移动到目的位置。

那么这个移动的方向是基于什么的呢?通过实际测试,我们发现是

View左边界 – View内容左边界 = scrollX
View上边界 – View内容上边界 = scrollY

这么一说 我们发现当scrollX 为正的时候,我们看到内容的移动方向是反方向,其实就去想象你自己用手去拉这个黄纸,左边和上边为正方向,得到的值就是scroll的值。

《Android里的View滑动》

当我们在运用的时候一定要切记

1、如果我们想让这个View移动,那么一定要在它的父View里运用这个方法。
2、一定要是反方向

点击子view,在父View(全屏View)中移动。
点击view,内容在其内移动。
至于代码大家自己可以亲自实现一下。

《Android里的View滑动》

但是我们平时移动不会这么僵硬的,更多时候需要Scroller来实现一些阻尼效果。既然讲到这里了,不讲Scroller肯定是过不去的了。

Scroller

用法很简单:

  1. 创建Scroller的实例
  2. 调用startScroll()方法来初始化滚动数据并刷新界面
  3. 重写computeScroll()方法,并在其内部完成平滑滚动的逻辑

没啥需要多说的,我们借用郭神的例子来实际探究一下Scroller的魅力所在。

我们要模仿的是ViewPager的效果。

《Android里的View滑动》 ezgif.com-video-to-gif.gif

首先我们要具备基本的自定义View的知识不了解的可以看一下 谷哥的小弟大神的系列文章。

public class XViewPage extends ViewGroup {
    /**
     * 手指此刻所在的屏幕坐标
     */
    private float mMoveX;
    /**
     * Scroller 的实例
     */
    private Scroller mScroller;

    /**
     * 判定拖动的最小距离像素值
     */
    private int mTouchSlop;

    /**
     * 按下X坐标值
     */
    private float mDownX;

    /**
     * 上一次触发ACTION_MOVE的X坐标值
     */
    private float mLastMoveX;
    /**
     * 界面可滚动右边界
     */
    private int rightBorder;
    /**
     * 界面可滚动左边界
     */
    private int leftBorder;

    public XViewPage(Context context) {
        this(context, null);
    }

    public XViewPage(Context context, AttributeSet attrs) {
        this(context, attrs, 0);

    }

    public XViewPage(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScroller = new Scroller(context);
        ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = configuration.getScaledPagingTouchSlop();
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                //把这几个子控件平铺
                childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());

            }
            //初始化左右边界
            leftBorder = getChildAt(0).getLeft();
            rightBorder = getChildAt(getChildCount() - 1).getRight();

        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int chileCount = getChildCount();
        for (int i = 0; i < chileCount; i++) {
            View childView = getChildAt(i);
            //测量每一个子View的大小
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = ev.getRawX();
                mLastMoveX = mDownX;
                break;
            case MotionEvent.ACTION_MOVE:
                mMoveX = ev.getRawX();
                float diff = Math.abs(mMoveX - mDownX);
                if (diff > mTouchSlop) {
                    return true;
                }
                break;
        }


        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mMoveX = event.getRawX();
                int scrolledX = (int) (mLastMoveX - mMoveX);
                if (getScrollX() + scrolledX < leftBorder) {
                    scrollTo(leftBorder, 0);
                    return true;
                } else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
                    scrollTo(rightBorder-getWidth(), 0);
                    return true;
                }

                scrollBy(scrolledX, 0);
                mLastMoveX = mMoveX;
                break;
            case MotionEvent.ACTION_UP:
                //当手抬起来的时候 根据当前的滚动值 来判断应该滚动到哪一个子view的界面
                int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                int dx = targetIndex * getWidth() - getScrollX();
                //调用startScroll() 来初始化滚动数据 并刷新界面
                mScroller.startScroll(getScrollX(), 0, dx, 0);
                postInvalidate();

                break;
        }


        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
      if(mScroller.computeScrollOffset()){
          scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
          invalidate();
      }
    }
}

总结一下过程:
1、重写onMeasure() 测量每一个子View的大小
2、重写onLayout() 横向平铺这些子View
3、 重写 onTouchEvent 和 onInterceptTouchEvent的 事件分发处理
4、重写 computeScroll()实现mScroller的scrollTo方法,达到顺滑的效果

我们发现ScrollTo在Android中的而运用实在是太广泛了。

动画给View平移效果

这个我们在做交互,转场动画的时候经常会用到。特别是3.0以上的属性动画,解决了身形不同的问题。

当3.0以下的时候,采用nineoldndroids这个开源动画,效果和属性动画一致,不同的是移动的只是一个影像而不是本体,在点击事件方面有着天生的缺陷,解决的办法也只能在以后后的位置隐藏一个相同的View来显示并做事件处理,这样移花接木的手段。

ObjectAnimator animator = ObjectAnimator.ofFloat(imageView, "alpha", 1f, 0f);
animator.setDuration(1000);//时间1s
animator.start();

动画我们将在之后的文章中详细讲解,这里就不多介绍了。

改变布局参数

这是一个治本的方式。可以对View的大小位置坐各种改变。

 ViewGroup.LayoutParams params = view.getLayoutParams();
    ViewGroup.MarginLayoutParams marginParams = null;
    if (params instanceof  ViewGroup.MarginLayoutParams) {
        marginParams = (ViewGroup.MarginLayoutParams) params;
    } else {
        marginParams = new ViewGroup.MarginLayoutParams(params);
    }
        marginParams.height = 500;
        marginParams.setMargins(10, 200, 30, 40);
        view.setX(100);
        view.setLayoutParams(marginParams);
       // view.requestLayout();

最后

总结一下三个滑动的使用场景

1、scrollX 和 scrollY 操作简单,适合对View的内容进行滑动(比如模仿ViewPager)
2、动画,操作简单适合,没有交互的View,实现复杂的动画效果
3、改变布局和View的参数,比较复杂,但是适用于有交互的View

大家根据不同需求选择和配合使用。

参考:

    原文作者:猪_队友
    原文地址: https://www.jianshu.com/p/13bceac031e6
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞