一撬码、先看效果
二、分析
上一篇博客 我們繪制了薄荷健康的直尺效果缘揪,可以說只是簡(jiǎn)單的繪制耍群,并沒有交互的操作,例如手勢(shì)滑動(dòng)找筝,數(shù)值回調(diào)蹈垢。這一篇我們來完善一下。
首先是手勢(shì)滑動(dòng)袖裕,如果還用上一篇的寫法曹抬,不好處理,慣性滑動(dòng)的話我們想到的是 Scroller 這個(gè)輔助類以及速度追蹤器急鳄。 Scroller 很熟悉谤民,自定義 View 的滑動(dòng)經(jīng)常用到堰酿,就是計(jì)算一系列的數(shù)值,然后調(diào)用 scrollTo() 這個(gè)方法將 View 滾動(dòng)到確定的位置张足,寫法都是固定的触创,參考百度。
重點(diǎn)說說速度追蹤器 VelocityTracker为牍,這個(gè)類干嘛的哼绑?我也不清楚,找了 一篇博客 觀察一下碉咆。用法很詳細(xì)抖韩,大致了解了一下,但是博客里有幾個(gè)重要的參數(shù)沒有說明疫铜,后面重點(diǎn)提茂浮。
上面的 gif 圖中間有一條綠線,這個(gè)綠線認(rèn)為是基準(zhǔn)線壳咕,代碼里用偏移量表示席揽。
三、代碼
相比較上一篇的代碼囱井,我們需要修改幾個(gè)地方驹尼,首先是初始化:
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);
// 最小響應(yīng)距離
touchSlop = viewConfiguration.getScaledTouchSlop();
mScroller = new Scroller(mContext);
// 慣性滑動(dòng)最低速度要求 低于這個(gè)速度認(rèn)為是觸摸
mMinimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
// 慣性滑動(dòng)的最大速度 觸摸速度不會(huì)超過這個(gè)值
mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
}
新增的部分標(biāo)注出來了。然后是觸摸部分 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;
}
//對(duì)每一個(gè)Event都需要交給速度追蹤器
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
return true;
}
很長(zhǎng)庞呕,一行行分析新翎。
case MotionEvent.ACTION_DOWN:
mLastX = event.getX();
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
記錄按下的位置,然后如果上一次的動(dòng)畫還在繼續(xù)住练,立即停止地啰。
再看 MOVE 里面:
case MotionEvent.ACTION_MOVE:
isFastScroll = false;
float moveX = event.getX();
currentOffset = (int) (moveX - mLastX);
scrollTo(getScrollX() - currentOffset, 0);
computeAndCallback(getScrollX());
mLastX = moveX;
break;
第一個(gè)布爾值是標(biāo)記是否正在慣性滑動(dòng),在后面會(huì)用到讲逛。為什么在這里置為false亏吝?因?yàn)橛|摸的時(shí)候不可能在慣性滑動(dòng)。然后計(jì)算每一次觸摸的偏移盏混,調(diào)用 scrollTo() 不斷的讓自己(View 本身)滾動(dòng)蔚鸥。
后面的 computeAndCallback() 方法暫時(shí)可以不看。最后還要記下每一次 MOVE 的坐標(biāo)许赃,因?yàn)槭怯?jì)算每一次的偏移的止喷,不是總的偏移。
最后看 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() 方法是手指離開屏幕的瞬間去計(jì)算 View 在手機(jī) x-y 方向的速度值混聊;
第2行 getXVelocity() 方法獲得 X 軸方向的速度值 initialVelocity弹谁;
第3行 判斷速度是否大于最低 mMinimumVelocity 要求,滿足的話,認(rèn)為需要慣性滑動(dòng)预愤,調(diào)用方法 flingX():
/**
* 慣性滑動(dòng)
*
* @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();
}
上面這個(gè)方法就是慣性滑動(dòng)的重點(diǎn)所在沟于。有了初速度,調(diào)用 mScroller.fling() 方法交給 Scroller 處理植康。這里要注意 fling() 方法的8個(gè)參數(shù)分別代表什么旷太,12參數(shù)代表滾動(dòng)開始的位置,34參數(shù)代表這個(gè)方向上的初速度销睁,56參數(shù)代表X滾動(dòng)的范圍泳秀,78參數(shù)代表Y滾動(dòng)的范圍。
第6行 也就是 else 不滿足最小滾動(dòng)速度的時(shí)候榄攀,認(rèn)為是觸摸事件的抬起,這個(gè)時(shí)候我們需要手動(dòng)的將 View 的刻度線滾動(dòng)到基準(zhǔn)線的位置金句,因?yàn)闈L動(dòng)的時(shí)候可能基準(zhǔn)線位于兩根刻度線之間檩赢,這個(gè)時(shí)候需要校準(zhǔn):
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);
首先獲取滾動(dòng)的長(zhǎng)度,如果對(duì) space 取余有余违寞,說明基準(zhǔn)線在兩個(gè)刻度之間贞瞒,需要減去這個(gè)余數(shù)。得到 space 的整倍數(shù)的偏移之后趁曼,
還要判斷邊界军浆,如果 x 在基準(zhǔn)線右邊,說明滾動(dòng)過頭了挡闰,需要回滾到基準(zhǔn)線上乒融。由于基準(zhǔn)線是偏移過的,所以 scrollTo 的時(shí)候需要補(bǔ)上這個(gè)偏移摄悯;
如果 x 在基準(zhǔn)線左邊赞季,說明向左滾過頭了,也需要回滾到基準(zhǔn)線上奢驯,同理申钩,后面也要加上偏移的量。最后調(diào)用 scrollTo() 就可以回到基準(zhǔn)線上瘪阁。
再來看下 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);
}
這個(gè)方法相比較第一個(gè)撒遣,有所改動(dòng),繪制刻度線的時(shí)候不需要再加上偏移量了管跺,直接從 View 的起始開始繪制义黎,滾動(dòng)就交給 scrollTo() 方法處理了。
這里繪制基準(zhǔn)線的時(shí)候同樣需要注意伙菜,除了加上基準(zhǔn)偏移轩缤,還要扣除余數(shù),否則,基準(zhǔn)線對(duì)不準(zhǔn)刻度線火的。
滾動(dòng)還必須要覆蓋的一個(gè)方法是 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();
}
}
}
這里的處理也是要注意細(xì)節(jié)壶愤,if 下面的代碼沒的說,但是 else 下面的代碼是只有快速慣性滾動(dòng)才能去判斷馏鹤,否則征椒,手指觸摸的時(shí)候也會(huì)去計(jì)算位置,導(dǎo)致移不動(dòng)湃累,
這個(gè)時(shí)候上面的 isFastScroll 就有用處了勃救。另外,這里也要加上基準(zhǔn)線扣除的余數(shù)治力,同時(shí)還要對(duì)space取余數(shù)蒙秒。
我們發(fā)現(xiàn)只要滾動(dòng),后面都會(huì)執(zhí)行 computeAndCallback() 方法:
/**
* 計(jì)算并回調(diào)位置信息
*
* @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);
}
}
就是一個(gè)回調(diào)宵统,返回當(dāng)前刻度下的值晕讲,這個(gè)值需要我們計(jì)算。
我們首先拿到基準(zhǔn)線對(duì)準(zhǔn)的 finalX 值马澈,這個(gè)值確定下來瓢省,減去對(duì) space 的取余數(shù),就能得到對(duì)應(yīng)的刻度個(gè)數(shù) finalX / space痊班,只要加上 startValue 就好了勤婚。
onRulerSelected 方法第一個(gè)參數(shù)是長(zhǎng)度,沒有特別用處涤伐。
到這馒胆,全部結(jié)束了。
附上 Github地址