Android自定义View-撸一个渐变的温度指示器(TmepView)
来源:andy
https://blog.csdn.net/Andy_l1/article/details/82910061
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分析
效果图(实现的部分为紫色框里面的内容)
分析:此效果继承了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;
? ?}
}
—————END—————