美文网首页
自定义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