Android 滑動選擇身高體重控件——RulerView

本文已授權(quán)微信公眾號:鴻洋(hongyangAndroid)在微信公眾號平臺原創(chuàng)首發(fā)

前言

隔一段時間工作不忙的時候就想溫習(xí)一下view相關(guān)的知識毁葱,比起學(xué)習(xí)其他東西,感覺做控件不會顯的枯燥粘昨,日復(fù)一日做著重復(fù)的工作,維護(hù)著項目窜锯,總想在里面添加一些新的東西张肾,比如新的界面開始用kotlin,用的第三方不是很滿意的控件锚扎,想想不是很難就自己來做吞瞪,閑來無聊就看看python入門,最近項目多了一個需要滑動選擇身高和運動時間的控件驾孔,在github上沒找到合適的芍秆,正好拋物線大神發(fā)起了一個自定義view的仿寫活動惯疙,一舉兩得,就有了該控件浪听。

封面圖

封面.png

效果圖

RulerViewGif.gif

2017/11/29 新添功能

gif2

使用computeScrollTo(float)

2017/12/22 新添功能

image.png

增加了scaleLimit屬性用來設(shè)置相鄰2個刻度之間的數(shù)量屬性

支持設(shè)置的屬性

        <attr name="scaleLimit" format="integer" />                  <!--相鄰2個刻度之間的數(shù)量-->
        <attr name="rulerHeight" format="dimension" />               <!--尺子的高度-->
        <attr name="rulerToResultgap" format="dimension" />          <!--尺子距離結(jié)果的高度-->
        <attr name="scaleGap" format="dimension" />                  <!--刻度間距-->
        <attr name="scaleCount" format="integer" />                  <!--刻度數(shù)-->
        <attr name="firstScale" format="float" />                    <!--默認(rèn)選中的刻度-->
        <attr name="maxScale" format="integer" />                    <!--最大刻度-->
        <attr name="minScale" format="integer" />                    <!--最小刻度-->
        <attr name="bgColor" format="color" />                       <!--背景色-->
        <attr name="smallScaleColor" format="color" />               <!--小刻度的顏色-->
        <attr name="midScaleColor" format="color" />                 <!--中刻度的顏色-->
        <attr name="largeScaleColor" format="color" />               <!--大刻度的顏色-->
        <attr name="scaleNumColor" format="color" />                 <!--刻度數(shù)的顏色-->
        <attr name="resultNumColor" format="color" />                <!--結(jié)果字體的顏色-->
        <attr name="unit" format="string" />                         <!--單位-->
        <attr name="unitColor" format="color" />                     <!--單位顏色-->
        <attr name="smallScaleStroke" format="dimension" />          <!--小刻度的寬度-->
        <attr name="midScaleStroke" format="dimension" />            <!--中刻度的寬度-->
        <attr name="largeScaleStroke" format="dimension" />          <!--大刻度的寬度-->
        <attr name="resultNumTextSize" format="dimension" />         <!--結(jié)果字體大小-->
        <attr name="scaleNumTextSize" format="dimension" />          <!--刻度字體大小-->
        <attr name="unitTextSize" format="dimension" />              <!--單位字體大小-->
        <attr name="showScaleResult" format="boolean" />             <!--是否顯示結(jié)果值-->
        <attr name="isBgRoundRect" format="boolean" />               <!--背景是否圓角-->

使用

compile 'com.github.superSp:RulerView:v1.5'

源碼地址

實現(xiàn)思路

  • 初始化畫筆螟碎,以及其他需要的參數(shù)
  • 重寫onMeasuer()確定尺子的大小
  • 重寫onDraw()繪畫出靜態(tài)尺子,并且將一些滑動時需要改變的參數(shù)設(shè)置為變量迹栓,繪制時只繪制當(dāng)前屏幕可見區(qū)域掉分,滑動尺子時,只刷新當(dāng)前屏幕模擬滑動并不是真正的滑動
  • 重寫onTouchEvent()處理滑動克伊,增加滑動速率監(jiān)聽VelocityTracker以及慣性滑動以及抬起手指時指針落在刻度上面需要的屬性動畫ValueAnimator

實現(xiàn)過程

測量

