溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Android實(shí)現(xiàn)自定義滑動(dòng)刻度尺方法示例

發(fā)布時(shí)間:2020-09-24 09:47:04 來源:腳本之家 閱讀:216 作者:_那個(gè)人 欄目:移動(dòng)開發(fā)

一 基礎(chǔ):

自定義View實(shí)現(xiàn)跟隨手指滾動(dòng)的刻度尺,實(shí)現(xiàn)了類似SeekBar的滑動(dòng)選中效果。項(xiàng)目地址,歡迎star!

UI圖:

Android實(shí)現(xiàn)自定義滑動(dòng)刻度尺方法示例

功能:

  • 通過設(shè)置最小值跟最大值的范圍,以及offset值。View將根據(jù)這些數(shù)據(jù)去計(jì)算出需要幾個(gè)小刻度和幾個(gè)長刻度,和每個(gè)長刻度上面顯示的數(shù)值。
  • 指針可以隨意的定制。
  • 當(dāng)滑動(dòng)停止后,刻度尺會(huì)根據(jù)四舍五入將距離指針最近的長刻度滑動(dòng)到指針的位置。
  • 支持范圍越界回彈。
  • 支持設(shè)置默認(rèn)值。

Android實(shí)現(xiàn)自定義滑動(dòng)刻度尺方法示例

二 實(shí)現(xiàn):

先扯一下,再看別人寫的控件的時(shí)候總有一種一臉懵逼的感覺,好多凌亂的變量和一大堆的計(jì)算邏輯都不知道干嘛用的。比如:PullToRefreshLayout。除非自己按著整體的設(shè)計(jì)流程寫一遍,一步步的寫,等出了bug你就明白那些操作的價(jià)值。結(jié)合之前讀第三方控件的經(jīng)驗(yàn),寫這個(gè)刻度尺控件的時(shí)候就一步步的去完成,從簡單的繪制,到點(diǎn)擊事件,再到滑動(dòng)fling,最后滑動(dòng)結(jié)束更正滑動(dòng)位置。每一步遇到的問題都記錄下來,之后再補(bǔ)全解決方法,這就是成長。

1.繪制刻度

這里省略了onMeasure,這里的需求只是計(jì)算一下高度就好了。接著看onDraw方法:

 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而不是設(shè)置line的寬度,其實(shí)最簡單的就是設(shè)置Paint的寬度然后canvas.drawLine()。剛繪制的時(shí)候就是采用的canvas.drawLine(),繪制完之后發(fā)現(xiàn)每個(gè)刻度的寬度都被削減了一半,canvas.drawLine()是在設(shè)置的(x,y)坐標(biāo)開始平分line的寬度的(這個(gè)你要去體驗(yàn)一下就會(huì)明白)。所以給定坐標(biāo)之后每個(gè)刻度看起來就像是被擠了一樣,所以才采用Rect簡單方便一點(diǎn)。進(jìn)入正題,繪制有幾個(gè)問題:

  • 怎么確定要繪制幾個(gè)Rect?

這個(gè)比較靈活,要看具體的需求了。也就是一大格里面包含幾個(gè)刻度,一般是包含10個(gè)刻度,刻度包括長短刻度。然后一大格刻度表示多少數(shù)值,也就是offSet值是多少。之后刻度的范圍也要明確并且能被offSet整除,比如范圍是(low,height),那么(height-low)/(offSet/10)就是你需要繪制多少個(gè)刻度。

 public void setScope(int start, int count,int offSet) {
  if(offSet != 0) {
   this.offSet = offSet;
  }
  lineNumbers = (count - start) / (this.offSet / 10);
 }
  • 怎么確定那個(gè)是長刻度?

這個(gè)問題要確定一大格之間有幾個(gè)小刻度了,一般為10個(gè)的話,那么當(dāng)前的index/10能整除就是到了該繪制長刻度的時(shí)候了,mRulerHelper.getCounts()就是我們計(jì)算出的總共有幾個(gè)刻度。

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

之后呢就是我們計(jì)算Rect的左邊跟繪制Text的坐標(biāo)了。。。不細(xì)講。。。具體可看這里啊。

有個(gè)問題就是你得明白R(shí)ect的left top right bottom分別表示那個(gè)區(qū)間:

