开年来公司不忙,就在闲逛时朋友说给写给小控件,并给出这样的效果:
我问他还要什么要求,提供的数据是什么,他竟然告诉我没要求,数据随机给就行。我的第一反应是这还不简单啊,就写个view简单画一下不就好了,于是上来就开写,但是写着写着发现还是有些麻烦,麻烦点就在他竟然不给数据。下面在这个控件编写过程记录下来。
效果如下:
首先看到这样一个效果时,需要确定这个view哪些属性支持定制。我给出的自定义属性包括:柱形条个数,柱形条宽度,柱形条颜色(支持设置颜色组),波动节奏快慢,柱形条间隙(由控件宽度、柱形条个数、柱形条宽度共同决定)。
于是控件有了如下属性:
/**
* 单根柱形宽度,默认为10
*/
private int pillarWidth = 10;
/**
* 柱形数量,默认为4
*/
private int pillarAmount = 4;
/**
* 柱形颜色
*/
private int pillarColor = Color.rgb(179, 100, 53);
/**
* 波动节奏
* 快-慢 0-1
*/
private double rhythm = 0.5;
/**
* 柱形颜色组,
* 当pillarColors等于pillarAmount时,对应柱形取pillarColors对应颜色
* 当pillarColors大于pillarAmount时,对应柱形取pillarColors前pillarAmount个对应颜色
* 当pillarColors小于pillarAmount时,pillarColors循环拼接后,对应柱形取pillarColors对应颜色
*/
private int[] pillarColors;
开始画view前需要确定view大小: @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//设置控件宽度
setMeasuredDimension(measureWidth(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
//控件内容宽度
contentWidth = getWidth() - getPaddingLeft() - getPaddingRight();
//控件内容高度
contentHight = getHeight() - getPaddingTop() - getPaddingBottom();
//控件的左、上、右、下座标
left = getPaddingLeft();
top = getPaddingTop();
right = getWidth() - getPaddingRight();
bottom = getHeight() - getPaddingBottom();
//柱形条间隙
gap = pillarAmount > 1 ? (float) (contentWidth - pillarAmount * pillarWidth) / (float) (pillarAmount - 1) : 0;
//初始化属性动画值数组
this.animValues = new float[pillarAmount];
//初始化波动实体数组
this.waves = new Wave[pillarAmount];
for (int i = 0; i < waves.length; i++)
{
int distance = (int) (Math.random() * contentHight);
waves[i] = new Wave(-1, distance, bottom, bottom - distance);
}
}
/**
* 计算控件宽度
* @param measureSpec
* @return
*/
private int measureWidth(int measureSpec)
{
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//根据柱形数量跟宽度计算出最小宽度
int minWidth = pillarWidth * pillarAmount + getPaddingLeft() + getPaddingRight();
if (specMode == MeasureSpec.EXACTLY)
{
return specSize > minWidth ? specSize : minWidth;
}
return minWidth;
}
21行animValues用来记录每个柱形条属性动画的动画值;
23行waves用来记录每次波动的波动情况情况;
25-28行为waves赋初始值(柱形条都在最低端,都向上运动,运动范围都为控件的内容区域高度contentHight)。
下面看一下Wave对象:
/**
* 波动实体类
*/
class Wave
{
/**
* 波动方向.
* 1:正向波动(向下),-1:负向波动(向上)
*/
int direction;
/**
* 波动距离
*/
float distance;
/**
* 起始位置
*/
float startPosition;
/**
* 目标位置
*/
float targetPosition;
public Wave()
{
}
public Wave(int direction, float distance, float startPosition, float targetPosition)
{
this.direction = direction;
this.distance = distance;
this.startPosition = startPosition;
this.targetPosition = targetPosition;
}
@Override
public String toString()
{
return "Wave{" +
"direction=" + direction +
", distance=" + distance +
", startPosition=" + startPosition +
", targetPosition=" + targetPosition +
'}';
}
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
//若动画未开启过,则为每根柱形条开启动画
if (!isStartAnim)
{
for (int i = 0; i < animValues.length; i++)
{
startAnim(i);//开启动画
}
isStartAnim = true;
}
//画柱形条
drawPillar(canvas);
}
第10行为柱形条开启动画的方法: /**
* 开启动画
*/
private void startAnim(final int pillarPosition)
{
final ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
{
@Override
public void onAnimationUpdate(ValueAnimator animation)
{
//对应柱形条绑定对应属性动画值
animValues[pillarPosition] = (float) animation.getAnimatedValue();
//每次动画都重画view
invalidate();
}
});
anim.addListener(new AnimatorListenerAdapter()
{
//监听动画重播时,需要产生下一次波动的随机波动周期以及波动实体
@Override
public void onAnimationRepeat(Animator animation)
{
super.onAnimationRepeat(animation);
//为了让波动不具备很强规则性,这里随机打乱周期,并将rhythm波动频率带入
anim.setDuration((long) (MAX_ANIM_PERIOD * (Math.random() / 4 + 0.75) * rhythm));
//获取下次波动实体
getWaves(pillarPosition);
}
});
//随机产生第一次波动周期
anim.setDuration((long) (MAX_ANIM_PERIOD * (Math.random() / 4 + 0.75) * rhythm));
//无限循环
anim.setRepeatCount(ValueAnimator.INFINITE);
//从头开始动画
anim.setRepeatMode(ValueAnimator.RESTART);
anim.start();
}
代码都有注释,应该不难看懂,接着看29行如何产生下次波动实体: /**
* 获取随机波动实体
*/
private void getWaves(int position)
{
//产生一个0.5-1的随机基数
double r = Math.random() / 2 + 0.5;
//将波动方向反向
waves[position].direction = -waves[position].direction;
/**
*根据随机基数获取波动距离
*当方向为1即方向向下运动时,波动距离的最大值为:底部座标-上次波动的目标值(bottom - waves[position].targetPosition)
* 当方向为-1即方向向上时,波动的最大值为:上次运动的目标值-顶部座标(waves[position].targetPosition - top)
*/
waves[position].distance = waves[position].direction == 1 ?
(int) ((bottom - waves[position].targetPosition) * r) :
(int) ((waves[position].targetPosition - top) * r);
//该次波动的起始值设置为上次波动的目标值
waves[position].startPosition = waves[position].targetPosition;
//该次波动的目标值为:波动起始值+波动反向*波动距离
waves[position].targetPosition = waves[position].startPosition +
waves[position].distance * waves[position].direction;
}
到这里,画view需要的参数都已准备好了,下面看柱形条的绘制过程:
/**
* 画柱形条
*
* @param canvas
*/
private void drawPillar(Canvas canvas)
{
//定义柱形条顶部座标,因为柱形条需要跟随属性动画波动,所以这是一个时刻变化的值
float pillarTop;
//循环画每根柱形条
for (int i = 0; i < pillarAmount; i++)
{
//是否设置了颜色集合,设置了便根据集合设置画笔颜色,未设置则为画笔设置单一颜色
if (useColors)
{
mPaint.setColor(pillarColors[i]);
}
else
{
mPaint.setColor(pillarColor);
}
/**
* 根据wave对象及属性动画值计算柱形条当前顶部座标
* 公式:波动起始座标+方向*波动距离*动画值
*/
pillarTop = waves[i].startPosition + waves[i].direction * waves[i].distance * animValues[i];
//画柱形条
canvas.drawRect(left + (gap + pillarWidth) * i, pillarTop, left + (gap + pillarWidth) * i + pillarWidth,
bottom, mPaint);
}
}
到这里整个view就已完成,其中的较为关键的点是通过wave对象封装了波动,将画柱形图需要的参数封装起来,不至于被几个随机数弄得头晕。整个流程还是不算复杂,代码都已注释,下面贴出整个FrequencyView代码:
package com.hexj.library.widget;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
/**
* <p>项目名称:MyApplication
* <p>包 名: com.hexj.library.widget
* <p>版 权: 深圳市铭淏网络科技有限公司 2016
* <p>描 述: ${TODO}
* <p>创 建 人: hexiangjun
* <p>创建时间: 2017-02-15 14:20
* <p>当前版本: V1.0.0
* <p>修订历史: (版本、修改时间、修改人、修改内容)
*/
public class FrequencyView extends View{
/**
* 动画最大周期 3s
*/
private final long MAX_ANIM_PERIOD = 1000;
private final Paint mPaint;
private final Context context;
/**
* 单根柱形宽度,默认为10
*/
private int pillarWidth = 10;
/**
* 柱形数量,默认为4
*/
private int pillarAmount = 4;
/**
* 柱形颜色
*/
private int pillarColor = Color.rgb(179, 100, 53);
/**
* 波动节奏
* 快-慢 0-1
*/
private double rhythm = 0.5;
/**
* 柱形颜色组,
* 当pillarColors等于pillarAmount时,对应柱形取pillarColors对应颜色
* 当pillarColors大于pillarAmount时,对应柱形取pillarColors前pillarAmount个对应颜色
* 当pillarColors小于pillarAmount时,pillarColors循环拼接后,对应柱形取pillarColors对应颜色
*/
private int[] pillarColors;
/**
* 内容部分宽度
*/
private int contentWidth;
/**
* 内容部分高度
*/
private int contentHight;
/**
* 内容区域的左、上、右、下座标
*/
private int left, top, right, bottom;
/**
* 柱形间隙
*/
private float gap;
/**
* 是否使用颜色集合
*/
private boolean useColors = false;
/**
* 动画是否开启
*/
private boolean isStartAnim = false;
/**
* 动画值
*/
private float[] animValues;
/**
* 波动范围
*/
private Wave[] waves;
public FrequencyView(Context context){
this(context, null);
}
public FrequencyView(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public FrequencyView(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
this.context = context;
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
//若动画未开启过,则为每根柱形条开启动画
if (!isStartAnim){
for (int i = 0; i < animValues.length; i++){
startAnim(i);//开启动画
}
isStartAnim = true;
}
//画柱形条
drawPillar(canvas);
}
/**
* 开启动画
*/
private void startAnim(final int pillarPosition) {
final ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
@Override
public void onAnimationUpdate(ValueAnimator animation){
//对应柱形条绑定对应属性动画值
animValues[pillarPosition] = (float) animation.getAnimatedValue();
//每次动画都重画view
invalidate();
}
});
anim.addListener(new AnimatorListenerAdapter(){
//监听动画重播时,需要产生下一次波动的随机波动周期以及波动实体
@Override
public void onAnimationRepeat(Animator animation){
super.onAnimationRepeat(animation);
//为了让波动不具备很强规则性,这里随机打乱周期,并将rhythm波动频率带入
anim.setDuration((long) (MAX_ANIM_PERIOD * (Math.random() / 4 + 0.75) * rhythm));
//获取下次波动实体
getWaves(pillarPosition);
}
});
//随机产生第一次波动周期
anim.setDuration((long) (MAX_ANIM_PERIOD * (Math.random() / 4 + 0.75) * rhythm));
//无限循环
anim.setRepeatCount(ValueAnimator.INFINITE);
//从头开始动画
anim.setRepeatMode(ValueAnimator.RESTART);
anim.start();
}
/**
* 画柱形条
*
* @param canvas
*/
private void drawPillar(Canvas canvas) {
//定义柱形条顶部座标,因为柱形条需要跟随属性动画波动,所以这是一个时刻变化的值
float pillarTop;
//循环画每根柱形条
for (int i = 0; i < pillarAmount; i++){
//是否设置了颜色集合,设置了便根据集合设置画笔颜色,未设置则为画笔设置单一颜色
if (useColors){
mPaint.setColor(pillarColors[i]);
}else{
mPaint.setColor(pillarColor);
}
/**
* 根据wave对象及属性动画值计算柱形条当前顶部座标
* 公式:波动起始座标+方向*波动距离*动画值
*/
pillarTop = waves[i].startPosition + waves[i].direction * waves[i].distance * animValues[i];
//画柱形条
canvas.drawRect(left + (gap + pillarWidth) * i, pillarTop, left + (gap + pillarWidth) * i + pillarWidth,
bottom, mPaint);
}
}
/**
* 获取随机波动实体
*/
private void getWaves(int position){
//产生一个0.5-1的随机基数
double r = Math.random() / 2 + 0.5;
//将波动方向反向
waves[position].direction = -waves[position].direction;
/**
*根据随机基数获取波动距离
*当方向为1即方向向下运动时,波动距离的最大值为:底部座标-上次波动的目标值(bottom - waves[position].targetPosition)
* 当方向为-1即方向向上时,波动的最大值为:上次运动的目标值-顶部座标(waves[position].targetPosition - top)
*/
waves[position].distance = waves[position].direction == 1 ?
(int) ((bottom - waves[position].targetPosition) * r) :
(int) ((waves[position].targetPosition - top) * r);
//该次波动的起始值设置为上次波动的目标值
waves[position].startPosition = waves[position].targetPosition;
//该次波动的目标值为:波动起始值+波动反向*波动距离
waves[position].targetPosition = waves[position].startPosition +
waves[position].distance * waves[position].direction;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//设置控件宽度
setMeasuredDimension(measureWidth(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
//控件内容宽度
contentWidth = getWidth() - getPaddingLeft() - getPaddingRight();
//控件内容高度
contentHight = getHeight() - getPaddingTop() - getPaddingBottom();
//控件的左、上、右、下座标
left = getPaddingLeft();
top = getPaddingTop();
right = getWidth() - getPaddingRight();
bottom = getHeight() - getPaddingBottom();
//柱形条间隙
gap = pillarAmount > 1 ? (float) (contentWidth - pillarAmount * pillarWidth) / (float) (pillarAmount - 1) : 0;
//初始化属性动画值数组
this.animValues = new float[pillarAmount];
//初始化波动实体数组
this.waves = new Wave[pillarAmount];
for (int i = 0; i < waves.length; i++) {
int distance = (int) (Math.random() * contentHight);
waves[i] = new Wave(-1, distance, bottom, bottom - distance);
}
}
/**
* 计算控件宽度
*
* @param measureSpec
* @return
*/
private int measureWidth(int measureSpec){
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//根据柱形数量跟宽度计算出最小宽度
int minWidth = pillarWidth * pillarAmount + getPaddingLeft() + getPaddingRight();
if (specMode == MeasureSpec.EXACTLY){
return specSize > minWidth ? specSize : minWidth;
}
return minWidth;
}
/**
* 设置柱形参数
*
* @param pillarAmount 柱形个数
* @param pillarWidth 柱形宽度
* @param pillarColor 柱形颜色
*/
public void setPillar(int pillarAmount, int pillarWidth, int pillarColor){
this.pillarAmount = pillarAmount > 0 ? pillarAmount : 0;
this.pillarWidth = pillarWidth > 0 ? dip2px(context, pillarWidth) : 0;
useColors = false;
if (pillarColor > 0){
this.pillarColor = pillarColor;
}
}
/**
* 设置柱形参数
*
* @param pillarAmount 柱形个数
* @param pillarWidth 柱形宽度
* @param pillarColors 柱形颜色集合
*/
public void setPillar(int pillarAmount, int pillarWidth, int[] pillarColors){
this.pillarAmount = pillarAmount > 0 ? pillarAmount : 0;
this.pillarWidth = pillarWidth > 0 ? dip2px(context, pillarWidth) : 0;
if (pillarColors != null && pillarColors.length > 0){
useColors = true;
this.pillarColors = new int[pillarAmount];
if (pillarColors.length == pillarAmount || pillarColors.length > pillarAmount){
System.arraycopy(pillarColors, 0, this.pillarColors, 0, pillarAmount);
}else{
for (int i = 0; i < pillarAmount; i++){
this.pillarColors[i] = pillarColors[i % pillarColors.length];
}
}
}
}
/**
* 设置波动节奏
*
* @param rhythm
*/
public void setRhythm(double rhythm){
double m = rhythm < 0.0 ? 0.0 : rhythm > 1 ? 1 : rhythm;
//对rhythm进行偏量计算
this.rhythm = (m / 4) + 0.75;
}
/**
* 波动实体类
*/
class Wave{
/**
* 波动方向.
* 1:正向波动(向下),-1:负向波动(向上)
*/
int direction;
/**
* 波动距离
*/
float distance;
/**
* 起始位置
*/
float startPosition;
/**
* 目标位置
*/
float targetPosition;
public Wave(){
}
public Wave(int direction, float distance, float startPosition, float targetPosition){
this.direction = direction;
this.distance = distance;
this.startPosition = startPosition;
this.targetPosition = targetPosition;
}
@Override
public String toString(){
return "Wave{" +
"direction=" + direction +
", distance=" + distance +
", startPosition=" + startPosition +
", targetPosition=" + targetPosition +
'}';
}
}
private int dip2px(Context context, float dpValue){
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
}
使用很简单:
布局: