【Android】View的解析—滑动篇
1.View与ViewGroup
- View:
View
是Android中所有UI组件的基类,提供了绘制(draw)、布局(layout)和事件处理(event handling)的基础功能。- 它是一个抽象类,不能直接实例化,但可以通过其子类来创建具体的UI组件,如
TextView
、Button
、ImageView
等。
- ViewGroup:
ViewGroup
是一个容器类,继承自View
,可以包含其他View
对象(即子视图)。- 它负责管理子视图的布局和事件分发。
ViewGroup
本身也是一个View
,这意味着它可以包含在另一个ViewGroup
中,从而构建复杂的UI层次结构。
- 关系:
- 继承关系:
ViewGroup
继承自View
,这意味着ViewGroup
拥有View
的所有属性和方法,并且可以包含子视图。 - 组合关系:
ViewGroup
通过组合多个View
对象来构建复杂的UI界面。每个ViewGroup
可以包含一个或多个子View
,这些子View
可以是View
的任何子类,包括其他ViewGroup
。 - 布局管理:
ViewGroup
负责其子视图的布局管理,包括确定子视图的位置和大小。不同的ViewGroup
子类(如LinearLayout
、RelativeLayout
等)提供了不同的布局策略。
- 继承关系:
- 常见的ViewGroup子类:
LinearLayout
:线性布局,子视图按垂直或水平方向排列。RelativeLayout
:相对布局,子视图可以相对于彼此或父视图进行定位。FrameLayout
:帧布局,用于叠加多个子视图,通常用于覆盖显示。TableLayout
:表格布局,子视图按照表格的行和列排列。ConstraintLayout
:约束布局,提供了更灵活的布局方式,允许通过约束来定义视图之间的关系。
2.坐标系
Android系统当中有两种坐标系,分别为Android坐标与View坐标
2.1Android坐标系
在Android当中我们将左上角的顶点作为Android坐标系的原点,向右为X轴正方向,向下为Y轴正方向,在getRawX
方法和getRawY
方法获得的也是Android坐标系的坐标
2.2View坐标系
View获取自身的宽和高
View自身就有getWidth()
和getHeight()
方法,方法本质是什么:
View自身的坐标
通过如下方法可以获得View到其父控件ViewGroup的距离:
getTop():获取View自身顶边到其父布局顶边的距离
getLeft():获取View自身左边到其父布局左边的距离
getRight():获取View自身右边到其父布局左边的距离
getBottom():获取View自身底边到其父布局顶边的距离
MotionEvent提供的方法
getX():获取点击事件距离控件左边的距离,即视图坐标
getY():获取点击事件距离控件顶边的距离,即视图坐标
getRawX():获取点击事件距离整个屏幕左边的距离,即绝对坐标
getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标
2.3View的滑动
- layout方法
此时我们需要自定义一个View并重写它的onTouchEvent
方法
- 这是
View
类的一个回调方法,用于处理触摸事件。当用户触摸屏幕时,系统会调用这个方法- 方法的参数
MotionEvent event
包含了触摸事件的详细信息,如触摸点的坐标、触摸动作等
@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(getLeft() + offsetX,getTop() + offsetY,
getRight() + offsetX,getBottom() + offsetY);
break;
}
return true;
}
其实这个方法就是将之前的View的坐标掌握之后根据距离算出此时控件的所在位置。
offsetLeftAndRight()
与offsetTopAndBottom()
其实根据这个方法的名字你就会想到,将上面的MOVE里面的方法替换掉即可,没错就是这样的:
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
- LayoutParams(改变布局参数)
LayoutParams
主要保存了一个View的布局参数,因此我们可以根据改变布局参数来改变它相应的位置,整体思路还是一样的将手指移动的变量传给布局参数,还是对上面的方法进行修改:
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
此处使用的约束布局因此getTop是距你所设的Top控件的距离
- 动画
我们将动画XML文件放在anim当中
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true">
<translate
android:fromXDelta="0"
android:toXDelta="300"/>
</set>
<translate>
:这是XML动画框架中的一个元素,用于创建平移动画。android:fromXDelta="0"
:这个属性定义了动画的起始位置。fromXDelta
表示动画开始时视图相对于其原始位置的X轴偏移量。值0
意味着动画开始时,视图不从其原始位置偏移。android:toXDelta="300"
:这个属性定义了动画的结束位置。toXDelta
表示动画结束时视图相对于其原始位置的X轴偏移量。值300
意味着动画结束时,视图将向右(正X方向)移动300像素。
在活动当中只需要一句slideView.setAnimation(AnimationUtils.loadAnimation(this, R.anim.translate));
即可,此时运行程序你会发现并没有发生变化,难道没有进行移动吗,其实移动了但是快速的回到了原位,速度太快你并没有看到,当我们想要它停留在所移动的位置只需要在XML文件当中加上android:fillAfter="true"
即可,他表示当动画结束是否使其停留在最后的位置。
但是当我们此时按下控件进行移动发现并没有用,控件没有随手指的移动变化而变化,但是当我们按在控件原本的位置进行移动发现控件跟着移动了,这是因为View的动画并不能改变View的位置参数。属性动画就解决了上述的问题,会在下面讲解。
- scrollTo与scrollBy
scrollTo(x, y)
根据方法我们就可以知道这是将控件移动到具体的一个坐标点,而scrollBy(dx, dy)
则表示移动的增量。
我们看到scrollto最终还是调用scrollBy方法
移动的都是View的内容,如果在ViewGroup当中使用,则是移动所有的子View。此时仍然改变MOVE里面的代码:((View)getParent()).scrollBy(-offsetX, -offsetY);
,此时就随着我们手指的移动进行移动了。
我们先得到了父布局,此时就是对应控件的上一个布局,此时按下控件发现页面所有的内容都跟着移动了。但是你会想到为什么是负的,此时你的布局就像是一个报纸,而手机就像是一个放大镜,当我们移动的时候相当于移动的是放大镜的位置,当我们想要将内容移动到手机的右下角,就是将手机向左上角移动。这样应该就理解了吧。
- Scroller
在使用上面的方法的时候我们看到是瞬时完成的,程序一打开就是图标移动后的位置,那我们的体验感就会很不好,Scroller
就解决了上述的问题,它的滑动是在一段时间间隔当中完成的。她本身并不能移动,需要与View的computeScroll()
方法配合才能达到相应的效果。
接下来就重写方法吧,这是一个用于计算当前滚动的位置和是否需要继续滚动的方法:
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
if(mScroller.computeScrollOffset())
检查mScroller
(一个Scroller
对象)是否计算出了新的滚动偏移量。computeScrollOffset()
方法返回true
表示还有剩余的滚动操作需要执行,false
表示滚动已经完成。- 如果
computeScrollOffset()
返回true,则执行滚动操作。getParent()
获取当前View
的父View
。(View)
将父View
强制类型转换为View
类型,这是为了确保能够调用scrollTo()
方法。scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
方法将父View
滚动到Scroller
计算出的当前 X 和 Y 坐标位置。
invalidate()
方法请求系统重绘当前View
。这是必要的,因为滚动操作改变了View
的显示状态,需要通过重绘来更新显示内容。
我们再写一个根据接收的参数进行滑动的方法:
public void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
//2000即为完成滑动的时间限制
mScroller.startScroll(scrollX, 0, delta, 0, 2000);
invalidate();
}
此时我们只需要在活动当中调用此方法就可以实现滑动,此时运行程序就看到布局不是直接到了所要滑动到的位置,你会看到一段滑动过程。到底是如何实现的,就做做分析吧,实际是第一次自己没有理解o((>ω< ))o:我们在使用的时候调用的就是我们所写的这个方法,此时我们所传进去的就是所要移动的位置,到了所写的方法之后就会记录下没滑动之前的位置,在此时我们只考虑了水平滑动(竖直滑动也是一样的逻辑),delta即为我们滑动到相应的位置所需滑动多少的距离,调用其开始滑动的方法,此方法传入的就是刚开始的坐标与所需滚动的距离以及完成滚动的时间,好吧还是没有完全弄懂,嗯嗯…看看下面的源码解析吧,看了源码就会理解了!!!
5.Scroller源码
对于构造方法就不多说了,但是要告诉大家一个新的概念插值器(Interpolator),如若不传就会使用默认的插值器(ViscousFluidInterpolator
)
ViscousFluidInterpolator
是 Android 中Scroller
类的一个默认插值器,它用于控制滚动动画的速度变化,使得滚动行为更加自然和流畅。ViscousFluidInterpolator
的速度变化类似于粘性流体,开始时滚动较快,然后逐渐减速,直到停止。这种插值器模拟了一种“粘性”或“阻尼”效果,使得滚动看起来更加自然。
插值器(Interpolator)是 Android 中用于控制动画或滚动行为中时间与变化量之间关系的类。它定义了动画或滚动过程中速度变化的规律,使得动画不仅仅是简单的线性变化,而是可以模拟各种物理运动,如加速、减速、弹跳等效果。
插值器的主要作用是:
- 控制动画速度:定义动画在不同阶段的快慢变化,比如开始时慢,中间快,结束时慢。
- 模拟物理效果:模拟现实中的物理运动,如弹簧的振动、摩擦力等。
- 增强用户体验:通过更自然的动画效果提升用户界面的交互体验。
常见的插值器
Android 提供了多种内置的插值器,常用的包括:
- LinearInterpolator:线性插值器,动画以恒定速度进行,没有加速或减速。
- AccelerateInterpolator:加速插值器,动画开始时慢,然后逐渐加速。
- DecelerateInterpolator:减速插值器,动画开始时快,然后逐渐减速。
- AccelerateDecelerateInterpolator:先加速后减速插值器,动画开始和结束时慢,中间快。
- AnticipateInterpolator:预期插值器,动画开始时向后“弹”一下,然后向前加速。
- OvershootInterpolator:超出插值器,动画结束时超出目标位置,然后回弹到目标位置。
- BounceInterpolator:弹跳插值器,动画结束时模拟弹跳效果。
- FastOutLinearInInterpolator 和 FastOutSlowInInterpolator:快速退出线性进入插值器和快速退出慢速进入插值器,常用于 Material Design 中的动画。
此处可以对照着上面的View的滚动去看,我们先使用了startScroll方法,看名字你是不是觉得他就执行了View的滚动,其实不然,看看源码他都做了什么吧:
看到源码并没有执行相关的滑动操作,而是进行了保存参数的作用,因此此方法只是进行了准备工作并没有进行滑动操作。那是如何进行滑动操作的呢?关键就在于我们后面调用的invalidate()
方法,此方法会导致View的重绘,View的重绘会调用View的draw方法,draw会调用我们所重写的computeScroll()
方法。
我们先获取了当前的scrollX和scrollY,然后调用scrollTo方法进行滑动,接着再让他进行重绘直到滑动到了所要滑动的位置,即无需进行滑动了。明明使用的是scrollTo方法为什么还会有滑动的过程呢?那就看看computeScrollOffset()
方法:
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
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.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
我们看到当滑动结束的时候即无需进行任何方向的滑动,就会返回false
,不再进行滑动。否则系统会算出一个动画持续的时间,如果动画持续的时间小于我们设置的滑动持续时间(mDuration
),就会执行switch语句,然后根据插值器来计算在该时间段内所移动的距离,将其赋值给mCurrX
与mCurrY
。就这样不断递归完成了我们的弹性滑动。
文章到这里就结束了!