《Android开发者艺术》
View的位置参数
- View初始位置主要由左上角与右下角初始坐标决定(mLeft,mRight,mTop,mBottom),单位是像素.该坐标系的坐标原点为View父容器的左上角.
getLeft()
:这类函数是用来获取View控件左边相对于父容器左边最开始的距离.getX()
:这类函数是用来获取View控件移动后左边相对于父容器左边的距离(不是初始距离).
// View.java
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
// 这些都是View控件在父容器中的初始坐标值,单位是像素.
protected int mLeft;// 初始左上角X坐标
protected int mRight;// 初始右下角X坐标
protected int mTop;// 初始左上角Y坐标
protected int mBottom;// 初始右下角Y坐标
// 获取View控件相对与父容器控件的初始左上角Y坐标
public final int getTop() {
return mTop;
}
// 设置View控件相对与父容器控件的的初始左上角Y坐标
public final void setTop(int top) {
...
}
public final int getBottom() {
return mBottom;
}
public final void setBottom(int bottom) {
...
}
public final int getLeft() {
return mLeft;
}
public final void setLeft(int left) {
...
}
public final int getRight() {
return mRight;
}
public final void setRight(int right) {
...
}
// 获取View控件左边距离父控件左边的距离.
public float getX() {
return mLeft + getTranslationX();
}
public void setX(float x) {
setTranslationX(x - mLeft);
}
// 获取View控件上边距离父控件上边的距离.
public float getY() {
return mTop + getTranslationY();
}
public void setY(float y) {
setTranslationY(y - mTop);
}
// 获取View控件相对于父容器左上角X轴偏移量
public float getTranslationX() {
return mRenderNode.getTranslationX();
}
// 设置X轴偏移量
public void setTranslationX(float translationX) {
if (translationX != getTranslationX()) {
invalidateViewProperty(true, false);
mRenderNode.setTranslationX(translationX);
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
notifySubtreeAccessibilityStateChangedIfNeeded();
}
}
// 获取View控件相对于父容器左上角Y轴偏移量
public float getTranslationY() {
return mRenderNode.getTranslationY();
}
// 设置Y轴偏移量
public void setTranslationY(float translationY) {
if (translationY != getTranslationY()) {
invalidateViewProperty(true, false);
mRenderNode.setTranslationY(translationY);
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
notifySubtreeAccessibilityStateChangedIfNeeded();
}
}
}
弹性滑动
scrollTo()
:实现了基于所传递参数,对View内容得绝对滑动(无法改变View在布局中的初始坐标).scrollBy()
:最终调用scrollTo()
,实现了基于View当前内容位置的相对滑动(无法改变View在布局中的初始坐标).
// View.java
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
// 单位像素,代表View内容左边与View控件左边之间的距离.
protected int mScrollX;
// 单位像素,代表View内容上边与View控件上边之间的距离.
protected int mScrollY;
// 基于View内容初始位置的绝对滑动
// 参数x: x为+,则View内容向屏幕左侧移动.x为-,则View内容向屏幕右侧移动.单位为像素
// 参数y: y为+,则View内容向屏幕上方移动.y为-,则View内容向屏幕下方移动.单位为像素
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
// 传入的像素参数最后赋值给了mScrollX和mScrollY.
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
// 基于View内容当前位置的相对滑动.
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
// 获取View内容左边与View控件左边间的距离.
public final int getScrollX() {
return mScrollX;
}
// 获取View内容上边与View控件上边间的距离
public final int getScrollY() {
return mScrollY;
}
}
- 弹性滑动使用
public class MyTextView extends android.support.v7.widget.AppCompatTextView {
/**
* 缓慢滑动到指定位置
*
* @param destX View内容左边距离View控件左边距离
* @param destY View内容上边距离View控件上边距离
*/
private Scroller mScroller;
private void smoothScrollTo(int destX, int destY) {
mScroller = new Scroller(this.getContext());
// 内容距离控件边的距离
int scrollX = getScrollX();
int scrollY = getScrollY();
// 计算需要滑动的距离
int delteX = destX - scrollX;
int delteY = destY - scrollY;
// 为Scroller设置初始值
mScroller.startScroll(scrollX, scrollY, delteX, delteY, 1000);
// 重新绘制
invalidate();
}
@Override
public void computeScroll() {
// 每次View控件重绘都会调用到这个地方
if (mScroller != null) {
// 先计算
// 可以得出目前时间还需不需要重绘
// 如果需要重绘,可以算出View内容边距离View控件边当前的距离.
if (mScroller.computeScrollOffset()){
// 从mScroller中取出当前计算的值,使用scrollTo()让内容移动
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
// 移动完成之后重新绘制,这样就能知道下一次是否停止移动了.
postInvalidate();
}
}
}
}
- Scroller是一个工具类, 用来计算一定时间之后View内容左边距离View控件左边距离,还有View内容上边距离View控件上边距离.
public class Scroller {
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;//默认是SCROLL_MODE
mFinished = false;
mDuration = duration;// 滑动消耗时长
mStartTime = AnimationUtils.currentAnimationTimeMillis();// 记录移动开始时间
// View内容左边距离View控件左边距离,也就是滑动起点距离.
mStartX = startX;
mStartY = startY;
// 起点距离加上需要滑动距离,最终得到View内容左边距离View控件左边距离,也就是滑动终点距离.
mFinalX = startX + dx;
mFinalY = startY + dy;
// 滑动距离
mDeltaX = dx;
mDeltaY = dy;
// 持续时间倒数
mDurationReciprocal = 1.0f / (float) mDuration;
}
public boolean computeScrollOffset() {
if (mFinished) {
// 滑动结束返回
return false;
}
// 从开始滑动到执行到此处流失的时间
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
// 如果流失的时间小于滑动总时长
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
// mMode默认为SCROLL_MODE
// 流失的时间占总时间的百分比,经过插值器运算得到一个值.
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
// 滑动的起点加上滑动距离乘以时间流逝百分比,得到当前View内容左边距离View控件左边的距离.
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
...
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
// 滑动结束
mFinished = true;
}
return true;
}
}
利用动画特性来实现移动
/**
* 缓慢滑动到指定位置
* @param destX View内容左边距离View控件左边距离
* @param destY View内容上边距离View控件上边距离
*/
private void smoothScrollTo(int destX, int destY) {
// 内容距离控件边的距离
final int scrollX = mTextView.getScrollX();
final int scrollY = mTextView.getScrollY();
// 计算需要滑动的距离
final int delteX = destX - scrollX;
final int delteY = destY - scrollY;
final ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 1).setDuration(1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 动画进度百分比
float animatedFraction = valueAnimator.getAnimatedFraction();
int x = (int) (scrollX + (delteX * animatedFraction));
int y = (int) (scrollY + (delteY * animatedFraction));
mTextView.scrollTo(x,y);
}
});
valueAnimator.start();
}
常见辅助类
- MotionEvent中的方法
getX()/getY()
: 获取手指触碰屏幕点的坐标,该坐标系原点为触碰到的当前View的左上角.getRawX()/getRawY()
:获取手指触碰屏幕点的坐标,该坐标系原点为手机屏幕左上角.
- TouchSlop为滑动最小距离,滑动时小于该值将忽略滑动.
// 获取方法
ViewConfiguration.get(context).getScaledTouchSlop()
- VelocityTracker:它是用来追踪手指在滑动过程中的速度,水平和竖直速度都能获取到.下面是使用方法.
mTextView.setOnTouchListener(new View.OnTouchListener() {
int pointerId;
// 获取VelocityTracker对象
VelocityTracker mVelocityTracker = VelocityTracker.obtain();
@Override
public boolean onTouch(View v, MotionEvent event) {
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 获取第一个触摸点Id
pointerId = event.getPointerId(0);
break;
case MotionEvent.ACTION_MOVE:
// 计算1s内的平均速度
mVelocityTracker.computeCurrentVelocity(1000);
// 获取第一个触摸点在界面滑动的速度。
// 当从右向左,水平方向的速度为-.
float velocityX = mVelocityTracker.getXVelocity(pointerId);
// 当从下向上,垂直方向的速度为-.
float velocityY = mVelocityTracker.getYVelocity(pointerId);
break;
case MotionEvent.ACTION_UP:
// 回收
mVelocityTracker.clear();
mVelocityTracker.recycle();
break;
}
return true;
}
});
- GestureDetector:手势检测,用于检测用户单击,滑动,长按,双击等行为.下面是使用方法.
- 实际开发中可以不用它的, 完全在OnTouchEvent中自己实现监听滑动也可以.另外还有一个OnDoubleTapListener,是用来监听双击这些的,如果实际中要监听双击倒是可以用到GestureDetector.
mTv.setOnTouchListener(new View.OnTouchListener() {
GestureDetector mGestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
// 手指触碰屏幕瞬间调用
return false;
}
@Override
public void onShowPress(MotionEvent e) {
// 手指触碰屏幕瞬间后没有松开或者拖动
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
// 手指触碰屏幕后松开,单机事件
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// 手指按下并拖动
return false;
}
@Override
public void onLongPress(MotionEvent e) {
// 长按屏幕
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// 按下屏幕快速滑动后松开
return false;
}
});
@Override
public boolean onTouch(View v, MotionEvent event) {
// 解决长按屏幕无法拖动现象
mGestureDetector.setIsLongpressEnabled(false);
mGestureDetector.onTouchEvent(event);
return true;
}
});