Android自定義控制元件(高手級)–JOJO同款能力分析圖

張風捷特烈發表於2018-12-28

JOJO是我看過腦洞最大的動漫(沒有之一),每季必追
最近打算做簡歷,想自定義個能力分析圖,首先就想到這裡:
廢話不多說,走起,噢啦,噢啦,噢啦,噢啦…

Android自定義控制元件(高手級)–JOJO同款能力分析圖

一、靜態圖的繪製

1.繪製外圈

為了減少變數值,讓尺寸具有很好的聯動性(等比擴縮),小黑條的長寬將取決於最大半徑mRadius
則:小黑條長:mRadius*0.08 小黑條寬:mRadius*0.05 所以r2=mRadius-mRadius*0.08

外圈繪製.png
public class AbilityView extends View { 
private float mRadius = dp(100);
//外圓半徑 private float mLineWidth = dp(1);
//線寬 private Paint mLinePaint;
//線畫筆 private Paint mFillPaint;
//填充畫筆 public AbilityView(Context context) {
this(context, null);

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

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

} private void init() {
mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLinePaint.setStrokeWidth(mLineWidth);
mLinePaint.setStyle(Paint.Style.STROKE);
mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mFillPaint.setStrokeWidth(0.05f * mRadius);

} @Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(mRadius, mRadius);
//移動座標系 drawOutCircle(canvas);

} /** * 繪製外圈 * @param canvas 畫布 */ private void drawOutCircle(Canvas canvas) {
canvas.save();
canvas.drawCircle(0, 0, mRadius, mLinePaint);
float r2 = mRadius - 0.08f * mRadius;
//下圓半徑 canvas.drawCircle(0, 0, r2, mLinePaint);
for (int i = 0;
i <
22;
i++) {//迴圈畫出小黑條 canvas.save();
canvas.rotate(360 / 22f * i);
canvas.drawLine(0, -mRadius, 0, -r2, mFillPaint);
canvas.restore();

} canvas.restore();

} protected float dp(float dp) {
return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());

}
}複製程式碼

2.內圈繪製

同樣尺寸和最外圓看齊,這裡繪製有一丟丟複雜,你需要了解canvas和path的使用
看不懂的可轉到canvaspath,如果看了這兩篇還問繪製有什麼技巧的,可轉到這裡,會告訴你技巧是什麼

內圈繪製.png
/** * 繪製內圈圓 * @param canvas 畫布 */private void drawInnerCircle(Canvas canvas) { 
canvas.save();
float innerRadius = 0.6f * mRadius;
canvas.drawCircle(0, 0, innerRadius, mLinePaint);
canvas.save();
for (int i = 0;
i <
6;
i++) {//遍歷6條線 canvas.save();
canvas.rotate(60 * i);
//每次旋轉60° mPath.moveTo(0, -innerRadius);
mPath.rLineTo(0, innerRadius);
//線的路徑 for (int j = 1;
j <
6;
j++) {
mPath.moveTo(-mRadius * 0.02f, innerRadius / 6 * j);
mPath.rLineTo(mRadius * 0.02f * 2, 0);

}//加5條小線 canvas.drawPath(mPath, mLinePaint);
//繪製線 canvas.restore();

} canvas.restore();

}複製程式碼

3.文字的繪製

文字的方向同向,感覺這樣看著好些,不管怎麼轉都可以

文字.png
//定義測試資料mAbilityInfo = new String[]{"破壞力", "速度", "射程距離", "持久力", "精密度", "成長性"
};
mAbilityMark = new int[]{100, 100, 60, 100, 100, 100
};
mMarkMapper = new String[]{"A", "B", "C", "D", "E"
};
複製程式碼
/** * 繪製文字 * * @param canvas 畫布 */private void drawInfoText(Canvas canvas) { 
float r2 = mRadius - 0.08f * mRadius;
//下圓半徑 for (int i = 0;
i <
6;
i++) {
canvas.save();
canvas.rotate(60 * i + 180);
mTextPaint.setTextSize(mRadius * 0.1f);
canvas.drawText(mAbilityInfo[i], 0, r2 - 0.06f * mRadius, mTextPaint);
mTextPaint.setTextSize(mRadius * 0.15f);
canvas.drawText(abilityMark2Str(mAbilityMark[i]), 0, r2 - 0.18f * mRadius, mTextPaint);
canvas.restore();

} mTextPaint.setTextSize(mRadius * 0.07f);
for (int k = 0;
k <
5;
k++) {
canvas.drawText(mMarkMapper[k], mRadius * 0.06f, mInnerRadius / 6 * (k + 1) + mRadius * 0.02f - mInnerRadius, mTextPaint);

}
}/** * 將分數對映成字串 * @param mark 分數100~0 * @return */private String abilityMark2Str(int mark) {
if (mark <
= 100 &
&
mark >
80) {
return mMarkMapper[0];

} else if (mark <
= 80 &
&
mark >
60) {
return mMarkMapper[1];

} else if (mark <
= 60 &
&
mark >
40) {
return mMarkMapper[2];

} else if (mark <
= 40 &
&
mark >
20) {
return mMarkMapper[3];

} else if (mark <
= 20 &
&
mark >
0) {
return mMarkMapper[4];

} return "∞";

}複製程式碼

4.最後一步:畫內容

本以為就連個點的事,沒想到…打了我半頁草稿紙(手動表情–可怕)
展現在你眼前的就是個for迴圈而已,實際上都是通過一點點分析,測試與發現規律算出來的
有什麼技巧?草稿紙拿出來畫圖,計算+分析…,只靠眼睛是不行的

繪製結果.png
//我不喜歡弄髒畫筆,再準備一支吧mAbilityPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mAbilityPaint.setColor(0x8897C5FE);
mAbilityPath = new Path();
複製程式碼
/** * 繪製能力面 * @param canvas */private void drawAbility(Canvas canvas) { 
float step = mInnerRadius / 6;
//每小段的長度 mAbilityPath.moveTo(0, -mAbilityMark[0] / 20.f * step);
//起點 for (int i = 1;
i <
6;
i++) {
float mark = mAbilityMark[i] / 20.f;
mAbilityPath.lineTo( (float) (mark * step * Math.cos(Math.PI/180*(-30+60*(i-1)))), (float) (mark * step * Math.sin(Math.PI/180*(-30+60*(i-1)))));

} mAbilityPath.close();
canvas.drawPath(mAbilityPath, mAbilityPaint);

}複製程式碼

這樣就完成了,你以為這樣就結束了?這才剛開始呢!


二、資料的提取與封裝

剛才用的是測試資料,都寫死在View中,這肯定是不行的
現在將資料封裝一下,再暴露介面方法,開啟View和外界的通路


1.View的尺寸限定

使用寬度作為直徑,無視高度,尺寸為圓形區域
如下所示:可看出所有的尺寸都是和按照mRadius來確定的,所以縮放時也會等比

尺寸.png
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mRadius = MeasureSpec.getSize(widthMeasureSpec) / 2;
mInnerRadius = 0.6f * mRadius;
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(widthMeasureSpec));

}複製程式碼

2.資料處理

為了方便檢視資料間關係,使用Map將能力與數值裝一下

private HashMap<
String, Integer>
mData;
//核心資料//資料的剛才的對接mData = new HashMap<
>
();
mData.put("破壞力", 100);
mData.put("速度", 100);
mData.put("射程距離", 60);
mData.put("持久力", 100);
mData.put("精密度", 100);
mData.put("成長性", 100);
mAbilityInfo = mData.keySet().toArray(new String[mData.size()]);
mAbilityMark = mData.values().toArray(new Integer[mData.size()]);
複製程式碼

3.資料與字元的對映關係:DataMapper

也就是100~80之間的代表字串可以自定義,比如”1″ 、 “I” 、”☆”隨你便
這也是我剛悟到的一種解耦方式,應該算是策略設計模式吧(只能分五個等級)
如果自定義分類情況重寫abilityMark2Str方法就行了

/** * 作者:張風捷特烈<
br/>
* 時間:2018/12/28 0028:12:21<
br/>
* 郵箱:1981462002@qq.com<
br/>
* 說明:資料對映抽象類 */public class DataMapper {
protected String[] mapper;
public DataMapper(String[] mapper) {
if (mapper.length != 5) {
throw new IllegalArgumentException("the length of mapper must be 5");

} this.mapper = mapper;

} public String[] getMapper() {
return mapper;

} /** * 數值與字串的對映關係 * * @param mark 數值 * @return 字串 */ public String abilityMark2Str(int mark) {
if (mark <
= 100 &
&
mark >
80) {
return mapper[0];

} else if (mark <
= 80 &
&
mark >
60) {
return mapper[1];

} else if (mark <
= 60 &
&
mark >
40) {
return mapper[2];

} else if (mark <
= 40 &
&
mark >
20) {
return mapper[3];

} else if (mark <
= 20 &
&
mark >
0) {
return mapper[4];

} return "∞";

}
}複製程式碼

給一個預設的對映類:WordMapper
也就是剛才在View裡寫的那個方法

/** * 作者:張風捷特烈<
br/>
* 時間:2018/12/28 0028:12:24<
br/>
* 郵箱:1981462002@qq.com<
br/>
* 說明:單詞對映 */public class WordMapper extends DataMapper {
public WordMapper() {
super(new String[]{"A", "B", "C", "D", "E"
});

}複製程式碼

View裡如何修改呢?

//定義成員變數private DataMapper mDataMapper;
//資料與字串對映規則//init裡mDataMapper = new WordMapper();
//初始化DataMapper--預設WordMapper//繪製文字的時候由mDataMapper提供資料private void drawInfoText(Canvas canvas) {
float r2 = mRadius - 0.08f * mRadius;
//下圓半徑 for (int i = 0;
i <
6;
i++) {
canvas.save();
canvas.rotate(60 * i + 180);
mTextPaint.setTextSize(mRadius * 0.1f);
canvas.drawText(mAbilityInfo[i], 0, r2 - 0.06f * mRadius, mTextPaint);
mTextPaint.setTextSize(mRadius * 0.15f);
canvas.drawText( mDataMapper.abilityMark2Str(mAbilityMark[i]), 0, r2 - 0.18f * mRadius, mTextPaint);
canvas.restore();

} mTextPaint.setTextSize(mRadius * 0.07f);
for (int k = 0;
k <
5;
k++) {
canvas.drawText(mDataMapper.getMapper()[k], mRadius * 0.06f, mInnerRadius / 6 * (k + 1) + mRadius * 0.02f - mInnerRadius, mTextPaint);

}
}//暴漏get、set方法---提供外界設定public DataMapper getDataMapper() {
return mDataMapper;

}public void setDataMapper(DataMapper dataMapper) {
mDataMapper = dataMapper;

}//暴漏設定資料方法給外部public HashMap<
String, Integer>
getData() {
return mData;

}public void setData(HashMap<
String, Integer>
data) {
mData = data;
mAbilityInfo = mData.keySet().toArray(new String[mData.size()]);
mAbilityMark = mData.values().toArray(new Integer[mData.size()]);
invalidate();

}複製程式碼

4.使用方法:

使用DataMapper將字串抽離出來,並且還可以根據數值來主要以返回字串

AbilityView abilityView = findViewById(R.id.id_ability_view);
mData = new HashMap<
>
();
mData.put("Java", 100);
mData.put("Kotlin", 70);
mData.put("JavaScript", 100);
mData.put("Python", 60);
mData.put("Dart", 50);
mData.put("C++", 60);
abilityView.setDataMapper(new DataMapper(new String[]{"神", "高", "普", "新", "入"
}));
abilityView.setData(mData);
複製程式碼
自定義.png

ok,搞定,你以為完了?No,精彩繼續


三、n條屬性任你比

搞了個6個,不得了了嗎?可見其中還有一個死的東西,那就是資料條數
這個就麻煩了,如果剛才是0->
1的創造,填充資料是1->
2的積累,那接下來就是2->
n的生命
好吧,我又打了半張草稿紙,終於算完了!View一共不到200行程式碼,感覺很優雅了
有興趣的自己研究(畫畫圖,打打草稿),沒興趣的直接拿去用,

n條屬性.png
/** * 作者:張風捷特烈<
br/>
* 時間:2018/12/28 0028:7:40<
br/>
* 郵箱:1981462002@qq.com<
br/>
* 說明:能力對比圖 */public class AbilityView extends View {
private static final String TAG = "AbilityView";
private float mRadius = dp(100);
//外圓半徑 private float mLineWidth = dp(1);
//線寬 private Paint mLinePaint;
//線畫筆 private Paint mFillPaint;
//填充畫筆 private Path mPath;
private HashMap<
String, Integer>
mData;
//核心資料 private Paint mTextPaint;
String[] mAbilityInfo;
Integer[] mAbilityMark;
private float mInnerRadius;
private Path mAbilityPath;
private Paint mAbilityPaint;
private DataMapper mDataMapper;
//資料與字串對映規則 public AbilityView(Context context) {
this(context, null);

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

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

} private void init() {
mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLinePaint.setStrokeWidth(mLineWidth);
mLinePaint.setStyle(Paint.Style.STROKE);
mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mFillPaint.setStrokeWidth(0.05f * mRadius);
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextSize(mRadius * 0.1f);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mAbilityPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mAbilityPaint.setColor(0x8897C5FE);
mAbilityPath = new Path();
mPath = new Path();
mData = new HashMap<
>
();
mDataMapper = new WordMapper();
//初始化DataMapper--預設WordMapper
} @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mRadius = MeasureSpec.getSize(widthMeasureSpec) / 2;
mInnerRadius = 0.6f * mRadius;
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(widthMeasureSpec));

} @Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mAbilityInfo == null) {
return;

} canvas.translate(mRadius, mRadius);
//移動座標系 drawOutCircle(canvas);
drawInnerCircle(canvas);
drawInfoText(canvas);
drawAbility(canvas);

} /** * 繪製能力面 * * @param canvas */ private void drawAbility(Canvas canvas) {
float step = mInnerRadius / (mDataMapper.getMapper().length + 1);
//每小段的長度 mAbilityPath.moveTo(0, -mAbilityMark[0] / 20.f * step);
//起點 for (int i = 1;
i <
mData.size();
i++) {
float mark = mAbilityMark[i] / 20.f;
mAbilityPath.lineTo( (float) (mark * step * Math.cos(Math.PI / 180 * (360.f / mData.size() * i - 90))), (float) (mark * step * Math.sin(Math.PI / 180 * (360.f / mData.size() * i - 90))));

} mAbilityPath.close();
canvas.drawPath(mAbilityPath, mAbilityPaint);

} /** * 繪製文字 * * @param canvas 畫布 */ private void drawInfoText(Canvas canvas) {
float r2 = mRadius - 0.08f * mRadius;
//下圓半徑 for (int i = 0;
i <
mData.size();
i++) {
canvas.save();
canvas.rotate(360.f / mData.size() * i + 180);
mTextPaint.setTextSize(mRadius * 0.1f);
canvas.drawText(mAbilityInfo[i], 0, r2 - 0.06f * mRadius, mTextPaint);
mTextPaint.setTextSize(mRadius * 0.15f);
canvas.drawText( mDataMapper.abilityMark2Str(mAbilityMark[i]), 0, r2 - 0.18f * mRadius, mTextPaint);
canvas.restore();

} mTextPaint.setTextSize(mRadius * 0.07f);
for (int k = 0;
k <
mDataMapper.getMapper().length;
k++) {
canvas.drawText(mDataMapper.getMapper()[k], mRadius * 0.06f, mInnerRadius / (mDataMapper.getMapper().length + 1) * (k + 1) + mRadius * 0.02f - mInnerRadius, mTextPaint);

}
} /** * 繪製內圈圓 * * @param canvas 畫布 */ private void drawInnerCircle(Canvas canvas) {
canvas.save();
canvas.drawCircle(0, 0, mInnerRadius, mLinePaint);
canvas.save();
for (int i = 0;
i <
mData.size();
i++) {//遍歷6條線 canvas.save();
canvas.rotate(360.f / mData.size() * i);
//每次旋轉60° mPath.moveTo(0, -mInnerRadius);
mPath.rLineTo(0, mInnerRadius);
//線的路徑 for (int j = 1;
j <
= mDataMapper.getMapper().length;
j++) {
mPath.moveTo(-mRadius * 0.02f, -mInnerRadius / (mDataMapper.getMapper().length + 1) * j);
mPath.rLineTo(mRadius * 0.02f * 2, 0);

}//加5條小線 canvas.drawPath(mPath, mLinePaint);
//繪製線 canvas.restore();

} canvas.restore();

} /** * 繪製外圈 * * @param canvas 畫布 */ private void drawOutCircle(Canvas canvas) {
canvas.save();
canvas.drawCircle(0, 0, mRadius, mLinePaint);
float r2 = mRadius - 0.08f * mRadius;
//下圓半徑 canvas.drawCircle(0, 0, r2, mLinePaint);
for (int i = 0;
i <
22;
i++) {//迴圈畫出小黑條 canvas.save();
canvas.rotate(360 / 22f * i);
canvas.drawLine(0, -mRadius, 0, -r2, mFillPaint);
canvas.restore();

} canvas.restore();

} protected float dp(float dp) {
return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());

} /////////////////////////////--------------------- public float getRadius() {
return mRadius;

} public void setRadius(float radius) {
mRadius = radius;

} public DataMapper getDataMapper() {
return mDataMapper;

} public void setDataMapper(DataMapper dataMapper) {
mDataMapper = dataMapper;

} public HashMap<
String, Integer>
getData() {
return mData;

} public void setData(HashMap<
String, Integer>
data) {
mData = data;
mAbilityInfo = mData.keySet().toArray(new String[mData.size()]);
mAbilityMark = mData.values().toArray(new Integer[mData.size()]);
invalidate();

}
}複製程式碼

好了,這下真的結束了


後記:捷文規範

1.本文成長記錄及勘誤表
專案原始碼 日期 備註
V0.1–github 2018-12-28 Android自定義控制元件(高手級)–JOJO同款能力分析圖
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的掘金 個人網站
3.宣告

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


icon_wx_200.png

來源:https://juejin.im/post/5c25bf0751882565a157711f#comment

相關文章