学习了Android的坐标系,接下来,我将讲解如何实现Android的View滑动,以及常见的实现view滑动的方法和对比,最后讲解每一个方法。
如何实现Android的View滑动?
实现View的滑动方法有很多,但是万变不离其宗,实现的思想总是相同的或者类似的。当触碰View时,记录当前触碰点的坐标;当手指移动时,记录移动后的触碰点的坐标;从何计算出相当于前一次坐标的偏移量,并通过偏移量修改View的坐标。
常见的实现View的方法
- layout
- offsetLeftAndRight + offsetTopAndBottom
- setLayoutParams 修改View的LayoutParams
- scrollBy / scrollTo
- Scroller + ((View)getParent()).scrollTo
- 动画
- ViewDragHelper
其中View的滑动可以瞬时的,也可以非瞬时的,按瞬时和非瞬时分类
- 瞬时滑动
- layout
- offsetLeftAndRight + offsetTopAndBottom
- setLayoutParams
- scrollBy / scrollTo
- 非瞬时滑动,
- Scroller + ((View)getParent()).scrollTo
- 动画
- ViewDragHelper
方法讲解和使用
1. layout
原理:当View绘制时,会调用onLayout()方法设置它的显示位置。我们调用layout对自己重新布局,它内部的实现调用onLayout,从何达到View的移动。layout实现的部分代码如果:
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b); // layout 内部调用onLayout
}
关键代码:
首先每一次触碰时,获取触碰点的坐标,代码如下所示:
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
接着在 ACTION_DOWN
记录触碰点的坐标,代码如下所示:
case MotionEvent.ACTION_DOWN:
//记录触摸点的坐标
mLastX = rawX;
mLastY = rawY;
break;
最后在 ACTION_MOVE
中计算偏移量,并将偏移量作用到layout
方法中,代码如下所示:
case MotionEvent.ACTION_MOVE:
//计算偏移量
int offsetX = rawX - mLastX;
int offsetY = rawY - mLastY;
//在当前的left、top、right、bottom的基础上添加偏移量
layout(getLeft() + offsetX, getTop() + offsetY,
getRight() + offsetX, getBottom() + offsetY);
//重新设置初始坐标
mLastX = rawX;
mLastY = rawY;
break;
注意:在上述代码我们使用的getRawX()、getRawY()获得绝对坐标来计算偏移量,当然我们可以用getX()、getY()获得相当坐标来计算偏移量。
2. offsetLeftAndRight + offsetTopAndBottom
offsetLeftAndRight 、offsetTopAndBottom 是系统封装了对View左右、上下移动的方法。使用它们的代码如下:
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
3. 修改View的LayoutParams
LayoutParams保存了View的布局参数,因此通过修改LayoutParams达到改变View位置的效果。关键代码如下:
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
params.leftMargin = getLeft() + offsetX;
params.topMargin = getTop() + offsetY;
setLayoutParams(params);
注意:根据父布局的类型不同,需要根据情况使用不同LayoutParams,如LinearLayout.LayoutParams、RelativeLayout.LayoutParams。这么我们用的是ViewGroup.MarginLayoutParams,因为它是ViewGroup的LayoutParams的一个子类,是其他布局的LayoutParams的父类。用它不用考虑父布局的类型。
4. scrollBy / scrollTo
scrollBy和scrollTo的区别很好理解,根据名字来理解。
- scrollTo(x, y)表示移动一个具体的坐标点(x, y)
- scrollBy(dx, dy)表示移动的增量为dx, dy
在获得偏移量之后,我们使用scrollBy来移动View。代码如下所示:
scrollBy(offsetX, offsetY)
你会发现View并没有移动。scrollBy 和scrollTo移动的是View的content,移动的是View的内容。在ViewGroup中使用scrollBy 、scrollTo,移动是所有的子View;在View中使用scrollBy 、scrollTo,移动使View的内容,例如TextView的content就是它的文本、ImageView的content就是它的drawable对象。
因此我们应该在ViewGroup中使用scrollBy 来移动它的子View,scrollBy 代码中调用scrollTo实现的。代码如下所示:
((View)getParent()).scrollBy(offsetX, offsetY);
但是,当再拖动View时,你会发现View虽然动了,却是在乱动。下面我将给大家讲解一下view移动的知识了。
如下面的图所示,UI可见区域,为屏幕,即红色矩阵范围。绿色区域就是View,content就是它的内容,画布。画布可以很大,可以很小,我们看不见的View,不代表它不存在,比如在下面的图中,可见区域只有一个Button(20, 10),还有一个Button我们就看不见。
而scrollBy 移动就是屏幕,当调用scrollBy(20, 10),屏幕在X轴正方向(右边)平移20,在Y轴反方向(下发)平移10,得到如下所示的结果。可以区域的button坐标,变成了(0, 0)。scrollBy(20, 10)屏幕沿X轴正方向,Y轴正方向移动了,但是button相当于屏幕来说却向X轴负方向,Y轴负方向移动了。因为参考系选择不同。
总结一下:
- scrollBy移动是View的content,如果要移动View,获取ViewGroup,调用scrollBy;
- scrollBy移动是屏幕,(dx, dy)是屏幕移动的方向,因为参考系的不同,其实对于View的content来说是移动是(-dx, -dy);
- scrollBy是基于scrollTo实现的;
综上所述,修改上面代码。即可达到我们想要的结果,代码如下所示:
((View)getParent()).scrollBy(-offsetX, -offsetY);
5. Scroller
使用Scroller类实现非瞬时滑动,即平滑移动的效果。Scroller实现的原理图如下所示。
使用Scroller有三个步骤。
- 初始化Scroller
mScroller = new Scroller(getContext());
- 重写View的computeScroll()方法。
重写computeScroll(),它是使用Scroller的核心,系统绘制View的时候,会在draw()方法,调用该方法。该方法实际上就是使用scrollTo,完成对View的移动。移动的坐标点,通过Scroller对象,获得。
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()){ //判断是否完成绘制
((View)getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
computeScrollOffset()判断是否完成整个滑动,getCurrX(),getCurrY()获得当前滑动的坐标。 computeScroll不能直接调用,我们通过invalidate()→draw()→computeScroll()方式,间接调用。
- startScroll 开始滑动过程。
通常可以使用getScrollX(),getScrollY()方法来获得父视图中content所滑动到点的坐标,这一个值的正负与scrollBy、scrollTo的情况一样。
public void startScroll(int startX, int startY, int dx, int dy)
public void startScroll(int startX, int startY, int dx, int dy, int duration)
例子:修改例子4的代码,达到手机离开屏幕时,让子View平滑的移动到初始位置。监听手指离开,之需要监听ACTION_UP
事件就可以了,关键代码如下:
case MotionEvent.ACTION_UP:
View view = (View)getParent();
mScroller.startScroll(view.getScrollX(), view.getScrollY(),
-view.getScrollX(), -view.getScrollY());
invalidate();//通知View进行重新绘制,从而调用computeScroll
break;
Scroller补充:Scroller原理简单讲解。
- Scroller可以设置不同Interpolator插值器,我们上一个例子是匀速的滑动,我们可以设置插值器达到加速、减少,先加速再减少的效果,在两点之间有无数个函数,所有我们想达到的效果可是无穷的。
- Scroller本身是不能滑动View,滑动View的是scrollTo方法。
- startScroll只是做属性值的初始化。
- computeScrollOffset用于判断是否完成滑动,它主要工作是根据插值器和已经发生的时间,更新当前的滑动坐标(mCurrX, mCurrY)。
6. 动画
动画这里简单的讲解一下,下面章节会详细讲解动画
- 视图动画,xml实现View移动
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true">
<translate
android:fromXDelta="0"
android:fromYDelta="0"
android:duration="3000"
android:toXDelta="500"
android:toYDelta="500"
/>
</set>
this.setAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.translate));
- 视图动画,Java实现View移动
TranslateAnimation animation = new TranslateAnimation( 0, 500, 0, 500);
this.setAnimation(animation);
animation.setDuration(3000);
animation.setFillAfter(true);
animation.startNow();
- 属性动画
ObjectAnimator animatorX = ObjectAnimator.ofFloat(this, "translationX", 0, 500);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(this, "translationY", 0, 500);
AnimatorSet animationSet = new AnimatorSet();
animationSet.setDuration(3000);
animationSet.playTogether(animatorX, animatorY);
animationSet.start();
7. ViewDragHelper
以后讲解
参考博客
https://www.jianshu.com/p/543b88fa609c
https://blog.csdn.net/guolin_blog/article/details/48719871