009-Android自定义View(4):自绘式详解(图文)

291 阅读7分钟

1. 简述

建议:熟悉View的绘制、Android事件传递、手势处理再常用自绘式。

方式:直接继承View ,重写绘制流程三步骤

measure() 测量 layout() 布局计算摆放坐标 draw() 绘制

2. 前提基础

2.1了解View关联Activity时的生命周期

可参考 : Android自定义View(1):基础

2.2 了解自定义View的自定义属性,以及获取、使用

可参考 : Android自定义View(1):基础

2.3 重要的生命周期函数

  • 1.onAttachedToWindow()

表示自身View 被父View通过addView添加到窗口中。 在这里,可以通过id获取同一布局文件的其他兄弟View。

  • 2.onMeasure()

作用:测量View的宽高。 Android中,View使用MeasureSpec存放尺寸数据:

    MeasureSpec = mode (前2位值)+ size (后30位值)
    MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size        
mode比较设置值
EXACTLY精准模式,View需要一个精确值 赋予 MeasureSpec的Size50dp 、 match_parent
AT_MOST最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值wrap_content
UNSPECIFIED无限制,原生View自用,一般不用。-

正确设置自定义View尺寸

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        
        //==============================================  
        //========= 设置自定义View的默认大小 ==============
        //==============================================
        //取出值
        int width = getCalSize(100, widthMeasureSpec);
        int height = getCalSize(100, heightMeasureSpec);

        //处理值

        //设置
        setMeasuredDimension(width, height);
    }

    private int getCalSize(int defaultSize, int measureSpec) {
        int calSize = defaultSize;

        int mode = MeasureSpec.getMode(measureSpec);//获取模式
        int size = MeasureSpec.getSize(measureSpec);//获取大小

        switch (mode) {
            case MeasureSpec.UNSPECIFIED: //未指定,就设置为默认大小
                calSize = defaultSize;
                break;

            case MeasureSpec.AT_MOST: //最大取值,将大小取最大值,也可设置自定义View的mMaxHeight / mMaxWidth
                calSize = size;
                break;

            case MeasureSpec.EXACTLY: //指定值
                calSize = size;
                break;
            default:
                break;

        }
        return calSize;
    }

  • 3.onLayout()

此方法用于分配子Viewde 布局位置、大小,【组合式】自定义View按需重写,在【自绘式】自定义View中不需要重写。

  • 4.onDraw()

使用Canvas【画布】和Paint【画笔】绘制所需View样式UI

注意 onDraw()是耗时方法,Google官方不建议在该方法内声明新对象,否则会产生作用重复的对象,拉低运行效率。本质就是开辟过多【用完不要】的内存空间,最后得GC回收,影响绘制、运行。

正确用法

//1.声明重复使用的画笔
private Paint mPaint;
//2.初始化
 public AlwaysMarqueeTextView(Context context,.......) {
        super(context, attrs);
        mPaint = new Paint();
    }
//3.多次使用
    @Override
    protected void onDraw(Canvas canvas) {
        //添加下划线
        super.onDraw(canvas);
        mPaint.setColor(Color.GREEN);
        canvas.drawLine( 0 ,getHeight()/2 ,getWidth() ,getHeight()/2 , mPaint);
    }

2.4 View的自刷新

  • 1.invalidate() 与 requestLayout区别 | 重绘 | 比较 | | ---- | --- | | invalidate() | 轻量刷新,更新文本、颜色、状态等,只调用 三大周期方法之一 onDraw() | | requestLayout | 重量、约等于重新绘制 重新调用onMeasure() onLayout() onDraw() 绘制 |

  • 2.View自刷新动画

    
    int mViewRadius = 0;
    int mViewRadiusReal = 50;
    private void anim(){
        ValueAnimator anim = ValueAnimator.ofInt(0, 100);
        anim.setDuration(200);
        anim.setInterpolator(new DecelerateInterpolator());
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            public void onAnimationUpdate(ValueAnimator animation) {
                int newRadius = (int) animation.getAnimatedValue();
                mViewRadius = mViewRadiusReal * newRadius /100;
                invalidate();
            }
        });
    }

3. 实践

该例未完善,仅供学习,如需使用,需自行扩展

3.1声明自定义属性

/res/values/attrs.xml文件新增

