android 自定义View (二) 继承View

android 自定义View 继承View

上篇关于自定义View的介绍
接下来将会针对自定义View三种情况一一实现。

源码地址(https://github.com/wangchongwei/customView)

最后实现效果如下图:

custom_view_final.jpg

继承View

创建一个class MyView 继承View

目标是写一个折线图

现在res/values下面新建一个attrs.xml文件,来申明我们需要的属性。


<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="LineView">
        <attr name="axieColor" format="color"/>
        <attr name="pointRadius" format="dimension" />

    </declare-styleable>

</resources>


先新建一个clas 继承View, 并初始化几个构造函数


public class MyView extends View {
    // 代码生成时,才会调用该构造函数
     public MyView(Context context) {
        super(context);
        this.context = context;
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    // xml配置时,会调用这个生命周期
    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        initData(attrs);
    }

    // 做初始化配置 获取各种颜色、尺寸,还有自定义的一些属性。
    private void initData(AttributeSet attrs) {
         Log.d(TAG, "initData: ");
        // 获取xml中配置的数据
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyView);
        paintColor = array.getColor(R.styleable.MyView_axieColor, context.getResources().getColor(R.color.black));

        // 画笔初始化
        paint = new Paint();
        paint.setColor(context.getResources().getColor(R.color.black));
        paint.setTextSize(40);
        paint.setStrokeWidth(10); // 线条粗细
    }
}

在onMeasure函数中对尺寸做约束



    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(TAG, "onMeasure: ");
        int height = measuretDimension(defaultHeight, heightMeasureSpec);
        int width = measuretDimension(0, widthMeasureSpec);
        top = 0;
        left = 0;
        bottom = top + height;
        right = left + width;
        setMeasuredDimension(width, height);

    }

    /**
     * 测量实际尺寸
     * @param defaultSize: 默认尺寸
     * @param measureSpec: 测量规格
     * @return
     */
    public int measureDimension(int defaultSize, int measureSpec) {
        int resultSize = defaultSize;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            // 没有做限制,取默认值
            case MeasureSpec.UNSPECIFIED:
                resultSize = defaultSize;
                break;

                // WRAP
            case MeasureSpec.AT_MOST:
                // 要取默认值和测量值中较小值
                // 当默认值为0时,取最大值, 即宽充满屏幕
                resultSize = defaultSize == 0 ?specSize : Math.min(defaultSize, specSize);
                break;

                // 具体值 或 MATCH
            case MeasureSpec.EXACTLY:
                resultSize = specSize;
                break;

                default:
                    break;
        }
        return resultSize;
    }


先绘制两个轴线

注意canvas.draw..()方法,中的位置参数都是相对定位尺寸,都是相对于该视图左上角的坐标定位,而onLayout中的四个位置参数都是相对于屏幕左上角。
这两个里的坐标不要弄混。

    /** 绘制两条轴线 */
    private void drawXY(Canvas canvas) {
        Log.d(TAG, "drawXY: ");
        // 绘制x轴
        canvas.drawLine(left + 20, bottom, right, bottom, paint);
        // 绘制y轴
        canvas.drawLine(left + 20, top, left + 20, bottom, paint);
    }

在MainActivity中配置该视图

<com.justin.customview.MyView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        app:layout_constraintTop_toBottomOf="@+id/text_hello"
        android:layout_marginTop="20dp"
        android:padding="10dp"
        app:axieColor="@color/black"
        android:id="@+id/myView"
        />

我们直接运行,效果如下:
<img src='../../../images/view.jpg' style="zoom:20%" />

x,y轴就画好了。但很明显我们设置的padding没起作用,因为padding是需要我们自己处理的。
新增一个方法初始化这些尺寸数据

    /**
     * 尺寸数据初始化
     */
    private void initSize () {
        // 获取padding尺寸
        paddingLeft = getPaddingLeft();
        paddingTop = getPaddingTop();
        paddingRight = getPaddingRight();
        paddingBottom = getPaddingBottom();
        StringBuilder sb = new StringBuilder();
        sb.append("paddingLeft =").append(paddingLeft)
                .append("paddingTop =").append(paddingTop)
                .append("paddingRight =").append(paddingRight)
                .append("paddingBottom =").append(paddingBottom);
        Log.d(TAG, "initSize: ".concat(sb.toString()));
        top = paddingTop;
        left = paddingLeft;
        bottom = height - top - paddingBottom;
        right = width - left;
        setMeasuredDimension(width, height);
    }

