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