Android自定义View实现绘制水波浪温度刻度表

目录
  • 前言
  • 1、onMeasure重新测量
  • 2、绘制刻度
  • 3. 设置刻度动画
  • 4. 绘制中心的圆与文字
  • 5. 水波纹动画
  • 后记

前言

之前的绘制圆环,我们了解了如何绘制想要的形状和进度的一些特点,那么此篇文章我们更近一步,绘制一个稍微复杂一点的刻度与波浪。来一起复习一下Android的绘制。

相对应的这种类型的自定义View网上并不少见,但是如果我们要做一些个性化的效果,最好还是自己绘制一份,也相对的比较容易控制效果,如果想实现上面的效果,我们一般来说分为以下几个步骤:

  • 重写测量方法,确保它是一个正方形
  • 绘制刻度
  • 绘制中心的圆与文字
  • 水波纹的动画
  • 设置进度与动画,一起动起来

思路我们已经有了,下面一步一步的来实现吧。

话不多说,Let's go

1、onMeasure重新测量

之前的圆环进度,我们并没有重写 onMeasure 方法,而是在布局中指定为固定的宽高,其实兼容性和健壮性并不好,万一写错了就会变形导致显示异常。

最好的办法是不管xml中设置为什么值,这里都能保证为一个正方形,要么是取宽度为准,让高度和宽度一致,要么就是宽度高度取最大值,让他们保持一致。由于我们是竖屏的应用,所以我就取宽度为准,让高度和宽度一致。

前面我们只是讲了 onDraw 并没有讲到 onMeasure , 这里简单的说一下。

我们为什么要重写 onMeasure ?

  • 为了自定义View尺寸的规则,如果你的自定义View的尺寸是根据父控件行为一致,就不需要重写onMeasure()方法。
  • 如果不重写onMeasure方法,那么自定义view的尺寸默认就和父控件一样大小,当然也可以在布局文件里面写死宽高,而重写该方法可以根据自己的需求设置自定义view大小。

一般来说我们重写的 onMeasure 长这样:

 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec,heightMeasureSpec)
}

widthMeasureSpec ,heightMeasureSpec 并不是真正的宽高,看名字就知道,它只是宽高测量的规格,我们通过 MeasureSpec 的一些静态方法,通过它们拿到一些信息。

static int getMode(int measureSpec):根据提供的测量值(规格)提取模式(上述三个模式之一)

测量的 Model 一共有三种

  • UNSPECIFIED(未指定),父元素部队自元素施加任何束缚,子元素可以得到任意想要的大小;
  • EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;
  • AT_MOST(至多),子元素至多达到指定大小的值。

我们常用的就是 EXACTLY 和 AT_MOST ,EXACTLY 对应的就是我们设置的match_parent或者300这样的精确值,而 AT_MOST 对应的就是wrap_content。

static int getSize(int measureSpec):根据提供的测量值(规格)提取大小值(这个大小也就是我们通常所说的大小)

通过此方法就能获取控件的宽度和高度值。

static int makeMeasureSpec(int size,int mode):根据提供的大小值和模式创建一个测量值(规格)

通过具体的宽高和model,创建对应的宽高测量规格,用于确定View的测量

onMeasure 的最终设置确定宽度的测量有两种方式,

  • setMeasuredDimension(width, height)
  • super.onMeasure(widthMeasureSpec,heightMeasureSpec)

实战:

比如我们的自定义温度刻度View,我们整个View要确保一个正方形,那么就拿到宽度,设置同样的高度,然后确定测量,流程如下:

    //重新测量
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //获取控件的宽度,高度
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int newWidthMeasureSpec = widthMeasureSpec;

        //如果没有指定宽度,默认给200宽度
        if (widthMode != MeasureSpec.EXACTLY) {
            newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(200, MeasureSpec.EXACTLY);
        }

        //获取到最新的宽度
        int width = MeasureSpec.getSize(newWidthMeasureSpec) - getPaddingLeft() - getPaddingRight();

        //我们要的是矩形,不管高度是多高,让它总是和宽度一致
        int height = width;

        centerPosition.x = width / 2;
        centerPosition.y = height / 2;
        radius = width / 2f;
        mRectF.set(0f, 0f, width, height);

        //最后设置生效-下面两种方式都可以
        // setMeasuredDimension(width, height);

        super.onMeasure(
                MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
        );

    }

这里有详细的注释,大致实现的效果如下:

2、绘制刻度

由于原本的 Canvas 内部没有绘制刻度这么一说,所以我们只能用绘制线条的方式,就是 drawLine 方法。

为了了解到坐标系和方便实现,我们可以先绘制一个圆环,定位我们刻度需要绘制的位置。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //画圆环
        canvas.drawArc(
                mRectF.left + 2f, mRectF.top + 2f, mRectF.right - 2f, mRectF.bottom - 2f,
                mStartAngle, mSweepAngle, false, mDegreeCirPaint
        );
    }

这个圆环是之前讲到过了,就不过多赘述了,实现效果如下:

由于开始绘制的地方在左上角位置,我们要移动到圆的中心点开始绘制,也就是红色点移动到蓝色点。

我们就需要x轴和y轴做一下偏移 canvas.translate(radius, radius);

默认的 drawLine 都是横向绘制,我们想要实现效果图的效果,就需要旋转一下画笔,也就是用到 canvas.rotate(rotateAngle);

那么旋转多少了,如果说最底部是90度,我们的起始角度是120度开始的,我们就起始旋转30度。后面每一次旋转就按照百分比来,比如我们100度的温度,那么就相当于要画100个刻度,我们就用需要绘制的角度除以100,就是每一个刻度的角度。

具体的刻度实现代码:

    private float mStartAngle = 120f;  // 圆弧的起始角度
    private float mSweepAngle = 300f; //绘制的起始角度和滑过角度(绘制300度)
    private float mTargetAngle = 300f;  //刻度的角度(根据此计算需要绘制有色的进度)

    private void drawDegreeLine(Canvas canvas) {
        //先保存
        canvas.save();

        // 移动画布
        canvas.translate(radius, radius);
        // 旋转坐标系,需要确定旋转角度
        canvas.rotate(30);

        // 每次旋转的角度
        float rotateAngle = mSweepAngle / 100;
        // 累计叠加的角度
        float currentAngle = 0;
        for (int i = 0; i <= 100; i++) {

            if (currentAngle <= mTargetAngle && mTargetAngle != 0) {
                // 计算累计划过的刻度百分比
                float percent = currentAngle / mSweepAngle;

                //动态的设置颜色
                mDegreelinePaint.setColor(evaluateColor(percent, Color.GREEN, Color.RED));

                canvas.drawLine(0, radius, 0, radius - 20, mDegreelinePaint);

                // 画过的角度进行叠加
                currentAngle += rotateAngle;

            } else {
                mDegreelinePaint.setColor(Color.WHITE);
                canvas.drawLine(0, radius, 0, radius - 20, mDegreelinePaint);
            }

            //画完一个刻度就要旋转移动位置
            canvas.rotate(rotateAngle);
        }

        //再恢复
        canvas.restore();

    }

加上圆环与刻度的效果图:

3. 设置刻度动画

前面的一篇我们使用的是属性动画不停的绘制从而实现进度的效果,那么这一次我们使用定时任务的方式也是可以实现动画的效果。

由于我们之前的 drawDegreeLine 方法内部控制绘制进度的变量就是 targetAngle 来控制的,所以我们通过入口方法设置温度的时候通过定时任务的方式来控制。

代码如下:

    //动画状态
    private boolean isAnimRunning;
    // 手动实现越来越慢的效果
    private int[] slow = {10, 10, 10, 8, 8, 8, 6, 6, 6, 6, 4, 4, 4, 4, 2};
    // 动画的下标
    private int goIndex = 0;

    //设置温度,入口的开始
    public void setupTemperature(float temperature) {
        mCurPercent = 0f;
        totalAngle = (temperature / 100) * mSweepAngle;
        targetAngle = 0f;
        mCurPercent = 0f;
        mCurTemperature = "0.0";
        mWaveUpValue = 0;

        startTimerAnim();
    }

      //使用定时任务做动画
    private void startTimerAnim() {

        if (isAnimRunning) {
            return;
        }

        mAnimTimer = new Timer();
        mAnimTimer.schedule(new TimerTask() {

            @Override
            public void run() {

                isAnimRunning = true;
                targetAngle += slow[goIndex];
                goIndex++;
                if (goIndex == slow.length) {
                    goIndex--;
                }
                if (targetAngle >= totalAngle) {
                    targetAngle = totalAngle;
                    isAnimRunning = false;
                    mAnimTimer.cancel();
                }

                // 计算的温度
                mCurPercent = targetAngle / mSweepAngle;
                mCurTemperature = mDecimalFormat.format(mCurPercent * 100);

                // 水波纹的高度
                mWaveUpValue = (int) (mCurPercent * (mSmallRadius * 2));

                postInvalidate();
            }
        }, 250, 30);

    }

那么刻度动画的效果如下:

4. 绘制中心的圆与文字

我们再动画中记录动画的百分比进度,和动画当前的温度。

    ...
    // 计算的温度
    mCurPercent = targetAngle / mSweepAngle;
    mCurTemperature = mDecimalFormat.format(mCurPercent * 100);

    postInvalidate();

    ...

我们记录一下小圆的半径和文本的画笔资源

   private float mSmallRadius = 0f;
    private Paint mTextPaint;
    private Paint mSmallCirclePaint;
    private float mCurPercent = 0f;  //进度
    private String mCurTemperature = "0.0";
    private DecimalFormat mDecimalFormat;

    private void init() {
        ...

        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        mTextPaint.setColor(Color.WHITE);

        mSmallCirclePaint = new Paint();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        ...

        //画小圆
        drawSmallCircle(canvas, evaluateColor(mCurPercent, Color.GREEN, Color.RED));

        //画中心的圆与文本
        drawTemperatureText(canvas);

    }

具体的文本与小圆的绘制

    private void drawSmallCircle(Canvas canvas, int evaluateColor) {
        mSmallCirclePaint.setColor(evaluateColor);
        mSmallCirclePaint.setAlpha(65);
        canvas.drawCircle(centerPosition.x, centerPosition.y, mSmallRadius, mSmallCirclePaint);
    }

    private void drawTemperatureText(Canvas canvas) {

        //提示文字
        mTextPaint.setTextSize(mSmallRadius / 6f);
        canvas.drawText("当前温度", centerPosition.x, centerPosition.y - mSmallRadius / 2f, mTextPaint);

        //温度文字
        mTextPaint.setTextSize(mSmallRadius / 2f);
        canvas.drawText(mCurTemperature, centerPosition.x, centerPosition.y + mSmallRadius / 4f, mTextPaint);

        //绘制单位
        mTextPaint.setTextSize(mSmallRadius / 6f);
        canvas.drawText("°C", centerPosition.x + (mSmallRadius / 1.5f), centerPosition.y, mTextPaint);

    }

由于进度和温度都是动画在 invalidate 之前赋值的,所以我们的文本和小圆天然就支持动画的效果了。

效果如下:

5. 水波纹动画

水波纹的效果,我们不能直接用 Canvas 来绘制,我们可以用刻度的方法用 drawLine的方式来绘制,如何绘制呢?相信大家也有了解,就是正弦函数了。

由于我们的效果是两个水波纹相互叠加起起伏伏的效果,所以我们定义两个函数。

总体的思路是:我们定义两个数组来管理我们的Y轴的值,通过正弦函数给Y轴赋值,然后在drawLine的时候取出对应的x轴的y值就可以绘制出来。

x轴其实就是我们的控件宽度,我们先用一个数组保存起来

    private float[] mFirstWaterLine;
    private float[] mSecondWaterLine;

     @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //获取控件的宽度,高度
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int newWidthMeasureSpec = widthMeasureSpec;

        //如果没有指定宽度,默认给200宽度
        if (widthMode != MeasureSpec.EXACTLY) {
            newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(200, MeasureSpec.EXACTLY);
        }

        //获取到最新的宽度
        int width = MeasureSpec.getSize(newWidthMeasureSpec) - getPaddingLeft() - getPaddingRight();

        //我们要的是矩形,不管高度是多高,让它总是和宽度一致
        int height = width;

        mFirstWaterLine = new float[width];
        mSecondWaterLine = new float[width];

        super.onMeasure(
                MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
        );

    }

然后我们再绘制之前就先对x轴对应的y值赋值,然后绘制的时候就取出对应的y值来 drawLine,具体的代码如下:

动画的时候先对横向运动和垂直运动的变量做一个赋值:

    private int mWaveUpValue = 0;
    private float mWaveMoveValue = 0f;

    //使用定时任务做动画
    private void startTimerAnim() {

        if (isAnimRunning) {
            return;
        }
        mAnimTimer = new Timer();
        mAnimTimer.schedule(new TimerTask() {

            @Override
            public void run() {

                ...

                // 计算的温度
                mCurPercent = targetAngle / mSweepAngle;
                mCurTemperature = mDecimalFormat.format(mCurPercent * 100);

                // 水波纹的高度
                mWaveUpValue = (int) (mCurPercent * (mSmallRadius * 2));

                postInvalidate();
            }
        }, 250, 30);

    }

    public void moveWaterLine() {
        mWaveTimer = new Timer();
        mWaveTimer.schedule(new TimerTask() {

            @Override
            public void run() {
                mWaveMoveValue += 1;
                if (mWaveMoveValue == 100) {
                    mWaveMoveValue = 1;
                }
                postInvalidate();
            }
        }, 500, 200);
    }

拿到了对应的变量值之后,然后开始绘制:

 /**
     * 绘制水波
     */
    private void drawWaterWave(Canvas canvas, int color) {

        int len = (int) mRectF.right;

        // 将周期定为view总宽度
        float mCycleFactorW = (float) (2 * Math.PI / len);

        // 得到第一条波的峰值
        for (int i = 0; i < len; i++) {
            mFirstWaterLine[i] = (float) (10 * Math.sin(mCycleFactorW * i + mWaveMoveValue) - mWaveUpValue);
        }
        // 得到第一条波的峰值
        for (int i = 0; i < len; i++) {
            mSecondWaterLine[i] = (float) (15 * Math.sin(mCycleFactorW * i + mWaveMoveValue + 10) - mWaveUpValue);
        }

        canvas.save();

        // 裁剪成圆形区域
        Path path = new Path();
        path.addCircle(len / 2f, len / 2f, mSmallRadius, Path.Direction.CCW);
        canvas.clipPath(path);
        path.reset();

        // 将坐标系移到底部
        canvas.translate(0, centerPosition.y + mSmallRadius);

        mSmallCirclePaint.setColor(color);

        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, mFirstWaterLine[i], i, len, mSmallCirclePaint);
        }
        for (int i = 0; i < len; i++) {
            canvas.drawLine(i, mSecondWaterLine[i], i, len, mSmallCirclePaint);
        }

        canvas.restore();

    }

一个是对Y轴赋值,一个是取出x轴对应的y轴进行绘制,这里需要注意的是我们裁剪出了一个小圆的图形,并且覆盖在小圆上面实现出效果图的样子。

运行的效果如下:

要记得对定时器进行资源你的关闭哦。

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mWaveTimer != null) {
            mWaveTimer.cancel();
        }
        if (mAnimTimer != null && isAnimRunning) {
            mAnimTimer.cancel();
        }
    }

使用的时候我们只需要设置温度即可开始动画。

       findViewById<View>(R.id.set_progress).click {

           val temperatureView = findViewById<TemperatureView>(R.id.temperature_view)
            temperatureView .setupTemperature(70f)
        }

后记

由于是自用定制的,本人也比较懒,所以并没有对一些配置的属性做自定义属性的抽取,比如圆环的间距,大小,颜色,波纹的间距,动画的快慢等等。

内部加了一点点测量的用法,但是主要还是绘制的流程,基本上把常用的几种绘制方式都用到了。以后有类似的效果大家也可以按需修改即可。

到此这篇关于Android自定义View实现绘制水波浪温度刻度表的文章就介绍到这了,更多相关Android自定义View绘制温度刻度表内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android自定义控件实现温度旋转按钮效果

    首先看下效果图 温度旋转按钮 实现思路 初始化一些参数 绘制刻度盘 绘制刻度盘下的圆弧 绘制标题与温度标识 绘制旋转按钮 绘制温度 处理滑动事件 提供一些接口方法 实现方法 初始化一些参数 public class TempControlView extends View { // 控件宽 private int width; // 控件高 private int height; // 刻度盘半径 private int dialRadius; // 圆弧半径 private int arcRa

  • Android自定义View实现水波纹扩散效果

    目录 1.创建RippleView.class, 继承与View 1.1特殊属性解释 1.2新建attrs.xml文件(res/values) 1.3初始化画笔 2.开始绘制onDraw() 效果:水波纹扩散 场景:雷达.按钮点击效果.搜索等 实现:先上效果图,之前记得支付宝有一个咻一咻,当时就是水波纹效果,实现起来一共两步,第一画内圆,第二画多个外圆,不同时创建有间隔创建然后缓慢增大外圆半径,到达最远距离时移除掉,扩散时把透明度从255-1不断赋值即可.复杂在第二步,开工. 开工 1.创建Ri

  • Android自定义View实现跟随手指移动

    对View的移动,实现的方法有好几种,原理是通过改变View的位置来移动View,下面来实现这样的效果 动画的方法 通过改变View的tranlationX和tranlationY的值来实现移动,首先来写一个自定义View类,重写onTouchEvent方法,实现构造方法 public class MyView extends View {     public MyView(Context context) {         super(context);     }     public

  • Android自定义View实现星星评分效果

    目录 前言 1.测量与图片的绘制 2.事件的交互与计算 3. 回调处理与自定义属性抽取 后记 前言 在前面的学习中,我们基本了解了一些 Canvas 的绘制,那么这一章我们一起复习一下图片的绘制几种方式,和事件的简单交互方式. 我们从易到难,作为基础的进阶控件,我们从最简单的交互开始,那就自定义一个星星评分的控件吧. 一个 App 必不可少的评论系统打分的控件,可以展示评分,可以点击评分,可以滑动评分.它的实现总体上可以分为以下的步骤: 强制测量大小为我们指定的大小 先绘制Drawable未评分

  • Android自定义View实现简单水波纹效果

    本文实例为大家分享了Android自定义View实现水波纹效果的具体代码,供大家参考,具体内容如下 效果如下: 原理 控制代码 //这里用的kotlin //主线程刷新控件  val mHandler = object : Handler() {         override fun handleMessage(msg: Message?) {             waterRippleView.refreshView()         }      //开启动画,开线程,延时刷新pe

  • Android自定义View实现绘制水波浪温度刻度表

    目录 前言 1.onMeasure重新测量 2.绘制刻度 3. 设置刻度动画 4. 绘制中心的圆与文字 5. 水波纹动画 后记 前言 之前的绘制圆环,我们了解了如何绘制想要的形状和进度的一些特点,那么此篇文章我们更近一步,绘制一个稍微复杂一点的刻度与波浪.来一起复习一下Android的绘制. 相对应的这种类型的自定义View网上并不少见,但是如果我们要做一些个性化的效果,最好还是自己绘制一份,也相对的比较容易控制效果,如果想实现上面的效果,我们一般来说分为以下几个步骤: 重写测量方法,确保它是一

  • Android自定义View之绘制圆形头像功能

    前言 做APP应用开发的时候,用户头像肯定是必不可少的,但是90%以上的需求头像都是圆形的.那么,如何通过自定义View的方式实现圆形头像呢,那么,本片博文会告诉你不仅仅是实现过程.一定会有意想不到的收获哦! 最终效果 国际惯例,我们先来看最终实现的效果图 自定义RoundImageView继承自ImageView public class RoundImageView extends ImageView { public RoundImageView(Context context) { su

  • Android自定义View实现绘制虚线的方法详解

    前言 说实话当第一次看到这个需求的时候,第一反应就是Canvas只有drawLine方法,并没有drawDashLine方法啊!这咋整啊,难道要我自己做个遍历不断的drawLine?不到1秒,我就放弃这个想法了,因为太恶心了.方法肯定是有的,只不过我不知道而已. 绘制方法 最简单的方法是利用ShapeDrawable,比如说你想用虚线要隔开两个控件,就可以在这两个控件中加个View,然后给它个虚线背景. 嗯,理论上就是这样子的,实现上也很简单. <!-- drawable 文件 --> <

  • Android自定义view实现圆的扩散效果

    本文实例为大家分享了Android自定义View的实现水波纹,供大家参考,具体内容如下 一.实现效果 MainActivity.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.andro

  • Android 自定义view实现水波纹动画效果

    在实际的开发中,很多时候还会遇到相对比较复杂的需求,比如产品妹纸或UI妹纸在哪看了个让人兴奋的效果,兴致高昂的来找你,看了之后目的很明确,当然就是希望你能给她: 在这样的关键时候,身子板就一定得硬了,可千万别说不行,爷们儿怎么能说不行呢: 好了,为了让大家都能给妹纸们想要的,后面会逐渐分享一些比较比较不错的效果,目的只有一个,通过自定义view实现我们所能实现的动效: 今天主要分享水波纹效果: 1.标准正余弦水波纹: 2.非标准圆形液柱水波纹: 虽说都是水波纹,但两者在实现上差异是比较大的,一个

  • Android自定义View 实现水波纹动画引导效果

    一.实现效果图 二.实现代码 1.自定义view package com.czhappy.showintroduce.view; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Pat

  • Android自定义View实现波浪动画

    本文实例为大家分享了Android自定义View实现波浪动画的具体代码,供大家参考,具体内容如下 效果演示 代码调用与实现效果 xml中调用 <developer.shivam.waveview.Wave android:layout_width="match_parent" android:layout_height="match_parent" app:amplitude="100" app:quadrant="0.5&quo

  • Android自定义View绘制贝塞尔曲线实现流程

    目录 前言 二阶贝塞尔曲线 三阶贝塞尔曲线 前言 对于Android开发,实现贝塞尔曲线还是比较方便的,有对应的API供你调用.由于一阶贝塞尔曲线就是一条直线,实际没啥多大用处,因此,下面主要讲解二阶和三阶. 二阶贝塞尔曲线 在Android中,使用quadTo来实现二阶贝塞尔 path.reset() path.moveTo(startX, startY) path.quadTo(currentX, currentY, endX, endY) canvas.drawPath(path, cur

随机推荐