開年來公司不忙,就在閒逛時朋友說給寫給小控件,並給出這樣的效果:
我問他還要什麼要求,提供的數據是什麼,他竟然告訴我沒要求,數據隨機給就行。我的第一反應是這還不簡單啊,就寫個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);
}
}
使用很簡單:
佈局: