您的位置:首页 > 移动开发 > Android开发

Android中实现View滑动的几种方式

2017-01-06 11:17 447 查看
View 是对 UI 控件的抽象,它代表了屏幕上的一个矩形区域。通过继承 View,并重写相应方法,我们就能够实现具有各种外观及行为的 UI 控件。

什么是View?

Android中的View类是所有UI控件的基类(Base class),也就是说我们平时所有到的各种UI控件,比如Button、ImagView等等都继承自View类。LinearLayout、FrameLayout等布局管理器的直接父类是ViewGroup,而ViewGroup也有View类派生。总的来说,View是对UI控件的抽象,它代表了屏幕上的一个矩形区域。通过继承View,并重写相应方法,我们就能够实现具有各种外观及行为的UI控件。Button等控件我们之所以能够直接拿来即用,是因为Google已经帮我们完成了继承View并重写方法的工作。

View的位置

View在屏幕上的位置由它的以下四个参数所决定:

top:View的左上角的纵坐标,对应着View类中的成员变量mTop,可由getTop方法获得;

left:View的左上角的横坐标,对应着View类中的成员变量mLeft,可由getLeft方法获得;

bottom:View的右下角的纵坐标,对应着View类中的成员变量mBottom,可由getBottom方法获得;

right:View的右下角的横坐标,对应着View类中的成员变量mRight,可由getRight方法获得。

注意,以上的坐标都是相对于父View来说的,也就是说,坐标都是相对坐标,因为子View的布局是由父View来完成的。如下图所示:



有了这四个参数,计算View的宽高就很容易了:

width = right - left;

height = bottom - top。

关于View还有两个参数需要我们注意:

translationX代表View平移的水平距离,

translationY代表View平移的竖直距离;

x、y分别为View的左上角的横纵坐标。

View若经过了平移,改变的是它的x、y(代表当前View的左上角位置),它的四个位置参数代表了View的原始位置信息,是始终不变的。View在平移的过程中始终满足如下关系:

x = left + translationX;
y = top + translationY。


安卓视图有两个坐标系,一个是Android坐标系,一个是视图坐标系。前者以屏幕的最左上角为原点,向右为X轴正方向,向下为Y轴正方向。后者以父视图的左上角为原点,其它与前者一致。

而获取坐标的方法也可以分为两类,View提供的获得坐标的方法和MotionEvent提高的方法。

View提供的方法有getTop(),getLeft(),getBottom(),getRight(),

而MotionEvent提供的方法有getX(),getY(),getRawX(),getRawY()。



getLeft()方法为View自身左边到父布局左边的距离,getTop()方法为View自身的顶边到父布局顶边的距离,getRight()为View自身的右边到父布局左边的距离,getBottom()为View自身底边到父布局顶边的距离。

而getX()为为触摸点到View左边的距离,getY()为触摸点到View顶边的距离。getRawX()为触摸事件到屏幕左边的距离,getRawY()为触摸事件到屏幕顶边的距离。

MotionEvent中 get 和 getRaw 的区别:

event.getX();       //触摸点相对于其所在组件坐标系的坐标
event.getY();

event.getRawX();    //触摸点相对于屏幕默认坐标系的坐标
event.getRawY();


如下图所示:

PS:其中相同颜色的内容是对应的,其中为了显示方便,蓝色箭头向左稍微偏移了一点.



getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()这两对函数之间的区别

getMeasuredWidth()、getMeasuredHeight()返回的是measure过程得到的mMeasuredWidth和mMeasuredHeight的值,而getWidth()和getHeight()返回的是mRight - mLeft和mBottom - mTop的值。

一般情况下layout过程会参考measure过程中计算得到的mMeasuredWidth和mMeasuredHeight来安排子视图在父视图中显示的位置,但这不是必须的,measure过程得到的结果可能完全没有实际用处,特别是对于一些自定义的ViewGroup,其子视图的个数、位置和大小都是固定的,这时候我们可以忽略整个measure过程,只在layout函数中传入的4个参数来安排每个子视图的具体位置。

实现View滑动的几种方式

