一,效果图:
图一为所做项目效果展示图:
图二为单独这个View的效果图:
二,使用方法:
1,由于是自定义的View,所以需要在attrs.xml里加入如下属性定义:
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <attr name="batteryLevel" format="float" />
    <attr name="radius" format="integer" />
    <attr name="flowRadius" format="dimension" />
    <attr name="textColor" format="color" />
    <attr name="textSize" format="dimension" />
    <attr name="flowViewColor" format="color" />

    <declare-styleable name="BatteryView">
        <attr name="batteryLevel" />
        <attr name="radius" />
        <attr name="flowRadius" />
        <attr name="textColor" />
        <attr name="textSize" />
        <attr name="flowViewColor"/>
    </declare-styleable>

</resources>

2,在布局文件中引入:

xmlns:zhangxutong="http://schemas.android.com/apk/res-auto"
<mcxtzhang.weixin521.help.BatteryView
    android:background="@drawable/battery_view_bg_circle"
    android:id="@+id/batteryView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:padding="10dp"
    zhangxutong:flowViewColor="#ffaaffaa"/>

以上自定义的属性,如果我们需要自定义波浪滚动的圆球的颜色,则传入 flowViewColor,否则可完全使用默认属性。默认属性如图一,为半透明白色。

3,定义自定义View:
代码如下图:有详细注释:
package mcxtzhang.weixin521.help;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

import mcxtzhang.weixin521.R;
import mcxtzhang.weixin521.utils.TipsUtils;

/**
 * @author zhangxutong
 */
public class BatteryView extends View {private Context mContext;
    /**
     * 电池电量
     */
    private float level;
    /**
     * 控件半径
     */
    private int radius;
    /**
     * 波浪圆的半径 (弃用,被控件半径-padding 取代)
     */
    private int flowRadius;
    /**
     * 波浪圆的颜色值
     */
    private int flowColor;
    /**
     * 绘制的电量文字颜色值
     */
    private int textColor;
    /**
     * 绘制的电量文字大小
     */
    private int textSize;
    /**
     * 波浪圆X轴偏移
     */
    private float FAI;
    /**
     * 波浪圆振幅
     */
    private float A;
    /**
     * 波浪圆的周期
     */
    private float W;
    /**
     * 波浪圆Y轴偏移
     */
    private float K;

    // 画图相关
    /**
     * 画笔(遮罩)
     */
    private Paint mPaint;
    /**
     * 画笔 绘制,电量
     */
    private Paint mTextPaint;
    /**
     *  绘制矩形波浪的Path
     */
    private Path mPath;
    /**
     * 设置抗锯齿
     */
    private PaintFlagsDrawFilter mDrawFilter;
    /**
     * 设置遮罩模式
     */
    private PorterDuffXfermode mPorterDuffXfermode;
    /**
     * 存放遮罩图片
     */
    private Bitmap mBitmap;
    /**
     * 控件宽度
     */
    private int mWidth;
    /**
     * 控件高度
     */
    private int mHeight;
    /**
     * 中心点 横坐标
     */
    private int mCenterX;
    /**
     * 中心点 纵坐标
     */
    private int mCenterY;
    /**
     * 遮罩取源区域
     */
    private Rect mSrcRect;
    /**
     * 遮罩绘制目标区域
     */
    private Rect mDestRect;
    /**
     * 用于得到将要绘制的电量文字信息的区域
     */
    private Rect mTextRect;
    /**
     * 用于构建将要绘制的电量文字的字符串
     */
    private StringBuilder sb;

    /**
     * 暴漏出给外部调用的接口,用于监听
     */
    interface OnLevelChangeListener {int onLevelChange();
    }OnLevelChangeListener mOnLevelChangeListener;

    public void setOnLevelChangeListener(OnLevelChangeListener onLevelChangeListener) {mOnLevelChangeListener = onLevelChangeListener;
    }/**
     * 监听电池广播
     */
    private BroadcastReceiver mBatteryChangeReceiver = new BroadcastReceiver() {@Override
        public void onReceive(Context context, Intent intent) {if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {LogI("onReceiver BatteryChanged");
                level = intent.getIntExtra("level", -1);
                int curScale = intent.getIntExtra("scale", -1);
                sb.delete(0, sb.length());
                sb.append("" + (int) level * 100 / curScale).append("%");
                //计算AK,以防在电量接近边界,最大最小的时候,绘图超过范围
                setA();
                //setK();
            }}};

