自定义View之从小白的角度撸一个渐变的温度指示器(TmepView)

1.概述

自定义View对需要对整个View体系,事件流程,绘制流程有深刻的理解,能绘制出复杂的图案和动画及交互效果,但万变不离其宗,都是通过确定大小,位置,绘制出相应的形状,由于项目的需要,自己绘制了一个渐变带指示器的温度条,本篇文章尽可能详细的一步一步来实现绘制的效果.(如果对自定义view有理解的,可以直接看最后源码,有详细的注释)

2.自定义View的流程

  • 1.继承View 或ViewGroup
  • 2.测量宽高,对应onMeasure来决定View的大小
  • 3.布局(给控件指定位置),对应onLayout决定View在ViewGroup中的位置
  • 4.绘制,对应onDraw,决定View的形状

注意
继承View
1、测量自己的宽高 onMeasure
2、绘制自己 onDraw
继承ViewGroup
1、测量子View和自己 onMeasure
2、布局,给子View设置位置 onLayout

2.1 onMeasure

用来确定当前view的宽高,并根据宽高等计算一些坐标默认的值

这里面需要了解的是MeasureSpec,封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求 .
MeasureSpec由size和mode组成(使用了二进制去减少对象的分配)

三种mode介绍(View类默认只支持EXACTLY,如果让View支持wrap_content,必须重写onMeasure来指定wrap_content的大小)

  • 1 UNSPECIFIED

父view不没有对子view施加任何约束,子view可以是任意大小(也就是未指定) 没有设置宽高时,如ListView等

  • 2 EXACTLY

精确值模式,父view决定子view的确切大小,子view被限定在给定的边界里,忽略本身想要的大小。View的最终大小就是SpecSize 所指定的值 (当设置width或height为match_parent时,模式为EXACTLY,因为 子view会占据剩余容器的空间,所以它大小是确定的)

  • 3.AT_MOST

最大值模式,父View指定了一个可用的大小值,子view最大可以达到的指定大小,不能超出父容器 (当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少, 这样子view会根据这个上限来设置自己的尺寸)

可以简单的理解为
wrap_parent -> MeasureSpec.AT_MOST
match_parent -> MeasureSpec.EXACTLY
具体值 -> MeasureSpec.EXACTLY

相关代码描述

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		//获取当前的mode
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //获取当前的高度
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        // 根据所传的值大小和模式创建一个合适的值
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(exceptHeight, MeasureSpec.EXACTLY);
        //重新设置宽高
        setMeasuredDimension(wght, wght)

    }

2.2 onLayout(简单介绍)

当继承了ViewGroup,需要重写此方法来确定子View的位置,当ViewGroup的位置被确定后,来遍历所有子View调用其layout方法确定四个顶点,也就确定了在容器中的位置

/** 
 * 当这个view和其子view被分配一个大小和位置时,被layout调用。 
 * @param changed 当前View的大小和位置改变了 
 * @param left 左部位置(相对于父视图) 
 * @param top 顶部位置(相对于父视图) 
 * @param right 右部位置(相对于父视图) 
 * @param bottom 底部位置(相对于父视图) 
 */  
protected void onLayout(boolean changed, int left, int top, int right, int bottom)

2.3 onDraw(简单介绍)

利用Canvas来绘制View的形状,主要用到的有Path Paint

3.TempView分析

效果图(实现的部分为紫色框里面的内容)
tempView

分析:此效果继承了View实现,从图中分析此View有三部分组成,文本度数(drawText实现),三角形的指针(path实现),圆角长方形(drawRoundRect实现).所以需要绘制三种图形,由于温度是时时变化的 ,指针和度数的位置会时时的变化,他们的位置需要最大值,最小值和当前温度来确定,

3.1 确定TempView的大小

        //主要确定view的整体高度,渐变长条的高度+指针的高度+文本的高度+文本与指针的间隙
        if (heightSpecMode == MeasureSpec.AT_MOST || heightSpecMode == MeasureSpec.UNSPECIFIED) {
            mHeight = mDefaultTextSize+mDefaultTempHeight+mDefaultIndicatorHeight+textSpace;
        } else {
            mHeight = heightSpecSize;
        }
        setMeasuredDimension(mWidth, mHeight);

3.2 绘制最底部的圆角矩形

圆角矩形为渐变色,表示温度的状态,渐变色使用Shader来实现

  • 1.Shader介绍
    安卓系统共实现了五种Sharder .分别
    BitmapShader:位图图像渲染。
    LinearGradient:线性渲染。
    SweepGradient:渐变渲染/梯度渲染。
    RadialGradient:环形渲染。
    ComposeShader:组合渲染
    由于此项目使用的是线性渐变的效果,我们具体介绍一下LinearGradient
 /**
     * 构造函数参数含义
     *
     * @param x0          渲染起点的X坐标 
     * @param y0           渲染起点的Y坐标 
     * @param x1           渲染终点的X坐标 
     * @param y1          渲染终点的Y坐标 
     * @param colors      渲染的颜色集合。 
     * @param positions   渲染颜色所占的比例,如果传null,则均匀渲染. 
     * @param tile        拉伸模式,有三种模式(1.CLAMP—— 是拉伸最后一个像素铺满。2.MIRROR——是横向纵向不足处不断翻转镜像平铺。 REPEAT ——类似电脑壁纸,横向纵向不足的重复放置。 )
    */
    public LinearGradient(float x0, float y0, float x1, float y1, @NonNull @ColorInt int colors[],
            @Nullable float positions[], @NonNull TileMode tile) {}
  • 2.绘制圆角矩形
    创建LinearGradient,准备了红黄绿三种原色
 /**
     * 分段颜色
     */
    private static final int[] SECTION_COLORS = {Color.GREEN, Color.YELLOW, Color.RED};
    shader = new LinearGradient(0, mHeight - mDefaultTempHeight, mWidth, mHeight, SECTION_COLORS, null, Shader.TileMode.MIRROR);

创建Paint

 mPaint = new Paint();
 mPaint.setAntiAlias(true);
 mPaint.setShader(shader); 

绘制圆角矩形

 //绘制圆角矩形 mDefaultTempHeight / 2确定圆角的圆心位置
 canvas.drawRoundRect(rectProgressBg, mDefaultTempHeight / 2, mDefaultTempHeight / 2, mPaint);
  • 3.绘制三角形指针,由于位置会变 所以要确定绘制的位置如图

位置介绍

代码如下

  		//当前位置占比
        selction = currentCount / maxCount;
        //绘制指针 指针的位置在当前温度的位置 也就是三角形的顶点落在当前温度的位置

        //定义三角形的左边点的坐标 x= tempView的宽度*当前位置占比-三角形的宽度/2  y=tempView的高度-圆角矩形的高度
        path.moveTo(mWidth * selction - mDefaultIndicatorWidth / 2, mHeight - mDefaultTempHeight);
        //定义三角形的右边点的坐标 = tempView的宽度*当前位置占比+三角形的宽度/2  y=tempView的高度-圆角矩形的高度
        path.lineTo(mWidth * selction + mDefaultIndicatorWidth / 2, mHeight - mDefaultTempHeight);
        //定义三角形的左边点的坐标 x= tempView的宽度*当前位置占比  y=tempView的高度-圆角矩形的高度-三角形的高度
        path.lineTo(mWidth * selction, mHeight - mDefaultTempHeight - mDefaultIndicatorHeight);
        path.close();
        paint.setShader(shader);
        canvas.drawPath(path, paint);
     
  • 4.绘制文本 ,文本的位置也是变化的 位置确定和三角形的位置一样
  		//绘制文本
        String text = (int) currentCount + "°c";
        //确定文本的位置 x=tempViwe的宽度*当前位置占比 y=tempView的高度-圆角矩形的高度-三角形的高度-文本的间隙
        canvas.drawText(text, mWidth * selction, mHeight - mDefaultTempHeight - mDefaultIndicatorHeight - textSpace, textPaint);

详细源码

package padd.qlckh.cn.tempad.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Shader;
import android.support.annotation.Nullable;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.View;

import padd.qlckh.cn.tempad.R;

/**
 * @author Andy
 * @date 2018/9/30 11:06
 * @link {http://blog.csdn.net/andy_l1}
 * Desc:    自定义温度指示条
 */
public class TmepView extends View {

    private Paint mPaint;
    private int mWidth;
    private int mHeight;
    /**
     * 设置温度的最大范围
     */
    private float maxCount = 100f;
    /**
     * 设置当前温度
     */
    private float currentCount = 20f;
    /**
     * 分段颜色
     */
    private static final int[] SECTION_COLORS = {Color.GREEN, Color.YELLOW, Color.RED};
    private Context mContext;

    private float selction;
    private Paint textPaint;
    private Path path;
    private Paint paint;
    /**
     * 指针的宽高
     */
    private int mDefaultIndicatorWidth = dipToPx(10);
    private int mDefaultIndicatorHeight = dipToPx(8);
    /**
     * 圆角矩形的高度
     */
    private int mDefaultTempHeight = dipToPx(20);
    private int mDefaultTextSize = 30;
    private int textSpace = dipToPx(5);
    private RectF rectProgressBg;
    private LinearGradient shader;


    public TmepView(Context context) {
        this(context, null);
    }

    public TmepView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public TmepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        initView(context);

    }

    private void initView(Context context) {
        this.mContext = context;
        //圆角矩形paint
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        //文本paint
        textPaint = new TextPaint();
        textPaint.setAntiAlias(true);
        textPaint.setTextSize(mDefaultTextSize);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setColor(mContext.getResources().getColor(R.color.theme_color));
        //三角形指针paint
        path = new Path();
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //确定圆角矩形的范围,在TmepView的最底部,top位置为总高度-圆角矩形的高度
        rectProgressBg = new RectF(0, mHeight - mDefaultTempHeight, mWidth, mHeight);
        shader = new LinearGradient(0, mHeight - mDefaultTempHeight, mWidth, mHeight, SECTION_COLORS, null, Shader.TileMode.MIRROR);
        mPaint.setShader(shader);
        //绘制圆角矩形 mDefaultTempHeight / 2确定圆角的圆心位置
        canvas.drawRoundRect(rectProgressBg, mDefaultTempHeight / 2, mDefaultTempHeight / 2, mPaint);
        //当前位置占比
        selction = currentCount / maxCount;
        //绘制指针 指针的位置在当前温度的位置 也就是三角形的顶点落在当前温度的位置

        //定义三角形的左边点的坐标 x= tempView的宽度*当前位置占比-三角形的宽度/2  y=tempView的高度-圆角矩形的高度
        path.moveTo(mWidth * selction - mDefaultIndicatorWidth / 2, mHeight - mDefaultTempHeight);
        //定义三角形的右边点的坐标 = tempView的宽度*当前位置占比+三角形的宽度/2  y=tempView的高度-圆角矩形的高度
        path.lineTo(mWidth * selction + mDefaultIndicatorWidth / 2, mHeight - mDefaultTempHeight);
        //定义三角形的左边点的坐标 x= tempView的宽度*当前位置占比  y=tempView的高度-圆角矩形的高度-三角形的高度
        path.lineTo(mWidth * selction, mHeight - mDefaultTempHeight - mDefaultIndicatorHeight);
        path.close();
        paint.setShader(shader);
        canvas.drawPath(path, paint);
        //绘制文本
        String text = (int) currentCount + "°c";
        //确定文本的位置 x=tempViwe的宽度*当前位置占比 y=tempView的高度-圆角矩形的高度-三角形的高度-文本的间隙
        canvas.drawText(text, mWidth * selction, mHeight - mDefaultTempHeight - mDefaultIndicatorHeight - textSpace, textPaint);

    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.EXACTLY || widthSpecMode == MeasureSpec.AT_MOST) {
            mWidth = widthSpecSize;
        } else {
            mWidth = 0;
        }
        //主要确定view的整体高度,渐变长条的高度+指针的高度+文本的高度+文本与指针的间隙
        if (heightSpecMode == MeasureSpec.AT_MOST || heightSpecMode == MeasureSpec.UNSPECIFIED) {
            mHeight = mDefaultTextSize+mDefaultTempHeight+mDefaultIndicatorHeight+textSpace;
        } else {
            mHeight = heightSpecSize;
        }
        setMeasuredDimension(mWidth, mHeight);
    }



    private int dipToPx(int dip) {
        float scale = getContext().getResources().getDisplayMetrics().density;
        return (int) (dip * scale + 0.5f * (dip >= 0 ? 1 : -1));
    }


    /***
     * 设置最大的温度值
     * @param maxCount
     */
    public void setMaxCount(float maxCount) {
        this.maxCount = maxCount;
    }

    /***
     * 设置当前的温度
     * @param currentCount
     */
    public void setCurrentCount(float currentCount) {
        if (currentCount > maxCount) {
            this.currentCount = maxCount - 5;
        } else if (currentCount < 0f) {
            currentCount = 0f + 5;
        } else {
            this.currentCount = currentCount;
        }
        invalidate();
    }

    /**
     * 设置温度指针的大小
     *
     * @param width
     * @param height
     */
    public void setIndicatorSize(int width, int height) {

        this.mDefaultIndicatorWidth = width;
        this.mDefaultIndicatorHeight = height;
    }

    public void setTempHeight(int height) {
        this.mDefaultTempHeight = height;
    }

    public void setTextSize(int textSize) {
        this.mDefaultTextSize = textSize;
    }

    public float getMaxCount() {
        return maxCount;
    }

    public float getCurrentCount() {
        return currentCount;
    }
}

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值