这样我们就对padding做了处理,接下来接着绘制我们需要的图形。
我们之前绘制了两个轴,xy,但我们平常绘制的轴都会有箭头,我们再在x轴右侧、y轴上侧绘制两个箭头


    private void drawArrow(Canvas canvas) {
        Path path = new Path();
        // 先绘制x轴三角
        //先移动到三角形一个点
        path.moveTo(right-20, bottom + 20);
        path.lineTo(right-20, bottom - 20); // 画线
        path.lineTo(right, bottom); // 画线
        path.close(); // 图形闭合
        canvas.drawPath(path, paint);

        // 绘制y轴三角
        path.moveTo(left - 20, top + 20);
        path.lineTo(left + 20, top + 20);
        path.lineTo(left, top);
        path.close();
        canvas.drawPath(path, paint);
    }

这里主要用到了drawPath函数。其实canvas对象还有很多其他的绘制图形的方法。

设置数据并绘制点

我们已经完成了绘制两条轴线,现在要开始绘制数据了。
首先我们要确认标准线,x轴的标准线肯定就是xValue的值,
但y轴的标准线是不定的,我们要先找出最大值,确定几条标准线,确定每条标准线的值。
我们先假设我们的值在0-100以内,取5条标准线,每条间距20.

先设置两个数据

 // 数据
    private float[] yValue;
    private String[] xValue;
    private int lineNum = 5;

    /** 设置数据并刷新 */
    public void setData(float[]yValue, String[]xValue) {
        this.yValue = yValue;
        this.xValue = xValue;
        postInvalidate();
    }

    /** 设置标准线数目 */
    public void setData(int lineNum) {
        this.lineNum = lineNum;
        postInvalidate();
    }

然后我们开始绘制标准线、各个点

    /** 绘制各个点 */
    private void drawPoint(Canvas canvas) {
        if(xValue == null || yValue == null) return;
        // 先绘制5条y轴标准线位置 取高度的90%作为图线的最高。
        float maxHeight = (float)((bottom - top) * 0.9);
        float itemHeight = maxHeight / 5;
        for(int i = 1; i <=5; i ++) {
            canvas.drawLine(left, bottom - itemHeight * i, left + 15, bottom - itemHeight * i, paint);
        }
        // 再绘制x轴的数据, x轴线的标准值就是x轴的值,数目也是xValue的值
        float maxWidth = (float)((right - left) * 0.9);
        float itemWidth = ((float) (maxWidth * 1.0)) / xValue.length;
        for (int i = 1; i <= xValue.length; i ++) {
            float x = left + itemWidth * i;
                    // 绘制轴线
            canvas.drawLine(x, bottom, x, bottom - 15, paint);
            // 绘制点
            float y = bottom - maxHeight * yValue[i-1] / yMax;
            canvas.drawCircle(x, y, 5, paint);
        }
    }

而在MainActivity.kt中,我们可以这样使用

// kotlin语法
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var xValue = arrayOf("一", "二", "san")
        var yValue = floatArrayOf(70f, 80f, 90f);
        myView.setData(yValue, xValue);
    }
}

此时的实现效果:

chat.jpg

现在我们完成了图形的大致绘制,但在x、y轴却没有一些文字说明,接下来我们就要加上这些

绘制x、y轴标准线值,将各个点连接起来。

/** 绘制各个点 */
    private void drawPoint(Canvas canvas) {
        if(xValue == null || yValue == null) return;
        // 先绘制5条y轴标准线位置 取高度的90%作为图线的最高。
        float maxHeight = (float)((bottom - top) * 0.9);
        float itemHeight = maxHeight / 5;
        int itemValue = yMax / 5;
        for(int i = 1; i <=5; i ++) {
            canvas.drawLine(left, bottom - itemHeight * i, left + 15, bottom - itemHeight * i, paint);
            // 绘制y轴标准值
            canvas.drawText(itemValue * i + "", left - 90, bottom - itemHeight * i, paint);
        }

        // 再绘制x轴的数据, x轴线的标准值就是x轴的值,数目也是xValue的值
        float maxWidth = (float)((right - left) * 0.9);
        float itemWidth = ((float) (maxWidth * 1.0)) / xValue.length;
        for (int i = 1; i <= xValue.length; i ++) {
            float x = left + itemWidth * i;
                    // 绘制轴线
            canvas.drawLine(x, bottom, x, bottom - 15, paint);
            // 绘制点
            float y = bottom - maxHeight * yValue[i-1] / yMax;
            canvas.drawCircle(x, y, 10, paint);

            // 绘制点与点之间的连线
            if(lastX > 0f) {
                canvas.drawLine(lastX, lastY, x, y, paint);
            }

            // 绘制x轴标准值
            canvas.drawText(xValue[i-1], x, bottom + 50, paint);

            lastX = x;
            lastY = y;
        }
    }

最后实现效果如下图:


custom_view_final.jpg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352