控件的高度=尺子的高度+結(jié)果值的高度+尺子距離結(jié)果值的高度
控件的寬度=屏幕寬度或者固定寬度

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int heightModule = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        switch (heightModule) {
            case MeasureSpec.AT_MOST:
                height = rulerHeight + (showScaleResult ? resultNumRect.height() : 0) + rulerToResultgap * 2 + getPaddingTop() + getPaddingBottom();
                break;
            case MeasureSpec.UNSPECIFIED:
            case MeasureSpec.EXACTLY:
                height = heightSize + getPaddingTop() + getPaddingBottom();
                break;
        }

        width = widthSize + getPaddingLeft() + getPaddingRight();

        setMeasuredDimension(width, height);

    }

繪制靜態(tài)尺子

  • 繪制背景
    drawRect()
    private void drawBg(Canvas canvas) {
        bgRect.set(0, 0, width, height);
        if (isBgRoundRect) {
            canvas.drawRoundRect(bgRect, 20, 20, bgPaint); //20->橢圓的用于圓形角x-radius
        } else {
            canvas.drawRect(bgRect, bgPaint);
        }
    }

  • 繪制尺子
    這一步是繪制控件中最為復(fù)雜的一步酥郭,需要考慮初始如何默認(rèn)選中初始刻度,手指抬起時候尺子需要滑動到的位置愿吹,計算當(dāng)前所處刻度等等不从。

繪制滑動類型的view時亮瓷,當(dāng)初的想法是一次性繪制出全部內(nèi)容兆览,之后使用canvas.clipRect()裁剪掉不可見區(qū)域仇让,但是如果內(nèi)容區(qū)域比較大昌妹,例如需要繪制1000個內(nèi)容,則沒滑動一次for循環(huán)需要執(zhí)行1000次疙渣,而且刻度越大時候循環(huán)越多晃危,占用內(nèi)存更大毛肋,會造成卡頓枫耳,因此換了另外一種思路乏矾,只繪制當(dāng)前屏幕可見區(qū)域內(nèi)容,這樣無論刻度有多大迁杨,暫用的內(nèi)存都很小钻心,滑動時,通過不斷刷新來模擬滑動铅协,做到以假亂真的效果捷沸。。狐史。

