Android View体系(三)--实现 View 的滑动七种方式

文章目录

  • 1 前言
  • 2 方式
    • 2.1 layout()
    • 2.2 动画
      • 2.2.1 View 动画
      • 2.2.2 属性动画
    • 2.3 scollTo与scollBy
    • 2.4 offsetTopAndBottom/offsetLeftAndRight
    • 2.5 LayoutParams 改变布局参数
    • 2.6 Scroller
      • 2.6.1 Scroller 原理
    • 2.7 ViewDragHelper
  • 3 参考文章

1 前言

View 的滑动在 Android 中非常广泛,例如我们在自定义控件,实现一些效果时候,常常需要使用到 View 的滑动,View 滑动方式包含如下七种。

  • layout()
  • 动画(View 动画和属性动画)
  • scollTo/scollBy
  • offsetTopAndBottom/offsetLeftAndRight
  • LayoutParams 改变布局参数
  • Scroller
  • ViewDragHelper

2 方式

2.1 layout()

View 在绘制时候,会调用 onLayout() 方法设置 View 的显示位置,因此,我们完全可以使用 layout() 函数重新对 View 位置进行布局,来实现滑动的效果。接下来,我们来自定义一个跟随手指移动的 View。

//1.自定义 View
public class CustomView extends View {
    private int mLastX;
    private int mLastY;

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

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //获取 X坐标 Y坐标
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                break;

            case MotionEvent.ACTION_MOVE:
                //计算移动的距离
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;
                //调用layout方法来重新布局
                layout(getLeft() + offsetX, getTop() + offsetY,
                        getRight() + offsetX, getBottom() + offsetY);
                break;
            default:
                break;
        }
        return true;
    }
}

//2.在布局文件中使用
<com.yoyiyi.blob.CustomView
     android:id="@+id/cv"
     android:layout_width="40dp"
     android:layout_height="40dp"
     android:background="#000000" />

上面代码非常简单,在 onTouchEvent 中我们获取到手指点击的坐标,然后计算出其移动距离,最后是由 layout() 重新进行布局。

2.2 动画

采用动画来进行移动,动画分为 View 动画和属性动画,最大区别是 View 动画只是对 View 的影像的变化,View 还是在原来位置,响应事件也在原来位置。

2.2.1 View 动画

//1.创建一个在 X 轴上 平移到坐标 200 动画
TranslateAnimation translateAnimation = new TranslateAnimation(0, 200, 0, 0);
translateAnimation.setDuration(1000);
translateAnimation.setFillAfter(true);
cv.setAnimation(translateAnimation);
translateAnimation.start();

2.2.2 属性动画

属性动画就更加简单了,代码如下。

ObjectAnimator translation = ObjectAnimator.ofFloat(cv, "translationX", 0, 200).setDuration(1000).start();

2.3 scollTo与scollBy

scollTo(x, y) 表示移动到具体的坐标点,scollBy(dx, dy) 表示移动增量 dx、dy,scollBy 最终也是调用 scollTo。需要注意的是这两个方法都是移动 View 的内容,如果是 ViewGroup 则移动子 View。

linearLayout.scollTo(300,0)
//需要注意,负数表示向正方向移动,这只是参考系不同而已
 ((View) getParent()).scrollBy(-offsetX, -offsetY);

2.4 offsetTopAndBottom/offsetLeftAndRight

这两个方法和 layout() 效果差不多,所以 2.1 的例子完全可以替换成如下方式。

offsetTopAndBottom(offsetY);
offsetLeftAndRight(offsetX);

2.5 LayoutParams 改变布局参数

LayoutParams 中保存了 View 的布局参数,我们可以通过获取到参数,再改变参数来实现滑动效果。

//1.使用父控件的 LayoutParams 
LinearLayout.LayoutParams params= (LinearLayout.LayoutParams)getLayoutParams();
params.leftMargin = getLeft() + offsetX;
params.topMargin = getTop() + offsetY;
setLayoutParams(params)

//2.使用 ViewGroup.MarginLayoutParams
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);

2.6 Scroller

使用 scollTo 和 scollBy 方法,滑动是一瞬间完成,体验不好,这里就需要使用到 Scroller 来实现弹性滑动。我们自定义一个能实现滑动 View,如下所示。

//1.自定义 View
public class CustomView extends View {
    private Scroller mScroller;

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

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScroller = new Scroller(context);
    }


    public void smoothScrollTo(int dstX,int dstY){
        int scrollX = getScrollX();
        int deltaX = dstX - scrollX;
        //6000 秒内滑向 destX
        mScroller.startScroll(scrollX, 0, deltaX, 0, 6000);
        invalidate();
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        //computeScrollOffset() 根据时间的流逝,计算当前的 scrollX 和 scrollY 的值
        if(mScroller.computeScrollOffset()){
            ((View) getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            //通过不断的重绘不断的调用computeScroll方法
            invalidate();
        }
    }
}

//2.使用,向右滑动 300 
cv.smoothScrollTo(-300,0);

2.6.1 Scroller 原理

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration; //滑动的时间
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX; //滑动的起点
        mStartY = startY; 
        mFinalX = startX + dx; //要滑动的距离
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

从上面代码看出,当我们创建一个 Scroller,并使用 startScroll(),只是保存了我们传递过来的参数,Scroller 就没有做其他的事情,那么它是如何工作的,在 startScroll() 方法后面我们调用 invalidate(),众所周知,这个方法会导致 View 重新绘制,也就是调用 View 的 draw(),而在 draw() 方法又会调用 computeScroll(),这是一个空实现,需要我们重写,上面的代码,我们调用 computeScrollOffset() ,这个方法作用根据时间的流逝,计算当前的 scrollX 和 scrollY 的值,接着获取 Scroller 当前的 scrollX 和 scrollY,然后通过,scrollTo 方法进行移动,接着又调用 invalidate() 进行第二次绘制,总结如下。

  • 创建 Scroller,调用 startScroll() 保存参数
  • 调用 invalidate() 重绘,会调用 View 的 draw()
  • View 的 draw() 会调用 computeScroll()
  • computeScroll() 调用 computeScrollOffset() 作用根据时间的流逝,计算当前的 scrollX 和 scrollY 的值
  • 获取到当前的 scrollX 和 scrollY,使用 scrollTo() 进行移动
  • invalidate() 进行第二次重绘,又调用 View 的 draw() ,如此不断重复

2.7 ViewDragHelper

ViewDragHelper 这个类负责手势操作,它是官方写的一个专门为自定义ViewGroup处理拖拽的手势类,所以 2.1 随手势移动自定义控件可以用 ViewDragHelper 实现,这里我们实现在自定义控件里面子 View 可以自由拖拽。

//1.自定义子 View
public class CustomView extends LinearLayout {
    private ViewDragHelper mHelper;

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

    public CustomView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mHelper =  ViewDragHelper.create(this, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(@NonNull View child, int pointerId) {
                //返回 true
                return true;
            }
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                return left;
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                return top;
            }
        });

    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        //接管事件
        return mHelper.shouldInterceptTouchEvent(event);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mHelper.processTouchEvent(event);
        //返回 true 消费事件
        return true;
    }
}

//2.使用自定义 View
<com.yoyiyi.blob.CustomView
     android:id="@+id/cv"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
     <ImageView
         android:layout_width="40dp"
         android:layout_height="40dp"
         android:background="#000000"/>
</com.yoyiyi.blob.CustomView>

3 参考文章

Android View滑动的七种方式总结
Android View体系(二)实现View滑动的六种方法

你可能感兴趣的:(View体系)