    /**
     * 设置Y轴偏移K, 以防在电量接近边界,最大最小的时候,绘图超过范围(弃用)
     */
    private void setK() {// K:-5~+5 , level :0~100
        /*float k = 20f/100f;
        K = k*level;*/
    }/**
     * 设置振幅A,A的计算公式为一个二次方程,在level = 50时,振幅最大,在两端最小,在0和100临界值时为0
     */
    private void setA() {//y = ax;
        /*float k = 10f / 50f ;
        A = level<50? k*level:-k*(level-100);*/
        //y  = ax2+bx+c ( 0,3) (50,10 )( 100,3)
        if (level == 0 || level == 100) {A = 0;
        } else {float a = -7f / 2500;
            float b = -100 * a;
            float c = 3;
            A = (float) (a * Math.pow(level, 2) + b * level + c);
        }//LogI("A:"+A);

    }@Override
    protected void onAttachedToWindow() {super.onAttachedToWindow();
        LogI("onAttachedToWindow");
        //onAttachedToWindow会在onDraw前调用,注册监听电量变化的广播
        mContext.registerReceiver(mBatteryChangeReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
    }@Override
    protected void onDetachedFromWindow() {super.onDetachedFromWindow();
        LogI("onDetachedFromWindow");
        //防止内存泄露,需要注销该广播
        mContext.unregisterReceiver(mBatteryChangeReceiver);
    }public BatteryView(Context context) {this(context, null);
    }public BatteryView(Context context, AttributeSet attrs) {this(context, attrs, 0);
    }public BatteryView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);
        mContext = context;
        LogI("inital func");
        init();
        // 获取我们在attr中定义的样式属性
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
                R.styleable.BatteryView, defStyleAttr, 0);
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++) {int attr = a.getIndex(i);
            switch (attr) {case R.styleable.BatteryView_batteryLevel:level = a.getFloat(attr, 0);
                    break;
                case R.styleable.BatteryView_flowRadius:flowRadius = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 150, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.BatteryView_radius:radius = a.getInt(attr, getWidth());
                    break;
                case R.styleable.BatteryView_textColor:textColor = a.getColor(attr, Color.WHITE);
                    break;
                case R.styleable.BatteryView_textSize:textSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.BatteryView_flowViewColor:TipsUtils.showToast(context,"flowViewColor");
                    flowColor = a.getColor(attr,0x88FFFFFF);
                    break;
            }}a.recycle();

        initPaint();

        //这个线程就是为了将波浪滚动起来,改变FAI,即X轴的偏移 ,稍后完善会用属性动画实现
        new Thread() {@Override
            public void run() {while (true) {changeFAI();
                    try {Thread.sleep(60);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }postInvalidate();
                }}private void changeFAI() {FAI += 0.2;
            }}.start();
    }/**
     * 初始化参数函数
     */
    private void init() {level = 0;
        radius = getWidth();
        flowRadius = 150;
        flowColor = 0x88FFFFFF;
        FAI = 0;
        A = 9;
        W = 0.75f;
        K = 0;
    }/**
     * 初始化画笔相关参数
     */
    private void initPaint(){// 绘图参数初始化
        // 抗锯齿
        mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
        //设置遮罩模式为,先绘制DST,再绘制SRC,取交集,留下DST
        mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
        mPath = new Path();
        // 防抖动
        mPaint.setDither(true);
        // 开启图像过滤
        mPaint.setFilterBitmap(true);
        //绘制出的滚动波浪的颜色
        mPaint.setColor(flowColor);
        // 初始化遮罩图片
        mBitmap = ((BitmapDrawable) getResources().getDrawable(R.drawable.battery_view_bg_round)).getBitmap();
        //初始化电量文字的Paint和Rect
        mTextPaint = new Paint();
        //设置文字样式,实心,居中,字体
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
      /*Typeface tf = Typeface.createFromAsset(mContext.getAssets(), "fonts/Roboto-Thin.ttf");
      mTextPaint.setTypeface(tf);*/
        mTextRect = new Rect();
        sb = new StringBuilder();
    }//private int temp = 0;
    @Override
    protected void onDraw(Canvas canvas) {//LogI("flowRadius" + flowRadius + "  getPaddingRight:" + getPaddingRight());
        //if(temp++==0)
        //LogI("onDraw");
        // 从canvas层面抗锯齿
        canvas.setDrawFilter(mDrawFilter);
        //新开启一个图层,用于绘制波浪
        int sc = canvas.saveLayer(0 + getPaddingLeft(), 0 + getPaddingTop(), mWidth - getPaddingRight(), mHeight - getPaddingBottom(), null,
                Canvas.ALL_SAVE_FLAG);
        //清除上次的path
        mPath.reset();
/*    level = 0;
      setA();
      //setK();
*/
        //利用正弦曲线方程 Y = A sin(wx+FAI)+k,将计算后的坐标传入Path.quadTo()方法(绘制贝塞尔曲线)中,构建波浪曲线。
        for (int x = mDestRect.left; x < mDestRect.right; x++) {float y = (float) (A * Math.sin(Math.PI / 250 * W * x + FAI) + K + (mDestRect.bottom - mDestRect.top)* 1.0f / 100 * (100 - level));
            if (x == mDestRect.left) {mPath.moveTo(x, y);
            }mPath.quadTo(x, y, x + 1, y);
        }//将Path连结起来,形成闭环
        mPath.lineTo(mDestRect.right, mDestRect.bottom);
        mPath.lineTo(mDestRect.left, mDestRect.bottom);
        mPath.close();
        //绘制波浪图形(图形上部是波浪,下部是矩形) (DST)
        canvas.drawPath(mPath, mPaint);
        //设置遮罩模式(图像混合模式)
        mPaint.setXfermode(mPorterDuffXfermode);
        //绘制用于遮罩的圆形 (SRC)
        canvas.drawBitmap(mBitmap, mSrcRect, mDestRect, mPaint);
        //设置遮罩模式为null
        mPaint.setXfermode(null);
        //将这个新图层绘制的bitmap,与上一个图层合并(显示)
        canvas.restoreToCount(sc);
        //canvas.restore(); 也可以

        //绘制电量文字的 字体大小和颜色 需要每次都重新设置 否则绘制不出文字,原因不明。
      /*mTextPaint.setTextSize(textSize);
      mTextPaint.setColor(textColor);
      mTextPaint.getTextBounds(sb.toString(), 0, sb.toString().length(), mTextRect);
      //绘制电池电量
      canvas.drawText(sb.toString(), mCenterX, mCenterY,mTextPaint);
      mTextPaint.setTextSize(24);*/

    }/**
     * 通过 onSizeChanged方法 获取当前view的宽高,以及中心点坐标
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {LogI("onSizeChanged:" + getPaddingLeft());
        mWidth = w;
        mHeight = h;
        mCenterX = w / 2;
        mCenterY = h / 2;
        mSrcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
      /*mDestRect = new Rect(mCenterX - flowRadius, mCenterY - flowRadius,
            mCenterX + flowRadius, mCenterY + flowRadius);*/
        //这边遮罩在绘制时应该考虑View的padding,
        mDestRect = new Rect(0 + getPaddingLeft(), 0 + getPaddingTop(), mWidth - getPaddingRight(), mHeight - getPaddingBottom());
        //mDestRect = new Rect(mWidth/2-flowRadius, mHeight/2-flowRadius,mWidth/2+flowRadius ,  mHeight/2+flowRadius);
    }//为了适配wrap_content,需要针对AT_MOST测量一下自己的宽高,这里是将wrap_content设置成默认的150dp
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {LogI("onMeasure");
        setMeasuredDimension(measuredWidth(widthMeasureSpec), measuredHeight(heightMeasureSpec));
    }private int measuredHeight(int heightMeasureSpec) {//初始化weight的默认值为150dp
        int measuredHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 150, getResources().getDisplayMetrics());
        //得到测量模式和测量值
        int specMode = MeasureSpec.getMode(heightMeasureSpec);
        int specSize = MeasureSpec.getSize(heightMeasureSpec);
        //如果是确定的值或Match_parent
        if (MeasureSpec.EXACTLY == specMode) {measuredHeight = specSize;
        } else { //AT_MOST:wrap_content  ,UNSPECIFIED 没用过。貌似子View想要多大就多大
            //取默认值和测量值的最小值
            measuredHeight = Math.min(specSize, measuredHeight);
        }return measuredHeight;
    }private int measuredWidth(int widthMeasureSpec) {int measuredWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 150, getResources().getDisplayMetrics());
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specSize = MeasureSpec.getSize(widthMeasureSpec);
        if (MeasureSpec.EXACTLY == specMode) {measuredWidth = specSize;
        } else {measuredWidth = Math.min(specSize, measuredWidth);
        }return measuredWidth;
    }private void LogI(String msg) {android.util.Log.i("zhangxutong/BatteryView", "" + msg);
    }}

重点是在onDraw里,利用path.quaTo()方法,绘制贝塞尔曲线(平滑的曲线),绘制的图形是一个正弦函数曲线图。即模拟出波浪效果。

关于如何让波浪动态滚动起来,是开启了一个线程,动态改变正弦曲线函数的FAI(横坐标的偏移量),则波浪会滚动起来。
========================================
附上源码和快速使用说明文档,
里面包含所有资源文件。
http://download.csdn.net/detail/zxt0601/9421642

(加入显示电量功能)模仿魅族、华为、小米电池续航管理软件, 动态水波纹滚动的圆形小球View相关推荐

  1. MacBook 电池电量未达到 100%?如何关闭电池健康管理

    在 2019 年 10 月推出的 Mac OS Catalina (v10.15.5) 中,Apple 为MacBook引入了一项很棒的功能:电池健康管理.这个想法很简单:在延长数月和数年的使用中保持 ...

  2. 华为小米手机信号测试软件,小米6/华为P10/vivo X20/一加5手机信号PK,谁是赢家

    描述 现在的手机品牌多,款式也多,但是谁才是最实用呢?我们使用手机的首要条件,就是通信信号好,速度快,这样才会大受消费者的喜爱.今天我们不妨看看这9款机子的手机信号测试.它们分别是华为P10.OPPO ...

  3. 小米电池温度测试软件,MIUI12怎么看电池温度 小米手机电池损耗寿命查看方法...

    MIUI12怎么看电池温度?最近有很多小米的用户朋友在MIUI12系统中对于电池相关信息的查看方法不太了解,想要知晓电池的温度.损耗.寿命情况该怎么操作呢?今天小编就为大家带来关于MIUI12怎么看电 ...

  4. 4个顶级的华为/小米/OPPO/Vivo手机屏幕解锁工具软件

    有好几次用户发现自己被锁定在他们的华为/小米/OPPO/Vivo设备之外,我们知道这可能是一种非常可怕的体验.在这种情况下,找到安卓手机解锁软件,重新获得手机中重要数据和文件的访问权限.看看这篇文章, ...

  5. 安卓 linux it之家,IT之家安卓版 7.07:紧凑排版+适配华为小米魅族OV系统级推送等...

    原标题:IT之家安卓版 7.07:紧凑排版+适配华为小米魅族OV系统级推送等 IT之家 安卓版/iOS版 7.07 今日更新上架发布! 画个重点,大家往下看" 7.x 后续产品规划" ...

  6. android魅族轮播图,用angularjs模仿魅族官网的图片轮播功能

    使用指令模仿魅族官网的图片轮播功能(angularjs中DOM操作都在指令中完成) html css .slider{ position: relative; width:900px; height: ...

  7. android小米通知不显示电量,Android开发笔记——小米通知‘坑’ app的通知一直显示在不重要通知里 ......

    Android8.0之后,通知引入渠道的概念,谷歌初衷应该是让用户能够自己管理通知级别以提高用户体验. 不过在小米这,我遇到了一个坑,在其他8.0的手机,渠道设置等级后,能够正常显示通知,代码如下:p ...

  8. 安卓推送功能小结(整合华为小米)

    接手的项目里面有一个小米和华为混用的推送功能,虽然能用,但是代码写的非常乱,正好最近又看了点设计模式,就花了点时间重构了一下,顺便做一个小结! 具体如何导入各个厂商的 SDK,文章末尾会讲下小米的,这 ...

  9. 小米手机电池校正_华为小米等安卓福音:超实用手机电池校正 解决电池虚标不耐用!...

    原标题:华为小米等安卓福音:超实用手机电池校正 解决电池虚标不耐用! 手机用久了,难免电量一点点的减弱,造成续航的下降,甚至电量充不满的情况 这是因为电量的虚标!因为苹果手手机以及华为手机,小米手机, ...

最新文章

  1. sublimelinter-php 错误代码提示
  2. python对list处理
  3. 第十七章 特殊成员_使用typedef简化函数指针的声明
  4. 计算机网络使用的通信线路分为两类,计算机网络技术阶段测试题
  5. 【问题解决】华硕A450C触控板失灵
  6. 新建raw data 分区
  7. GitHub中文社区
  8. ES6class与ES3构造函数区别
  9. ios备忘录下载安卓版_如何下载和平精英国际服?
  10. 广东公需科目公需课十四五答案考试查询器
  11. Testbench编写指南(2)文件的读写操作
  12. 浏览器插件,轻松-快速获取网站源代码
  13. group by 和where
  14. 一辞脚本分享的导入抖音号关注私信脚本,导入抖音号关注私信软件详细教学
  15. NTFS FOR MAC(paragon Software),关于升级10.15.7最新系统后,无法使用写入加载的临时解决办法
  16. 趣玩算法--OpenCV华容道AI自动解题
  17. RabbitMQ初步学习(Mac)
  18. 关闭 macOS Google Chrome 黑暗模式风格
  19. [Mysql]数据库创建严谨度:由低到高
  20. 这届年轻人,过得有多难?

热门文章

  1. Teardrop攻击——发送虚假IP包信息
  2. 关于微信小程序的多选和全选实现
  3. PMEdit一个富文本框可以编辑文本、图片并可以显示GIF动画
  4. 老男孩老师的博客地址
  5. 从安装过程品国产Linux操作系统的技术来源与异同之处
  6. Win32汇编 - 基本知识总结
  7. Json:前台对象数组传到后台解析
  8. 新房和二手房税费比较
  9. 手把手教你实现百度基础地图+定位功能+设置中心点+添加Marker
  10. 白杨SEO:什么是产品运营?产品运营要求有哪些?SEO如何转行产品运营?