<resources>
    <!--xml快捷属性-->
    <declare-styleable name="DiyView">
        <attr name="chart_x_color" format="color" />
        <attr name="chart_line_color" format="color" />
        <attr name="chart_line_width" format="dimension"/>
        <attr name="chart_line_space" format="dimension"/>
    </declare-styleable>
</resources>

3.2 自定义View类java文件

package com.cupster.base_super_resource;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import java.util.Map;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

public class DiyView extends View {


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

    public DiyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initAttrs(context, attrs);
        mPaint = new Paint();
        mPath = new Path();
    }

    public DiyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttrs(context, attrs);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public DiyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initAttrs(context, attrs);
    }
    
    private Paint mPaint;
    private Path mPath;
    private int chart_x_color = Color.parseColor("#808080");
    private int chart_line_color = Color.parseColor("#6ce4d8");
    private float chart_line_width = 10.0f;
    private float space = 10.0f;
    private float zeroX = 120f;
    private float zeroY = 120f;
    private float axisY = 500f;
    private float axisX = 900f;
    private float scalX = 1.0f;//TODO 扩展:数据缩放呈现
    private float scalY = 1.0f;//TODO 扩展:数据缩放呈现
    private float[] lines = new float[]{0,0,0,0,0,0};
    private String[] xLabs = new String[]{"1月","2月","3月","4月","5月","6月"};

    private void initAttrs(Context context, AttributeSet attrs) {
        //1.获取属性数组
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DiyView);

        //2.获取属性值
        chart_x_color = typedArray.getColor(R.styleable.DiyView_chart_x_color, Color.parseColor("#404040"));
        chart_line_color = typedArray.getColor(R.styleable.DiyView_chart_line_color, Color.parseColor("#0000ee"));
        chart_line_width = typedArray.getDimension(R.styleable.DiyView_chart_line_width, 10.0f);
        space = typedArray.getDimension(R.styleable.DiyView_chart_line_space, 10.0f);
        typedArray.recycle();
        //初始化假数据
//        lines = new float[]{
//                //view的左上角为坐标系的(0,0)
//                //line1: x1:【原点x轴坐标值zeroX + 间距space +柱形宽的一半值chart_line_width/2】,y1:【原点y轴坐标值=y轴高度值axisY+y轴顶点在View坐标系的Y轴值zeroY】
//                //       x2:【原点x轴坐标值zeroX + 间距space +柱形宽的一半值chart_line_width/2】,y2:【原点y轴坐标值 - 要绘制的条形图高度-此处为假数据】
//                zeroX + space + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width / 2, axisY + zeroY - 20 * 1,
//                //line2: x1:【原点x轴坐标值zeroX + 间距space +柱形宽的一半值chart_line_width/2 +[第一个条形图的宽chart_line_width+第二个间距space] 】,y1:【原点y轴坐标值=y轴高度值axisY+y轴顶点在View坐标系的Y轴值zeroY】
//                //       x2 = x1  ,y2 = axisY+zeroY - 要绘制的高度值
//                zeroX + space + chart_line_width + space + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width + space + chart_line_width / 2, axisY + zeroY - 20 * 2,
//                zeroX + space + chart_line_width + space + chart_line_width + space + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width + space + chart_line_width + space + chart_line_width / 2, axisY + zeroY - 20 * 3,
//                zeroX + space + chart_line_width * 3 + space * 3 + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width * 3 + space * 3 + chart_line_width / 2, axisY + zeroY - 20 * 4,
//                zeroX + space + chart_line_width * 4 + space * 4 + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width * 4 + space * 4 + chart_line_width / 2, axisY + zeroY - 20 * 5,
//                zeroX + space + chart_line_width * 5 + space * 5 + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width * 5 + space * 5 + chart_line_width / 2, axisY + zeroY - 20 * 6,
//                zeroX + space + chart_line_width * 6 + space * 6 + chart_line_width / 2, axisY + zeroY, zeroX + space + chart_line_width * 6 + space * 6 + chart_line_width / 2, axisY + zeroY - 20 * 7,
//                zeroX + space * (1 + 7) + chart_line_width * 7 + chart_line_width / 2, axisY + zeroY, zeroX + space * (1 + 7) + chart_line_width * 7 + chart_line_width / 2, axisY + zeroY - 20 * 8,
//                //line9: x1:【原点x轴坐标值zeroX + 间距space * n +柱形宽的一半值chart_line_width *(n-1)+柱形宽的一半值chart_line_width/2 】,y1:【原点y轴坐标值=y轴高度值axisY+y轴顶点在View坐标系的Y轴值zeroY】
//                //       x2 = x1  ,y2 = axisY+zeroY - 要绘制的高度值
//                zeroX + space * (9) + chart_line_width * (9 - 1) + chart_line_width / 2, axisY + zeroY, zeroX + space * (9) + chart_line_width * (9 - 1) + chart_line_width / 2, axisY + zeroY - 20 * 9,
//                zeroX + space * 10 + chart_line_width * (10 - 1) + chart_line_width / 2, axisY + zeroY, zeroX + space * (10) + chart_line_width * (10 - 1) + chart_line_width / 2, axisY + zeroY - 20 * 10,
//                zeroX + space * 11 + chart_line_width * (11 - 1) + chart_line_width / 2, axisY + zeroY, zeroX + space * (11) + chart_line_width * (11 - 1) + chart_line_width / 2, axisY + zeroY - 20 * 11,
//                zeroX + space * 12 + chart_line_width * (12 - 1) + chart_line_width / 2, axisY + zeroY, zeroX + space * (12) + chart_line_width * (12 - 1) + chart_line_width / 2, axisY + zeroY - 20 * 12
//                //lineX:最终固定公式为
//                // //line9: x1:【zeroX + space * n +chart_line_width*(n-0.5f) 】,y1:【axisY+zeroY】
//                //          x2 = x1                                            ,y2 = axisY+zeroY - 要绘制的高度值
//                //          x1                                y1                         x2                                    y2
//                , zeroX + space * 13 + chart_line_width * 12.5f, axisY + zeroY, zeroX + space * 13 + chart_line_width * 12.5f, axisY + zeroY - 111
//        };
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        Log.d("DiyView", "onAttachedToWindow");
    }

//    @Override
//    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//        Log.d("DiyView","onMeasure");
//        //==============================================
//        //========= 设置自定义View的默认大小 ==============
//        //==============================================
//        //取出值
//        int width = getCalSize(600, widthMeasureSpec);
//        int height = getCalSize(300, heightMeasureSpec);
//
//        //TODO 处理值
//
//        //设置
//        setMeasuredDimension(width, height);
//    }
//
//    private int getCalSize(int defaultSize, int measureSpec) {
//        int calSize = defaultSize;
//
//        int mode = MeasureSpec.getMode(measureSpec);//获取模式
//        int size = MeasureSpec.getSize(measureSpec);//获取大小
//
//        switch (mode) {
//            case MeasureSpec.UNSPECIFIED: //未指定,就设置为默认大小
//                calSize = defaultSize;
//                break;
//
//            case MeasureSpec.AT_MOST: //最大取值,将大小取最大值,也可设置自定义View的mMaxHeight / mMaxWidth
//                calSize = size;
//                break;
//
//            case MeasureSpec.EXACTLY: //指定值
//                calSize = size;
//                break;
//            default:
//                break;
//        }
//        return calSize;
//    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.d("DiyView", "onLayout");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d("DiyView", "onDraw");

        mPaint.reset();
        mPath.reset();
        //1.画坐标轴
        mPaint.setColor(Color.parseColor("#404040"));
        mPaint.setStyle(Paint.Style.STROKE);
        mPath.moveTo(zeroX, zeroY);//y轴顶点(120,120)
        mPath.rLineTo(0, axisY);//y轴方向
        mPath.rLineTo(axisX, 0);//x轴方向
        canvas.drawPath(mPath, mPaint);//绘制
        //2.画x轴label
        mPaint.reset();
        mPaint.setColor(chart_x_color);
        mPaint.setTextSize(18);//px
        mPaint.setStyle(Paint.Style.FILL);
        //假数据
//        canvas.drawText("1月", zeroX + space, zeroY + axisY + 28, mPaint);
//        canvas.drawText("2月", zeroX + space * 2 + chart_line_width * (2 - 1), zeroY + axisY + 28, mPaint);
//        canvas.drawText("3月", zeroX + space * 3 + chart_line_width * (3 - 1), zeroY + axisY + 28, mPaint);
//        canvas.drawText("4月", zeroX + space * 4 + chart_line_width * (4 - 1), zeroY + axisY + 28, mPaint);
//        canvas.drawText("5月", zeroX + space * 5 + chart_line_width * (5 - 1), zeroY + axisY + 28, mPaint);
//        canvas.drawText("6月", zeroX + space * 6 + chart_line_width * (6 - 1), zeroY + axisY + 28, mPaint);
//        canvas.drawText("7月", zeroX + space * 7 + chart_line_width * (7 - 1), zeroY + axisY + 28, mPaint);
//        canvas.drawText("8月", zeroX + space * 8 + chart_line_width * (8 - 1), zeroY + axisY + 28, mPaint);
//        canvas.drawText("9月", zeroX + space * 9 + chart_line_width * (9 - 1), zeroY + axisY + 28, mPaint);
//        canvas.drawText("10月", zeroX + space * 10 + chart_line_width * (10 - 1), zeroY + axisY + 28, mPaint);
//        canvas.drawText("11月", zeroX + space * 11 + chart_line_width * (11 - 1), zeroY + axisY + 28, mPaint);
//        canvas.drawText("12月", zeroX + space * 12 + chart_line_width * (12 - 1), zeroY + axisY + 28, mPaint);
        for (int i = 0 ; i < xLabs.length ; i++){
            canvas.drawText(xLabs[i], zeroX + space*(i+1) + i*chart_line_width, zeroY + axisY + 28, mPaint);
        }
        //3.画条形图
        if (lines==null || lines.length<1){
            return;
        }
        mPaint.reset();
        mPaint.setColor(chart_line_color);
        mPaint.setStrokeWidth(chart_line_width);
        canvas.drawLines(lines, mPaint);
    }

    public void setHeightArr(Map<String ,Float> datas) {
        //0.判空
        if (datas == null || datas.size() < 1) {
            return;
        }
        //1.取数据
        float[] arr = new float[datas.size()];
        xLabs = new String[datas.size()];
        int i = 0;
        for (Map.Entry entry : datas.entrySet()){
            xLabs[i] = entry.getKey()==null ? "":(String) entry.getKey();
            arr[i] = entry.getValue() ==null? 0.0f :(float) entry.getValue();
            i++;
        }
        lines = new float[arr.length * 4];
        //2.检查最大值,变更Y轴上限
        for (int n = 0; n < arr.length; n++) {
            if (arr[n] > axisY) {
                axisY = arr[n] +20;
            }
        }
        //3.装载条形图数据
        for (int n = 0; n < arr.length; n++) {
            lines[4*n]     = zeroX + space * (n+1) + chart_line_width * ((n+1) - 1 + 0.5f);
            lines[1 + 4*n] = axisY + zeroY;
            lines[2 + 4*n] = zeroX + space * (n+1) + chart_line_width * ((n+1) - 1 + 0.5f);
            lines[3 + 4*n] = axisY + zeroY - arr[n];
        }
        //通知更新View
        invalidate();
    }
}

3.3 布局文件使用

   <com.cupster.base_super_resource.DiyView
            android:id="@+id/diy_chart"

            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:chart_line_color="#3ca1f9"
            app:chart_line_width="12dp"
            app:chart_line_space="5dp"
            app:chart_x_color="#ff8696"
            />

3.4 Activity中使用

package com.cupster.base_super_resource;

import android.os.Bundle;
import android.util.ArrayMap;
import java.util.Map;
import androidx.appcompat.app.AppCompatActivity;

public class BaseSuperResourceActivity extends AppCompatActivity {

    DiyView diyChart;
    Map<String ,Float> datas = new ArrayMap<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_base_super_resource);

         diyChart = findViewById(R.id.diy_chart);

         datas.put("1月",100f);
         datas.put("2月",200f);
         datas.put("3月",300f);
         datas.put("4月",500f);
         datas.put("5月",50f);
         datas.put("6月",400f);
         datas.put("7月",200f);
         datas.put("8月",30f);
         datas.put("9月",180f);
         datas.put("10月",330f);
         datas.put("11月",20f);
         datas.put("12月",400f);

        diyChart.postDelayed(new Runnable() {
            @Override
            public void run() {
                diyChart.setHeightArr(datas);
            }
        },5000);
    }
}