Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说Android自定义控件(高手级)——JOJO同款能力分析图[亲测有效],希望能够帮助你!!!。
JOJO是我看过脑洞最大的动漫(没有之一),每季必追
最近打算做简历,想自定义个能力分析图,首先就想到这里:
废话不多说,走起,噢啦,噢啦,噢啦,噢啦...
1.绘制外圈
为了减少变量值,让尺寸具有很好的联动性(等比扩缩),小黑条的长宽将取决于最大半径mRadius
则:小黑条长:mRadius*0.08 小黑条宽:mRadius*0.05 所以r2=mRadius-mRadius*0.08
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的使用
看不懂的可转到canvas和path,如果看了这两篇还问绘制有什么技巧的,可转到这里,会告诉你技巧是什么
/** * 绘制内圈圆 * @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.文字的绘制
文字的方向同向,感觉这样看着好些,不管怎么转都可以
//定义测试数据 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循环而已,实际上都是通过一点点分析,测试与发现规律算出来的
有什么技巧?草稿纸拿出来画图,计算+分析...,只靠眼睛是不行的
//我不喜欢弄脏画笔,再准备一支吧 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来确定的,所以缩放时也会等比
@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)); }
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方法就行了
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里写的那个方法
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);
ok,搞定,你以为完了?No,精彩继续
搞了个6个,不得了了吗?可见其中还有一个死的东西,那就是数据条数
这个就麻烦了,如果刚才是0->1的创造,填充数据是1->2的积累,那接下来就是2->n的生命
好吧,我又打了半张草稿纸,终于算完了!View一共不到200行代码,感觉很优雅了
有兴趣的自己研究(画画图,打打草稿),没兴趣的直接拿去用,
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(); } }
好了,这下真的结束了 喜欢请点个关注吧~