阅读 3363

Android自定义控件(高手级)--JOJO同款能力分析图

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

@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方法就行了

/**
 * 作者:张风捷特烈<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

关注下面的标签,发现更多相似文章
评论