音頻跳動的View--FrequencyView

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

使用很簡單:


佈局:











發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章