美文网首页Android开发Android开发Android进阶之路
Android自定义控件(高手级)--JOJO同款能力分析图

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

作者: e4e52c116681 | 来源:发表于2018-12-28 14:10 被阅读61次

    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

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/xjislqtx.html