自定義View實現跟隨手指滾動的刻度尺,實現了相似SeekBar的滑動選中效果。項目地址,歡迎star!java
UI圖:git
功能:github
先扯一下,再看別人寫的控件的時候總有一種一臉懵逼的感受,好多凌亂的變量和一大堆的計算邏輯都不知道幹嗎用的。好比:PullToRefreshLayout。除非本身按着總體的設計流程寫一遍,一步步的寫,等出了bug你就明白那些操做的價值。結合以前讀第三方控件的經驗,寫這個刻度尺控件的時候就一步步的去完成,從簡單的繪製,到點擊事件,再到滑動fling,最後滑動結束更正滑動位置。每一步遇到的問題都記錄下來,以後再補全解決方法,這就是成長。json
這裏省略了onMeasure,這裏的需求只是計算一下高度就行了。接着看onDraw方法:canvas
private void drawRuler(Canvas canvas) {
mTextIndex = 0;
for (int index = 0; index <= mRulerHelper.getCounts(); index++) {
boolean longLine = mRulerHelper.isLongLine(index);
int lineCount = mLineWidth * index;
mRect.left = index * mLineSpace + lineCount + mMarginLeft;
mRect.top = getStartY(longLine);
mRect.right = mRect.left + mLineWidth;
mRect.bottom = getEndY();
if (longLine) {
if (!mRulerHelper.isFull()) {
mRulerHelper.addPoint(mRect.left);
}
String text = mRulerHelper.getTextByIndex(mTextIndex);
mTextIndex++;
canvas.drawText(text, mRect.centerX(), getMeasuredHeight() - dpFor14, mTextPaint);
}
canvas.drawRect(mRect, mLinePaint);
mRect.setEmpty();
}
}
複製代碼
這裏解釋一下爲何刻度採用Rect而不是設置line的寬度,其實最簡單的就是設置Paint的寬度而後canvas.drawLine()。剛繪製的時候就是採用的canvas.drawLine(),繪製完以後發現每一個刻度的寬度都被削減了一半,canvas.drawLine()是在設置的(x,y)座標開始平分line的寬度的(這個你要去體驗一下就會明白)。因此給定座標以後每一個刻度看起來就像是被擠了同樣,因此才採用Rect簡單方便一點。進入正題,繪製有幾個問題:ide
怎麼肯定要繪製幾個Rect?學習
這個比較靈活,要看具體的需求了。也就是一大格里麪包含幾個刻度,通常是包含10個刻度,刻度包括長短刻度。而後一大格刻度表示多少數值,也就是offSet值是多少。以後刻度的範圍也要明確而且能被offSet整除,好比範圍是(low,height),那麼(height-low)/(offSet/10)就是你須要繪製多少個刻度。this
public void setScope(int start, int count,int offSet) {
if(offSet != 0) {
this.offSet = offSet;
}
lineNumbers = (count - start) / (this.offSet / 10);
}
複製代碼
怎麼肯定那個是長刻度?spa
這個問題要肯定一大格之間有幾個小刻度了,通常爲10個的話,那麼當前的index/10能整除就是到了該繪製長刻度的時候了,mRulerHelper.getCounts()就是咱們計算出的總共有幾個刻度。.net
for (int index = 0; index <= mRulerHelper.getCounts(); index++) {
boolean longLine = mRulerHelper.isLongLine(index);
...
if (longLine) {
canvas.drawText(text, mRect.centerX(), getMeasuredHeight() - dpFor14, mTextPaint);
}
canvas.drawRect(mRect, mLinePaint);
}
複製代碼
以後呢就是咱們計算Rect的左邊跟繪製Text的座標了。。。不細講。。。具體可看這裏啊。
有個問題就是你得明白Rect的left top right bottom分別表示那個區間:
目前採起的是點擊該View的事件全攔截,感受也沒別的什麼需求須要過濾事件了。事件處理起來很簡單的就是計算出每次移動的差值就行了:
case MotionEvent.ACTION_DOWN:
mPressUp = false;
isFling = false;
startX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
mPressUp = false;
float distance = event.getX() - startX;
if (mPreDistance != distance) {
doScroll((int) -distance, 0, 0);
invalidate();
}
startX = event.getX();
break;
複製代碼
問題就是:
怎麼實現滑動的效果?
刻度尺若是範圍很大的話總寬度確定會超出屏幕的,可是Canvas不會繪製屏幕以外的部分,除非等到屏幕以外的部分顯示出來。另外讓View滑動的方法不少,最初使用的是scrollTo方法,該方法滑動的是View的內容,也符合咱們要的效果,不過結果查強人意。差值計算以後稍微一滑動,刻度直接沒了,成了一片空白,看起來那個變化值也不大,ok!這是一個疑問ScrollTo+invalidate內容不會顯示,直接沒了。以後呢換成了Scroller,這個玩意不用太多的介紹了,使用以後便達到了咱們想要的效果,同樣的變化值。
private void doScroll(int dx, int dy, int duration) {
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, duration);
}
複製代碼
是否有疑問?既然屏幕以外的東西Canvas不會去繪製,那麼滑動的時候確定是將屏幕以外的部分滑到屏幕中,也就是在滑動的過程當中要繼續繪製。從上面的繪製代碼能看到這個繪製過程當中跟滑動並無任何的聯繫,只是單純的for循環繪製而已,爲何呢?第一 咱們scrollTo移動的是View的內容,一開始View的實際寬度會超過屏幕的寬度,當沒有滑動的時候,View只會繪製屏幕中的可見區域,即便for循環依然執行也不會繪製到屏幕外面,而後在滑動的時候會不斷的觸發invalidate()方法,也就是for循環會被觸發,View開始在新出現的未繪製的區域繪製。已經繪製過的區域會被滑出屏幕,這樣就會給用戶一個平滑的效果。作完以上兩步你的刻度尺已經有了滑動的效果了。下面就是解決邊界的問題。
UI說當超過邊界以後鬆手回彈,這樣的交互效果好。這種交互其實最簡單了,在手指離開的時候計算當前的x座標距離中心指針的x座標的距離,而後讓Scroller去執行回彈的效果。不過這個操做是整個控件中最爲重要的一步,由於當手指擡起的時候,中間指針必須指向一個長刻度,不能停留再短刻度上面,那這個操做就跟邊界回彈的操做重合了,邊界回彈也是讓最小或者最大長刻度滑動到中間指針的位置。因此鬆手以後的操做就分爲三種:
currentX :滑動中止時的x座標。
Point:中間指針位置。
low:刻度尺的最小邊界。
height:刻度尺的最大邊界。
當前的currentX小於中間指針刻度Point的x座標,而且小於刻度的最小值low的x座標。
-----------------Point-currentX--low------height----------
當前的currentX小於中間指針刻度Point的x座標,而且大於刻度的最小值low表示的x座標小於刻度尺的最大刻度height的x座標。
------low-------currentX--Point--------height----------
當前的currentX大於中間指針刻度Point的x座標,而且大於刻度的最大值height表示的x座標。
------low-------height-----currentX-Point-------
簡單的表示了一下三種位置。
處理就是,先計算出滑動結束以後的當前x座標跟中間Point的x座標的距離,而後不爲0就使用Scroller滑動:
//計算距離
public int getScrollDistance(int x) {
for (int i = 0; i < mPoints.size(); i++) {
int pointX = mPoints.get(i);
if (0 == i && x < pointX) {
//當前的x比第一個位置的x座標都小 也就是須要往右移動到第一個長線的位置.
setCurrentText(0);
return x - pointX;
} else if (i == mPoints.size() - 1 && x > pointX) {
//當前的x比最後一個左邊的x都大,也就是須要往左移動到最後一個長線位置.
setCurrentText(texts.size() - 1);
return x - pointX;
} else {
if (i + 1 < mPoints.size()) {
int nextX = mPoints.get(i + 1);
if (x > pointX && x <= nextX) {
int distance = (nextX - pointX) / 2;
int dis = x - pointX;
if (dis > distance) {
//說明往下一個移動
setCurrentText(i + 1);
return x - nextX;
} else {
setCurrentText(i);
//往前一個移動
return x - pointX;
}
}
}
}
}
return 0;
}
複製代碼
開始執行滑動:
public void scrollFinish() {
int finalX = mScroller.getFinalX();
int centerPointX = mRulerHelper.getCenterPointX();
int currentX = centerPointX + finalX;
int scrollDistance = mRulerHelper.getScrollDistance(currentX);
if (0 != scrollDistance) {
//第一個參數是滾動開始時的x的座標
//第二個參數是滾動開始時的y的座標
//第三個參數是在X軸上滾動的距離, 負數向右滾動.
//第四個參數是在Y軸上滾動的距離,負數向下滾動.
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -scrollDistance, 0, 300);
invalidate();
if (scrollSelected != null) {
scrollSelected.selected(getCurrentText());
}
}
}
複製代碼
這樣已經可使用了,滑動的刻度尺已經完成了。不過交給UI一看,人家說這東西怎麼那麼難滑動呢,每次怎麼只能滑一大格呢,我要那種fling的感受。確實,由於在MotionEvent.ACTION_UP的時候都會去矯正一下位置,因此給使用者的感受就是一次只能滑一格,滑動體驗很很差,只能去增長fling。。。
增長fling多簡單啊,Scroller不是有這個方法嗎mScroller.fling(),使用方法這裏再也不介紹了。fling增長以後,用戶的體驗確實好了不少,不過一個新的問題出現了,就是在fling中止以後怎麼矯正位置呢?這是個大問題,卡住了好大一下子,最終找到了解決方法:
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
//這裏是結束以後調用矯正位置的方法。scrollFinish()。
if (mScroller.getCurrX() == mScroller.getFinalX() && mPressUp && isFling) {
mPressUp = false;
isFling = false;
scrollFinish();
}
scrollTo(mScroller.getCurrX(), 0);
invalidate();
}
super.computeScroll();
}
複製代碼
效果在文章一開始已經展現出來了,指針並無在該自定義View中繪製,底部的線也是,由於對於指針的需求是多變的,因此用了一個自定義的ViewGroup去完成剩餘的指針和底部的實線。底部的實線放在Group中是由於咱們的UI效果,底部的實線上面能夠沒有刻度,也就是這個底部的線是固定在底部,比我畫在刻度下面跟隨刻度滑動要簡單的多。想到以後的變體,感受刻度自己的View跟指針分開是比較好擴展的,Group只須要給刻度尺控件傳入中間指針的(x,y)座標就行了。