一個漂亮而強大的自定義view

代碼地址以下:<br>http://www.demodashi.com/demo/13502.htmlhtml

簡介

主要提供一個漂亮而強大的自定義SeekBar,進度變化由提示牌 (sign)展現,具備強大的屬性設置,支持設置section(節點)、mark(標記)、track(軌跡)、thumb(拖動塊)、progress(進度)、sign(提示框)等功能java

主要功能

  • 強大的track(軌跡)和second track (選中軌跡)的最小值、最大值、軌跡粗細,顏色等設置;
  • 靈活的數字顯示,支持設置進度展現、節點文本展現採用整數仍是浮點數;
  • 支持設置進度單位,例如 10s,15km/h、平方,對數等;
  • 支持手柄拖動塊thumb半徑、顏色、陰影、透明度等;
  • 支持節點個數、文字大小、顏色設置;
  • 支持指示牌寬高、顏色、圓角半徑、三角arrow指示、border邊框、跟隨thumb移動等;
  • 支持設置拖動進度監聽回掉;
  • 支持格式化進度數字,徹底自定義進度樣式
  • ......

演示

xml

<com.zhouyou.view.seekbar.SignSeekBar
        android:id="@+id/seek_bar"
        android:layout_width="match_parent"
        android:layout_height="16dp"
        app:ssb_section_text_position="bottom_sides"
        app:ssb_show_progress_in_float="false"
        app:ssb_show_section_mark="false"
        app:ssb_show_section_text="true"
        app:ssb_show_sign="true"
        app:ssb_show_thumb_text="false"
        app:ssb_sign_arrow_height="5dp"
        app:ssb_sign_arrow_width="10dp"
        app:ssb_sign_border_color="@color/color_red"
        app:ssb_sign_border_size="1dp"
        app:ssb_sign_color="@color/color_gray"
        app:ssb_sign_show_border="true"/>

java

signSeekBar.getConfigBuilder()
                .min(0)
                .max(4)
                .progress(2)
                .sectionCount(4)
                .trackColor(ContextCompat.getColor(getContext(), R.color.color_gray))
                .secondTrackColor(ContextCompat.getColor(getContext(), R.color.color_blue))
                .thumbColor(ContextCompat.getColor(getContext(), R.color.color_blue))
                .sectionTextColor(ContextCompat.getColor(getContext(), R.color.colorPrimary))
                .sectionTextSize(16)
                .thumbTextColor(ContextCompat.getColor(getContext(), R.color.color_red))
                .thumbTextSize(18)
                .signColor(ContextCompat.getColor(getContext(), R.color.color_green))
                .signTextSize(18)
                .autoAdjustSectionMark()
                .sectionTextPosition(SignSeekBar.TextPosition.BELOW_SECTION_MARK)
                .build();

回調

signSeekBar.setOnProgressChangedListener(new SignSeekBar.OnProgressChangedListener() {
            @Override
            public void onProgressChanged(SignSeekBar signSeekBar, int progress, float progressFloat,boolean fromUser) {
            //fromUser 表示是不是用戶觸發 是不是用戶touch事件產生
                String s = String.format(Locale.CHINA, "onChanged int:%d, float:%.1f", progress, progressFloat);
                progressText.setText(s);
            }

            @Override
            public void getProgressOnActionUp(SignSeekBar signSeekBar, int progress, float progressFloat) {
                String s = String.format(Locale.CHINA, "onActionUp int:%d, float:%.1f", progress, progressFloat);
                progressText.setText(s);
            }

            @Override
            public void getProgressOnFinally(SignSeekBar signSeekBar, int progress, float progressFloat,boolean fromUser) {
                String s = String.format(Locale.CHINA, "onFinally int:%d, float:%.1f", progress, progressFloat);
                progressText.setText(s + getContext().getResources().getStringArray(R.array.labels)[progress]);
            }
        });

Attributes

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SignSeekBar">
        <attr name="ssb_min" format="float|reference"/> <!--min < max, default: 0.0f-->
        <attr name="ssb_max" format="float|reference"/> <!--min < max, default: 100.0f-->
        <attr name="ssb_progress" format="float|reference"/> <!--real time progress value, default: min-->
        <attr name="ssb_is_float_type" format="boolean"/> <!--support for float type-->
        <attr name="ssb_track_size" format="dimension|reference"/> <!--height of right-track(on the right of thumb), default: 2dp-->
        <!--height of left-track(on the left of thumb), default: 2dp higher than right-track's height-->
        <attr name="ssb_second_track_size" format="dimension|reference"/>
        <attr name="ssb_thumb_radius" format="dimension|reference"/> <!--radius of thumb, default: 2dp higher than left-track's height-->
        <!--radius of thumb when be dragging, default: 2 times of left-track's height-->
        <attr name="ssb_thumb_radius_on_dragging" format="dimension|reference"/>
        <attr name="ssb_track_color" format="color|reference"/> <!--color of right-track, default: R.color.colorPrimary-->
        <attr name="ssb_second_track_color" format="color|reference"/> <!--color of left-track, default: R.color.colorAccent-->
        <attr name="ssb_thumb_color" format="color|reference"/> <!--color of thumb, default: same as left-track's color-->
        <attr name="ssb_section_count" format="integer|reference"/> <!--shares of whole progress(max - min), default: 10-->
        <attr name="ssb_show_section_mark" format="boolean"/> <!--show demarcation points or not, default: false-->
        <attr name="ssb_auto_adjust_section_mark" format="boolean"/> <!--auto scroll to the nearest section_mark or not, default: false-->
        <attr name="ssb_show_section_text" format="boolean"/> <!--show section-text or not, default: false-->
        <attr name="ssb_section_text_size" format="dimension|reference"/> <!--text size of section-text, default: 14sp-->
        <attr name="ssb_section_text_color" format="color|reference"/> <!--text color of section-text, default: same as right-track's color-->
        <!--text position of section-text relative to track, sides, bottom_sides, below_section_mark, default: sides-->
        <attr name="ssb_section_text_position">
            <enum name="sides" value="0"/>
            <enum name="bottom_sides" value="1"/>
            <enum name="below_section_mark" value="2"/>
        </attr>
        <attr name="ssb_section_text_interval" format="integer"/> <!--the interval of two section-text, default: 1-->
        <attr name="ssb_show_thumb_text" format="boolean"/> <!--show real time progress-text under thumb or not, default: false-->
        <attr name="ssb_thumb_text_size" format="dimension|reference"/> <!--text size of progress-text, default: 14sp-->
        <attr name="ssb_thumb_text_color" format="color|reference"/> <!--text color of progress-text, default: same as left-track's color-->
        <attr name="ssb_show_progress_in_float" format="boolean"/> <!--show Sign-progress in float or not, default: false-->
        <attr name="ssb_touch_to_seek" format="boolean"/> <!--touch anywhere on track to quickly seek, default: false-->
        <attr name="ssb_seek_by_section" format="boolean"/> <!--seek by section, the progress may not be linear, default: false-->
        <attr name="ssb_sign_color" format="color|reference"/> <!--color of sign, default: same as left-track's color-->
        <attr name="ssb_sign_border_color" format="color|reference"/> <!--color of sign, border-->
        <attr name="ssb_sign_show_border" format="boolean"/> <!--color of sign, default: same as left-track's color-->
        <attr name="ssb_sign_text_size" format="dimension|reference"/> <!--text size of sign-progress, default: 14sp-->
        <attr name="ssb_sign_border_size" format="dimension|reference"/> <!--border size, default: 1dp-->
        <attr name="ssb_sign_text_color" format="color|reference"/> <!--text color of sign-progress, default: #ffffffff-->
        <attr name="ssb_anim_duration" format="integer"/> <!--duration of animation, default: 200ms-->
        <attr name="ssb_show_sign" format="boolean"/> <!--hide sign, default: false-->
        <attr name="ssb_text_space" format="dimension|reference"/><!--default:2dp-->
        <attr name="ssb_sides_labels" format="reference"/><!--default:null-->
        <attr name="ssb_thumb_bg_alpha" format="float|reference"/> <!--0.0f-1.0f, default: 0.2f-->
        <attr name="ssb_thumb_ratio" format="float|reference"/> <!--0.0f-1.0f, default: 0.7f-->
        <attr name="ssb_show_thumb_shadow" format="boolean"/> <!--0.0f-1.0f, default: false-->
        <attr name="ssb_sign_arrow_autofloat" format="boolean"/> <!--sign arrow auto float, default: true-->
        <attr name="ssb_sign_height" format="dimension|reference"/> <!--sign height,default:22dp-->
        <attr name="ssb_sign_width" format="dimension|reference"/> <!--sign_width ,default:72dp-->
        <attr name="ssb_sign_arrow_height" format="dimension|reference"/> <!--sign arrow height ,default:3dp-->
        <attr name="ssb_sign_arrow_width" format="dimension|reference"/> <!--sign arrow width, default:5dp-->
        <attr name="ssb_sign_round" format="dimension|reference"/> <!--sign round, default:3dp-->
    </declare-styleable>
</resources>

主要實現思路介紹

概況

本庫自定義控件主要是用了Canvas相關的drawXXX系列方法、一些簡單的算法和動畫來完成的。好比拖動軌跡、滑塊thumb拖動、放大、自動滾動最近節點、指示牌、區段節點標記、進度單位顯示等。接下來會講解下主要的實現思路,對於自定義View的其它基本流程,屬性獲取和設置、onMeasure的重寫等都不重點介紹,想了解完整流程請看源碼android

track(軌道)繪製

畫軌道比較簡單,主要實現方式就是畫兩條不一樣顏色的線條(其實畫的是一條分爲左右兩部分,銜接的地方是有個thumb遮擋着),主要是要求出滑動thumb的中心點mThumbCenterX,mThumbCenterX的計算很是重要,本庫的不少計算都是圍繞mThumbCenterX,mThumbCenterX是經過onTouchEvent事件MotionEvent 根據down、move事件實時計算出中心點x座標。git

// draw track
        mPaint.setColor(mSecondTrackColor);
        mPaint.setStrokeWidth(mSecondTrackSize);
        canvas.drawLine(xLeft, yTop, mThumbCenterX, yTop, mPaint);

        // draw second track
        mPaint.setColor(mTrackColor);
        mPaint.setStrokeWidth(mTrackSize);
        canvas.drawLine(mThumbCenterX, yTop, xRight, yTop, mPaint);

track(軌道)接觸有效計算

MotionEvent的getX()和getY()得到的永遠是相對view的觸摸位置座標,getRawX()和getRawY()得到的是相對屏幕的位置,軌道計算用的是getX,getY 相對於容器的位置座標x,y,計算x,y座標是否在軌道的矩形方框內,從而判斷是否在軌道上。github

private boolean isTrackTouched(MotionEvent event) {
        return isEnabled() && event.getX() >= getPaddingLeft() && event.getX() <= getMeasuredWidth() - getPaddingRight()
                && event.getY() >= getPaddingTop() && event.getY() <= getMeasuredHeight() - getPaddingBottom();
    }

thumb(滑塊)接觸有效計算

thumb就是軌道上的圓形滑塊,如何判斷手指拖動的區域是否在滑塊上呢,使用圓的標準方程(x-a)²+(y-b)²=r²來判斷算法

private boolean isThumbTouched(MotionEvent event) {
        if (!isEnabled())
            return false;
        float mCircleR = isThumbOnDragging ? mThumbRadiusOnDragging : mThumbRadius;
        float x = mTrackLength / mDelta * (mProgress - mMin) + mLeft;
        float y = getMeasuredHeight() / 2f;
        return (event.getX() - x) * (event.getX() - x) + (event.getY() - y) * (event.getY() - y)
                <= (mLeft + mCircleR) * (mLeft + mCircleR);
    }

thumb(滑塊)透明度實現

滑塊的透明度,是將滑塊的顏色值進行計算加上alpha,求出一個新的顏色值,主要是使用Color這個工具類的方法,你們常常用到的是Color的parseColor(@Size(min=1) String colorString)方法,庫中主要用的是Color的另外一些方法alpha(int color)、red(int color)、green(int color)、blue(int color)方法分別求出argb值,求出的透明度通過計算修改後,再用 Color.argb(alpha, r, g, b)組合得出一個新的顏色值。canvas

/**
     * 計算新的透明度顏色
     *
     * @param color 舊顏色
     * @param ratio 透明度係數
     */
    public int getColorWithAlpha(int color, float ratio) {
        int newColor = 0;
        int alpha = Math.round(Color.alpha(color) * ratio);
        int r = Color.red(color);
        int g = Color.green(color);
        int b = Color.blue(color);
        newColor = Color.argb(alpha, r, g, b);
        return newColor;
    }

thumb(滑塊) 最近節點位置計算方法

根據節點個數mSectionCount和兩個節點之間的間隔mSectionOffset,與滑塊當前位置mThumbCenterX的比較,求出最近一個節點的位置。app

//計算最近節點位置,mSectionCount:節點個數,mSectionOffset:兩個節點間隔距離,mThumbCenterX:滑塊中心點位置
        float x = 0;
        for (i = 0; i <= mSectionCount; i++) {
            x = i * mSectionOffset + mLeft;
            if (x <= mThumbCenterX && mThumbCenterX - x <= mSectionOffset) {
                break;
            }
        }

thumb(滑塊) 滾動最近節點動畫效果實現

滑塊自動滾動到最近節點增長了動畫移動效果,使用ValueAnimator實現動畫,Property Animation提供了Animator.AnimatorListener和Animator.AnimatorUpdateListener兩個監聽器用於動畫在播放過程當中的重要動畫事件。其中AnimatorUpdateListener監聽中onAnimationUpdate() 方法,動畫每播放一幀時調用,在動畫過程當中,可偵聽此事件來獲取並使用 ValueAnimator 計算出來的屬性值。利用傳入事件的 ValueAnimator 對象,調用其 getAnimatedValue() 方法便可獲取當前的屬性值,就是修改後滑塊的位置mThumbCenterX。此動畫還配合有Interpolator,動畫播放採用LinearInterpolator線性插值的方式執行動畫。插值器它定義了動畫變化過程當中的屬性變化規則,它根據時間比例因子計算出一個插值因子,用於設定目標對象的動畫執行是否爲線性變化、非線性變化或先加速後減速等等。Android系統自己內置了一些通用的Interpolator(插值器),以下:ide

類或接口名 說明
AccelerateDecelerateInterpolator 在動畫開始與結束的地方速率改變比較慢,在中間的時候加速
AccelerateInterpolator 在動畫開始的地方速率改變比較慢,而後開始加速
AnticipateInterpolator 開始的時候向後而後向前甩
AnticipateOvershootInterpolator 開始的時候向後而後向前甩必定值後返回最後的值
BounceInterpolator 動畫結束的時候彈起
CycleInterpolator 動畫循環播放特定的次數,速率改變沿着正弦曲線
DecelerateInterpolator 在動畫開始的地方快而後慢
LinearInterpolator 以常量速率改變
OvershootInterpolator 向前甩必定值後再回到原來位置

完整源碼:工具

private void autoAdjustSection() {
        int i;
        //計算最近節點位置,mSectionCount:節點個數,mSectionOffset:兩個節點間隔距離,mThumbCenterX:滑塊中心點位置
        float x = 0;
        for (i = 0; i <= mSectionCount; i++) {
            x = i * mSectionOffset + mLeft;
            if (x <= mThumbCenterX && mThumbCenterX - x <= mSectionOffset) {
                break;
            }
        }

        BigDecimal bigDecimal = BigDecimal.valueOf(mThumbCenterX);
        //BigDecimal setScale保留1位小數,四捨五入,2.35變成2.4
        float x_ = bigDecimal.setScale(1, BigDecimal.ROUND_HALF_UP).floatValue();
        boolean onSection = x_ == x; // 就在section處,不做valueAnim,優化性能

        AnimatorSet animatorSet = new AnimatorSet();
        ValueAnimator valueAnim = null;
        if (!onSection) {
            if (mThumbCenterX - x <= mSectionOffset / 2f) {
                valueAnim = ValueAnimator.ofFloat(mThumbCenterX, x);
            } else {
                valueAnim = ValueAnimator.ofFloat(mThumbCenterX, (i + 1) * mSectionOffset + mLeft);
            }
            valueAnim.setInterpolator(new LinearInterpolator());
            valueAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    mThumbCenterX = (float) animation.getAnimatedValue();
                    mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
                    invalidate();

                    if (mProgressListener != null) {
                        mProgressListener.onProgressChanged(SignSeekBar.this, getProgress(), getProgressFloat(),true);
                    }
                }
            });
        }
        if (!onSection) {
            animatorSet.setDuration(mAnimDuration).playTogether(valueAnim);
        }
        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
                isThumbOnDragging = false;
                isTouchToSeekAnimEnd = true;
                invalidate();
                if (mProgressListener != null) {
                    mProgressListener.getProgressOnFinally(SignSeekBar.this, getProgress(), getProgressFloat(),true);
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
                isThumbOnDragging = false;
                isTouchToSeekAnimEnd = true;
                invalidate();
            }
        });
        animatorSet.start();
    }

採用BigDecimal處理小數

代碼中的小數採用BigDecimal來處理,只介紹setScale相關方法,其它更多方法能夠本身去學習,這裏只是拋磚引玉。 BigDecimal.setScale()方法用於格式化小數點 setScale(1)表示保留一位小數,默認用四捨五入方式 setScale(1,BigDecimal.ROUND_DOWN)直接刪除多餘的小數位,如2.35會變成2.3 setScale(1,BigDecimal.ROUND_UP)進位處理,2.35變成2.4 setScale(1,BigDecimal.ROUND_HALF_UP)四捨五入,2.35變成2.4 setScaler(1,BigDecimal.ROUND_HALF_DOWN)四捨五入,2.35變成2.3,若是是5則向下舍

Sign 提示框--三角形邊框繪製

單純的進度提示框實現比較簡單,主要是由矩形框+三角形組成,可是加邊框繪製的時候比較麻煩一點,須要留出矩形和三角形交接的地方不能畫線,這裏作了假象交接的地方其實額外繪製了三角形的底邊,顏色採用的是矩形庫填充的顏色。三角形邊框繪製以下:

private void drawTriangleBoder(Canvas canvas, Point point1, Point point2, Point point3, Paint paint) {
        triangleboderPath.reset();
        triangleboderPath.moveTo(point1.x, point1.y);
        triangleboderPath.lineTo(point2.x, point2.y);
        paint.setColor(signPaint.getColor());
        float value = mSignBorderSize / 6;
        paint.setStrokeWidth(mSignBorderSize + 1f);
        canvas.drawPath(triangleboderPath, paint);
        triangleboderPath.reset();
        paint.setStrokeWidth(mSignBorderSize);
        triangleboderPath.moveTo(point1.x - value, point1.y - value);
        triangleboderPath.lineTo(point3.x, point3.y);
        triangleboderPath.lineTo(point2.x + value, point2.y - value);
        paint.setColor(mSignBorderColor);
        canvas.drawPath(triangleboderPath, paint);
    }

Sign 提示框--進度單位unit實現方式

進度單位不少需求也是須要的,不是單純的用canvas.drawText來繪製。這裏採用的是StaticLayout。使用Canvas的drawText繪製文本是不會自動換行的,即便一個很長很長的字符串,drawText也只顯示一行,超出部分被隱藏在屏幕以外。能夠逐個計算每一個字符的寬度,經過必定的算法將字符串分割成多個部分,而後分別調用drawText一部分一部分的顯示, 可是這種顯示效率會很低。StaticLayout是android中處理文字換行的一個工具類,StaticLayout已經實現了文本繪製換行處理,也支持標籤屬性<small>,m/s<sup>2</sup>,μmol/l,μ/l從而實現強大靈活的單位設置。

private void createValueTextLayout() {
        String value = isShowProgressInFloat ? String.valueOf(getProgressFloat()) : String.valueOf(getProgress());
        if (value != null && unit != null && !unit.isEmpty())
            value += String.format(" <small>%s</small>", unit);
        Spanned spanned = Html.fromHtml(value);
        valueTextLayout = new StaticLayout(spanned, valueTextPaint, mSignWidth, Layout.Alignment.ALIGN_CENTER, 1, 0, false);
    }

圓圈中心繪製文本

圓圈中心繪製文字,高度是比較難控制的,特別是中文,不能簡單的經過bounds.height()來獲取高度的方式計算,須要先求出baseline這種方式來處理,求baseline的方式是固定的。下面提供一個通用的方法:

/**
     * 精確畫圓圈中心文字(通用方法),其中文字的高度是最難計算適配的,採用此方法,能夠完美解決
     *
     * @param canvas  畫板
     * @param paint   畫筆panit
     * @param centerX 圓圈中心X座標
     * @param centerY 圓圈中心Y座標
     * @param radius  半徑
     * @param text    顯示的文本
     */
    private void drawCircleText(Canvas canvas, Paint paint, float centerX, float centerY, float radius, String text) {
        paint.setTextAlign(Paint.Align.LEFT);
        Rect bounds = new Rect();
        paint.getTextBounds(text, 0, text.length(), bounds);
        Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt();
        float baseline = centerY - radius + (2 * radius - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top;
        canvas.drawText(text, centerX - radius + radius - bounds.width() / 2, baseline, paint);
    }

項目結構目錄截圖

一個漂亮而強大的自定義view

代碼地址以下:<br>http://www.demodashi.com/demo/13502.html

注:本文著做權歸做者,由demo大師代發,拒絕轉載,轉載須要做者受權

相關文章
相關標籤/搜索