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


一、簡單自定義屬性生成器
1.玩安卓的應該都寫過自定義控制元件的自定義屬性:如下

我寫著寫著感覺好枯燥,基本上流程相似,也沒有什麼技術難度,想:這種事不就應該交給機器嗎?
2.通過attrs.xml自動生成相應程式碼
秉承著 能用程式碼解決的問題,絕對不動手。能夠靠智商解決的問題,絕對不靠體力
的大無畏精神:
寫了一個小工具,將程式碼裡的內容自動生成一下:基本上就是字串的切割和拼裝, 工具附在文尾
使用方法與注意點:
1.拷貝到AndroidStudio的test裡,將attrs.xml的檔案路徑設定一下,執行
2.自定義必須符合命名規則,如 z_pb_on_height
,專屬字首如 z_
,單詞間下劃線連線即可
3.它並不是什麼高大上的東西,只是簡單的字串切割拼組,只適用簡單的自定義屬性 [dimension|color|boolean|string]
(不過一般的自定義屬性也夠用了)

在開篇之前:先看一下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(); 複製程式碼
一、水平的進度條

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 | ofollow,noindex">Android原生繪圖進度條+簡單自定義屬性程式碼生成器 |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的CSDN | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援

附錄:簡單自定義屬性生成器
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; } } 複製程式碼