美文网首页
自定义View详解

自定义View详解

作者: kjy_112233 | 来源:发表于2018-09-17 17:33 被阅读0次

    一、自定义View

    (1)View的绘制流程

    • onMeasure:测量View的宽高
    • onLayout:计算当前View及子View位置
    • onDraw:绘制视图

    (2)坐标系

    • Android坐标系:以屏幕的左上角为原点,原点向右为X轴方向,原点向下为Y轴方向。
    • View坐标系:
      • 自身坐标系
        getTop:获取View自身顶边到父布局顶边的距离
        getLeft:获取View自身左边到其父布局左边的距离
        getRight:获取View自身右边到其父布局左边的距离
        getBottom:获取View自身底部到其父布局的顶边的距离
      • MotionEvent获取触摸点坐标
        getX:获取点击事件距离View控件左边的距离
        getY:获取点击事件距离View控件顶边的距离
        getRawX:获取点击事件距离整个屏幕左边的距离
        getRawY:获取点击事件距离整个屏幕顶边的距离
    • 获取view自身的宽高
      width = getRight() - getLeft();
      height = getBottom() - getTop();

    (3)自定义属性

    • 在values/attrs.xml自定义属性名称和取值类型
    • 类型:
      string:字符串
      color:颜色值
      demension:尺寸值
      integer:整型值
      enum:枚举值
      reference:参考某一资源ID
      float:浮点值
      boolean:布尔值
      fraction:百分数
      flag:位或运算
        <declare-styleable name="study_view">
            <attr name="text" format="string" />
            <attr name="textColor" format="color" />
            <attr name="textSize" format="dimension" />
        </declare-styleable>
    
    • 在自定义View的构造方法中通过TypedArray获取自定义属性的值,在onDraw中绘制
    public class StudyView extends View {
    
        private Rect rect;
        private String text;
        private Paint paint;
    
        //在Java代码中new时调用
        public StudyView(Context context) {
            super(context);
        }
    
        //在xml布局文件中使用时自动调用
        public StudyView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            initAttr(context, attrs);
        }
    
        private void initAttr(Context context, AttributeSet attrs) {
            //获取自定义属性的值
            @SuppressLint("CustomViewStyleable")
            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.study_view);
            text = typedArray.getString(R.styleable.study_view_text);
            int textColor = typedArray.getColor(R.styleable.study_view_textColor, Color.BLACK);
            float textSize = typedArray.getDimension(R.styleable.study_view_textSize, 100);
            typedArray.recycle();//回收typedArray
            //设置绘制文字画笔
            paint = new Paint();
            paint.setColor(textColor);
            paint.setTextSize(textSize);
            //获得绘制文本的宽和高
            rect = new Rect();
            assert text != null;
            paint.getTextBounds(text, 0, text.length(), rect);
        }
    
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            //绘制文字
            canvas.drawText(text, getWidth() / 2 - rect.width() / 2, getHeight() / 2 + rect.height() / 2, paint);
        }
    }
    

    (4)通过onMeasure设置View的实际宽高

        //MeasureSpec是View的静态内部类。用来描述父控件对子控件尺寸的约束
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            //保存测量宽度和测量高度
            setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
        }
    
    
        /**
         * @return 控件的宽度
         */
        private int measureWidth(int widthMeasureSpec) {
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);//获取宽的模式
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);//获取宽的尺寸
            if (widthMode == MeasureSpec.EXACTLY) {//match_parent、具体值
                return widthSize;//控件的宽度
            } else {//wrap_content
                float textWidth = rect.width();//文本的宽度
                return (int) (getPaddingLeft() + textWidth + getPaddingRight());//控件的宽度
            }
        }
    
        /**
         * @return 控件的高度
         */
        private int measureHeight(int heightMeasureSpec) {
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);//获取高的模式
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);//获取高的尺寸
            if (heightMode == MeasureSpec.EXACTLY) {//match_parent、具体值
                return heightSize;
            } else {//wrap_content
                float textHeight = rect.height();//文本的高度
                return (int) (getPaddingTop() + textHeight + getPaddingBottom());
            }
        }
    

    (5)实现View自动换行

    • Paint方法
      paint.setAntiAlias(true):抗锯齿效果
      paint.setColor(paintColor):设置画笔颜色
      paint.setStyle(Paint.Style.FILL):设置画笔的风格FILL,FILL_OR_STROKE,STROKE
      paint.setTextSize(10):设置画笔字体的大小
      paint.setStrokeWidth(20):设置画笔的宽度
      paint.setShadowLayer(5, 10, 10, paintColor):设置画笔的阴影效果
      paint.setStrikeThruText(true):设置字体是否加删除线
      paint.setUnderlineText(true):设置字体是否加下划线
      paint.setFakeBoldText(true):设置字体是否加粗
      paint.setTextSkewX(10):设置字体倾斜度
      paint.setTextScaleX(10):设置字体横向缩放
      paint.setLetterSpacing(10):设置字行间距
      paint.setShader(linearGradient);//设置画笔的渐变色
    • 绘制渐变色
      LinearGradient:线型渐变色
      RadialGradient:圆形渐变色
      BitmapShader:位图型渐变色
      ComposeShader:组合渐变色
    • Canvas方法
      canvas.drawLine:绘制直线
      canvas.drawRect:绘制矩形
      canvas.drawCircle:绘制圆形
      canvas.drawArc:绘制饼状图
      canvas.drawText:绘制字符
    public class StudyView extends View {
    
        private String text;
        private int textSize;
        private int textColor;
    
        private Paint paint;
        private Rect textBranch;
        private int drawTextHeight;
        private List<String> textList = new ArrayList<>();
    
        //在Java代码中new时调用
        public StudyView(Context context) {
            this(context, null);
        }
    
        //在xml布局文件中使用时自动调用
        public StudyView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            initAttr(context, attrs);
            init();
        }
    
        private void initAttr(Context context, AttributeSet attrs) {
            //获取自定义属性的值
            @SuppressLint("CustomViewStyleable")
            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.study_view);
            text = typedArray.getString(R.styleable.study_view_text);
            textColor = typedArray.getColor(R.styleable.study_view_textColor, Color.BLACK);
            textSize = (int) typedArray.getDimension(R.styleable.study_view_textSize, 100);
            typedArray.recycle();//回收typedArray
        }
    
        private void init() {
            //初始化画笔
            paint = new Paint();
            paint.setColor(textColor);
            paint.setTextSize(textSize);
            paint.setAntiAlias(true);
            paint.setStrokeWidth(1);
    
            //获得绘制文本的宽高
            textBranch = new Rect();
            paint.getTextBounds(text, 0, text.length(), textBranch);
    
            //计算各线在位置
            Paint.FontMetrics fontMetrics = paint.getFontMetrics();
            //每行文字的绘制高度,建议低于基线的距离 - 建议距基线以上的距离
            drawTextHeight = (int) (fontMetrics.descent - fontMetrics.ascent);
        }
    
    
        //MeasureSpec是View的静态内部类。用来描述父控件对子控件尺寸的约束
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            //设置文本自动分行
            setBranch(widthMeasureSpec);
            setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
        }
    
        private void setBranch(int widthMeasureSpec) {
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);//获取宽的尺寸
            if (textList.size() == 0) {
                //显示文本最大宽度
                float specMaxWidth = widthSize - getPaddingLeft() - getPaddingRight();
                //实际显示的行数
                int lineNum;
                if (textBranch.width() > specMaxWidth) {
                    //获取带小数的行数
                    float lineNumF = textBranch.width() * 1.0f / specMaxWidth;
                    //如果有小数就进1
                    if ((lineNumF + "").contains(".")) {
                        lineNum = (int) (lineNumF + 0.5);
                    } else {
                        lineNum = (int) lineNumF;
                    }
                    //每行展示文字的长度
                    int lineLength = (int) (text.length() / lineNumF);
                    for (int i = 0; i < lineNum; i++) {
                        String lineStr;
                        //判断是否可以一行显示
                        if (text.length() < lineLength) {
                            lineStr = text;
                        } else {
                            lineStr = text.substring(0, lineLength);
                        }
                        textList.add(lineStr);
                        //重新赋值text
                        if (!TextUtils.isEmpty(text)) {
                            if (text.length() > lineLength) {
                                text = text.substring(lineLength);
                            }
                        } else {
                            break;
                        }
                    }
                } else {
                    textList.add(text);
                }
            }
        }
    
        /**
         * @return 控件的宽度
         */
        private int measureWidth(int widthMeasureSpec) {
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);//获取宽的模式
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);//获取宽的尺寸
            if (widthMode == MeasureSpec.EXACTLY) {//match_parent、具体值
                return widthSize;//控件的宽度
            } else {//wrap_content
                //获取文本的宽度
                float textWidth;
                if (textList.size() > 1) {
                    textWidth = widthSize;
                } else {
                    textWidth = textBranch.width();
                }
                return (int) (getPaddingLeft() + textWidth + getPaddingRight());//控件的宽度
            }
        }
    
        /**
         * @return 控件的高度
         */
        private int measureHeight(int heightMeasureSpec) {
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);//获取高的模式
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);//获取高的尺寸
            if (heightMode == MeasureSpec.EXACTLY) {//match_parent、具体值
                return heightSize;
            } else {//wrap_content
                //获取文本的高度
                float textHeight = drawTextHeight * textList.size();
                return (int) (getPaddingTop() + textHeight + getPaddingBottom());
            }
        }
    
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            for (int i = 0; i < textList.size(); i++) {
                paint.getTextBounds(textList.get(i), 0, textList.get(i).length(), textBranch);
                canvas.drawText(textList.get(i), getPaddingLeft(), (getPaddingTop() + drawTextHeight * (i + 1)), paint);
            }
    
        }
    }
    

    (6)实现自定义饼状图

    • 实现饼状图并显示百分比
    public class SectorView extends View {
        private int[] colors = {Color.RED, Color.BLACK, Color.CYAN, Color.MAGENTA, Color.GREEN, Color.BLUE};
    
        private Paint paint;
        private RectF rectF;
    
        private float radius;
        private float startC;
    
        public SectorView(Context context) {
            this(context, null);
        }
    
        public SectorView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        private void init() {
            paint = new Paint();
            paint.setStyle(Paint.Style.FILL);
            paint.setTextSize(30);
            rectF = new RectF();
        }
    
        private float centerX;
        private float centerY;
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            //获取设置总数
    
            //获取圆心坐标
            centerX = getPivotX();
            centerY = getPivotY();
    
            //获取圆的半径
            if (radius == 0)
                radius = (float) (Math.min(getWidth(), getHeight()) / 2) / 2;
            //设置矩形区域
            rectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
    
            //绘制每个扇形
            for (int color : colors) {
                //当前扇形的颜色
                paint.setColor(color);
                float sweep = 360.0f * (1.0f / colors.length);
                //绘制圆弧
                canvas.drawArc(rectF, startC, sweep, true, paint);
                //计算文字的中心点位置
                float textAngle = startC + sweep / 2;
                float x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180));
                float y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180));
                paint.setColor(Color.WHITE);
                //获取绘制的文字
                String text = (int) (sweep / 360.0f * 100) + "%";
                //根据角度设置字体显示位置
                if (textAngle >= 0 && textAngle < 90)
                    canvas.drawText(text, x, y + paint.getTextSize(), paint);
                else if (textAngle >= 90 && textAngle < 180)
                    canvas.drawText(text, x - paint.getTextSize(), y + paint.getTextSize(), paint);
                else if (textAngle >= 180 && textAngle <= 270)
                    canvas.drawText(text, x - paint.getTextSize(), y, paint);
                else if (textAngle > 270 && textAngle < 360)
                    canvas.drawText(text, x, y, paint);
                //下一个扇形的开始角度
                startC += sweep;
            }
        }
    }
    
    • 实现饼状图点击效果
    public class SectorView extends View {
        private float[] endAngles = new float[6];
        private float[] startAngles = new float[6];
        private int[] colors = {Color.RED, Color.BLACK, Color.CYAN, Color.MAGENTA, Color.GREEN, Color.BLUE};
    
        private Paint paint;
        private RectF rectF;
    
        private int select = -1;
        private float radius;
    
        public SectorView(Context context) {
            this(context, null);
        }
    
        public SectorView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        private void init() {
            paint = new Paint();
            paint.setStyle(Paint.Style.FILL);
            paint.setTextSize(30);
            rectF = new RectF();
        }
    
        private float centerX;
        private float centerY;
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            //饼状图的起始角度
            float startC = 0;
            //获取圆心坐标
            centerX = getPivotX();
            centerY = getPivotY();
            //获取圆的半径
            if (radius == 0)
                radius = (float) (Math.min(getWidth(), getHeight()) / 2) / 2;
            //绘制每个扇形
            for (int i = 0; i < colors.length; i++) {
                //设置矩形区域
                rectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
                //当前扇形的颜色
                paint.setColor(colors[i]);
                //当前扇形的角度
                float sweep = 360.0f * (1.0f / colors.length);
                //保存起始角度
                startAngles[i] = startC;
                //保存结束角度
                endAngles[i] = startC + sweep;
                //计算文字的中心点位置
                float textAngle = startC + sweep / 2;
                float x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180));
                float y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180));
                //获取绘制的文字
                String text = (int) (sweep / 360.0f * 100) + "%";
                //j计算文字的起始点位
                if (textAngle >= 0 && textAngle < 90) {
                    if (select == i) {
                        int top = (int) (Math.sin(Math.toRadians(textAngle)) * 25);
                        int left = (int) (Math.cos(Math.toRadians(textAngle)) * 25);
                        rectF.left += left;
                        rectF.right += left;
                        rectF.top += top;
                        rectF.bottom += top;
                        x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180)) + left;
                        y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180)) + top;
                    } else {
                        x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180));
                        y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180));
                    }
                    y += paint.getTextSize();
                } else if (textAngle >= 90 && textAngle < 180) {
                    if (select == i) {
                        int top = (int) (Math.sin(Math.toRadians(180 - textAngle)) * 25);
                        int left = (int) (Math.cos(Math.toRadians(180 - textAngle)) * 25);
                        rectF.left -= left;
                        rectF.right -= left;
                        rectF.top += top;
                        rectF.bottom += top;
                        x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180)) - left;
                        y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180)) + top;
                    } else {
                        x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180));
                        y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180));
                    }
                    x -= paint.getTextSize();
                    y += paint.getTextSize();
                } else if (textAngle >= 180 && textAngle <= 270) {
                    if (select == i) {
                        int top = (int) (Math.sin(Math.toRadians(270 - textAngle)) * 25);
                        int left = (int) (Math.cos(Math.toRadians(270 - textAngle)) * 25);
                        rectF.left -= left;
                        rectF.right -= left;
                        rectF.top -= top;
                        rectF.bottom -= top;
                        //获取扇形弧度的中心点坐标
                        x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180)) - left;
                        y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180)) - top;
                    } else {
                        x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180));
                        y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180));
                    }
                    x -= paint.getTextSize();
                } else if (textAngle > 270 && textAngle < 360) {
                    if (select == i) {
                        int top = (int) (Math.sin(Math.toRadians(360 - textAngle)) * 25);
                        int left = (int) (Math.cos(Math.toRadians(360 - textAngle)) * 25);
                        rectF.left += left;
                        rectF.right += left;
                        rectF.top -= top;
                        rectF.bottom -= top;
                        //获取扇形弧度的中心点坐标
                        x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180)) + left;
                        y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180)) - top;
                    } else {
                        x = (float) (centerX + radius / 2 * Math.cos(textAngle * Math.PI / 180));
                        y = (float) (centerY + radius / 2 * Math.sin(textAngle * Math.PI / 180));
                    }
                }
                //绘制圆弧
                canvas.drawArc(rectF, startC, sweep, true, paint);
                paint.setColor(Color.WHITE);
                //绘制百分比文字
                canvas.drawText(text, x, y, paint);
                //下一个扇形的开始角度
                startC += sweep;
            }
        }
    
        @SuppressLint("ClickableViewAccessibility")
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                //获取点击位置的坐标
                float x = event.getX();
                float y = event.getY();
                //获取点击角度
                float angle = 0;
                //判断当前点击位置在第几象限
                if (x >= centerX && y >= centerY)
                    angle = (float) (Math.atan((y - centerY) / (x - centerX)) * 180 / Math.PI);
                else if (x <= centerX && y >= centerY)
                    angle = (float) (Math.atan((centerX - x) / (y - centerY)) * 180 / Math.PI + 90);
                else if (x <= centerX && y <= centerY)
                    angle = (float) (Math.atan((centerY - y) / (centerX - x)) * 180 / Math.PI + 180);
                else if (x >= centerX && y <= centerY)
                    angle = (float) (Math.atan((x - centerX) / (centerY - y)) * 180 / Math.PI + 270);
                for (int i = 0; i < startAngles.length; i++) {
                    if (startAngles[i] <= angle && endAngles[i] >= angle) {
                        select = i;
                        invalidate();
                        return true;
                    }
                }
                return true;
            }
            return super.onTouchEvent(event);
        }
    }
    

    相关文章

      网友评论

          本文标题:自定义View详解

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