View的位置主要由他的四个顶点来确定,分别对应View的四个属性,left左上角横坐标,top左上角纵坐标,right右下角横坐标,bottom右下角纵坐标,这些坐标都是相对于父容器来说的,因此它是一种相对坐标,Android中x,y轴的正方向是右下,如图:
如何获取这些变量?
int left = button.getLeft();
int right = button.getRight();
int top = button.getTop();
int bottom = button.getBottom();
计算view的宽高
int width = right-left;
int height = bottom-top;
从Android 3.0之后,View增加了几个额外的参数,x,y,translationX,translationY,其中x,y指的是View的左上角坐标,translationX,translationY,指的是View左上角相对于父容器的偏移量,这几个参数也是相对于父容器的坐标,并且translationX,translationY,默认值为0
如何获取
float x = button.getX();
float y = button.getY();
float translationX = button.getTranslationX();
float translationY = button.getTranslationY();
这三个参数的换算关系
x=left+translationX;
y=top+translationY;
需要注意的是,在view的平移过程中,left和top始终不变表示view的左上角坐标,改变的是x,y,translationX,translationY这四个参数
MotionEvent
当手指触碰到屏幕后产生的一系列事件,典型的事件如下:
ACTION_DOWN:当手指接触屏幕
ACTION_MOVE:当手指在屏幕上移动
ACTION_UP:当手指离开屏幕的一瞬间
正常情况下:
点击屏幕后松开 DOWN➡️UP
点击屏幕后移动然后松开 DOWN➡️MOVE…MOVE➡️UP
同时我们可以通过MotionEvent获取点击事件发生的x,y坐标,为此系统提供了俩个方法,getX,getY:返回相当于当前View左上角的x,y坐标,getRawX(),getRawY():返回相对于屏幕左上角的x,y坐标
//相对于本View左上角的xy坐标
float x = motionEvent.getX();
float y = motionEvent.getY();
//相对于屏幕左上角的xy坐标
float rawX = motionEvent.getRawX();
float rawY = motionEvent.getRawY();
TouchSlop
TouchSlop表示系统能够识别的最小滑动距离,如果你的手指滑动距离小于此值,那么系统认为你没有滑动,这是一个常量和设备有关,不同设备可能不一样
如何获取
int scaledTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
当处理滑动的时候,我们可以利用这个变量做一些过滤,优化用户体验
VelocityTracker速度追踪,用于追踪手指在滑动中的速度,包括水平和竖直方向的速度,他的使用很简单
首先初始化VelocityTracker
velocityTracker = VelocityTracker.obtain();
然后再touchEvent中追踪速度,在up的时候获取当前速度
@Override
public boolean onTouchEvent(MotionEvent event) {
velocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
velocityTracker.computeCurrentVelocity(1000);
float xVelocity = velocityTracker.getXVelocity();
float yVelocity = velocityTracker.getYVelocity();
Log.d("mmm", "x=" + xVelocity + " y=" + yVelocity);
break;
}
return super.onTouchEvent(event);
}
看下log
01-18 22:18:18.579 4065-4065/com.baidu.bpit.aibaidu.view D/mmm: x=-2210.5298 y=327.59885
注意获取速度之前需要先计算速度,也就是velocityTracker.computeCurrentVelocity(1000);这个方法需要放在velocityTracker.getXVelocity();前面
回收
velocityTracker.clear();
velocityTracker.recycle();
手势检测,用于辅助检测用的单击,滑动,长按,双击等行为
如何使用?
首先创建一个GestureDetector对象,并实现OnGestureListener接口,可以从接口中监听行为
gestureDetector = new GestureDetector(this);
//解决长按后无法拖动的现象
gestureDetector.setIsLongpressEnabled(false);
接管View的onToucEvent方法
@Override
public boolean onTouchEvent(MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
有三种方式实现View的滑动
scrollBy/scrollTo
为了实现View的滑动,View专门提供了这个方法来实现这个功能
简单使用scrollTo
首先定义一个xml
看下代码
private void initView() {
Button button = findViewById(R.id.button);
linearLayout = findViewById(R.id.lin);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
linearLayout.scrollTo(-100,-100);
}
});
}
}
解释一下这段代码,LinearLayout里面嵌套一个Button,当点击button的时候,调用 LinearLayout的scrollTo方法,点击之后Button会移动100,100的距离,也就是说scrollTo并不改变自己的位置,他只改变自己内容的位置,连续点击Button后,Button只是移动了一次,这是因为scrollTo方法只是针对原始位置的绝对滑动,所以你不管点多少次,只会移动一次
简单使用scrollBy
跟上方代码一样,只改动下方scrollBy
private void initView() {
Button button = findViewById(R.id.button);
linearLayout = findViewById(R.id.lin);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
linearLayout.scrollBy(-100,-100);
}
});
}
现在点击Button会调用LinearLayout的scrollBy方法,点击之后,Button会移动100,100的距离,说明scrollBy也是并不改变自己的位置,他只改变自己内容的位置,多次点击后,Button一直在移动,说明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;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* 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方法,他实现了基于当前位置的相对滑动,scrollTo实现了所传递参数的绝对滑动,我们看到有俩个参数mScrollX,mScrollY,这俩个参数可以通过 getScrollX();getScrollY();获取,那么这俩个参数是什么意思呢?
假如我们想让button向左移动100px那么只需要把这个LayoutParams的marginLeft增加100px即可这样就实现了目的
private void initView() {
final Button button = findViewById(R.id.button);
linearLayout = findViewById(R.id.lin);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) button.getLayoutParams();
layoutParams.leftMargin+=100;
button.setLayoutParams(layoutParams);
}
});
}
下面实现一个跟手滑动的View,拖动他可以在整个屏幕滑动,重写View的onTouchEvent,然后计算移动的距离,然后重新赋值坐标
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取现对于屏幕的xy坐标
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
Log.d("mmm", "rawX" + rawX + "rawY" + rawY);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = rawX;
mLastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = rawX - mLastX;
int offsetY = rawY - mLastY;
layout(getLeft() + offsetX,
getTop() + offsetY,
getRight() + offsetX,
getBottom() + offsetY);
mLastX=rawX;
mLastY=rawY;
break;
case MotionEvent.ACTION_UP:
break;
default:
}
return true;
}
如果直接把一个View从一个地方瞬间移到另一个位置,看起来比较生硬,我们需要一种渐进式的移动,实现的方式有很多,但是思想都是统一的,把一个大的滑动,分成若干次小的滑动,并在一个时间段内完成
使用Scroller实现弹性滑动
Scroller本身并没有办法实现弹性滑动,他需要和View的computeScroll方法配合使用才能共同完成这个功能
如何使用?
首先初始化一个对象
private void init(Context context) {
scroller = new Scroller(context);
}
然后重写View的computeScroll方法和自己的方法
public void smoothScrollTo(int destX, int dextY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
//在1s内滑向delta,效果就是慢慢滑动
scroller.startScroll(scrollX, 0, delta, 0,1000);
invalidate();
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
调用
private void initView() {
final MyButton button = findViewById(R.id.button);
linearLayout = findViewById(R.id.lin);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
linearLayout.smoothScrollTo(-500,0);
}
});
}
就可以实现弹性滑动
Scroller源码分析
先看一下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;
}
我们可以看到这个方法只是保存了传进来的几个参数,并没有做其他工作,调用这个方法并不能使View进行移动,那么Scroller到底是怎么让View弹性滑动的呢?
其实正的流程是这样的,下方的invalidate方法会导致View的重绘在View的Draw方法会调用computeScroll方法,computeScroll方法在View里是一个空实现,需要我们自己实现,当调用computeScroll方法的时候,我们会向Scroller获取当前的getCurrX,getCurrY然后通过scrollTo去实现滑动,接着又调用postInvalidate触发第二次重绘,这样就会导致computeScroll再次调用,移动到新的位置,如此循环,直到computeScrollOffset方法返回false为止,我们看下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;
}
这个会根据当前时间的流逝算出当前的scrollX,scrollY,他返回true就表示滑动还未结束,返回fasle就表示滑动结束
经过上面分析我们就知道了Scroller的原理,概括一下,Scroller本身并不能实现View的滑动,他需要配合View的computeScroll方法,他不断的让View重绘,每一次重绘就会根据时间间隔,算出当前的位置,然后通过scrollTo进行滑动,这样每一次重绘都会移动一小点距离,多次小幅度移动就组成了弹性滑动,这就是Scroller的工作机制
动画本身就是一个渐进的过程,因此通过他实现滑动天然就是弹性滑动,比如View动画,属性动画,但是这里并不是在说这个问题,我们可以利用动画实现一些动画实现不了的效果,比如我们模仿Scroller实现弹性滑动
public int startX = 0;
public int delayX = -100;
private void animite() {
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 1).setDuration(1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedFraction = animation.getAnimatedFraction();
linearLayout.scrollTo(startX + (int) (animatedFraction * delayX), 0);
}
});
valueAnimator.start();
}
上述代码,动画本质并没有作用在任何对象上,他只是1000ms完成了动画,利用这个特性,我们可以获取每一帧的完成比例,根据这个比例计算出滑动的距离,实现弹性滑动
他的核心思想就是通过发送一系列的延时消息达到一种渐进式的效果可以使用: