1. 程式人生 > >Android原生繪圖進度條+簡單自定義屬性程式碼生成器

Android原生繪圖進度條+簡單自定義屬性程式碼生成器

零、前言

1.感覺切拼字串是個很有意思的事,好的拼接方式可以自動生成一些很實用的東西
2.本文自定義控制元件並不是很高大上的東西,目的在於計錄自定義控制元件的書寫規範與行文流程
3.建議大家自定義控制元件時自定義屬性有自己專屬字首,有利無害,何樂不為
4.本文是根據鴻洋在慕課網上的教程敲的:詳見,自己修改並優化了一點邏輯和顯示效果

先看一下效果:

圓形進度條.gif

橫向進度條.gif

一、簡單自定義屬性生成器

1.玩安卓的應該都寫過自定義控制元件的自定義屬性:如下

自定義控制元件.png

我寫著寫著感覺好枯燥,基本上流程相似,也沒有什麼技術難度,想:這種事不就應該交給機器嗎?

2.通過attrs.xml自動生成相應程式碼

秉承著能用程式碼解決的問題,絕對不動手。能夠靠智商解決的問題,絕對不靠體力的大無畏精神:
寫了一個小工具,將程式碼裡的內容自動生成一下:基本上就是字串的切割和拼裝,工具附在文尾

使用方法與注意點:
1.拷貝到AndroidStudio的test裡,將attrs.xml的檔案路徑設定一下,執行
2.自定義必須符合命名規則,如z_pb_on_height,專屬字首如z_,單詞間下劃線連線即可
3.它並不是什麼高大上的東西,只是簡單的字串切割拼組,只適用簡單的自定義屬性[dimension|color|boolean|string]

(不過一般的自定義屬性也夠用了)

自動生成.png

在開篇之前:先看一下Android系統內自定義控制元件的書寫風格,畢竟跟原生看齊沒有什麼壞處
看一下LinearLayout的原始碼:

1.構造方法使用最多引數的那個,其他用this(XXX)呼叫
 public LinearLayout(Context context) {
      this(context, null);
  }

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

  public LinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
      this(context, attrs, defStyleAttr, 0);
  }

  public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
      super(context, attrs, defStyleAttr, defStyleRes);
      ...
      }
複製程式碼
2.自定義屬性的書寫

1).先將自定義屬性的成員變數定義好
2).如果自定義屬性不是很多,一個一個a.getXXX,預設值直接寫在後面就行了
3).看了一下TextView的原始碼,自定義屬性很多,它是先定義預設值的變數,再使用,而且用switch來對a.getXXX進行賦值

final TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes);

        int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1);
        if (index >= 0) {
            setOrientation(index);
        }

        index = a.getInt(com.android.internal.R.styleable.LinearLayout_gravity, -1);
        if (index >= 0) {
            setGravity(index);
        }

        boolean baselineAligned = a.getBoolean(R.styleable.LinearLayout_baselineAligned, true);
        if (!baselineAligned) {
            setBaselineAligned(baselineAligned);
        }
        ......
        a.recycle();
複製程式碼

一、水平的進度條

條形進度條分析.png

1.自定義控制元件屬性:values/attrs.xml
    <!--自定義進度條-->
    <declare-styleable name="TolyProgressBar">
        <!--進度條相關-->
        <!--背景色-->
        <attr name="z_pb_bg_color" format="color"/>
        <!--背景高-->
        <attr name="z_pb_bg_height" format="dimension"/>
        <!--進度色-->
        <attr name="z_pb_on_color" format="color"/>
        <!--進度高-->
        <attr name="z_pb_on_height" format="dimension"/>
        
        <!--文字相關-->
        <!--文字顏色-->
        <attr name="z_pb_txt_color" format="color"/>
        <!--文字大小-->
        <attr name="z_pb_txt_size" format="dimension"/>
        <!--文字兩邊的空距-->
        <attr name="z_pb_txt_offset" format="dimension"/>
        <!--文字是否消失-->
        <attr name="z_pb_txt_gone" format="boolean"/>
    </declare-styleable>
複製程式碼
2.初始程式碼:將進行一些常規處理
public class TolyProgressBar extends ProgressBar {

    private Paint mPaint;
    private int mPBWidth;
    private RectF mRectF;
    private Path mPath;
    private float[] mFloat8Left;//左邊圓角陣列
    private float[] mFloat8Right;//右邊圓角陣列
    
    private float mProgressX;//進度理論值
    private float mEndX;//進度條尾部
    private int mTextWidth;//文字寬度
    private boolean mLostRight;//是否不畫右邊
    private String mText;//文字

    private int mPbBgColor = 0xffC9C9C9;
    private int mPbOnColor = 0xff54F340;
    private int mPbOnHeight = dp(6);
    private int mPbBgHeight = dp(6);
    private int mPbTxtColor = 0xff525252;
    private int mPbTxtSize = sp(10);
    private int mPbTxtOffset = sp(10);
    private boolean mPbTxtGone= false;

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

    public TolyProgressBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TolyProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TolyProgressBar);
        mPbOnHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_on_height, mPbOnHeight);
        mPbTxtOffset = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_txt_offset, mPbTxtOffset);
        mPbOnColor = a.getColor(R.styleable.TolyProgressBar_z_pb_on_color, mPbOnColor);
        mPbTxtSize = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_txt_size, mPbTxtSize);
        mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
        mPbBgHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_bg_height, mPbBgHeight);
        mPbBgColor = a.getColor(R.styleable.TolyProgressBar_z_pb_bg_color, mPbBgColor);
        mPbTxtGone =  a.getBoolean(R.styleable.TolyProgressBar_z_pb_txt_gone, mPbTxtGone);
        a.recycle();

        init();
    }
    
    private void init() {
         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setTextSize(mPbTxtSize);
        mPaint.setColor(mPbOnColor);
        mPaint.setStrokeWidth(mPbOnHeight);

        mRectF = new RectF();
        mPath = new Path();


        mFloat8Left = new float[]{//僅左邊兩個圓角--為背景
                mPbOnHeight / 2, mPbOnHeight / 2,//左上圓角x,y
                0, 0,//右上圓角x,y
                0, 0,//右下圓角x,y
                mPbOnHeight / 2, mPbOnHeight / 2//左下圓角x,y
        };

        mFloat8Right = new float[]{
                0, 0,//左上圓角x,y
                mPbBgHeight / 2, mPbBgHeight / 2,//右上圓角x,y
                mPbBgHeight / 2, mPbBgHeight / 2,//右下圓角x,y
                0, 0//左下圓角x,y
        };
    }
    
}
    
    private int sp(int sp) {
        return (int) TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
    }

    private int dp(int dp) {
        return (int) TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }
    
複製程式碼
2.測量:
    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = measureHeight(heightMeasureSpec);
        setMeasuredDimension(width, height);
        mPBWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();//進度條實際寬度
    }
複製程式碼
    /**
     * 測量高度
     *
     * @param heightMeasureSpec
     * @return
     */
    private int measureHeight(int heightMeasureSpec) {
        int result = 0;
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        int size = MeasureSpec.getSize(heightMeasureSpec);

        if (mode == MeasureSpec.EXACTLY) {
            //控制元件尺寸已經確定:如:
            // android:layout_height="40dp""match_parent"
            result = size;
        } else {
            int textHeight = (int) (mPaint.descent() - mPaint.ascent());
            result = getPaddingTop() + getPaddingBottom() + Math.max(
                    Math.max(mPbBgHeight, mPbOnHeight), Math.abs(textHeight));

            if (mode == MeasureSpec.AT_MOST) {//最多不超過
                result = Math.min(result, size);
            }
        }
        return result;
    }
複製程式碼
3.繪製:
    @Override
    protected synchronized void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();
        canvas.translate(getPaddingLeft(), getHeight() / 2);

        parseBeforeDraw();//1.繪製前對數值進行計算以及控制的flag設定

        if (getProgress() == 100) {//進度達到100後文字消失
            whenOver();//2.
        }
        if (mEndX > 0) {//當進度條尾部>0繪製
            drawProgress(canvas);//3.
        }
        if (!mPbTxtGone) {//繪製文字
            mPaint.setColor(mPbTxtColor);
            int y = (int) (-(mPaint.descent() + mPaint.ascent()) / 2);
            canvas.drawText(mText, mProgressX, y, mPaint);
        } else {
            mTextWidth = 0 - mPbTxtOffset;
        }
        if (!mLostRight) {//繪製右側
            drawRight(canvas);/4.
        }

        canvas.restore();
    }
複製程式碼
1).praseBeforeDraw()
/**
 * 對數值進行計算以及控制的flag設定
 */
private void parseBeforeDraw() {
    mLostRight = false;//lostRight控制是否繪製右側
    float radio = getProgress() * 1.f / getMax();//當前百分比率
    mProgressX = radio * mPBWidth;//進度條當前長度
    mEndX = mProgressX - mPbTxtOffset / 2;       //進度條當前長度-文字間隔的左半
    mText = getProgress() + "%";
    if (mProgressX + mTextWidth > mPBWidth) {
        mProgressX = mPBWidth - mTextWidth;
        mLostRight = true;
    }
    //文字寬度
    mTextWidth = (int) mPaint.measureText(mText);
}
複製程式碼
2).whenOver()
/**
 * 當結束是執行:
 */
private void whenOver() {
    mPbTxtGone = true;
    mFloat8Left = new float[]{//只有進度達到100時讓進度圓角是四個
            mPbBgHeight / 2, mPbBgHeight / 2,//左上圓角x,y
            mPbBgHeight / 2, mPbBgHeight / 2,//右上圓角x,y
            mPbBgHeight / 2, mPbBgHeight / 2,//右下圓角x,y
            mPbBgHeight / 2, mPbBgHeight / 2//左下圓角x,y
    };
}
複製程式碼
3).drawProgress()
/**
 * 繪製左側:(進度條)
 *
 * @param canvas
 */
private void drawProgress(Canvas canvas) {
    mPath.reset();
    mRectF.set(0, mPbOnHeight / 2, mEndX, -mPbOnHeight / 2);
    mPath.addRoundRect(mRectF, mFloat8Left, Path.Direction.CW);//順時針畫
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(mPbOnColor);
    canvas.drawPath(mPath, mPaint);//使用path繪製一端是圓頭的線
}
複製程式碼
4).drawRight()
/**
 * 繪製左側:(背景)
 *
 * @param canvas
 */
private void drawRight(Canvas canvas) {
    float start = mProgressX + mPbTxtOffset / 2 + mTextWidth;
    mPaint.setColor(mPbBgColor);
    mPaint.setStrokeWidth(mPbBgHeight);
    mPath.reset();
    mRectF.set(start, mPbBgHeight / 2, mPBWidth, -mPbBgHeight / 2);
    mPath.addRoundRect(mRectF, mFloat8Right, Path.Direction.CW);//順時針畫
    canvas.drawPath(mPath, mPaint);//使用path繪製一端是圓頭的線
}
複製程式碼
xml裡使用:
<top.toly.reslib.my_design.logic.TolyProgressBar
    android:id="@+id/id_toly_pb2"
    android:layout_width="300dp"
    android:layout_height="wrap_content"
    android:paddingTop="10dp"
    android:paddingBottom="10dp"

    android:progress="20"
    app:z_pb_bg_color="@color/red"
    app:z_pb_bg_height="10dp"

    app:z_pb_on_color="#224ee3"
    app:z_pb_on_height="15dp"

    app:z_pb_txt_color="@color/rosybrown"
    app:z_pb_txt_offset="5dp"
    app:z_pb_txt_size="10dp"/>
複製程式碼

三、圓形進度條
1.自定義屬性
<!--圓形進度條-->
<declare-styleable name="TolyRoundProgressBar">
<!--進度條半徑-->
<attr name="z_pb_radius" format="dimension"/>
</declare-styleable>
複製程式碼
2.程式碼實現:
/**
 * 作者:張風捷特烈<br/>
 * 時間:2018/11/9 0009:11:49<br/>
 * 郵箱:[email protected]<br/>
 * 說明:圓形進度條
 */
public class TolyRoundProgressBar extends TolyProgressBar {

    private int mPbRadius = dp(30);//進度條半徑
    private int mMaxPaintWidth;

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

    public TolyRoundProgressBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TolyRoundProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TolyRoundProgressBar);
        mPbRadius = (int) a.getDimension(R.styleable.TolyRoundProgressBar_z_pb_radius, mPbRadius);
        mPbOnHeight = (int) (mPbBgHeight * 1.8f);//讓進度大一點
        a.recycle();
        
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setDither(true);
    }

    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mMaxPaintWidth = Math.max(mPbBgHeight, mPbOnHeight);
        int expect = mPbRadius * 2 + mMaxPaintWidth + getPaddingLeft() + getPaddingRight();
        int width = resolveSize(expect, widthMeasureSpec);
        int height = resolveSize(expect, heightMeasureSpec);
        int realWidth = Math.min(width, height);
        mPaint.setStrokeCap(Paint.Cap.ROUND);

        mPbRadius = (realWidth - getPaddingLeft() - getPaddingRight() - mMaxPaintWidth) / 2;
        setMeasuredDimension(realWidth, realWidth);
    }

    @Override
    protected synchronized void onDraw(Canvas canvas) {

        String txt = getProgress() + "%";
        float txtWidth = mPaint.measureText(txt);
        float txtHeight = (mPaint.descent() + mPaint.ascent()) / 2;
        canvas.save();
        canvas.translate(getPaddingLeft() + mMaxPaintWidth / 2, getPaddingTop() + mMaxPaintWidth / 2);
        drawDot(canvas);
        mPaint.setStyle(Paint.Style.STROKE);
        //背景
        mPaint.setColor(mPbBgColor);
        mPaint.setStrokeWidth(mPbBgHeight);
        canvas.drawCircle(mPbRadius, mPbRadius, mPbRadius, mPaint);
        //進度條
        mPaint.setColor(mPbOnColor);
        mPaint.setStrokeWidth(mPbOnHeight);
        float sweepAngle = getProgress() * 1.0f / getMax() * 360;//完成角度
        canvas.drawArc(
                0, 0, mPbRadius * 2, mPbRadius * 2,
                -90, sweepAngle, false, mPaint);
        //文字
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mPbTxtColor);
        canvas.drawText(txt, mPbRadius - txtWidth / 2, mPbRadius - txtHeight / 2, mPaint);
        canvas.restore();
    }

    /**
     * 繪製一圈點
     *
     * @param canvas
     */
    private void drawDot(Canvas canvas) {
        canvas.save();
        int num = 40;
        canvas.translate(mPbRadius, mPbRadius);
        for (int i = 0; i < num; i++) {
            canvas.save();
            int deg = 360 / num * i;
            canvas.rotate(deg);
            mPaint.setStrokeWidth(dp(3));
            mPaint.setColor(mPbBgColor);
            mPaint.setStrokeCap(Paint.Cap.ROUND);
            if (i * (360 / num) < getProgress() * 1.f / getMax() * 360) {
                mPaint.setColor(mPbOnColor);
            }
            canvas.drawLine(0, mPbRadius * 3 / 4, 0, mPbRadius * 4 / 5, mPaint);
            canvas.restore();
        }
        canvas.restore();
    }
}

複製程式碼

後記:捷文規範

1.本文成長記錄及勘誤表
專案原始碼 日期 備註
V0.1--無 2018-11-9 Android原生繪圖進度條+簡單自定義屬性程式碼生成器
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的CSDN 個人網站
3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援


icon_wx_200.png


附錄:簡單自定義屬性生成器

public class Attrs2Code {
    @Test
    public void main() {
        File file = new File("C:\\Users\\Administrator\\Desktop\\attrs.xml");
        initAttr("z_", file);
    }

    public static void initAttr(String preFix, File file) {
        HashMap<String, String> format = format(preFix, file);
        String className = format.get("className");
        String result = format.get("result");
        StringBuilder sb = new StringBuilder();
        sb.append("TypedArray a = context.obtainStyledAttributes(attrs, R.styleable." + className + ");\r\n");
        format.forEach((s, s2) -> {
            String styleableName = className + "_" + preFix + s;
            if (s.contains("_")) {
                String[] partStrArray = s.split("_");
                s = "";
                for (String part : partStrArray) {
                    String partStr = upAChar(part);
                    s += partStr;
                }
            }
            if (s2.equals("dimension")) {
                // mPbBgHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_bg_height, mPbBgHeight);
                sb.append("m" + s + " = (int) a.getDimension(R.styleable." + styleableName + ", m" + s + ");\r\n");
            }
            if (s2.equals("color")) {
                // mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
                sb.append("m" + s + " =  a.getColor(R.styleable." + styleableName + ", m" + s + ");\r\n");
            }
            if (s2.equals("boolean")) {
                // mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
                sb.append("m" + s + " =  a.getBoolean(R.styleable." + styleableName + ", m" + s + ");\r\n");
            }
            if (s2.equals("string")) {
                // mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
                sb.append("m" + s + " =  a.getString(R.styleable." + styleableName + ");\r\n");
            }
        });
        sb.append("a.recycle();\r\n");
        System.out.println(result);
        System.out.println(sb.toString());
    }

    /**
     * 讀取檔案+解析
     *
     * @param preFix 字首
     * @param file   檔案路徑
     */
    public static HashMap<String, String> format(String preFix, File file) {
        HashMap<String, String> container = new HashMap<>();
        if (!file.exists() && file.isDirectory()) {
            return null;
        }
        FileReader fr = null;
        try {
            fr = new FileReader(file);
            //字元陣列迴圈讀取
            char[] buf = new char[1024];
            int len = 0;
            StringBuilder sb = new StringBuilder();
            while ((len = fr.read(buf)) != -1) {
                sb.append(new String(buf, 0, len));
            }
            String className = sb.toString().split("<declare-styleable name=\"")[1];
            className = className.substring(0, className.indexOf("\">"));
            container.put("className", className);
            String[] split = sb.toString().split("<");
            String part1 = "private";
            String type = "";//型別
            String name = "";
            String result = "";
            String def = "";//預設值

            StringBuilder sb2 = new StringBuilder();
            for (String s : split) {
                if (s.contains(preFix)) {
                    result = s.split(preFix)[1];
                    name = result.substring(0, result.indexOf("\""));
                    type = result.split("format=\"")[1];
                    type = type.substring(0, type.indexOf("\""));
                    container.put(name, type);
                    if (type.contains("color") || type.contains("dimension") || type.contains("integer")) {
                        type = "int";
                        def = "0";
                    }
                    if (result.contains("fraction")) {
                        type = "float";
                        def = "0.f";
                    }
                    if (result.contains("string")) {
                        type = "String";
                        def = "\"toly\"";
                    }
                    if (result.contains("boolean")) {
                        type = "boolean";
                        def = "false";

                    }
                    if (name.contains("_")) {
                        String[] partStrArray = name.split("_");
                        name = "";
                        for (String part : partStrArray) {
                            String partStr = upAChar(part);
                            name += partStr;
                        }
                        sb2.append(part1 + " " + type + " m" + name + "= " + def + ";\r\n");
                    }
                    container.put("result", sb2.toString());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fr != null) {
                    fr.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return container;
    }

    /**
     * 將字串僅首字母大寫
     *
     * @param str 待處理字串
     * @return 將字串僅首字母大寫
     */
    public static String upAChar(String str) {
        String a = str.substring(0, 1);
        String tail = str.substring(1);
        return a.toUpperCase() + tail;
    }
}
複製程式碼