Android自定义View——电池

Android自定义View——电池

概述

最近的工作中有一个需求,需要显示电量,产品说前面的开发已经做了,就在另一个项目里,复制过来就好了。行,那就复制呗,结果一看,真是惊呆了,用的是一个layer-list的drawable,里面就11种状态,从0到100%。如果要显示11%呢,怎么办?好吧,这种坑已经见怪不怪了,相对于其它坑,这算不上问题,填坑吧。

电池View的需求

  1. 图形显示1%到100%的电量;
  2. 数字显示1-100,可配置;
  3. 电池低电量,电量充足,充电时显示不同的颜色;
  4. 可以设置电池外边框的颜色和stroke大小;
  5. 可以选择横向显示或者纵向显示

自定义View

通过继承View自定义一个BatteryView实现这个电池View的需求。自定义View的概念可以参考这篇博客,已经写得非常清楚了。自定义View可以分为四类:

  1. 自定义组合控件,如自定义公共的ActionBar;
  2. 继承系统提供的特定的View,ViewGroup,如TextView,LinearLayout;
  3. 继承 android.view.View 实现自己的View
  4. 继承 android.view.ViewGroup 实现自己的ViewGroup。

关于自定义View的知识,这里就不详细描述了,要实现自定义电池View的需求,可以选择第三种,继承 android.view.View,重写onDraw,并在这个方法中绘制电池的需求。

BatteryView 实现

为了提高效率,在这篇博客Android 自定义电池控件的基础上进行修改(考虑了padding和增加了几个属性)。实现步骤如下:

  1. 新建attrs.xml定义属性文件,并增加BatteryView的属性;
  2. 继承View,实现BatteryView;
  3. 在构造函数中解析各个属性;
  4. 重写onDraw,根据属性绘制电池的外轮廓,电池电量,电池帽子以及电量百分比。

比较简单,直接贴上代码:

  1. attrs.xml
    <declare-styleable name="Battery">
        <attr name="batteryOrientation">
            <enum name="horizontal" value="0"/>
            <enum name="vertical" value="1"/>
        attr>
        <attr name="batteryColor" format="color"/> 
        <attr name="batteryStrokeWidth" format="dimension" /> 
        <attr name="batteryLowBatteryColor" format="color" /> 
        <attr name="batteryHealthBatteryColor" format="color" /> 
        <attr name="batteryHappyBatteryColor" format="color" />  
        <attr name="batteryChargingColor" format="color" /> 
        <attr name="batteryPower" format="integer"/>
        <attr name="batteryShowPercent" format="boolean" />  
        <attr name="batteryPercentColor" format="color"/> 
        <attr name="batteryPercentSize" format="dimension" /> 

    declare-styleable>
  1. BatteryView.java
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.Dimension;
import androidx.annotation.IntDef;

import com.bottle.app.R;
import com.bottle.app.device.DisplayUtil;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * 自定义水平\垂直电池控件
 */
public class BatteryView extends View {

    public static final int HORIZONTAL = 0;
    public static final int VERTICAL = 1;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({HORIZONTAL, VERTICAL})
    @interface Orientation {
    }

    private int mPower = 100;          // 电量
    private int orientation;           // 方向
    private int mStrokeColor;          // 电池边框的颜色
    private int mStrokeWidth;           // 电池边框的宽度
    private int mLowBatteryColor;      // 低电量时显示的颜色
    private int mHealthBatteryColor;   // 中等电量时的颜色
    private int mHappyBatteryColor;    // 电量较多时的颜色
    private boolean mCharging;         // 是否在充电
    private int mChargingColor;        // 充电时的颜色
    private boolean mShowPercent;      // 是否显示百分比
    private int mPercentColor;         // 百分比数字的颜色
    private float mPercentSize;        // 百分比文字大小

    private int width;
    private int height;
    private int headWidth;             // 电池头的长度
    private int headHeight;             // 电池头的高度
    private int mLowBattery = 20;      // 低电量阈值
    private int mHealthBattery = 50;   // 健康电联阈值

    private Paint mPaint = new Paint();

    public void setPower(int power) {
        this.mPower = power;
        if (mPower < 0) {
            mPower = 0;
        } else if (mPower > 100) {
            mPower = 100;
        }
        invalidate();
    }

    public void setColor(@ColorRes int color) {
        this.mStrokeColor = color;
    }

    public void setOrientation(@Orientation int orientation) {
        this.orientation = orientation;
    }

    public void setStrokeColor(@ColorInt int strokeColor) {
        mStrokeColor = strokeColor;
    }

    public void setStrokeWith(@Dimension int strokeWidth) {
        mStrokeWidth = strokeWidth;
    }