[圖片上傳失敗...(image-5d1f26-1554206618213)]

2.處理點(diǎn)擊事件

目前采取的是點(diǎn)擊該View的事件全攔截,感覺也沒別的什么需求需要過濾事件了。事件處理起來很簡單的就是計(jì)算出每次移動(dòng)的差值就好了:

   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;

問題就是:

  • 怎么實(shí)現(xiàn)滑動(dòng)的效果?

刻度尺如果范圍很大的話總寬度肯定會(huì)超出屏幕的,但是Canvas不會(huì)繪制屏幕之外的部分,除非等到屏幕之外的部分顯示出來。另外讓View滑動(dòng)的方法很多,最初使用的是scrollTo方法,該方法滑動(dòng)的是View的內(nèi)容,也符合我們要的效果,不過結(jié)果查強(qiáng)人意。差值計(jì)算之后稍微一滑動(dòng),刻度直接沒了,成了一片空白,看起來那個(gè)變化值也不大,ok!這是一個(gè)疑問ScrollTo+invalidate內(nèi)容不會(huì)顯示,直接沒了。之后呢?fù)Q成了Scroller,這個(gè)玩意不用太多的介紹了,使用之后便達(dá)到了我們想要的效果,一樣的變化值。

 private void doScroll(int dx, int dy, int duration) {
  mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, duration);
 }

是否有疑問?既然屏幕之外的東西Canvas不會(huì)去繪制,那么滑動(dòng)的時(shí)候肯定是將屏幕之外的部分滑到屏幕中,也就是在滑動(dòng)的過程中要繼續(xù)繪制。從上面的繪制代碼能看到這個(gè)繪制過程中跟滑動(dòng)并沒有任何的聯(lián)系,只是單純的for循環(huán)繪制而已,為什么呢?第一 我們scrollTo移動(dòng)的是View的內(nèi)容,一開始View的實(shí)際寬度會(huì)超過屏幕的寬度,當(dāng)沒有滑動(dòng)的時(shí)候,View只會(huì)繪制屏幕中的可見區(qū)域,即使for循環(huán)依然執(zhí)行也不會(huì)繪制到屏幕外面,然后在滑動(dòng)的時(shí)候會(huì)不斷的觸發(fā)invalidate()方法,也就是for循環(huán)會(huì)被觸發(fā),View開始在新出現(xiàn)的未繪制的區(qū)域繪制。已經(jīng)繪制過的區(qū)域會(huì)被滑出屏幕,這樣就會(huì)給用戶一個(gè)平滑的效果。做完以上兩步你的刻度尺已經(jīng)有了滑動(dòng)的效果了。下面就是解決邊界的問題。

3.邊界的處理

UI說當(dāng)超過邊界之后松手回彈,這樣的交互效果好。這種交互其實(shí)最簡單了,在手指離開的時(shí)候計(jì)算當(dāng)前的x坐標(biāo)距離中心指針的x坐標(biāo)的距離,然后讓Scroller去執(zhí)行回彈的效果。不過這個(gè)操作是整個(gè)控件中最為重要的一步,因?yàn)楫?dāng)手指抬起的時(shí)候,中間指針必須指向一個(gè)長刻度,不能停留再短刻度上面,那這個(gè)操作就跟邊界回彈的操作重合了,邊界回彈也是讓最小或者最大長刻度滑動(dòng)到中間指針的位置。所以松手之后的操作就分為三種:

currentX :滑動(dòng)停止時(shí)的x坐標(biāo)。

Point:中間指針位置。

low:刻度尺的最小邊界。

height:刻度尺的最大邊界。

  • 當(dāng)前的currentX小于中間指針刻度Point的x坐標(biāo),并且小于刻度的最小值low的x坐標(biāo)。

-----------------Point-currentX--low------height----------

  • 當(dāng)前的currentX小于中間指針刻度Point的x坐標(biāo),并且大于刻度的最小值low表示的x坐標(biāo)小于刻度尺的最大刻度height的x坐標(biāo)。

------low-------currentX--Point--------height----------

  • 當(dāng)前的currentX大于中間指針刻度Point的x坐標(biāo),并且大于刻度的最大值height表示的x坐標(biāo)。

------low-------height-----currentX-Point-------

簡單的表示了一下三種位置。

處理就是,先計(jì)算出滑動(dòng)結(jié)束之后的當(dāng)前x坐標(biāo)跟中間Point的x坐標(biāo)的距離,然后不為0就使用Scroller滑動(dòng):

//計(jì)算距離
public int getScrollDistance(int x) {
  for (int i = 0; i < mPoints.size(); i++) {
   int pointX = mPoints.get(i);
   if (0 == i && x < pointX) {
    //當(dāng)前的x比第一個(gè)位置的x坐標(biāo)都小 也就是需要往右移動(dòng)到第一個(gè)長線的位置.
    setCurrentText(0);
    return x - pointX;
   } else if (i == mPoints.size() - 1 && x > pointX) {
    //當(dāng)前的x比最后一個(gè)左邊的x都大,也就是需要往左移動(dòng)到最后一個(gè)長線位置.
    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) {
       //說明往下一個(gè)移動(dòng)
       setCurrentText(i + 1);
       return x - nextX;
      } else {
       setCurrentText(i);
       //往前一個(gè)移動(dòng)
       return x - pointX;
      }
     }
    }
   }
  }
  return 0;
 }

開始執(zhí)行滑動(dòng):

 public void scrollFinish() {
  int finalX = mScroller.getFinalX();
  int centerPointX = mRulerHelper.getCenterPointX();
  int currentX = centerPointX + finalX;
  int scrollDistance = mRulerHelper.getScrollDistance(currentX);
  if (0 != scrollDistance) {
   //第一個(gè)參數(shù)是滾動(dòng)開始時(shí)的x的坐標(biāo)
   //第二個(gè)參數(shù)是滾動(dòng)開始時(shí)的y的坐標(biāo)
   //第三個(gè)參數(shù)是在X軸上滾動(dòng)的距離, 負(fù)數(shù)向右滾動(dòng).
   //第四個(gè)參數(shù)是在Y軸上滾動(dòng)的距離,負(fù)數(shù)向下滾動(dòng).
   mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -scrollDistance, 0, 300);
   invalidate();
   if (scrollSelected != null) {
    scrollSelected.selected(getCurrentText());
   }
  }
 }

這樣已經(jīng)可以使用了,滑動(dòng)的刻度尺已經(jīng)完成了。不過交給UI一看,人家說這東西怎么那么難滑動(dòng)呢,每次怎么只能滑一大格呢,我要那種fling的感覺。確實(shí),因?yàn)樵贛otionEvent.ACTION_UP的時(shí)候都會(huì)去矯正一下位置,所以給使用者的感覺就是一次只能滑一格,滑動(dòng)體驗(yàn)很不好,只能去增加fling。。。

4.fling

增加fling多簡單啊,Scroller不是有這個(gè)方法嗎mScroller.fling(),使用方法這里不再介紹了。fling增加之后,用戶的體驗(yàn)確實(shí)好了很多,不過一個(gè)新的問題出現(xiàn)了,就是在fling停止之后怎么矯正位置呢?這是個(gè)大問題,卡住了好大一會(huì)兒,最終找到了解決方法:

 @Override
 public void computeScroll() {
  if (mScroller.computeScrollOffset()) {
   //這里是結(jié)束之后調(diào)用矯正位置的方法。scrollFinish()。
   if (mScroller.getCurrX() == mScroller.getFinalX() && mPressUp && isFling) {
    mPressUp = false;
    isFling = false;
    scrollFinish();
   }
   scrollTo(mScroller.getCurrX(), 0);
   invalidate();
  }
  super.computeScroll();
 }

三 結(jié)束

效果在文章一開始已經(jīng)展示出來了,指針并沒有在該自定義View中繪制,底部的線也是,因?yàn)閷?duì)于指針的需求是多變的,所以用了一個(gè)自定義的ViewGroup去完成剩余的指針和底部的實(shí)線。底部的實(shí)線放在Group中是因?yàn)槲覀兊腢I效果,底部的實(shí)線上面可以沒有刻度,也就是這個(gè)底部的線是固定在底部,比我畫在刻度下面跟隨刻度滑動(dòng)要簡單的多。想到之后的變體,感覺刻度本身的View跟指針分開是比較好擴(kuò)展的,Group只需要給刻度尺控件傳入中間指針的(x,y)坐標(biāo)就好了。

總結(jié)

以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)億速云的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI