android复习之自定义View

一、背景

很多业务都具有特定性,做出来的设计稿也各有千秋,而且很多时候三方类库并没有很好的解决方案,这时候就需要自己自定义一个出来了,下面简单复习下自定义view。

二、自定义view

记得很久以前在郭霖老师博客中看到过,实现自定义view主要有3种方法:自绘控件、组合控件、继承控件。

android的所有控件、所有布局都直接或间接继承view。

三、自绘控件

上上周,公司商户组有个能够展示店铺各项数据的数据中心的新需求,如下图,需求的主视图是一个雷达图view,该view的背景是围绕一个圆心的5条逐步加大的圆,像一块pizza一样哈哈,pizza被平均切成6块,落刀的每个边缘显示各项数据信息,pizza的正中间放着个大块黑椒鸡肉用来显示"综合得分"数据,最后将各项数据两两相连成一个撒有碎香肠蔬菜芝士的蓝色区域。


一开始想用MPAndroidChart库去实现,但是该库的雷达图仅适用于蜘蛛网背景的雷达图,与设计稿的不同,只好自己写一个。如此美味的pizza,等不及了哈哈。

首先来自定义一个5条圆形的背景,代码如下:

public class PizzaHahaView extends View {

    private Paint mPaintPolygon;    //绘制圆形轮廓的画笔

    private static final int NUM_OF_SIDES = 6;  //雷达的边数

    private float mAngel = (float) (2 * Math.PI / NUM_OF_SIDES);    //角度

    private Path mPathPolygon;  //多边形的绘制路径

    private float mStartRadius = 42F;   //多边形的起始半径

    private int mCenterX;   //中心点 x 轴坐标

    private int mCenterY;   //中心点 y 轴坐标

    public PizzaHahaView(Context context) {
        super(context);
    }


    public PizzaHahaView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaintPolygon = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPathPolygon = new Path();
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        mCenterX = w / 2;
        mCenterY = h / 2;
    }

    @Override
    protected void onDraw(Canvas canvas) {

        mPaintPolygon.setStyle(Paint.Style.STROKE);
        mPaintPolygon.setColor(Color.GRAY);
        mPaintPolygon.setStrokeWidth(1F);

        for (int count = 1; count <= 5; count++) {
            float newRadius = mStartRadius * count;

            mPathPolygon.moveTo(mCenterX + newRadius, mCenterY);

            for (int i = 1; i < NUM_OF_SIDES; i++) {
                float x = (float) (mCenterX + Math.cos(mAngel * i) * newRadius);
                float y = (float) (mCenterY + Math.sin(mAngel * i) * newRadius);

                mPathPolygon.lineTo(x, y);

                if ( i == NUM_OF_SIDES)
                    mPaintPolygon.setColor(Color.BLACK);
            }

            mPathPolygon.close();

            canvas.drawCircle(mCenterX, mCenterY, newRadius, mPaintPolygon);

            mPathPolygon.reset();
        }
    }
}
在以上代码中,首先在PizzaHahaView的构造函数中初始化了一些数据,当view中所有的子控件均被映射成xml后触发onFinishInflate(),当view的大小发生变化则调用onSizeChanged(),接下来是最重要的onDraw(),几乎所有逻辑都在这了,首先设置了画笔为空心画,并且设置了颜色和粗细度,接着循环遍历5条线,中间涉及到常用的初衷三角函数计算,从moveTo()的坐标开始移动到lineTo()的坐标,最后用canvas.drawCirclw()绘制出圆,效果图如下:



其余部分的绘制方法基本类似,设置好画笔paint和路径path,用canvas的drawPath()绘制路径、drawText()绘制文本等绘制出来,完整代码和最终效果如下,必要的注释写在代码中了:

public class PizzaHahaView extends View {

    private Paint mPaintPolygon;    //绘制圆形轮廓的画笔

    private Paint mPaintLine;   //绘制直线的画笔

    private Paint mPaintRegion; //绘制分值区的画笔

    private Paint mPaintRegionOutline;  //分值区的轮廓,蓝色加粗

    private Paint mTextPaint;   //文本画笔

    private Paint mDataPaint;   //数据的画笔


    /**
     * 雷达的边数
     */
    private static final int NUM_OF_SIDES = 6;
    /**
     * 角度
     */
    private float mAngel = (float) (2 * Math.PI / NUM_OF_SIDES);

    /**
     * 多边形的绘制路径
     */
    private Path mPathPolygon;
    /**
     * 多边形的起始半径
     */
    private float mStartRadius = 42F;

    /**
     * 直线的绘制路径
     */
    private Path mPathLine;

    /**
     * 分值区的绘制路径
     */
    private Path mPathRegion;

    /**
     * 中心点 x 轴坐标
     */
    private int mCenterX;
    /**
     * 中心点 y 轴坐标
     */
    private int mCenterY;

    private String[] titles = {"进货额", "新用户数", "营业时长", "订单量", "销售总额", "代销金额"};
    private String[] number = {"0", "0", "0", "0", "0", "0"};

    public String[] getNumber() {
        return number;
    }

    public void setNumber(String[] number) {
        this.number = number;
        init();
    }

    private double avgScore = 0.00;    //综合得分

    public double getAvgScore() {
        return avgScore;
    }

    public void setAvgScore(double avgScore) {
        this.avgScore = avgScore;
        init();
    }

    /**
     * 分值区域数值
     */
    private float[] percents = {0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F};

    public float[] getPercents() {
        return percents;
    }

    public void setPercents(float[] percents) {
        this.percents = percents;
        init();
    }

    public DataCenterView(Context context) {
        super(context);
    }


    public DataCenterView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mPaintPolygon = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintPolygon.setStyle(Paint.Style.STROKE);
        mPaintPolygon.setColor(Color.GRAY);
        mPaintPolygon.setStrokeWidth(1F);

        mPaintLine = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintLine.setStyle(Paint.Style.STROKE);
        mPaintLine.setColor(Color.parseColor("#E7E7E7"));
        mPaintLine.setStrokeWidth(1F);

        mPaintRegion = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintRegion.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaintRegion.setColor(Color.parseColor("#5032A2F8"));
        mPaintRegion.setStrokeWidth(1F);

        mPaintRegionOutline = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintRegionOutline.setStyle(Paint.Style.STROKE);
        mPaintRegionOutline.setColor(Color.parseColor("#32A2F8"));
        mPaintRegionOutline.setStrokeWidth(1F);

        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setColor(Color.BLACK);
        mTextPaint.setStrokeWidth(1F);

        mDataPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mDataPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mDataPaint.setColor(Color.BLACK);
        mDataPaint.setStrokeWidth(1F);

        mPathPolygon = new Path();
        mPathLine = new Path();
        mPathRegion = new Path();
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        mCenterX = w / 2;
        mCenterY = h / 2;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        drawCircle(canvas);
        drawLine(canvas);
        drawRegion(canvas);
        drawText(canvas);
    }

    /**
     * 绘制分值区
     *
     * @param canvas
     */
    private void drawRegion(Canvas canvas) {

        float radius = mStartRadius * 5;

        for (int i = 0; i < NUM_OF_SIDES; i++) {
            float x = (float) (Math.cos(mAngel * i + Math.PI / 6) * radius * percents[i]);
            float y = (float) (Math.sin(mAngel * i + Math.PI / 6) * radius * percents[i]);

            if (i == 0) {
                mPathRegion.moveTo(mCenterX + x, mCenterY + y);
            } else {
                mPathRegion.lineTo(mCenterX + x, mCenterY + y);
            }
        }

        mPathRegion.close();

        canvas.drawPath(mPathRegion, mPaintRegion);
    }

    /**
     * 绘制直线
     *
     * @param canvas
     */
    private void drawLine(Canvas canvas) {
        float radius = mStartRadius * 5;
        float angel = (float) (2 * Math.PI / 6);

        for (int i = 0; i < 6; i++) {
            mPathLine.moveTo(mCenterX, mCenterY);

            float x = (float) (mCenterX + Math.cos(angel * i + Math.PI / 6) * radius);
            float y = (float) (mCenterY + Math.sin(angel * i + Math.PI / 6) * radius);

            mPathLine.lineTo(x, y);

            canvas.drawPath(mPathLine, mPaintLine);
        }
    }

    /**
     * 绘制圆形
     *
     * @param canvas
     */
    private void drawCircle(Canvas canvas) {
        for (int count = 1; count <= 5; count++) {
            float newRadius = mStartRadius * count;

            mPathPolygon.moveTo(mCenterX + newRadius, mCenterY);

            for (int i = 1; i < NUM_OF_SIDES; i++) {
                float x = (float) (mCenterX + Math.cos(mAngel * i) * newRadius);
                float y = (float) (mCenterY + Math.sin(mAngel * i) * newRadius);

                mPathPolygon.lineTo(x, y);

                if ( i == NUM_OF_SIDES) {
                    mPaintPolygon.setColor(Color.BLACK);
                }
            }

            mPathPolygon.close();

            canvas.drawCircle(mCenterX, mCenterY, newRadius, mPaintPolygon);

            mPathPolygon.reset();
        }
    }

    /**
     * 绘制数据
     *
     * @param canvas
     */
    private void drawText(Canvas canvas) {
        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
        Log.d("Aige", "ascent:" + fontMetrics.ascent);
        Log.d("Aige", "top:" + fontMetrics.top);
        Log.d("Aige", "leading:" + fontMetrics.leading);
        Log.d("Aige", "descent:" + fontMetrics.descent);
        Log.d("Aige", "bottom:" + fontMetrics.bottom);

        float fontHeight = fontMetrics.descent - fontMetrics.ascent;
        mTextPaint.setTextSize(28);
        mTextPaint.setColor(Color.parseColor("#32A2F8"));

        float avgScoreTitleDis = mTextPaint.measureText("综合得分");//文本长度
        float avgScoreDis = mTextPaint.measureText(Double.toString(avgScore));
        canvas.drawText("综合得分", mCenterX - avgScoreTitleDis / 2, mCenterY, mTextPaint);
        canvas.drawText(avgScore+"", mCenterX - avgScoreDis / 2, mCenterY + 30, mTextPaint);

        for (int i = 0; i < 6; i++) {
            float x,y;

            mTextPaint.setColor(Color.parseColor("#000000"));
            if (mAngel * i + Math.PI == Math.PI / 2) {
                x = (float) (mCenterX + (mStartRadius + fontHeight * 10) * Math.cos(mAngel * i + Math.PI / 6));
                y = (float) (mCenterY + (mStartRadius + fontHeight * 10) * Math.sin(mAngel * i + Math.PI / 6));
            } else if (mAngel * i + Math.PI == Math.PI / 2 * 3) {
                x = (float) (mCenterX + (mStartRadius + fontHeight * 10) * Math.cos(mAngel * i + Math.PI / 6));
                y = (float) (mCenterY + (mStartRadius + fontHeight * 10) * Math.sin(mAngel * i + Math.PI / 6));
            } else {
                x = (float) (mCenterX + (mStartRadius + fontHeight * 10) * Math.cos(mAngel * i + Math.PI / 6));
                y = (float) (mCenterY + (mStartRadius + fontHeight * 10) * Math.sin(mAngel * i + Math.PI / 6));
            }

            if (mAngel * i >= 0 && mAngel * i <= Math.PI / 2) {//第4象限
                float titlesDis = mTextPaint.measureText(titles[i]);//文本长度
                float numberDis = mTextPaint.measureText(number[i]);//数字长度
                if ("进货额".equals(titles[i])) {
                    canvas.drawText(titles[i], x + mStartRadius * 5 / 2 - titlesDis / 2, mCenterY + mStartRadius * 5 / 2, mTextPaint);                           //进货额
                    canvas.drawText(number[i], x + mStartRadius * 5 / 2, mCenterY + mStartRadius * 5 / 2 + titlesDis / 3, mTextPaint);// x - numberDis + 70, y + 100
                } else {
                    canvas.drawText(titles[i], x - titlesDis / 2, y + titlesDis / 4 * 2, mTextPaint);                            //新用户数
                }
            } else if (mAngel * i >= 3 * Math.PI / 2 && mAngel * i <= Math.PI * 2) {//第3象限
                float titlesDis = mTextPaint.measureText(titles[i]);//文本长度
                float numberDis = mTextPaint.measureText(number[i]);//数字长度
                canvas.drawText(titles[i], x + mStartRadius * 5 / 2 - titlesDis / 2, mCenterY - mStartRadius * 5 / 2 - titlesDis / 4, mTextPaint);                              //代销金额
                canvas.drawText(number[i], x + mStartRadius * 5 / 2, mCenterY - mStartRadius * 5 / 2, mTextPaint);
            } else if (mAngel * i > Math.PI / 2 && mAngel * i <= Math.PI) {//第2象限
                float titlesDis = mTextPaint.measureText(titles[i]);//文本长度
                float numberDis = mTextPaint.measureText(number[i]);//数字长度
                canvas.drawText(titles[i], x - mStartRadius * 5 / 2 - titlesDis / 2, mCenterY + mStartRadius * 5 / 2, mTextPaint);                                     //营业时长
                canvas.drawText(number[i], x - mStartRadius * 5 / 2, mCenterY + mStartRadius * 5 / 2 + titlesDis / 4, mTextPaint);
            } else if (mAngel * i >= Math.PI && mAngel * i < 3 * Math.PI / 2) {//第1象限
                float titlesDis = mTextPaint.measureText(titles[i]);//文本长度
                float numberDis = mTextPaint.measureText(number[i]);//数字长度
                if ("订单量".equals(titles[i])) {
                    canvas.drawText(titles[i], x - mStartRadius * 5 / 2 - titlesDis / 2, mCenterY - mStartRadius * 5 / 2 - titlesDis / 4, mTextPaint);                            //订单量
                    canvas.drawText(number[i], x - mStartRadius * 5 / 2, mCenterY - mStartRadius * 5 / 2, mTextPaint);
                } else {
                    canvas.drawText(titles[i], x - titlesDis / 2, y - titlesDis / 4 * 2, mTextPaint);                            //销售总额
                    canvas.drawText(number[i], x - numberDis / 2, y - titlesDis / 4, mTextPaint);
                }
            }

            Log.i("radar", x + " " + y + " " + titles[i]);
        }
    }
}


 效果图如下,自用魅族MX4,传说中的: 


一盘可口美味色泽上佳的pizza出炉了哈哈,自绘控件大致上就这个思路。

四、组合控件

组合控件,将多个原生控件组合在一起使用的控件。

首先,将原生控件组合在一个XML里面;

接着,自定义一个class,让他继承FrameLayout,在它的构造方法里用LayoutInflater找到该XML文件并将其实例化;

eg. LayoutInflater.from(this).inflate(R.layout.***, this);

最后,即可初始化那些添加进去的原生组件,然后进行相关业务处理。

五、继承控件

即是继承原有的控件,来增加一些新的功能。

参考郭林老师的PowerImageView,它是继承ImageView的。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值