一、先看效果
二、分析
上一篇博客 我們繪制了薄荷健康的直尺效果,可以說只是簡單的繪制,并沒有交互的操作,例如手勢滑動,數值回調。這一篇我們來完善一下。
首先是手勢滑動,如果還用上一篇的寫法,不好處理,慣性滑動的話我們想到的是 Scroller 這個輔助類以及速度追蹤器。 Scroller 很熟悉,自定義 View 的滑動經常用到,就是計算一系列的數值,然后調用 scrollTo() 這個方法將 View 滾動到確定的位置,寫法都是固定的,參考百度。
重點說說速度追蹤器 VelocityTracker,這個類干嘛的?我也不清楚,找了 一篇博客 觀察一下。用法很詳細,大致了解了一下,但是博客里有幾個重要的參數沒有說明,后面重點提。
上面的 gif 圖中間有一條綠線,這個綠線認為是基準線,代碼里用偏移量表示。
三、代碼
相比較上一篇的代碼,我們需要修改幾個地方,首先是初始化:
private void init(Context context, AttributeSet attrs) {
mContext = context;
centerLinePaint = new Paint();
centerLinePaint.setAntiAlias(true);
centerLinePaint.setColor(Color.parseColor("#49BA72"));
centerLinePaint.setStrokeWidth(5);
grayLinePaint = new Paint();
grayLinePaint.setAntiAlias(true);
grayLinePaint.setColor(Color.parseColor("#66666666"));
grayLinePaint.setStrokeWidth(5);
txtPaint = new Paint();
txtPaint.setAntiAlias(true);
txtPaint.setColor(Color.parseColor("#333333"));
txtPaint.setTextSize(50);
// 新增部分
ViewConfiguration viewConfiguration = ViewConfiguration.get(mContext);
// 最小響應距離
touchSlop = viewConfiguration.getScaledTouchSlop();
mScroller = new Scroller(mContext);
// 慣性滑動最低速度要求 低于這個速度認為是觸摸
mMinimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
// 慣性滑動的最大速度 觸摸速度不會超過這個值
mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
}
新增的部分標注出來了。然后是觸摸部分 onTouchEvent():
@Override
public boolean onTouchEvent(MotionEvent event) {
obtainVelocityTracker();
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastX = event.getX();
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
isFastScroll = false;
float moveX = event.getX();
currentOffset = (int) (moveX - mLastX);
scrollTo(getScrollX() - currentOffset, 0);
computeAndCallback(getScrollX());
mLastX = moveX;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) mVelocityTracker.getXVelocity();
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
isFastScroll = true;
flingX(-initialVelocity);
} else {
int x = getScrollX();
if (x % space != 0) {
x -= x % space;
}
if (x < -BASELINE_OFFSET) {
x = -BASELINE_OFFSET + BASELINE_OFFSET % space;
} else if (x > (endValue - startValue) * space * 10 - BASELINE_OFFSET) {
x = (endValue - startValue) * space * 10 - BASELINE_OFFSET + BASELINE_OFFSET % space;
}
scrollTo(x, 0);
computeAndCallback(x);
}
releaseVelocityTracker();
break;
}
//對每一個Event都需要交給速度追蹤器
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
return true;
}
很長,一行行分析。
case MotionEvent.ACTION_DOWN:
mLastX = event.getX();
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
記錄按下的位置,然后如果上一次的動畫還在繼續,立即停止。
再看 MOVE 里面:
case MotionEvent.ACTION_MOVE:
isFastScroll = false;
float moveX = event.getX();
currentOffset = (int) (moveX - mLastX);
scrollTo(getScrollX() - currentOffset, 0);
computeAndCallback(getScrollX());
mLastX = moveX;
break;
第一個布爾值是標記是否正在慣性滑動,在后面會用到。為什么在這里置為false?因為觸摸的時候不可能在慣性滑動。然后計算每一次觸摸的偏移,調用 scrollTo() 不斷的讓自己(View 本身)滾動。
后面的 computeAndCallback() 方法暫時可以不看。最后還要記下每一次 MOVE 的坐標,因為是計算每一次的偏移的,不是總的偏移。
最后看 UP 和 CANCEL 事件:
case MotionEvent.ACTION_CANCEL:
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) mVelocityTracker.getXVelocity();
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
isFastScroll = true;
flingX(-initialVelocity);
} else {
int x = getScrollX();
if (x % space != 0) {
x -= x % space;
}
if (x < -BASELINE_OFFSET) {
x = -BASELINE_OFFSET + BASELINE_OFFSET % space;
} else if (x > (endValue - startValue) * space * 10 - BASELINE_OFFSET) {
x = (endValue - startValue) * space * 10 - BASELINE_OFFSET + BASELINE_OFFSET % space;
}
scrollTo(x, 0);
computeAndCallback(x);
}
releaseVelocityTracker();
break;
第1行 computeCurrentVelocity() 方法是手指離開屏幕的瞬間去計算 View 在手機 x-y 方向的速度值;
第2行 getXVelocity() 方法獲得 X 軸方向的速度值 initialVelocity;
第3行 判斷速度是否大于最低 mMinimumVelocity 要求,滿足的話,認為需要慣性滑動,調用方法 flingX():
/**
* 慣性滑動
*
* @param velocityX
*/
public void flingX(int velocityX) {
mScroller.fling(getScrollX(), getScrollY(), velocityX, 0, -BASELINE_OFFSET, (endValue - startValue) * space * 10 - BASELINE_OFFSET, 0, 0);
awakenScrollBars(mScroller.getDuration());
invalidate();
}
上面這個方法就是慣性滑動的重點所在。有了初速度,調用 mScroller.fling() 方法交給 Scroller 處理。這里要注意 fling() 方法的8個參數分別代表什么,12參數代表滾動開始的位置,34參數代表這個方向上的初速度,56參數代表X滾動的范圍,78參數代表Y滾動的范圍。
第6行 也就是 else 不滿足最小滾動速度的時候,認為是觸摸事件的抬起,這個時候我們需要手動的將 View 的刻度線滾動到基準線的位置,因為滾動的時候可能基準線位于兩根刻度線之間,這個時候需要校準:
int x = getScrollX();
if (x % space != 0) {
x -= x % space;
}
if (x < -BASELINE_OFFSET) {
x = -BASELINE_OFFSET + BASELINE_OFFSET % space;
} else if (x > (endValue - startValue) * space * 10 - BASELINE_OFFSET) {
x = (endValue - startValue) * space * 10 - BASELINE_OFFSET + BASELINE_OFFSET % space;
}
scrollTo(x, 0);
computeAndCallback(x);
首先獲取滾動的長度,如果對 space 取余有余,說明基準線在兩個刻度之間,需要減去這個余數。得到 space 的整倍數的偏移之后,
還要判斷邊界,如果 x 在基準線右邊,說明滾動過頭了,需要回滾到基準線上。由于基準線是偏移過的,所以 scrollTo 的時候需要補上這個偏移;
如果 x 在基準線左邊,說明向左滾過頭了,也需要回滾到基準線上,同理,后面也要加上偏移的量。最后調用 scrollTo() 就可以回到基準線上。
再來看下 onDraw() 方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = startValue * 10; i < endValue * 10 + 1; i++) {
int lineHeight = 80;
if (i % 5 == 0) {
if (i % 10 == 0) {
lineHeight = 120;
int x = (i - startValue * 10) * space;
if (x > 0 || x < width) {
canvas.drawText(String.valueOf(i / 10), x, lineHeight + 50, txtPaint);
}
}
} else {
lineHeight = 50;
}
int startX = (i - startValue * 10) * space;
if (startX > 0 || startX < width) {
canvas.drawLine(startX, 0, startX, lineHeight, grayLinePaint);
}
}
int startX = BASELINE_OFFSET + getScrollX() - BASELINE_OFFSET % space;
canvas.drawLine(startX, 0, startX, 180, centerLinePaint);
}
這個方法相比較第一個,有所改動,繪制刻度線的時候不需要再加上偏移量了,直接從 View 的起始開始繪制,滾動就交給 scrollTo() 方法處理了。
這里繪制基準線的時候同樣需要注意,除了加上基準偏移,還要扣除余數,否則,基準線對不準刻度線。
滾動還必須要覆蓋的一個方法是 computeScroll():
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int x = mScroller.getCurrX();
scrollTo(x, 0);
computeAndCallback(x);
postInvalidate();
} else {
if (isFastScroll) {
int x = mScroller.getCurrX() + BASELINE_OFFSET % space;
if (x % space != 0) {
x -= x % space;
}
scrollTo(x, 0);
computeAndCallback(x);
postInvalidate();
}
}
}
這里的處理也是要注意細節,if 下面的代碼沒的說,但是 else 下面的代碼是只有快速慣性滾動才能去判斷,否則,手指觸摸的時候也會去計算位置,導致移不動,
這個時候上面的 isFastScroll 就有用處了。另外,這里也要加上基準線扣除的余數,同時還要對space取余數。
我們發現只要滾動,后面都會執行 computeAndCallback() 方法:
/**
* 計算并回調位置信息
*
* @param scrollX
*/
private void computeAndCallback(int scrollX) {
if (mListener != null) {
int finalX = BASELINE_OFFSET + scrollX;
if (finalX % space != 0) {
finalX -= finalX % space;
}
mListener.onRulerSelected((endValue - startValue) * 10, startValue * 10 + finalX / space);
}
}
就是一個回調,返回當前刻度下的值,這個值需要我們計算。
我們首先拿到基準線對準的 finalX 值,這個值確定下來,減去對 space 的取余數,就能得到對應的刻度個數 finalX / space,只要加上 startValue 就好了。
onRulerSelected 方法第一個參數是長度,沒有特別用處。
到這,全部結束了。
附上 Github地址