    public void setLowBatteryColor(@ColorInt int lowBatteryColor) {
        mLowBatteryColor = lowBatteryColor;
    }

    public void setHealthBatteryColor(@ColorInt int healthBatteryColor) {
        mHealthBatteryColor = healthBatteryColor;
    }

    public void setHappyBatteryColor(@ColorInt int happyBatteryColor) {
        mHappyBatteryColor = happyBatteryColor;
    }

    public void setCharging(boolean charging) {
        mCharging = charging;
    }

    public void setChargingColor(@ColorInt int chargingColor) {
        mChargingColor = chargingColor;
    }

    public void setLowBattery(int lowBattery) {
        mLowBattery = lowBattery;
    }

    public void setHealthBattery(int healthBattery) {
        mHealthBattery = healthBattery;
    }

    public void setShowPercent(boolean showPercent) {
        mShowPercent = showPercent;
    }

    public void setPercentColor(@ColorInt int percentColor) {
        mPercentColor = percentColor;
    }

    public void setPercentSize(@Dimension int percentSize) {
        mPercentSize = percentSize;
    }

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

    public BatteryView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Battery);
        mStrokeColor = typedArray.getColor(R.styleable.Battery_batteryColor, Color.BLACK);
        mStrokeWidth = typedArray.getDimensionPixelOffset(
                R.styleable.Battery_batteryStrokeWidth, DisplayUtil.dip2px(context, 1));
        mLowBatteryColor = typedArray.getColor(R.styleable.Battery_batteryLowBatteryColor, Color.RED);
        mHealthBatteryColor = typedArray.getColor(R.styleable.Battery_batteryHealthBatteryColor, Color.parseColor("#76AE00"));
        mHappyBatteryColor = typedArray.getColor(R.styleable.Battery_batteryHappyBatteryColor, Color.GREEN);
        mChargingColor = typedArray.getColor(R.styleable.Battery_batteryColor, Color.YELLOW);
        orientation = typedArray.getInt(R.styleable.Battery_batteryOrientation, 0);
        mPower = typedArray.getInt(R.styleable.Battery_batteryPower, 100);
        mShowPercent = typedArray.getBoolean(R.styleable.Battery_batteryShowPercent, false);
        mPercentColor = typedArray.getColor(R.styleable.Battery_batteryPercentColor, Color.BLACK);
        mPercentSize = typedArray.getDimensionPixelOffset(
                R.styleable.Battery_batteryPercentSize, DisplayUtil.sp2px(context, 11));
        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = getMeasuredWidth();
        height = getMeasuredHeight();
        if (orientation == HORIZONTAL) {
            headWidth = (height - getPaddingTop() - getPaddingBottom() - mStrokeWidth * 2) / 2;
        } else {
            headWidth = (width - getPaddingStart() - getPaddingEnd() - mStrokeWidth * 2) / 2;
        }
        headHeight = headWidth / 2;
        int max = DisplayUtil.dip2px(getContext(), 8);
        if (headHeight > max) {
            // 对电池头做一个高度限制
            headHeight = max;
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (orientation == HORIZONTAL) {
            drawHorizontalBattery(canvas);
        } else {
            drawVerticalBattery(canvas);
        }
    }

    private void drawHorizontalBattery(Canvas canvas) {
        float strokeWithHalf = mStrokeWidth / 2f;
        // 1.绘制电池外边框
        mPaint.setColor(mStrokeColor);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mStrokeWidth);
        RectF r1 = new RectF(getPaddingStart() + strokeWithHalf,
                getPaddingTop()  + strokeWithHalf,
                width - getPaddingEnd() - headHeight,
                height - getPaddingBottom() - strokeWithHalf);
        canvas.drawRect(r1, mPaint);

        // 2.绘制电池电量
        mPaint.setStyle(Paint.Style.FILL);
        float offset = (width - getPaddingStart() - getPaddingEnd() - mStrokeWidth * 2 - headHeight) * mPower / 100.f;
        RectF r2 = new RectF(getPaddingStart() + mStrokeWidth,
                getPaddingTop() + mStrokeWidth,
                getPaddingStart() + mStrokeWidth + offset + strokeWithHalf, // padding + 边框宽度+ 电量 + 半个边框宽度(右边边框的一半)
                height - getPaddingBottom() - mStrokeWidth);
        if (mCharging) {
            mPaint.setColor(mChargingColor);
        } else if (mPower < mLowBattery) {
            mPaint.setColor(mLowBatteryColor);
        } else if (mPower < mHealthBattery) {
            mPaint.setColor(mHealthBatteryColor);
        } else {
            mPaint.setColor(mHappyBatteryColor);
        }
        canvas.drawRect(r2, mPaint);

        // 3.画电池头
        RectF r3 = new RectF(width - getPaddingEnd() - headHeight,
                height / 2f - headWidth / 2f,
                width - getPaddingEnd(),
                height / 2f + headWidth / 2f);
        mPaint.setColor(mStrokeColor);
        canvas.drawRect(r3, mPaint);

        // 4.绘制数字百分比
        if (mShowPercent) {
            mPaint.setAntiAlias(true);
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setTextAlign(Paint.Align.CENTER);
            mPaint.setTextSize(mPercentSize);
            mPaint.setColor(mPercentColor);
            mPaint.setStrokeWidth(1);
            String text = String.valueOf(mPower);
            canvas.drawText(text,
                    width / 2f - mPaint.measureText(text) / 2,
                    height / 2f + (mPaint.getFontMetrics().bottom - mPaint.getFontMetrics().top) / 2 - mPaint.getFontMetrics().bottom,
                    mPaint);
        }
    }

    private void drawVerticalBattery(Canvas canvas) {
        float strokeWithHalf = mStrokeWidth / 2f;
        // 1.绘制电池外边框
        mPaint.setColor(mStrokeColor);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mStrokeWidth);
        RectF r1 = new RectF(getPaddingStart() + strokeWithHalf,
                getPaddingTop() + headHeight,
                width - getPaddingEnd(),
                height - getPaddingBottom() - strokeWithHalf);
        canvas.drawRect(r1, mPaint);

        // 2.绘制电池电量
        mPaint.setStyle(Paint.Style.FILL);
        float offset = (height - getPaddingTop() - getPaddingBottom() - mStrokeWidth * 2 - headHeight) * mPower / 100.f;
        RectF r2 = new RectF(getPaddingStart() + mStrokeWidth,
                height - getPaddingTop() - mStrokeWidth - offset - strokeWithHalf ,
                width - getPaddingEnd()  - strokeWithHalf,
                height - getPaddingBottom() - mStrokeWidth);
        if (mCharging) {
            mPaint.setColor(mChargingColor);
        } else if (mPower < mLowBattery) {
            mPaint.setColor(mLowBatteryColor);
        } else if (mPower < mHealthBattery) {
            mPaint.setColor(mHealthBatteryColor);
        } else {
            mPaint.setColor(mHappyBatteryColor);
        }
        canvas.drawRect(r2, mPaint);

        // 3.画电池头
        RectF r3 = new RectF(width / 2f - headWidth / 2f,
                getPaddingTop(),
                width / 2f + headWidth / 2f,
                getPaddingTop() + headHeight);
        mPaint.setColor(mStrokeColor);
        canvas.drawRect(r3, mPaint);

        // 4.绘制数字百分比
        if (mShowPercent) {
            mPaint.setAntiAlias(true);
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setTextAlign(Paint.Align.CENTER);
            mPaint.setTextSize(mPercentSize);
            mPaint.setColor(mPercentColor);
            mPaint.setStrokeWidth(1);
            String text = String.valueOf(mPower);
            canvas.drawText(text,
                    width / 2f,
                    height / 2f + (mPaint.getFontMetrics().bottom - mPaint.getFontMetrics().top) / 2 - mPaint.getFontMetrics().bottom,
                    mPaint);
        }
    }

}

测试

  1. 在布局文件中添加代码
    <com.bottle.app.widget.BatteryView
        android:id="@+id/batteryView3"
        android:layout_width="@dimen/dp_49"
        android:layout_height="@dimen/dp_80"
        android:padding="@dimen/dp_2"
        app:batteryShowPercent="true"
        app:batteryPower="100"
        app:batteryColor="@android:color/black"
        app:batteryOrientation="vertical" />
  1. 在Activity中实例化后,并在电量改变时调用方法修改电量值
private Runnable mAction = new Runnable() {
        @Override
        public void run() {
            int pow = power++ % 100;
            mBatteryView.setPower(pow);
            mBatteryView.postDelayed(mAction, 100);
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBatteryView = findViewById(R.id.batteryView);
        mBatteryView.post(mAction);
    }

总结与思考

在实现的过程中需要注意的点如下:

  1. 要考虑padding,否则当用户设置padding时只会显示在左上角;
  2. 绘制文字的时候通过Paint计算文字的宽高,否则无法剧中;
  3. 根据需求,考虑需要哪些属性,要考虑多些,才能更好的自定义起外观。

不足:

  1. 没有实现圆角;
  2. 没有提供充电中的动画,需要调用者自己写动画。

参考

Android 自定义电池控件
Android自定义View全解

你可能感兴趣的:(android)