private void drawScaleAndNum(Canvas canvas) {
        canvas.translate(0, (showScaleResult ? resultNumRect.height() : 0) + rulerToResultgap);//移動畫布到結(jié)果值的下面

        int num1;//確定刻度位置
        float num2;

        if (firstScale != -1) {   //第一次進(jìn)來的時候計算出默認(rèn)刻度對應(yīng)的假設(shè)滑動的距離moveX
            moveX = getWhichScalMovex(firstScale);          //如果設(shè)置了默認(rèn)滑動位置亿胸,計算出需要滑動的距離
            lastMoveX = moveX;
            firstScale = -1;                                //將結(jié)果置為-1,下次不再計算初始位置
        }

        num1 = -(int) (moveX / scaleGap);                   //滑動刻度的整數(shù)部分
        num2 = (moveX % scaleGap);                         //滑動刻度的小數(shù)部分

        canvas.save();                                      //保存當(dāng)前畫布

        rulerRight = 0;                                    //準(zhǔn)備開始繪制當(dāng)前屏幕,從最左面開始

        if (isUp) {   //這部分代碼主要是計算手指抬起時预皇,慣性滑動結(jié)束時,刻度需要停留的位置
            num2 = ((moveX - width / 2 % scaleGap) % scaleGap);     //計算滑動位置距離整點刻度的小數(shù)部分距離
            if (num2 <= 0) {
                num2 = scaleGap - Math.abs(num2);
            }
            leftScroll = (int) Math.abs(num2);                        //當(dāng)前滑動位置距離左邊整點刻度的距離
            rightScroll = (int) (scaleGap - Math.abs(num2));         //當(dāng)前滑動位置距離右邊整點刻度的距離

            float moveX2 = num2 <= scaleGap / 2 ? moveX - leftScroll : moveX + rightScroll; //最終計算出當(dāng)前位置到整點刻度位置需要滑動的距離

            if (valueAnimator != null && !valueAnimator.isRunning()) {      //手指抬起婉刀,并且當(dāng)前沒有慣性滑動在進(jìn)行吟温,創(chuàng)建一個慣性滑動
                valueAnimator = ValueAnimator.ofFloat(moveX, moveX2);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        moveX = (float) animation.getAnimatedValue();            //不斷滑動去更新新的位置
                        lastMoveX = moveX;
                        invalidate();
                    }
                });
                valueAnimator.addListener(new AnimatorListenerAdapter() {       //增加一個監(jiān)聽,用來返回給使用者滑動結(jié)束后的最終結(jié)果刻度值
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        //這里是滑動結(jié)束時候回調(diào)給使用者的結(jié)果值
                        if (onChooseResulterListener != null) {
                            onChooseResulterListener.onEndResult(resultText);
                        }
                    }
                });
                valueAnimator.setDuration(300);
                valueAnimator.start();
                isUp = false;
            }

            num1 = (int) -(moveX / scaleGap);                //重新計算當(dāng)前滑動位置的整數(shù)以及小數(shù)位置
            num2 = (moveX % scaleGap);
        }
        canvas.translate(num2, 0);    //不加該偏移的話突颊,滑動時刻度不會落在0~1之間只會落在整數(shù)上面,其實這個都能設(shè)置一種模式了鲁豪,畢竟初衷就是指針不會落在小數(shù)上面

        //這里是滑動時候不斷回調(diào)給使用者的結(jié)果值
        resultText = String.valueOf(new WeakReference<>(new BigDecimal((width / 2 - moveX) / (scaleGap * scaleCount))).get().setScale(1, BigDecimal.ROUND_HALF_UP).floatValue() + minScale);
        if (onChooseResulterListener != null) {
            onChooseResulterListener.onScrollResult(resultText); //接口不斷回調(diào)給使用者結(jié)果值
        }
        //繪制當(dāng)前屏幕可見刻度,不需要裁剪屏幕,while循環(huán)只會執(zhí)行·屏幕寬度/刻度寬度·次,大部分的繪制都是if(curDis<width)這樣子內(nèi)存暫用相對來說會比較高潘悼。。
        while (rulerRight < width) {
            if (num1 % scaleCount == 0) {    //繪制整點刻度以及文字
                if ((moveX >= 0 && rulerRight < moveX - scaleGap) || width / 2 - rulerRight <= getWhichScalMovex(maxScale + 1) - moveX) {
                    //當(dāng)滑動出范圍的話爬橡,不繪制治唤,去除左右邊界
                } else {
                    //繪制刻度,繪制刻度數(shù)字
                    canvas.drawLine(0, 0, 0, midScaleHeight, midScalePaint);
                    scaleNumPaint.getTextBounds(num1 / scaleGap + minScale + "", 0, (num1 / scaleGap + minScale + "").length(), scaleNumRect);
                    canvas.drawText(num1 / scaleCount + minScale + "", -scaleNumRect.width() / 2, lagScaleHeight +
                            (rulerHeight - lagScaleHeight) / 2 + scaleNumRect.height(), scaleNumPaint);
                }

            } else {   //繪制小數(shù)刻度
                if ((moveX >= 0 && rulerRight < moveX) || width / 2 - rulerRight < getWhichScalMovex(maxScale) - moveX) {
                    //當(dāng)滑動出范圍的話糙申,不繪制宾添,去除左右邊界
                } else {
                    //繪制小數(shù)刻度
                    canvas.drawLine(0, 0, 0, smallScaleHeight, smallScalePaint);
                }
            }
            ++num1;  //刻度加1
            rulerRight += scaleGap;  //繪制屏幕的距離在原有基礎(chǔ)上+1個刻度間距
            canvas.translate(scaleGap, 0); //移動畫布到下一個刻度
        }

        canvas.restore();
        //繪制屏幕中間用來選中刻度的最大刻度
        canvas.drawLine(width / 2, 0, width / 2, lagScaleHeight, lagScalePaint);

    }

繪制結(jié)果

 //繪制上面的結(jié)果 結(jié)果值+單位
    private void drawResultText(Canvas canvas, String resultText) {
        if (!showScaleResult) {   //判斷用戶是否設(shè)置需要顯示當(dāng)前刻度值,如果否則取消繪制
            return;
        }
        canvas.translate(0, -resultNumRect.height() - rulerToResultgap / 2);  //移動畫布到正確的位置來繪制結(jié)果值
        resultNumPaint.getTextBounds(resultText, 0, resultText.length(), resultNumRect);
        canvas.drawText(resultText, width / 2 - resultNumRect.width() / 2, resultNumRect.height(), //繪制當(dāng)前刻度結(jié)果值
                resultNumPaint);
        resultNumRight = width / 2 + resultNumRect.width() / 2 + 10;
        canvas.drawText(unit, resultNumRight, kgRect.height() + 2, kgPaint);            //在當(dāng)前刻度結(jié)果值的又面10px的位置繪制單位
    }