我们在使用View的过程中,经常需要实现View的滑动效果。比如ListView、跟随手指而移动的自定义View等等,前者的滑动效果是SDK为我们提供的,而对于我们自定义View的滑动效果就需要我们自己来实现。下面我们来详细介绍以下实现View滑动的几种方式。

(1)使用scrollTo/scrollBy实现View的滑动

实现滑动的最朴素直接的方式就是使用View类自带的scrollTo/scrollBy方法了。scrollBy方法是滑动指定的位移量,而scrollTo方法是滑动到指定位置。这两个方法的源码如下:

/**
* Set the scrolled position of your view. This will cause a call to

* {@link #onScrollChanged(int, int, int, int)} and the view will be

* invalidated.

* @param x the x position to scroll to

* @param y the y position to scroll to

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

/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}


通过以上代码我们可以看到,scrollBy方法内部也是调用了scrollTo方法来实现。以上源码中我们注意到了mScrollX和mScrollY成员变量,前者是View的左边缘减去View的内容的左边缘,后者是View的上边缘减去View的内容的上边缘。示意图如下:



上图中,黑色边框代表View在屏幕上对应的矩形区域,蓝色边框代表View的内容。在上图中,我们调用scrollTo/scrollBy把View向右滚动了一定距离。

实际上,调用scrollBy/scrollTo方法只能实现View的内容的滚动,而View的四个位置参数getTop(),getLeft(),getBottom(),getRight()是保持不变的。想一下我们平常使用ListView时,滚动的就是ListView的内容,而ListView本身在屏幕上的位置是不变的。

上图中,黑色左边(即View的左边缘)减去蓝色左边(即View的内容的左边缘)即可得到mScrollX。由此我们还可以知道,向右滚动时mScrollX负的,向左滚动时mScrollX是正的。同理我们可以知道,向下滚动时,mScrollY是负的,向上滚动时,mScrollY是正的。

经过以上的分析,我们了解到使用scrollTo/scrollBy方法实现View的滑动是很简单直接的,那么简单的背后有什么代价呢?代价就是滑动不是“弹性的”,弹性滑动指的是View的滑动应该是一个先加速再逐渐减速到停止的过程,这样看起来很平滑,不会很突兀。scrollTo/scrollBy方法实现的滑动看起来就会很突兀,这样的用户体验很不好。在解决这个问题之前,我们先来看看实现View滑动的其他方法。

(2)使用动画来实现View的滑动

使用动画来实现View的滑动主要通过改变View的translationX和translationY参数来实现,使用动画的好处在于滑动效果是平滑的。上面我们提到过,View的x、y参数决定View的当前位置,通过改变translationX和translationY,我们就可以改变View的当前位置。我们可以使用属性动画或者补间动画来实现View的平移。

首先,我们先来看一下如何使用补间动画来实现View的平移。补间动画资源定义如下(anim.xml):

xml version="1.0" encoding="utf-8"?>
<set
xmlns:android="http://schemas.android.com/apk/res/android"
android:fillafter="true">

<translate android:duration="100" android:fromxdelta="0"
android:fromydelta="0" android:interpolator="@android:anim/linear_interpolator"
android:toxdelta="100"
android:toydelta="100">

</translate></set>


然后在onCreat方法中调用startAnimation方法即可。使用补间动画实现View的滑动有一个缺陷,那就是移动的知识View的“影像”,这意味着其实View并未真正的移动,只是我们看起来它移动了而已。拿Button来举例,假若我们通过补间动画移动了一个Button,我们会发现,在Button的原来位置点击屏幕会出发点击事件,而在移动后的Button上点击不会触发点击事件。

接下来,我们看看如何用属性动画来实现View的平移。使用属性动画实现View的平移更加简单,只需要以下一条语句:

ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();


以上代码即实现了使用属性动画把targetView在100ms内向右平移100px。使用属性动画的限制在于真正的属性动画只可以在Android 3.0+使用(一些第三方库实现的兼容低版本的属性动画不是真正的属性动画),优点就是它可以真正的移动View而不是仅仅移动View的影像。

经过以上的描述,使用属性动画实现View的滑动看起来是个不错的选择,而且一些View的复杂的滑动效果只有通过动画才能比较方便的实现。

(3)通过改变布局参数来实现View的滑动

通过改变布局参数来实现View的滑动的思想很简单:比如向右移动一个View,只需要把它的marginLeft参数增大,向其它方向移动同理,只需改变相应的margin参数。还有一种比较拐弯抹角的方法是在要移动的View的旁边预先放一个View(初始宽高设为0)。然后比如我们要向右移动View,只需把预先放置的那个View的宽度增大,这样就把View“挤”到右边了。代码示例如下:

MarginLayoutParams params = (MarginLayoutParams) mButton.getLayoutParams();
params.leftMargin += 100;
mButton.requestLayout();


以上代码即实现了把mButton向右滑动100px。通过改变布局参数来实现的滑动效果也不是平滑的。

(4)使用Scroller来实现弹性滑动

上面我们提到了使用scrollTo/scrollBy方法实现View的滑动效果不是平滑的,好消息是我们可以使用Scroller方法来辅助实现View的弹性滑动。使用Scroller实现弹性滑动的惯用代码如下:

Scroller scroller = new Scroller(mContext);

private void smoothScrollTo(int dstX, int dstY) {
int scrollX = getScrollX();
int delta = dstX - scrollX;
scroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}

@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurY());
postInvalidate();
}
}


我们来看一下以上的代码。第4行中,我们获取到View的mScrollX参数并存到scrollX变量中。然后在第5行计算要滑动的位移量。第6行调用了startScroll方法,我们来看看startScroll方法的源码:

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;

mViscousFluidScale = 8.0f;

mViscousFluidNormalize = 1.0f;
mViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
}


从以上的源码我们可以看到,startScroll方法中并没有进行实际的滚动操作,而是把startX、startY、deltaX、deltaY等参数都保存了下来。那么究竟怎么实现View的滑动的呢?我们先回到Scroller惯用代码。我们看到第7行调用了invalidate方法,这个方法会请求重绘View,这会导致View的draw的方法被调用,draw的方法内部会调用computeScroll方法。我们来看看第13行,调用了scrollTo方法,并传入mScroller.getCurrX()和mScroller.getCurrY()方法作为参数。那么获取到的这两个参数是什么呢?这两个参数是在第12行调用的computeScrollOffset方法中设置的,我们来看看这个方法中设置这两个参数的相关代码:

public boolean computeScrollOffset() {
...
int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.rounc(y * mDeltaY);
break;
...
}
}
return true;
}


以上代码中第8行和第9行设置的mCurrX和mCurrY即为以上scrollTo的两个参数,表示本次滑动的目标位置。computeScrollOffset方法返回true表示滑动过程还未结束,否则表示结束。

通过以上的分析,我们大概了解了Scroller实现弹性滑动的原理:invaldate方法会导致View的draw方法被调用,而draw会调用computeScroll方法,因此重写了computeScroll方法,而computeScrollOffset方法会根据时间的流逝动态的计算出很小的一段时间应该滑动多少距离。也就是把一次滑动拆分成无数次小距离滑动从而实现“弹性滑动”。

例子如下:

// 视图坐标方式
@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;
//方法一:
//layout方法的参数:offsetX为水平方向偏移量,offsetY为竖直方向偏移量
// 在当前left、top、right、bottom的基础上加上偏移量
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

//方法二 :offsetLeftAndRight()和offsetTopAndBottom
// offsetLeftAndRight(offsetX);
// offsetTopAndBottom(offsetY);

//方法三 LayoutParams
//你的父布局是什么,这个“LinearLayout”就写什么(前提是有父布局)
//此外还可以这么写
//ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
// 效果是一样的。
//  LinearLayout.LayoutParams layoutParams=(LinearLayout.LayoutParams)getLayoutParams();
// layoutParams.topMargin=getTop()+offsetY;
// layoutParams.leftMargin=getLeft()+offsetX;
// setLayoutParams(layoutParams);

//方法四:scroller
//使用这个方法的重点在于重写computeScroll()方法(系统在绘制View时会在onDraw()方法中调用此方法)
//((View)getParent()).scrollBy(-offsetX,-offsetY);

//方法五:
//所有的视图都画在一张画布上,而上面盖了一块木板,木板上有屏幕那么大的一个空缺,
// 我们看到的实际上是透过木板看到的画布上的景象,
// 你看不到,不代表画布上没画。
// 而scroll两个方法其实都是在移动木板而不是画布,因此方向是相反的。所以是负值
((View) getParent()).scrollBy(-offsetX, -offsetY);
break;

//            方法四
//            case MotionEvent.ACTION_UP:
//                View viewGroup=(View)getParent();
//                mscroller.startScroll(viewGroup.getScrollX(),viewGroup.getScrollY(),-viewGroup.getScrollX(),-viewGroup.getScrollY());
//                invalidate();
//                break;
}
return true;
}

//方法四
@Override
public void computeScroll() {
super.computeScroll();
if (mscroller.computeScrollOffset()) {
((View) getParent()).scrollTo(mscroller.getCurrX(), mscroller.getCurrY());
invalidate();
}
}


参考:

详解实现Android中实现View滑动的几种方式

android 中文 api (64) —— Scroller

Android实现滑动的七种方法实践

Android中View绘制流程以及invalidate()等相关方法分析

Android中滑屏实现—-手把手教你如何实现触摸滑屏以及Scroller类详解

scrollTo和scrollBy

在Android View视图是没有边界的,Canvas是没有边界的,只不过我们通过绘制特定的View时对 Canvas对象进行了一定的操作,例如 : translate(平移)、clipRect(剪切)等,以便达到我们的对该Canvas对象绘制的要求 ,我们可以将这种无边界的视图称为“视图坐标”—–它不受物理屏幕限制。通常我们所理解的一个Layout布局文件只是该视图的显示区域,超过了这个显示区域将不能显示到父视图的区域中 ,对应的,我们可以将这种有边界的视图称为“布局坐标”—— 父视图给子视图分配的布局(layout)大小。而且, 一个视图的在屏幕的起始坐标位于视图坐标起始处.

其实是相对于父类视图的左上角坐标为原点(0,0),而不是整体ViewGroup的左上角为原点。

由于布局坐标只能显示特定的一块内容,所以我们只有移动布局坐标的坐标原点就可以将视图坐标的任何位置显示出来。

mScrollX:表示离视图起始位置的x水平方向的偏移量

mScrollY:表示离视图起始位置的y垂直方向的偏移量

分别通过getScrollX() 和getScrollY()方法获得。

注意:mScrollX和mScrollY指的并不是坐标,而是偏移量。

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();
}
}
}

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


scrollTo和scrollBy的参数:

x为负,右移
x为正,左移

y为负,下移
y为正,上移


scrollTo(int x,int y):

如果偏移位置发生了改变,就会给mScrollX和mScrollY赋新值,改变当前位置。

注意:x,y代表的不是坐标点,而是偏移量。

scrollTo是将View中的内容移动到指定的坐标x、y处,此x、y是相对于View的左上角,而不是屏幕的左上角。

例如:

我要移动view到坐标点(100,100),那么我的偏移量就是(0,,0) - (100,100) = (-100 ,-100) ,我就要执行view.scrollTo(-100,-100),达到这个效果。

scrollBy(int x,int y):

从源码中看出,它实际上是调用了scrollTo(mScrollX + x, mScrollY + y);

mScrollX + x和mScrollY + y,即表示在原先偏移的基础上在发生偏移,通俗的说就是相对我们当前位置偏移。

scrollBy(int x, int y)则是改变View中的相对位置,参数x、y为距离上一次的相对位置。

根据父类VIEW里面移动,如果移动到了超出的地方,就不会显示。

android 布局之滑动探究 scrollTo 和 scrollBy 方法使用说明

图解MotionEvent中getRawX、getRawY与getX、getY以及View中的getScrollX、getScrollY

android之位置坐标
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: