Android自定義View之審核進度條

概述:
在做商城類的App時溪胶,都會有售后的需求壶唤,而售后流程通常會因為不同的業(yè)務,而分為不確定的幾個步驟类嗤,如下圖所示:
image.png
那么問題就來了糊肠,像這樣的效果如何實現(xiàn)呢?讓我們先放下這個問題遗锣,先看看UI模仿的京東原圖是怎樣的:

動圖:
image.gif

從上面的效果圖中我們可以觀察到货裹,當步驟少的話,等分居中顯示精偿,當步驟多的話弧圆,可以橫向拖動顯示。

一 分析

1 需要實現(xiàn)的效果:相信不要多言笔咽,上面已經(jīng)完全展示搔预。
2 需要注意的點:步驟不固定,文字和圖片居中對齊叶组,文字可能多行拯田,多行的文字也是對稱的。

二 思路

相同的效果甩十,會有不同的實現(xiàn)思路船庇。這里提供兩個方法實現(xiàn):
1 將一個步驟單獨作為自定義view實現(xiàn)。
2 將所有步驟做為自定義view實現(xiàn)枣氧。
本文將才用第一種實現(xiàn)方式溢十,實現(xiàn)我們的效果。將一個步驟作為自定義view時达吞,我們還可以再分成三個部分: 左邊線张弛,中間圖片早抠,右邊線钦无。

三 實現(xiàn)

1 自定義屬性(在values目錄下新建attrs.xml):
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 申請進度的自定義view-->
    <declare-styleable name="AuditProgressView">
        <!--當前步驟是否完成 -->
        <attr name="apv_isCurrentComplete" format="boolean" />
        <!--下一個步驟是否完成 -->
        <attr name="apv_isNextComplete" format="boolean" />
        <!--是不是第一個步驟 -->
        <attr name="apv_isFirstStep" format="boolean" />
        <!--是不是最后一個步驟 -->
        <attr name="apv_isLastStep" format="boolean" />
        <!--共有幾個步驟 -->
        <attr name="apv_stepCount" format="integer" />
        <!--步驟提示 -->
        <attr name="apv_text" format="string|reference" />
    </declare-styleable>

</resources>

上面的屬性是必須定義的屬性份汗。對于其他例如文字顏色 線條顏色 文字大小之類的屬性都沒有定義洋丐,有需要的可以自己復制源碼進行修改踩麦。

布局代碼:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">


    <!--動態(tài)設(shè)置數(shù)據(jù)-->
    <HorizontalScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scrollbars="none">
        <LinearLayout
            android:id="@+id/ll_audit_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal" />
    </HorizontalScrollView>
    
    <!--分割線-->
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_margin="3dp"
        android:background="@android:color/darker_gray" />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="12dp"
        android:gravity="center_horizontal"
        android:text="上面代碼動態(tài)設(shè)置 下面xml布局設(shè)置"/>
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_margin="3dp"
        android:background="@android:color/darker_gray" />


    <!--XML靜態(tài)設(shè)置數(shù)據(jù)-->
    <HorizontalScrollView
        android:layout_width="match_parent"
        android:layout_height="90dp"
        android:scrollbars="none">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
            <com.p.h.view.AuditProgressView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:apv_isCurrentComplete="true"
                app:apv_isFirstStep="true"
                app:apv_isLastStep="false"
                app:apv_isNextComplete="true"
                app:apv_stepCount="4"
                app:apv_text="提交行程" />
            <com.p.h.view.AuditProgressView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:apv_isCurrentComplete="true"
                app:apv_isFirstStep="false"
                app:apv_isLastStep="false"
                app:apv_isNextComplete="true"
                app:apv_stepCount="4"
                app:apv_text="支付費用" />
            <com.p.h.view.AuditProgressView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:apv_isCurrentComplete="false"
                app:apv_isFirstStep="false"
                app:apv_isLastStep="false"
                app:apv_isNextComplete="false"
                app:apv_stepCount="4"
                app:apv_text="乘車出行" />
            <com.p.h.view.AuditProgressView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:apv_isCurrentComplete="false"
                app:apv_isFirstStep="false"
                app:apv_isLastStep="true"
                app:apv_isNextComplete="false"
                app:apv_stepCount="4"
                app:apv_text="支付尾款" />
        </LinearLayout>
    </HorizontalScrollView>

</LinearLayout>

自定義View的整體代碼:

package com.p.h.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import androidx.annotation.Nullable;
import com.p.h.R;

/**
 * describe: 自定義支付進度條 PanHui
 */
public class AuditProgressView extends View {
    // 標記該步驟是否完成
    private boolean mIsCurrentComplete;
    // 標記下一個步驟是否完成
    private boolean mIsNextComplete;
    // 根據(jù)是否完成的標記 確定繪制的圖片
    private Bitmap audit_drawBitmap;
    // 繪制文字
    private String text;
    // 畫布寬高
    private int width, height;
    private Paint paint;
    // 圖片距離view頂部的距離
    private int paddingTop;
    // 有幾個步驟
    private int stepCount;

    // 是否是第一步 第一步不需要 畫左邊線條
    private boolean mIsFirstStep;
    // 是否是最后一步 最后一步 不需要畫右邊線條
    private boolean mIsLastStep;

    public AuditProgressView(Context context) {
        this(context, null);
    }

    public AuditProgressView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public AuditProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.AuditProgressView, defStyleAttr, 0);
        mIsCurrentComplete = array.getBoolean(R.styleable.AuditProgressView_apv_isCurrentComplete, false);
        mIsNextComplete = array.getBoolean(R.styleable.AuditProgressView_apv_isNextComplete, false);
        mIsFirstStep = array.getBoolean(R.styleable.AuditProgressView_apv_isFirstStep, false);
        mIsLastStep = array.getBoolean(R.styleable.AuditProgressView_apv_isLastStep, false);
        stepCount = array.getInteger(R.styleable.AuditProgressView_apv_stepCount, 2);
        text = array.getString(R.styleable.AuditProgressView_apv_text);
        array.recycle();

        paddingTop = dp2px(getContext(), 22);
        paint = new Paint();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(widthMeasureSpec);

        // 在寬高不是精確模式時,定義最小寬高

        if (widthMode != MeasureSpec.EXACTLY) {
            width = getDisplayMetrics(getContext()).widthPixels / stepCount;
        }

        if (heightMode != MeasureSpec.EXACTLY) {
            height = dp2px(getContext(), 90);
        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 根據(jù) 當前步驟是否完成 確定中間的圖片
        if (mIsCurrentComplete) {
            audit_drawBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ok);
        } else {
            audit_drawBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.notok);
        }

        // 獲取自定義View的寬高
        width = getWidth();
        height = getHeight();

        // 繪制圖片
        canvas.drawBitmap(audit_drawBitmap, width / 2 - audit_drawBitmap.getWidth() / 2, height / 2 - audit_drawBitmap.getHeight() / 2, paint);

        // 根據(jù)當前步驟是否完成 確定繪制文字顏色
        String mString = text;
        TextPaint tp = new TextPaint();
        if (mIsCurrentComplete) {
            tp.setColor(Color.parseColor("#000000"));
        } else {
            tp.setColor(Color.parseColor("#CCCCCC"));
        }

        // 繪制多行文字
        tp.setStyle(Paint.Style.FILL);
        Point point = new Point(width / 2, dp2px(getContext(), 70));
        tp.setTextSize(sp2px(getContext(), 14));
        textCenter(mString, tp, canvas, point, dp2px(getContext(), 57), Layout.Alignment.ALIGN_CENTER, 1, 0, false);

        // 繪制線條                                    //寬度
        paint.setStrokeWidth(dp2px(getContext(), 2));

        // 根據(jù)是不是第一個步驟 確定是否有左邊線條
        if (!mIsFirstStep) {
            // 左邊(線條顏色)
            // 根據(jù)當前步驟是否完成 來確定左邊線條的顏色
            if (mIsCurrentComplete) {
                paint.setColor(Color.parseColor("#6CD89E"));
            } else {
                paint.setColor(Color.parseColor("#CCCCCC"));
            }                                                                                                              //距離進度點左右間距
            canvas.drawLine(0, height / 2, width / 2 - audit_drawBitmap.getWidth() / 2 - dp2px(getContext(), 0), height / 2, paint);
        }

        // 根據(jù)是不是最后的步驟 確定是否有右邊線條
        if (!mIsLastStep) {
            // 右邊(線條顏色)
            // 根據(jù)下一個步驟是否完成 來確定右邊線條的顏色
            if (mIsNextComplete) {
                paint.setColor(Color.parseColor("#6CD89E"));
            } else {
                paint.setColor(Color.parseColor("#CCCCCC"));
            }                                                                                   //距離進度點左右間距
            canvas.drawLine(width / 2 + audit_drawBitmap.getWidth() / 2 + dp2px(getContext(), 0), height / 2, width, height / 2, paint);
        }
    }


    //繪制多行文字
    private void textCenter(String string, TextPaint textPaint, Canvas canvas, Point point, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad) {
        StaticLayout staticLayout = new StaticLayout(string, textPaint, width, align, spacingmult, spacingadd, includepad);
        canvas.save();
        canvas.translate(-staticLayout.getWidth() / 2 + point.x, -staticLayout.getHeight() / 2 + point.y);
        staticLayout.draw(canvas);
        canvas.restore();
    }

    //當前已完成
    public void setIsCurrentComplete(boolean isCurrentComplete) {this.mIsCurrentComplete = isCurrentComplete;}

    //下一個是否已完成
    public void setIsNextComplete(boolean isNextComplete) {this.mIsNextComplete = isNextComplete;}

    //是否為第一步
    public void setIsFirstStep(boolean isFirstStep) {this.mIsFirstStep = isFirstStep;}

    //是否為最后一步
    public void setIsLastStep(boolean isLastStep) {this.mIsLastStep = isLastStep;}

    //顯示內(nèi)容
    public void setText(String text) {this.text = text;}

    //一屏幕寬度顯示出來的總步數(shù)
    public void setStepCount(int stepCount) {this.stepCount = stepCount;}


    /**
     * 獲取屏幕Metrics參數(shù)
     *
     * @param context
     * @return
     */
    public static DisplayMetrics getDisplayMetrics(Context context) {
        return context.getResources().getDisplayMetrics();
    }

    /**
     * 根據(jù)手機的分辨率從 dp 的單位 轉(zhuǎn)成為 px(像素)
     */
    public static int dp2px(Context context, float dpValue) {
        float density = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * density + 0.5f);
    }

    /**
     * 根據(jù)手機的分辨率從 px(像素) 的單位 轉(zhuǎn)成為 dp
     */
    public static int px2dp(Context context, float pxValue) {
        float density = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / density + 0.5f);
    }

    /**
     * 根據(jù)手機的分辨率從 px(像素) 的單位 轉(zhuǎn)成為 sp
     */
    public static int px2sp(Context context, float pxValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValue / fontScale + 0.5f);
    }

    /**
     * 根據(jù)手機的分辨率從 sp 的單位 轉(zhuǎn)成為 px(像素)
     */
    public static int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
}

使用方法:

package com.p.h;

import android.os.Bundle;
import android.widget.LinearLayout;
import androidx.appcompat.app.AppCompatActivity;
import com.p.h.view.AuditProgressView;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //綁定布局
        LinearLayout content = findViewById(R.id.ll_audit_content);

        //設(shè)置數(shù)據(jù)及狀態(tài)
        content.addView(createView(5, true, true, true, false, "提交行程"));
        content.addView(createView(5, true, true, false, false, "支付費用"));
        content.addView(createView(5, true, true, false, false, "乘車出行"));
        content.addView(createView(5, false, false, false, true, "支付尾款"));
    }

    //初始化設(shè)置數(shù)據(jù)的方法
    public AuditProgressView createView(int stepCount, boolean isCurrentComplete, boolean isNextComplete, boolean isFirstStep, boolean isLastStep, String text) {
        AuditProgressView view = new AuditProgressView(this);
        view.setStepCount(stepCount);
        view.setIsCurrentComplete(isCurrentComplete);
        view.setIsNextComplete(isNextComplete);
        view.setIsFirstStep(isFirstStep);
        view.setIsLastStep(isLastStep);
        view.setText(text);
        return view;
    }
}

好啦,全部代碼都在這了 每一步注釋我都注釋的特別清楚 這樣應該可以看懂了吧 其實特別簡單的 如果還有不懂的私信我...

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末觉至,一起剝皮案震驚了整個濱河市馋记,隨后出現(xiàn)的幾起案子腻窒,更是在濱河造成了極大的恐慌滩字,老刑警劉巖造虏,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件御吞,死亡現(xiàn)場離奇詭異,居然都是意外死亡漓藕,警方通過查閱死者的電腦和手機陶珠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來享钞,“玉大人揍诽,你說我怎么就攤上這事±跏” “怎么了暑脆?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長狐肢。 經(jīng)常有香客問我添吗,道長,這世上最難降的妖魔是什么处坪? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任根资,我火速辦了婚禮,結(jié)果婚禮上同窘,老公的妹妹穿的比我還像新娘玄帕。我一直安慰自己,他們只是感情好想邦,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布裤纹。 她就那樣靜靜地躺著,像睡著了一般丧没。 火紅的嫁衣襯著肌膚如雪鹰椒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天呕童,我揣著相機與錄音漆际,去河邊找鬼。 笑死夺饲,一個胖子當著我的面吹牛奸汇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播往声,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼擂找,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了浩销?” 一聲冷哼從身側(cè)響起贯涎,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎慢洋,沒想到半個月后塘雳,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體陆盘,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年败明,在試婚紗的時候發(fā)現(xiàn)自己被綠了礁遣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡肩刃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出杏头,到底是詐尸還是另有隱情盈包,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布醇王,位于F島的核電站呢燥,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏寓娩。R本人自食惡果不足惜叛氨,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望棘伴。 院中可真熱鬧寞埠,春花似錦、人聲如沸焊夸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阱穗。三九已至饭冬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間揪阶,已是汗流浹背昌抠。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鲁僚,地道東北人炊苫。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像蕴茴,于是被迫代替她去往敵國和親劝评。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345