處理滑動

主要是記錄moveX柜裸,以及添加velocityTracker速度監(jiān)聽器缕陕,以及處理慣性滑動

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        currentX = event.getX();
        isUp = false;
        velocityTracker.computeCurrentVelocity(500);
        velocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //按下時如果屬性動畫還沒執(zhí)行完,就終止,記錄下當(dāng)前按下點的位置
                if (valueAnimator != null && valueAnimator.isRunning()) {
                    valueAnimator.end();
                    valueAnimator.cancel();
                }
                downX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                //滑動時候,通過假設(shè)的滑動距離,做超出左邊界以及右邊界的限制。
                moveX = currentX - downX + lastMoveX;
                if (moveX >= width / 2) {
                    moveX = width / 2;
                } else if (moveX <= getWhichScalMovex(maxScale)) {
                    moveX = getWhichScalMovex(maxScale);
                }
                break;
            case MotionEvent.ACTION_UP:
                //手指抬起時候制造慣性滑動
                lastMoveX = moveX;
                xVelocity = (int) velocityTracker.getXVelocity();
                autoVelocityScroll(xVelocity);
                velocityTracker.clear();
                break;
        }
        invalidate();
        return true;
    }

處理慣性滑動的代碼

這里就是調(diào)節(jié)了疙挺,根據(jù)得到的速率調(diào)節(jié)出比較舒服的滑動扛邑。。铐然。

private void autoVelocityScroll(int xVelocity) {
        //慣性滑動的代碼,速率和滑動距離,以及滑動時間需要控制的很好,應(yīng)該網(wǎng)上已經(jīng)有關(guān)于這方面的算法了吧蔬崩。。這里是經(jīng)過N次測試調(diào)節(jié)出來的慣性滑動
        if (Math.abs(xVelocity) < 50) {
            isUp = true;
            return;
        }
        if (valueAnimator.isRunning()) {
            return;
        }
        valueAnimator = ValueAnimator.ofInt(0, xVelocity / 20).setDuration(Math.abs(xVelocity / 10));
        valueAnimator.setInterpolator(new DecelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                moveX += (int) animation.getAnimatedValue();
                if (moveX >= width / 2) {
                    moveX = width / 2;
                } else if (moveX <= getWhichScalMovex(maxScale)) {
                    moveX = getWhichScalMovex(maxScale);
                }
                lastMoveX = moveX;
                invalidate();
            }

        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                isUp = true;
                invalidate();
            }
        });

        valueAnimator.start();
    }

供外部使用的獲取結(jié)果值的接口

    public interface OnChooseResulterListener {
        void onEndResult(String result);      //結(jié)束滑動時候返回的結(jié)果
        void onScrollResult(String result);   //滑動時不斷產(chǎn)生的結(jié)果
    }

最后再貼一下使用以及地址

compile 'com.github.superSp:RulerView:v1.5'

源碼地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末搀暑,一起剝皮案震驚了整個濱河市沥阳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌险掀,老刑警劉巖沪袭,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異樟氢,居然都是意外死亡冈绊,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門埠啃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來死宣,“玉大人,你說我怎么就攤上這事碴开∫愀茫” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵潦牛,是天一觀的道長眶掌。 經(jīng)常有香客問我,道長巴碗,這世上最難降的妖魔是什么朴爬? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮橡淆,結(jié)果婚禮上召噩,老公的妹妹穿的比我還像新娘母赵。我一直安慰自己,他們只是感情好具滴,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布凹嘲。 她就那樣靜靜地躺著,像睡著了一般构韵。 火紅的嫁衣襯著肌膚如雪周蹭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天贞绳,我揣著相機(jī)與錄音谷醉,去河邊找鬼。 笑死冈闭,一個胖子當(dāng)著我的面吹牛俱尼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播萎攒,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼遇八,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了耍休?” 一聲冷哼從身側(cè)響起刃永,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎羊精,沒想到半個月后斯够,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡喧锦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年读规,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片燃少。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡束亏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出阵具,到底是詐尸還是另有隱情碍遍,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布阳液,位于F島的核電站怕敬,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏帘皿。R本人自食惡果不足惜赖捌,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧越庇,春花似錦、人聲如沸奉狈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仁期。三九已至桑驱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間跛蛋,已是汗流浹背熬的。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留赊级,地道東北人押框。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像理逊,于是被迫代替她去往敵國和親橡伞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345